npm package: @statedelta-axiom/view-state
Description: Reactive view computation system — Pass 0 of the tick pipeline
Author: Anderson D. Rosa
Latest version: 0.1.0

# @statedelta-actions/view-state

Sistema de Views reativas — Pass 0 do pipeline de tick (PPP: Preparar → Processar → Propagar).

Views transformam dados brutos de estado em **sinais semânticos** pré-computados e cacheados por departamento. Rules de domínio consomem esses sinais em vez de derivar dados inline — separação entre derivação de dados e lógica de negócio.

```typescript
// SEM views — rule precisa derivar tudo inline, sem acesso a transição
{
  id: "critical-heal",
  when: (ctx) => /* como saber se ACABOU de ficar low? impossível sem transição */,
}

// COM views — rule consome sinal já computado
{
  id: "critical-heal",
  when: (ctx) => ctx.get("combat", "$justBecameLowHp") === true,
}
```

## Filosofia

### View = Departamento Nomeado

Uma View não é uma lista flat de computeds. É um **departamento** — um Record state nomeado com computed operations que populam esse Record. Cada departamento tem escopo isolado, prioridade de computação, e condição de ativação (`when`).

Views inativas (`when: false`) têm **zero custo**. O evaluator nem olha pras ops.

### Computeds = Declarações Reativas

Computed operations são declarações reativas de derivação. Cada op **produz um valor** — a op não muta a view, ela retorna algo e o evaluator escreve no Record da view. São como diretivas reativas passivas: computam dados e o sistema persiste o resultado.

Três fast paths, qualificados em register-time:

| resolve | directives | Fast Path | Comportamento |
|---------|-----------|-----------|---------------|
| Ausente | Ausente | **BIND** | Projeção direta: `get(deps[0])` (interno do evaluator) |
| Presente | Ausente | **NATIVE** | Função do consumer: `resolve(ctx)` |
| Ausente | Presente | **ACTION** | Via ActionEngine: hidden action `view:{viewId}:{name}` |
| Presente | Presente | — | **Erro de validação** — mutuamente exclusivos |

Ops BIND e NATIVE resolvem nativamente — não tocam no ActionEngine. Apenas ops com `directives` registram hidden actions no engine compartilhado.

### Evaluator = Stateless

O evaluator **não possui estado de dados**. Não cria stores, não gerencia Records internamente.

- **Records das views** são criados via `createRecord` callback (DI). O consumer fornece a factory — tipicamente o Store do `@statedelta-apex`. O evaluator chama a factory no `register()` e persiste resultados no Record via `record.set(name, value)`.
- **Leitura interna** (`get`) vem via accessor genérico passado no `evaluate()`. Uso exclusivo interno: state dep detection e fast path BIND. Não é passado às ops.
- **Leitura das ops** (`ctx`) vem via contexto opaco `TCtx` passado no `evaluate()`. O consumer monta ctx com `ctx.get()`, `ctx.getDelta()`, o que precisar. A lib não sabe e não precisa saber.
- **Dirty checking** (cache reativo) vem via callback `hasChanged` opcional na config. O consumer implementa (tipicamente usando TransitionState do RS). Sem callback, todas as ops recomputam a cada evaluate.

O evaluator é uma **função de transformação**: recebe accessors, computa ops na ordem, escreve nos Records, faz lock. Sem side-effects próprios, sem estado persistente.

### Três Canais Separados (ADR-038)

| Canal | Direção | Interface | Fornecido por | Usado pra |
|-------|---------|-----------|---------------|-----------|
| **ctx** | Leitura (ops) | `TCtx` (opaco) | Consumer (evaluate arg) | `resolve(ctx)`, `when(ctx)` |
| **get** | Leitura (interno) | `(path) => unknown` | Consumer (evaluate arg) | State dep detection, BIND |
| **Record** | Escrita | `IViewRecord.set(key, value)` | Factory (register-time) | Persistir resultados |

### Reatividade Posicional, Não Baseada em Grafo

O evaluate é um **sweep linear com dirty checking** — não usa grafo de reatividade. A ordem de avaliação é fixa (definida no registro): priority entre views, topo sort dentro de cada view. Cada op consulta um `changed` set pra decidir se recomputa. Sem push, sem subscription, sem grafo de invalidação.

Mais próximo de um **game loop**: update tudo frame a frame, na ordem, com early-exit pra quem não mudou.

### Opt-in Progressivo

O sistema de views segue o mesmo princípio do monorepo:

- **Sem views** — rules derivam dados inline. Funciona, mas polui lógica de domínio.
- **Com views (BIND/NATIVE)** — 90% dos casos. Projeções e funções puras. Sem engine.
- **Com views (ACTION)** — ops complexas via ActionEngine. Analyzer vê no mesmo grafo. Composition control garante read-only.

Feature not used = zero overhead.

## Instalação

```bash
pnpm add @statedelta-actions/view-state
```

Requer `@statedelta-actions/actions` como dependência.

## Quick Start

```typescript
import { createActionEngine } from "@statedelta-actions/actions";
import { createViewEvaluator } from "@statedelta-actions/view-state";

// 1. ActionEngine compartilhado (mesmo usado por rules/events)
const actionEngine = createActionEngine({ handlers: { /* ... */ } });

// 2. Factory de Records (consumer fornece — ex: createRecord do @statedelta-apex)
const createRecord = (viewId: string) => ({
  _data: {} as Record<string, unknown>,
  _locked: false,
  set(key: string, value: unknown) {
    if (this._locked) throw new Error(`Record "${viewId}" is locked`);
    this._data[key] = value;
  },
  get(key: string) { return this._data[key]; },
  has(key: string) { return key in this._data; },
  lock() { this._locked = true; },
  unlock() { this._locked = false; },
});

// 3. Criar evaluator — TCtx é o tipo do contexto do consumer
const evaluator = createViewEvaluator<MyCtx>({ actionEngine, createRecord });

// 4. Registrar views
evaluator.register([
  {
    id: "combat",
    priority: 100,
    when: (ctx) => ctx.get("game:zone") === "combat",
    computed: [
      // Bind implícito — projeção direta (evaluator resolve internamente)
      { name: "hp", deps: ["units:player-1:hp"] },
      { name: "hpMax", deps: ["units:player-1:hpMax"] },

      // Resolve nativo — recebe ctx, retorna valor
      { name: "$hpPercent", deps: ["hp", "hpMax"],
        resolve: (ctx) =>
          (ctx.get("combat:hp") as number) / (ctx.get("combat:hpMax") as number),
      },

      // Edge — transição detectada via ctx.getDelta
      { name: "$damaged", deps: ["hp"],
        resolve: (ctx) => {
          const d = ctx.getDelta("combat:hp");
          return d.changed && (d.diff ?? 0) < 0;
        },
      },
    ],
  },
]);

// 5. Avaliar (chamado pelo TickRunner a cada tick)
// ctx: contexto do consumer (opaco pro evaluator)
// get: accessor interno — state dep detection + BIND
evaluator.evaluate(ctx, get);

// 6. Resultado no Record da view
const record = evaluator.getRecord("combat");
record.get("hp");         // 75
record.get("$hpPercent"); // 0.75
record.get("$damaged");   // true (hp diminuiu desde o tick anterior)
```

## ViewDefinition

Uma View é um departamento nomeado. Contém metadados e computed operations.

```typescript
interface ViewDefinition<TCtx = unknown> {
  readonly id: string;
  readonly priority: number;
  readonly when?: (ctx: TCtx) => boolean;
  readonly computed: readonly ComputedOp<TCtx>[];
  readonly tags?: readonly string[];
  readonly declarations?: Record<string, unknown>;
  readonly metadata?: Record<string, unknown>;
}
```

### priority

Maior número = executa primeiro (como z-index). Views são sorted por priority desc no registro. Se view `combat` (priority 100) precisa estar pronta antes de `ui` (priority 50), combat roda primeiro.

### when

Guard da view inteira. Recebe `ctx: TCtx` — o contexto de domínio do consumer. Se `false`, todas as ops da view são skippadas — zero custo. Reavaliado a cada tick.

Se `when` lançar exceção, a view é skippada (tratada como `false`).

### computed

Lista de computed operations. Dentro da view, a ordem de avaliação é determinada por **topological sort** dos deps — não pela ordem de declaração. Ciclo entre ops da mesma view = erro fatal no registro.

## ComputedOp — Tipo Unificado

Um tipo, três campos ortogonais. Sem dispatch por `type` — o runtime qualifica por presença de campos em register-time.

```typescript
interface ComputedOp<TCtx = unknown> {
  readonly name: string;
  readonly deps: readonly string[];
  readonly when?: (ctx: TCtx) => boolean;
  readonly resolve?: (ctx: TCtx) => unknown;
  readonly directives?: readonly Record<string, unknown>[];
  readonly tags?: readonly string[];
  readonly declarations?: Record<string, unknown>;
  readonly metadata?: Record<string, unknown>;
}
```

### name

Nome da op dentro da view. Deve ser único dentro da view. O nome qualificado é `{viewId}:{name}` — ex: `"combat:$hpPercent"`.

Convenção: ops derivadas usam prefixo `$` — `$hpPercent`, `$damaged`, `$isLowHp`. Binds usam nome direto — `hp`, `hpMax`, `members`.

### deps

Paths dos quais a op depende. O mecanismo central de reatividade: se nenhum dep mudou, a op é skippada (cache nível 1).

Deps podem ser:

- **Locais** (nome curto, sem `:`): referência a outra op da mesma view. Normalizado em register-time pra path qualificado. `deps: ["hp"]` dentro de view `"combat"` → `deps: ["combat:hp"]`.
- **Cross-view** (path qualificado com `:`): lê op de outra view. `deps: ["combat:$hpPercent"]` lê da view `"combat"`. Se a view já foi computada (priority maior), valor fresco. Se não, valor do tick anterior.
- **Estado externo** (path com `:`): lê estado do sistema. `deps: ["units:player-1:hp"]` lê do router/accessor.

O evaluator não distingue — deps são strings. O `get` accessor resolve.

### when (guard)

Condicional secundária — avaliada **após** verificação de deps changed. Recebe `ctx: TCtx`. Se `false`, a op mantém valor anterior (cache). Sempre boolean, nunca usado como valor de retorno.

```typescript
// Guard: só computa se hp mudou pra baixo
{ name: "$hitCount", deps: ["$damaged"],
  when: (ctx) => ctx.getDelta("combat:$damaged").to === true,
  resolve: (ctx) => (ctx.getDelta("combat:$hitCount").from ?? 0) + 1,
}
```

Se `when` lançar exceção, a op é skippada.

### resolve (caminho nativo)

Função que recebe `ctx: TCtx` e retorna o valor da op. O consumer monta ctx com `get()`, `getDelta()`, e o que mais precisar. A lib não conhece a estrutura do ctx.

```typescript
// Derived — função pura via ctx
{ name: "$hpPercent", deps: ["hp", "hpMax"],
  resolve: (ctx) =>
    (ctx.get("combat:hp") as number) / (ctx.get("combat:hpMax") as number),
}

// Edge — transição via ctx.getDelta
{ name: "$damaged", deps: ["hp"],
  resolve: (ctx) => {
    const d = ctx.getDelta("combat:hp");
    return d.changed && (d.diff ?? 0) < 0;
  },
}
```

### directives (caminho ActionEngine)

Lista de diretivas que são registradas como hidden action `view:{viewId}:{name}` no ActionEngine compartilhado. O resultado da hidden action (`result.data`) é o valor da op.

```typescript
{ name: "$teamPower", deps: ["units:team-1:members"],
  directives: [
    { type: "action", id: "query:team-power", as: "power" },
    { type: "return", resolve: (ctx, scope) => ({ value: scope.power }) },
  ],
}
```

**`resolve` e `directives` são mutuamente exclusivos.** Se ambos presentes, erro de validação no registro.

Hidden actions `view:*` devem ser read-only — sem `state`, `emit`, `notify`. Segurança via Analyzer (readonly propagator + composition control).

### Combinações

| resolve | directives | Comportamento |
|---------|-----------|---------------|
| Ausente | Ausente | **Bind implícito**: evaluator retorna `get(deps[0])` internamente. Requer ao menos 1 dep. |
| Presente | Ausente | **Função nativa**: retorna `resolve(ctx)`. |
| Ausente | Presente | **ActionEngine**: cria hidden action, retorna `result.data`. |
| Presente | Presente | **Erro** — mutuamente exclusivos. |

## Registro

```typescript
const result = evaluator.register(views);
```

### Pipeline de registro

```
Para cada ViewDefinition:
  │
  ├── 1. Valida view (id, priority, computed não-vazio)
  │
  ├── 2. Valida cada op (name, deps, resolve/directives exclusivos)
  │     Duplicata de name na mesma view → erro
  │
  ├── 3. Normaliza deps locais → qualificados
  │     "hp" dentro de view "combat" → "combat:hp"
  │     "units:player:hp" → mantido (já qualificado)
  │
  ├── 4. Topo sort dentro da view (Kahn's algorithm)
  │     Ciclo → erro fatal, view rejeitada
  │
  ├── 5. Qualifica fast path de cada op (BIND / NATIVE / ACTION)
  │     ACTION → monta ActionDefinition, registra no ActionEngine
  │
  ├── 6. Cria Record via createRecord(viewId)
  │
  └── 7. Armazena view + rebuilda sorted views (priority desc)
```

### ViewRegisterResult

```typescript
interface ViewRegisterResult {
  readonly registered: readonly string[];
  readonly errors: readonly ViewRegisterError[];
  readonly warnings: readonly RegisterWarning[];
}

interface ViewRegisterError {
  readonly viewId: string;
  readonly opName?: string;
  readonly code: string;
  readonly message: string;
}
```

### Códigos de erro

| Código | Quando |
|--------|--------|
| `INVALID_VIEW` | id vazio, priority inválido, computed vazio |
| `INVALID_OP` | name vazio, deps não-array, resolve+directives, bind sem deps |
| `DUPLICATE_ID` | View com id já registrado |
| `DUPLICATE_OP` | Duas ops com mesmo name dentro da mesma view |
| `CYCLE_DETECTED` | Ciclo de deps entre ops da mesma view |
| `ACTION_REGISTER_ERROR` | Falha ao registrar hidden action no ActionEngine |

### Exemplos de registro

```typescript
// View simples — binds e derived
evaluator.register([{
  id: "combat",
  priority: 100,
  computed: [
    { name: "hp", deps: ["units:player-1:hp"] },
    { name: "hpMax", deps: ["units:player-1:hpMax"] },
    { name: "$hpPercent", deps: ["hp", "hpMax"],
      resolve: (ctx) =>
        (ctx.get("combat:hp") as number) / (ctx.get("combat:hpMax") as number),
    },
  ],
}]);

// View com guard e edge
evaluator.register([{
  id: "combat",
  priority: 100,
  when: (ctx) => ctx.get("game:zone") === "combat",
  computed: [
    { name: "hp", deps: ["units:player-1:hp"] },
    { name: "$damaged", deps: ["hp"],
      resolve: (ctx) => {
        const d = ctx.getDelta("combat:hp");
        return d.changed && (d.diff ?? 0) < 0;
      },
    },
    { name: "$hitCount", deps: ["$damaged"],
      when: (ctx) => ctx.getDelta("combat:$damaged").to === true,
      resolve: (ctx) => (ctx.getDelta("combat:$hitCount").from ?? 0) + 1,
    },
  ],
}]);
```

## Desregistrar

```typescript
const removed = evaluator.unregister("combat"); // true se encontrado
```

Remove a view de todos os registros internos. Unregistra hidden actions do ActionEngine. O Record da view é descartado.

## Avaliação (evaluate / evaluateAsync)

```typescript
// Sync (default)
evaluator.evaluate(ctx, get);

// Async (quando ActionEngine tem hooks async)
await evaluator.evaluateAsync(ctx, get);
```

Se o ActionEngine conectado tiver hooks async (`actionEngine.isAsync === true`),
`evaluate()` lança erro — use `evaluateAsync()`. O evaluator herda o flag
de async do ActionEngine (`evaluator.isAsync`). Ops BIND e NATIVE são sempre
síncronas — só ops ACTION usam `invokeAsync()` no path async.

### Parâmetros

| Parâmetro | Tipo | Descrição |
|-----------|------|-----------|
| `ctx` | `TCtx` | Contexto de domínio do consumer. Passado às ops via `resolve(ctx)` e `when(ctx)`. Opaco pro evaluator. |
| `get` | `(path: string) => unknown` | Accessor interno — state dep detection e fast path BIND. Não é passado às ops. |

### O `ctx`

Contexto opaco do consumer. O evaluator passa `ctx` pros ops (`resolve(ctx)`, `when(ctx)`) sem interpretar. O consumer monta ctx com `get()`, `getDelta()`, e o que mais precisar:

```typescript
// Consumer monta ctx
const ctx = {
  get: (path) => router(path),
  getDelta: (path) => transitionState.delta(path),
};

// get é pra uso interno do evaluator
const get = ctx.get;

evaluator.evaluate(ctx, get);
```

### O `get` interno

Accessor genérico que lê **qualquer** path acessível no sistema. Uso exclusivo interno do evaluator — state dep detection e fast path BIND. **Não é passado às ops.** Ops usam `ctx`.

O consumer é responsável por fazer `ctx.get()` resolver paths de views lendo dos Records — é wiring externo, não concern do evaluator.

### O sweep linear

```
evaluate(ctx, get):

  changed = Set<string>   // paths que mudaram (global, entre views)

  for cada view (priority desc):

    // Guard da view
    if view.when && !view.when(ctx):
      continue              // skip total, zero custo

    // Unlock record (locked do tick anterior)
    record.unlock()

    // 1. Detectar mudanças em state paths desta view
    for cada state path nos deps:
      if !hasChanged || hasChanged(path, get(path)):
        changed.add(path)

    // 2. Avaliar ops em ordem topológica
    for cada op:

      // Cache nível 1: algum dep mudou?
      if !hasChanged:
        // sem dirty checker → sempre executa (sem cache)
      else:
        if nenhum dep no changed set:
          continue          // skip — deps unchanged

      // Guard da op
      if op.when && !op.when(ctx):
        continue            // skip — guard falhou

      // Resolve por fast path
      switch op.fastPath:
        BIND:    result = get(op.deps[0])
        NATIVE:  result = op.resolve(ctx)
        ACTION:  result = actionEngine.invoke(...).data          // sync
                          (await actionEngine.invokeAsync(...)).data  // async

      // Escreve no Record da view (chave local)
      record.set(op.name, result)

      // Cache nível 2: valor mudou?
      if !hasChanged || hasChanged(qualifiedName, result):
        changed.add(qualifiedName)    // propaga pra dependentes

    // 3. Lock — Record read-only até o próximo tick
    record.lock()
```

### Cache em dois níveis

Cache reativo requer `hasChanged` na config. Sem `hasChanged`, todas as ops recomputam a cada evaluate (sem cache — comportamento padrão).

**Nível 1 — Deps unchanged.** Nenhum dep mudou (via `hasChanged`). Skip direto — nem invoca resolve/action. Reutiliza valor anterior no Record. Custo: O(deps.length) — iteração + lookup no `changed` set.

**Nível 2 — Value firewall.** Deps mudaram, op executou, mas resultado é o mesmo (via `hasChanged`). Dependentes **não** recomputam — cascata cortada.

```
hp: 70 (mudou de 100)
  → $hpPercent recomputa: 70/100 = 0.7 (era 0.8) → MUDOU
    → $isLowHp recomputa: 0.7 < 0.2 = false (era false) → NÃO MUDOU (firewall)
      → $justBecameLowHp: deps não mudaram → SKIP (nível 1)
```

Comparação shallow (`===`). Primitivos direto. Objetos dependem de structural sharing.

### Primeiro tick (sem cache)

Sem `hasChanged` configurado (ou antes do RS popular o TransitionState), todos os computeds executam — sem cache.

Ops com `ctx.getDelta()` retornam `{ changed: false }` no primeiro tick — sem transição detectável. Isso é correto: não houve mudança, houve inicialização.

Após o primeiro tick, o RS/Factory captura valores no TransitionState pra habilitar cache no próximo tick.

### Error resilience

Erros durante evaluate são reportados via `onEvalError` callback (configurável). Se o callback retorna, a op é skippada. Se lança, o evaluate é interrompido. Sem callback, skip silencioso.

| Fonte do erro | source | Comportamento |
|---------------|--------|---------------|
| `view.when()` throws | `view-when` | View skippada (tratada como `false`) |
| `op.when()` throws | `op-when` | Op skippada |
| `get(deps[0])` throws | `bind` | Op BIND skippada |
| `op.resolve()` throws | `native` | Op skippada |
| `actionEngine.invoke()` throws | `action` | Op skippada |
| `actionEngine.invoke()` retorna erros | `action` | Op skippada (engine capturou internamente) |

```typescript
const evaluator = createViewEvaluator<MyCtx>({
  actionEngine,
  createRecord,
  onEvalError: (err) => {
    console.warn(`[${err.source}] ${err.viewId}:${err.opName}`, err.error);
    // retorna → op skippada
    // throw → interrompe evaluate
  },
});
```

## IViewRecord — Interface do Record

Interface mínima que o evaluator precisa pro Record de cada view. O consumer fornece a implementação real (ex: `IRecordState` do `@statedelta-apex/record-state`).

```typescript
interface IViewRecord {
  set(key: string, value: unknown): void;
  get(key: string): unknown;
  has(key: string): boolean;
  lock(): void;
  unlock(): void;
}
```

| Método | Quando | Quem chama |
|--------|--------|------------|
| `set(key, value)` | Escrever resultado computado | Evaluator, durante evaluate |
| `get(key)` | Ler valor já computado | Consumer (via ctx wiring) |
| `has(key)` | Verificar existência | Consumer (via ctx wiring) |
| `lock()` | Tornar read-only após avaliar a view | Evaluator |
| `unlock()` | Liberar escrita antes de reavaliar no próximo tick | Evaluator |

### createRecord callback

O evaluator recebe `createRecord` via config:

```typescript
interface ViewEvaluatorConfig<TCtx> {
  readonly actionEngine: IActionEngine<TCtx>;
  readonly createRecord: (viewId: string) => IViewRecord;
  readonly onEvalError?: OnEvalError;  // callback de erro no evaluate
}
```

Chamado durante `register()` — um Record por view. O evaluator não sabe como o Record funciona internamente. Pode ser um `IRecordState` do apex, pode ser um Map wrapper, pode ser qualquer coisa que implemente `IViewRecord`.

```typescript
// Exemplo: consumer usando @statedelta-apex
import { createRecord as createApexRecord } from "@statedelta-apex/record-state";

const evaluator = createViewEvaluator({
  actionEngine,
  createRecord: (viewId) => {
    const record = createApexRecord(viewId, {});
    return record; // IRecordState implementa IViewRecord
  },
});
```

## Integração com ActionEngine

### Engine compartilhado

Views, rules e events usam o **mesmo ActionEngine**. Hidden actions de ops ACTION coexistem no mesmo registry:

```
ActionEngine (registry unificado)
  │
  ├── view:combat:$teamPower       ← ViewEvaluator (ACTION)
  │                                   (ops BIND e NATIVE não registram no engine)
  │
  ├── query:team-power             ← Actions read-only (reutilizáveis)
  │
  ├── rule:combat-heal             ← RuleEngine
  ├── event:on-healed              ← EventProcessor
  │
  └── Analyzer valida tudo no mesmo grafo
```

### Diretivas permitidas para ops ACTION

Hidden actions `view:*` devem ser read-only. Só diretivas puras:

- `let` / `const` — binding no scope
- `return` — resultado do computed
- `action` — invocar query actions read-only

Side-effects (`state`, `emit`, `notify`, `halt`, `throw`) são proibidos. Segurança via Analyzer:

```typescript
// Manifest rule — analyzer rejeita se view:* invocar ação com side-effects
{
  source: { tags: { include: ["view"] } },
  target: { properties: { readonly: false } },
  effect: "deny",
  message: "computed views can only invoke read-only actions",
}
```

### Query actions

Computeds podem invocar query actions — actions read-only registradas no engine compartilhado:

```typescript
// Query registrada uma vez, usada por computeds e rules
actionEngine.register([{
  id: "query:team-stats",
  directives: [
    { let: "members", resolve: (ctx, scope) =>
        ({ value: scope.get("units:team-1:members") }) },
    { let: "total", resolve: (ctx, scope) =>
        ({ value: scope.members.reduce((sum, id) =>
            sum + scope.get(`units:${id}:power`), 0) }) },
    { type: "return", resolve: (ctx, scope) =>
        ({ value: { total: scope.total } }) },
  ],
}]);

// Computed consome a query
{ name: "$teamPower", deps: ["units:team-1:members"],
  directives: [
    { type: "action", id: "query:team-stats", as: "stats" },
    { type: "return", resolve: (ctx, scope) => ({ value: scope.stats }) },
  ],
}
```

### Action memo é transparente pro evaluator

Se o ActionEngine tiver action hooks de memoização, hidden actions `view:*` podem ser memoizadas pelo engine. O evaluator não sabe — invoca normalmente e recebe o resultado (cacheado ou não). São dois caches independentes:

- **View cache** (evaluator) — deps unchanged → não invoca a action
- **Action memo** (engine) — action invocada mas deps do memo unchanged → engine retorna último resultado sem executar diretivas

## Dependências entre Views

Views podem depender de qualquer estado ou view. Não existe mecanismo especial de cross-view.

### Acesso uniforme

O `ctx.get()` do consumer resolve tudo da mesma forma:

```typescript
ctx.get("units:player-1:hp")     // estado normal
ctx.get("combat:$hpPercent")     // op da view "combat"
ctx.get("team:$stats")           // op de outra view
```

### Frescor por priority

Se view `combat` (priority 100) já foi computada e view `ui` (priority 50) depende de `combat:$hpPercent`, lê valor **fresco** — combat rodou primeiro.

Se view `ui` dependesse de view com priority menor (ainda não computada), leria valor do **tick anterior** (via accessor/ctx). Determinístico, não quebra, não crasha.

```typescript
evaluator.register([
  // Priority 100 — computa primeiro
  {
    id: "combat",
    priority: 100,
    computed: [
      { name: "hp", deps: ["units:player-1:hp"] },
      { name: "$pct", deps: ["hp"],
        resolve: (ctx) => (ctx.get("combat:hp") as number) / 100,
      },
    ],
  },

  // Priority 50 — combat já computou quando ui roda
  {
    id: "ui",
    priority: 50,
    computed: [
      // Cross-view: lê $pct da view "combat"
      { name: "hpBar", deps: ["combat:$pct"] },
      { name: "$color", deps: ["hpBar"],
        resolve: (ctx) =>
          (ctx.get("ui:hpBar") as number) < 0.3 ? "red" : "green",
      },
    ],
  },
]);
```

## Normalização de Deps

Deps locais (sem `:`) são expandidos pra path qualificado em register-time:

```
View "combat":
  { name: "$isLowHp", deps: ["$hpPercent"] }
    → normalizado: deps: ["combat:$hpPercent"]

  { name: "hp", deps: ["units:player-1:hp"] }
    → mantido: deps: ["units:player-1:hp"]

  { name: "$showTeamWipe", deps: ["team:$teamWiped"] }
    → mantido: deps: ["team:$teamWiped"]
```

A regra: se o dep contém `:` → mantém. Se não contém `:` e é nome de outra op da mesma view → qualifica com `{viewId}:`. Se não é nome de op local → mantém como está (dep externo sem `:` — ex: `"$tick"`).

## Integração com TickRunner (Pass 0)

O TickRunner consome o ViewEvaluator via interface `IViewComputer`:

```typescript
interface IViewComputer<TCtx = unknown> {
  evaluate(ctx: TCtx, get: (path: string) => unknown): void;
  evaluateAsync(ctx: TCtx, get: (path: string) => unknown): Promise<void>;
}
```

```
TR.run(ctx):                        TR.runAsync(ctx):
  │                                   │
  ├── Pass 0:                         ├── Pass 0:
  │     viewComputer.evaluate(...)    │     await viewComputer.evaluateAsync(...)
  │                                   │
  ├── Pass 1:                         ├── Pass 1:
  │     ruleEngine.evaluate(ctx)      │     await ruleEngine.evaluateAsync(ctx)
  │                                   │
  └── Pass 2+: drain de eventos       └── Pass 2+: drain async
```

## Edge Patterns e Delta (vocabulário DSL)

Edge (boolean de transição) e delta (objeto de mudança) são **vocabulário do DSL JSON** — não do runtime. O compilador DSL → JS normaliza pra `ComputedOp` com `resolve`.

O runtime não conhece "edge" ou "delta". Recebe `ComputedOp` com `resolve` ou `directives`.

### Edge — compilado do DSL

```typescript
// DSL JSON:
{ name: "$damaged", edge: "decrease", deps: ["hp"] }

// Compila pra:
{ name: "$damaged", deps: ["hp"],
  resolve: (ctx) => {
    const d = ctx.getDelta("combat:hp");
    return d.changed && (d.diff ?? 0) < 0;
  },
}

// DSL com edge avançado:
{ name: "$lowHp", deps: ["hp"], edge: { op: "crossBelow", value: 20 } }

// Compila pra:
{ name: "$lowHp", deps: ["hp"],
  resolve: (ctx) => {
    const d = ctx.getDelta("combat:hp");
    return d.changed && (d.from as number) >= 20 && (d.to as number) < 20;
  },
}
```

### Delta — compilado do DSL

```typescript
// DSL JSON:
{ name: "$hpDelta", delta: true, deps: ["hp"] }

// Compila pra:
{ name: "$hpDelta", deps: ["hp"],
  resolve: (ctx) => ctx.getDelta("combat:hp"),
}
```

## Full API Reference

### IViewEvaluator\<TCtx\>

```typescript
interface IViewEvaluator<TCtx = unknown> {
  // Registration
  register(views: readonly ViewDefinition<TCtx>[]): ViewRegisterResult;
  unregister(id: string): boolean;

  // Evaluation
  evaluate(ctx: TCtx, get: (path: string) => unknown): void;
  evaluateAsync(ctx: TCtx, get: (path: string) => unknown): Promise<void>;

  // Introspection
  has(id: string): boolean;
  readonly size: number;
  readonly isAsync: boolean;
  getRecord(viewId: string): IViewRecord | undefined;
  readonly actionEngine: IActionEngine<TCtx>;

  /**
   * Coleta todos os state paths observados por todas as views registradas.
   * Paths externos (deps que não são ops locais da view).
   * Usado pelo consumer (RS) pra gerenciar o TransitionStore.
   */
  collectStateDeps(): ReadonlySet<string>;
}
```

### ViewEvaluatorConfig\<TCtx\>

```typescript
interface ViewEvaluatorConfig<TCtx = unknown> {
  readonly actionEngine: IActionEngine<TCtx>;
  readonly createRecord: (viewId: string) => IViewRecord;
  readonly onEvalError?: OnEvalError;
  readonly hasChanged?: HasChangedFn;
}

/**
 * Callback de dirty checking — informa se um path mudou.
 * Recebe o path (qualificado) e o valor atual.
 * Retorna true se o valor mudou em relação ao anterior.
 * Sem este callback, todas as ops recomputam a cada evaluate.
 */
type HasChangedFn = (path: string, currentValue: unknown) => boolean;

type OnEvalError = (error: ViewEvalError) => void;

interface ViewEvalError {
  readonly viewId: string;
  readonly opName?: string;
  readonly qualifiedName?: string;
  readonly source: EvalErrorSource;
  readonly error: unknown;
}

type EvalErrorSource = "view-when" | "op-when" | "bind" | "native" | "action";
```

### ViewDefinition\<TCtx\>

```typescript
interface ViewDefinition<TCtx = unknown> {
  readonly id: string;
  readonly priority: number;
  readonly when?: (ctx: TCtx) => boolean;
  readonly computed: readonly ComputedOp<TCtx>[];
  readonly tags?: readonly string[];
  readonly declarations?: Record<string, unknown>;
  readonly metadata?: Record<string, unknown>;
}
```

### ComputedOp\<TCtx\>

```typescript
interface ComputedOp<TCtx = unknown> {
  readonly name: string;
  readonly deps: readonly string[];
  readonly when?: (ctx: TCtx) => boolean;
  readonly resolve?: (ctx: TCtx) => unknown;
  readonly directives?: readonly Record<string, unknown>[];
  readonly tags?: readonly string[];
  readonly declarations?: Record<string, unknown>;
  readonly metadata?: Record<string, unknown>;
}
```

### IViewRecord

```typescript
interface IViewRecord {
  set(key: string, value: unknown): void;
  get(key: string): unknown;
  has(key: string): boolean;
  lock(): void;
  unlock(): void;
}
```

### HasChangedFn

```typescript
type HasChangedFn = (path: string, currentValue: unknown) => boolean;
```

### CreateRecordFn

```typescript
type CreateRecordFn = (viewId: string) => IViewRecord;
```

## Exports

```typescript
// Factory
export { createViewEvaluator } from "./evaluator";

// Types
export type {
  IViewRecord,
  CreateRecordFn,
  ComputedOp,
  ViewDefinition,
  ViewRegisterResult,
  ViewRegisterError,
  RegisterWarning,
  HasChangedFn,
  ViewEvaluatorConfig,
  IViewEvaluator,
  IViewComputer,
  ViewEvalError,
  EvalErrorSource,
  OnEvalError,
} from "./types";
```

## Use Case: RPG — View de Combate

```typescript
const evaluator = createViewEvaluator<MyCtx>({ actionEngine, createRecord });

evaluator.register([{
  id: "combat",
  priority: 100,
  when: (ctx) => ctx.get("game:zone") === "combat",
  computed: [
    // Bind — projeções diretas de estado
    { name: "hp", deps: ["units:player-1:hp"] },
    { name: "hpMax", deps: ["units:player-1:hpMax"] },

    // Derived — função pura
    { name: "$hpPercent", deps: ["hp", "hpMax"],
      resolve: (ctx) =>
        (ctx.get("combat:hp") as number) / (ctx.get("combat:hpMax") as number),
    },

    // Edge — transição detectada
    { name: "$damaged", deps: ["hp"],
      resolve: (ctx) => {
        const d = ctx.getDelta("combat:hp");
        return d.changed && (d.diff ?? 0) < 0;
      },
    },
    { name: "$healed", deps: ["hp"],
      resolve: (ctx) => {
        const d = ctx.getDelta("combat:hp");
        return d.changed && (d.diff ?? 0) > 0;
      },
    },

    // Delta — objeto de mudança
    { name: "$hpDelta", deps: ["hp"],
      resolve: (ctx) => ctx.getDelta("combat:hp"),
    },

    // Derived de derived
    { name: "$isLowHp", deps: ["$hpPercent"],
      resolve: (ctx) => (ctx.get("combat:$hpPercent") as number) < 0.2,
    },

    // Edge de derived — "acabou de ficar low?"
    { name: "$justBecameLowHp", deps: ["$isLowHp"],
      resolve: (ctx) => {
        const d = ctx.getDelta("combat:$isLowHp");
        return d.changed && d.to === true;
      },
    },

    // Contador com guard
    { name: "$hitCount", deps: ["$damaged"],
      when: (ctx) => ctx.getDelta("combat:$damaged").to === true,
      resolve: (ctx) => (ctx.getDelta("combat:$hitCount").from ?? 0) + 1,
    },
  ],
}]);
```

## Use Case: Cross-view — UI consumindo Combat

```typescript
evaluator.register([
  {
    id: "combat",
    priority: 100,
    computed: [
      { name: "hp", deps: ["units:player-1:hp"] },
      { name: "$pct", deps: ["hp"],
        resolve: (ctx) => (ctx.get("combat:hp") as number) / 100,
      },
      { name: "$damaged", deps: ["hp"],
        resolve: (ctx) => {
          const d = ctx.getDelta("combat:hp");
          return d.changed && (d.diff ?? 0) < 0;
        },
      },
    ],
  },
  {
    id: "ui",
    priority: 50,   // menor que combat → combat já computou
    computed: [
      // Cross-view: lê da view "combat"
      { name: "hpBar", deps: ["combat:$pct"] },
      { name: "$showDamageFlash", deps: ["combat:$damaged"],
        resolve: (ctx) => {
          const d = ctx.getDelta("combat:$damaged");
          return d.changed && d.to === true;
        },
      },

      // Local: depende de op da própria view
      { name: "$hpBarColor", deps: ["hpBar"],
        resolve: (ctx) =>
          (ctx.get("ui:hpBar") as number) < 0.2 ? "red" : "green",
      },
    ],
  },
]);
```

## Use Case: View com Query Action (directives)

```typescript
evaluator.register([{
  id: "team",
  priority: 200,
  computed: [
    // Op com directives → hidden action no engine
    { name: "$stats", deps: ["units:team-1:members"],
      directives: [
        { type: "action", id: "query:team-stats", as: "stats" },
        { type: "return", resolve: (ctx, scope) => ({ value: scope.stats }) },
      ],
    },

    // Resolve nativo — depende de op ACTION
    { name: "$teamWiped", deps: ["$stats"],
      resolve: (ctx) => {
        const d = ctx.getDelta("team:$stats");
        return d.changed && d.to === true;
      },
    },
  ],
}]);
```

## Use Case: Rules consumindo Views

```typescript
// Após evaluate(), rules consomem os sinais pré-computados
ruleEngine.register([
  {
    id: "critical-heal",
    priority: 100,
    when: (ctx) => ctx.get("combat", "$justBecameLowHp") === true,
    then: [
      { type: "state", target: "units:player-1:hp", op: "set", value: 50 },
      { type: "emit", event: "emergency-heal" },
    ],
  },
  {
    id: "combo-bonus",
    priority: 50,
    when: (ctx) => ctx.get("combat", "$hitCount") >= 5,
    then: [
      { type: "state", target: "units:player-1:combo", op: "set", value: true },
    ],
  },
]);
```

## Performance

### Spectrum

```
Zero views          BIND/NATIVE only        ACTION (directives)
────────────────────────────────────────────────────────────────
Zero overhead       Sweep linear, funções   + ActionEngine invoke
                    puras, O(deps) cache    + hidden action compile
```

### O que é O(1) e o que não é

| Operação | Custo |
|----------|-------|
| Register | O(ops) — validação + topo sort + qualificação |
| Evaluate (tick) | O(views × ops) — sweep linear com cache |
| Cache nível 1 check | O(deps.length) por op |
| BIND resolve | O(1) — `get(deps[0])` |
| NATIVE resolve | O(resolve) — depende da função |
| ACTION resolve | O(invoke) — depende do ActionEngine |
| Cross-view read | O(1) — mesmo `get` |

### Cache effectiveness

A maioria dos ticks, a maioria das ops é skippada pelo cache nível 1 (deps unchanged). Quando deps mudam, o cache nível 2 (value firewall) corta cascatas — ops que recomputam mas produzem o mesmo resultado não propagam.

### v0 Limitations

- **Sem JIT** — sweep interpretado. JIT como evolução se performance exigir.
- **Sem tracking dinâmico** — deps estáticos declarados. Tracking dinâmico é evolução futura.
- **Sem grafo de reatividade** — sweep linear visita todas as ops de views ativas. Grafo é otimização interna futura (sem impacto na API).

## Module Structure

```
src/
├── index.ts          — Public exports
├── types.ts          — All types and interfaces
├── evaluator.ts      — createViewEvaluator + ViewEvaluatorImpl
├── topo-sort.ts      — Kahn's algorithm (per-view)
└── validate.ts       — validateView, validateOp
```

## Licença

MIT