@statedelta-actions/analyzer
v0.6.1
Published
Static analysis layer for ActionEngine — capabilities, dependency graph, access control
Maintainers
Readme
@statedelta-actions/analyzer
Camada de análise estática para o ActionEngine. Graph queries, detecção de ciclos, inferência de capabilities, composition control e detecção de conflitos de declaração.
Filosofia
O engine é o V8. O analyzer é o TypeScript.
O ActionEngine é runtime puro: valida, armazena, compila, executa. Não sabe o que "capabilities", "ciclos" ou "access control" significam.
O ActionAnalyzer é o type checker externo: observa o engine (read-only), extrai fatos, valida contratos e expõe consultas. Nunca muta o engine.
Três modos de operação:
| Modo | O que usa | Analogia |
|------|-----------|----------|
| JS mode | Engine only | JavaScript puro — sem checagem |
| TS mode | Engine + Analyzer | TypeScript — análise estática, graph queries |
| TS strict | Engine + Analyzer + Composition manifest | strict: true — análise + composition control + contratos |
O consumer decide o nível de análise. O engine funciona sozinho. O analyzer é opt-in.
Instalação
pnpm add @statedelta-actions/analyzerPeer dependencies: @statedelta-actions/core, @statedelta-actions/graph, @statedelta-actions/actions.
Início Rápido
import { createActionEngine } from "@statedelta-actions/actions";
import { createActionAnalyzer } from "@statedelta-actions/analyzer";
// 1. Criar engine
const engine = createActionEngine({ handlers });
// 2. Criar analyzer
const analyzer = createActionAnalyzer({ engine });
// 3. Wire lifecycle events
const unsubs = [
engine.on("register", (e) => analyzer.processRegistration(e)),
engine.on("unregister", (e) => analyzer.processUnregistration(e)),
];
// 4. Usar normalmente — engine opera, analyzer observa
engine.register([
{ id: "heal", directives: [{ type: "state", path: "hp", value: 100 }] },
{ id: "combat", directives: [{ type: "invoke", action: "heal" }] },
]);
// 5. Consultar o analyzer
analyzer.capabilitiesOf("combat"); // Set { "invoke", "write" } (transitivo)
analyzer.dependenciesOf("combat"); // Set { "heal" }
analyzer.dependentsOf("heal"); // Set { "combat" }
analyzer.inCycle("heal"); // false
// 6. Cleanup
unsubs.forEach((fn) => fn());
analyzer.dispose();Três Modos de Operação
JS mode — Engine only
const engine = createActionEngine({ handlers });
engine.register([...]);
engine.invoke("myAction", ctx);
// Sem análise. Runtime puro. Zero overhead.Funciona: registration, invoke, JIT. Não funciona: ciclos, capabilities, composition control, declarations, dependency queries.
TS mode — Engine + Analyzer
const engine = createActionEngine({ handlers });
const analyzer = createActionAnalyzer({ engine });
// Wire events...
engine.on("register", (e) => analyzer.processRegistration(e));
engine.on("unregister", (e) => analyzer.processUnregistration(e));
engine.register([...]);
// Graph queries disponiveis
analyzer.capabilitiesOf("myAction");
analyzer.inCycle("myAction");
analyzer.getStats();Tudo do JS mode + ciclos, capabilities transitivas, declarations, dependency queries.
TS strict — Engine + Analyzer + Composition Control
const analyzer = createActionAnalyzer({
engine,
accessManifest: {
rules: [
{
from: ["write"],
to: ["readonly"],
effect: "deny",
},
],
},
});
// Wire events...
const result = analyzer.processRegistration(event);
result.accessViolations; // AccessViolation[] — edges que violam regrasTudo do TS mode + composition control com manifest, validacao de edges em register-time.
Wiring
O analyzer não auto-subscribe. O consumer decide a estratégia de wiring (direto, batched, tick-boundary).
Dois lifecycle events:
| Evento | Método do Analyzer | Quando o engine emite |
|--------|-------------------|----------------------|
| "register" | processRegistration(e) | Após register() ou endBatch(), se houve registros |
| "unregister" | processUnregistration(e) | Após unregister(), se a action existia |
const unsubs = [
engine.on("register", (e) => analyzer.processRegistration(e)),
engine.on("unregister", (e) => analyzer.processUnregistration(e)),
];
// Cleanup
unsubs.forEach((fn) => fn());Um listener por evento. Múltiplos listeners causam processamento duplicado.
Graph Queries
Todas delegam diretamente pro DependencyGraph. Zero lógica extra.
| Método | Retorno | Descrição |
|--------|---------|-----------|
| capabilitiesOf(id) | ReadonlySet<string> \| undefined | Capabilities resolvidas (own + transitivas) |
| propertyOf<T>(id, name) | T \| undefined | Valor resolved de qualquer propriedade |
| inferredOf<T>(id, name) | T \| undefined | Valor inferido (ignora declarations) |
| dependenciesOf(id) | ReadonlySet<string> | Dependências diretas |
| dependentsOf(id) | ReadonlySet<string> | Quem depende dessa action |
| conflictsOf(id) | readonly DeclarationConflict[] | Conflitos inferred vs declared |
| inCycle(id) | boolean | Se participa de ciclo |
| getStats() | GraphStats | Estatísticas do grafo |
analyzer.capabilitiesOf("combat");
// Set { "invoke", "write" } — transitivo via heal
analyzer.propertyOf<number>("combat", "maxDepth");
// 1
analyzer.inferredOf<boolean>("action", "readonly");
// valor inferido, ignora declarations
analyzer.dependenciesOf("combat"); // Set { "heal" }
analyzer.dependentsOf("heal"); // Set { "combat" }
analyzer.inCycle("A"); // true/false
analyzer.conflictsOf("action");
// [{ property: "readonly", declared: true, inferred: false }]
analyzer.getStats();
// { totalActions, totalEdges, leafCount, branchCount, cycleCount, maxDepth, isolatedCount }Acesso direto ao grafo: analyzer.graph (readonly DependencyGraph).
Composition Control
Controle de dependências arquiteturais baseado em tags com pattern matching. Avalia edges (source -> target) contra regras declarativas. Define quem pode depender de quem — dev tooling pra manter a arquitetura limpa.
from e to são NodeMatchers — avaliados contra:
from(source / quem invoca) →ownCapabilities ∪ tagsdo sourceto(target / quem é invocado) →resolved capabilities ∪ tagsdo target (transitivo)
NodeMatcher aceita string, string[] (OR), ou { any | all | none: TagMatcher }. Tag patterns suportam glob ("state:*"), negação ("!write"), literal e listas.
const analyzer = createActionAnalyzer({
engine,
accessManifest: {
rules: [
// Actions com capability "write" não podem invocar actions taggeadas "protected-*"
{
from: ["write"],
to: "protected-*",
effect: "deny",
},
// Forma equivalente com any/all/none:
// { from: { any: "write" }, to: { any: "protected-*" }, effect: "deny" }
],
},
});Violações aparecem no AnalysisResult.accessViolations. A action e registrada — o consumer decide a política.
setAccessManifest — Manifest mutável
// Aplica novo manifest em runtime (recompila + revalida tudo)
const result = analyzer.setAccessManifest({
rules: [
{ from: ["read"], to: ["write"], effect: "deny" },
],
});
result.accessViolations; // Violações das edges existentes
// Desativa composition control (zero overhead)
analyzer.setAccessManifest(null);Para detalhes de TagMatcher, NodeMatcher, AccessRule, pipeline de compilacao e semântica de matching, veja ARCHITECTURE.md.
Nota sobre nomenclatura: O código usa
accessManifest,AccessRule,AccessViolationetc. Conceitualmente, isso e composition control — controle de dependências arquiteturais (quem pode depender de quem), não controle de acesso de usuario. ACL real (RBAC, autenticacao) e extensão futura, completamente separada. Ver ARCHITECTURE-VISION.md.
Tier Validation
Controle hierárquico por nível numerico. Actions com tier declarado participam de uma hierarquia: action com tier menor não pode invocar action com tier maior. Actions sem tier sao livres.
const analyzer = createActionAnalyzer({
engine,
tierValidation: true, // injeta tierPropagator + valida edges
});
// Wire events...
engine.on("register", (e) => analyzer.processRegistration(e));
engine.on("unregister", (e) => analyzer.processUnregistration(e));
engine.register([
{ id: "admin-action", directives: [...], tier: 500 },
{ id: "user-action", directives: [{ type: "action", id: "admin-action" }], tier: 100 },
]);
// result.tierViolations:
// [{ sourceId: "user-action", targetId: "admin-action", sourceTier: 100, targetTier: 500, message: "..." }]Semântica
| Source tier | Target tier | Resultado | |------------|-------------|-----------| | undefined | undefined | Livre | | undefined | 500 | Livre | | 100 | undefined | Livre | | 100 | 100 | OK (igual) | | 100 | 500 | Violacao | | 500 | 100 | OK (higher → lower) |
undefined = livre. Actions sem tier não participam da validacao. Tier e declaração explicita de intenção — so tem efeito quando o dev declara.
Dois mecanismos complementares
| Mecanismo | O que detecta | Exemplo | |-----------|--------------|---------| | Edge validation | Invocacao direta com tier insuficiente | A(100) → B(500) | | DECLARATION_CONFLICT | Tier declarado menor que inferido da sub-árvore | A declara 100, sub-árvore exige 500 |
Edge validation pega violações diretas. DECLARATION_CONFLICT pega violações transitivas (A → B → C onde C exige tier alto). Juntos cobrem todos os cenarios.
Consulta de tier
// Tier inferido (max da sub-árvore, incluindo declarado próprio)
analyzer.propertyOf<number>("action-A", "tier"); // 500
// Tier declarado (so o que a action declarou explicitamente)
analyzer.graph.get("action-A")?.declarations.get("tier"); // undefined (não declarou)Quando usar
- Actions com tier: actions de dominio que participam de hierarquia arquitetural (admin, system, domain-specific).
- Actions sem tier: utility actions (log, format, validate) — qualquer um pode chamar.
- Rules: o RuleEngine pode injetar
tierviapriority. ComtierValidation: true, o analyzer valida que uma rule de priority 300 não invoca (direta ou transitivamente) actions que exijam tier > 300.
Async / Interactive Propagators (opt-in)
Habilita análise rica sobre dois conceitos do engine que afetam compilação: async (action invoca handler async transitivamente) e interactive (action invoca generator handler ou type:"pause" transitivamente).
Engine ja resolve compilação sozinho via mini-graph interno (ADR-026 do actions). Esses propagators no analyzer adicionam:
DECLARATION_CONFLICT— action declara contrato público que contradiz inferência transitiva- Queries semânticas —
analyzer.propertyOf<boolean>(id, "async"),propertyOf<boolean>(id, "interactive") - Composition rules — manifest pode bloquear edges baseadas nessas propriedades
import {
asyncPropagator,
interactivePropagator,
DEFAULT_PROPAGATORS,
} from "@statedelta-actions/graph";
const analyzer = createActionAnalyzer({
engine,
propagators: {
...DEFAULT_PROPAGATORS,
async: asyncPropagator,
interactive: interactivePropagator,
},
});Sources (extract)
Propagator | Source local
---|---
async | capability async (handler.analyze marca) ou declaração async: true
interactive | diretiva type: "pause" direta, capability interactive, ou declaração interactive: true
Ambos com contagion any_true — qualquer dep tendo a propriedade propaga.
DECLARATION_CONFLICT
Action assina contrato público que contradiz a inferência transitiva → conflito reportado.
engine.register([
{ id: "fetchUser", directives: [{ type: "fetch" }] }, // capability async
{ id: "syncFacade",
directives: [{ type: "action", id: "fetchUser" }],
declarations: { async: false } }, // mente ao consumer
]);
analyzer.conflictsOf("syncFacade");
// [{ property: "async", declared: false, inferred: true }]Mesmo padrão pra interactive. Consumer decide a política (rejeitar boot, warning, ignorar).
Composition rules sobre async/interactive
Manifest pode usar properties no matcher pra bloquear edges:
const analyzer = createActionAnalyzer({
engine,
propagators: { ...DEFAULT_PROPAGATORS, async: asyncPropagator },
accessManifest: {
rules: [
{
source: { tags: { include: ["query"] } },
target: { properties: { async: true } },
effect: "deny",
reason: "query actions must not invoke async paths",
},
],
},
});Independência do engine (ADR-027)
Engine não consulta analyzer. O mini-graph interno cobre compilação. Esses propagators sao puramente dev-tooling — opt-in, externo, simétrico. Engine continua autônomo sem analyzer.
sync() — Hot-Plug
O analyzer pode ser criado depois de actions ja registradas. sync() le o registry inteiro do engine e reconstrói a análise completa:
// Actions registradas antes do analyzer existir
engine.register([...]);
// Analyzer criado depois — perdeu os eventos
const analyzer = createActionAnalyzer({ engine });
const result = analyzer.sync(); // Le tudo: registry completo
// result: AnalysisResult
// Wire pra eventos futuros
engine.on("register", (e) => analyzer.processRegistration(e));
// ...sync() é idempotente. Chamar duas vezes reconstrói do zero com resultado idêntico. Útil como escape hatch.
dispose()
Limpa estado interno (cache do validator, grafo). Chamadas subsequentes sao no-op.
// Cancelar listeners primeiro
unsubs.forEach((fn) => fn());
// Cleanup do analyzer
analyzer.dispose();Referência de Configuração
interface ActionAnalyzerConfig<TCtx = unknown> {
/** Engine pra observar (read-only). Obrigatorio. */
engine: IAnalyzableEngine<TCtx>;
/**
* Grafo de dependências.
* Se fornecido: usa direto (consumer controla propagators).
* Se omitido: cria internamente com DEFAULT_PROPAGATORS.
*/
graph?: DependencyGraph;
/**
* Propagators pro grafo.
* So usado se `graph` não foi fornecido.
* Default: DEFAULT_PROPAGATORS (capabilities, leaf, maxDepth).
*/
propagators?: Record<string, Propagator<any>>;
/**
* Manifest de composition control (controle de dependências arquiteturais).
* Se omitido: sem composition control (zero overhead).
* Pode ser alterado em runtime via setAccessManifest().
*/
accessManifest?: AccessManifest;
}| Config | Comportamento |
|--------|---------------|
| graph fornecido | Usa direto. Consumer controla propagators. |
| graph omitido, propagators fornecido | Cria graph com propagators fornecidos. |
| graph e propagators omitidos | Cria graph com DEFAULT_PROPAGATORS. |
Exports
// Types: Analyzer
IActionAnalyzer, ActionAnalyzerConfig, IAnalyzableEngine,
AnalysisResult, AnalysisWarning, AnalysisConflict,
UnregistrationResult
// Types: Lifecycle Events
RegisterEvent, UnregisterEvent
// Factory
createActionAnalyzer
// Analysis (pure function)
analyzeAction
// Composition Control (code still uses "access" naming)
AccessValidator, AccessOrchestrator,
AccessOrchestratorDeps, TagMatcher, NodeMatcher,
AccessRule, AccessManifest, AccessViolation, AccessValidationResult
// Composition Matching (pure functions — advanced usage / testing)
compilePattern, compileTagMatcher, compileNodeMatcher,
compileRule, compileManifestLicença
MIT
Para arquitetura interna, detalhes de implementacao e decisoes de design, veja ARCHITECTURE.md.
