@statedelta-actions/analyzer
v0.1.0
Published
Static analysis layer for ActionEngine — capabilities, dependency graph, access control
Downloads
50
Readme
@statedelta-actions/analyzer
Camada de analise estatica para o ActionEngine. Graph queries, deteccao de ciclos, inferencia de capabilities, composition control e deteccao de conflitos de declaracao.
Filosofia
O engine e o V8. O analyzer e o TypeScript.
O ActionEngine e runtime puro: valida, armazena, compila, executa. Nao sabe o que "capabilities", "ciclos" ou "access control" significam.
O ActionAnalyzer e o type checker externo: observa o engine (read-only), extrai fatos, valida contratos e expoe consultas. Nunca muta o engine.
Tres modos de operacao:
| Modo | O que usa | Analogia |
|------|-----------|----------|
| JS mode | Engine only | JavaScript puro — sem checagem |
| TS mode | Engine + Analyzer | TypeScript — analise estatica, graph queries |
| TS strict | Engine + Analyzer + Composition manifest | strict: true — analise + composition control + contratos |
O consumer decide o nivel de analise. O engine funciona sozinho. O analyzer e opt-in.
Instalacao
pnpm add @statedelta-actions/analyzerPeer dependencies: @statedelta-actions/core, @statedelta-actions/graph, @statedelta-actions/actions.
Inicio Rapido
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();Tres Modos de Operacao
JS mode — Engine only
const engine = createActionEngine({ handlers });
engine.register([...]);
engine.invoke("myAction", ctx);
// Sem analise. Runtime puro. Zero overhead.Funciona: registration, invoke, JIT. Nao 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: [
{
source: { tags: { include: ["write"] } },
target: { tags: { include: ["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 nao auto-subscribe. O consumer decide a estrategia de wiring (direto, batched, tick-boundary).
Dois lifecycle events:
| Evento | Metodo do Analyzer | Quando o engine emite |
|--------|-------------------|----------------------|
| "register" | processRegistration(e) | Apos register() ou endBatch(), se houve registros |
| "unregister" | processUnregistration(e) | Apos 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. Multiplos listeners causam processamento duplicado.
Graph Queries
Todas delegam diretamente pro DependencyGraph. Zero logica extra.
| Metodo | Retorno | Descricao |
|--------|---------|-----------|
| 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> | Dependencias 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 | Estatisticas 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 dependencias 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.
const analyzer = createActionAnalyzer({
engine,
accessManifest: {
rules: [
{
source: { tags: { include: ["write"] } },
target: { ids: ["protected-*"] },
effect: "deny",
},
],
},
});Violacoes aparecem no AnalysisResult.accessViolations. A action e registrada — o consumer decide a politica.
setAccessManifest — Manifest mutavel
// Aplica novo manifest em runtime (recompila + revalida tudo)
const result = analyzer.setAccessManifest({
rules: [{ source: { tags: { include: ["read"] } }, target: { tags: { include: ["write"] } }, effect: "deny" }],
});
result.accessViolations; // Violacoes das edges existentes
// Desativa composition control (zero overhead)
analyzer.setAccessManifest(null);Para detalhes de TagMatcher, NodeMatcher, AccessRule, pipeline de compilacao e semantica de matching, veja ARCHITECTURE.md.
Nota sobre nomenclatura: O codigo usa
accessManifest,AccessRule,AccessViolationetc. Conceitualmente, isso e composition control — controle de dependencias arquiteturais (quem pode depender de quem), nao controle de acesso de usuario. ACL real (RBAC, autenticacao) e extensao futura, completamente separada. Ver ARCHITECTURE-VISION.md.
Tier Validation
Controle hierarquico por nivel numerico. Actions com tier declarado participam de uma hierarquia: action com tier menor nao 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: "..." }]Semantica
| 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 nao participam da validacao. Tier e declaracao explicita de intencao — 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-arvore | A declara 100, sub-arvore exige 500 |
Edge validation pega violacoes diretas. DECLARATION_CONFLICT pega violacoes transitivas (A → B → C onde C exige tier alto). Juntos cobrem todos os cenarios.
Consulta de tier
// Tier inferido (max da sub-arvore, incluindo declarado proprio)
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 (nao 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 nao invoca (direta ou transitivamente) actions que exijam tier > 300.
sync() — Hot-Plug
O analyzer pode ser criado depois de actions ja registradas. sync() le o registry inteiro do engine e reconstroi a analise 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() e idempotente. Chamar duas vezes reconstroi do zero com resultado identico. Util 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();Referencia de Configuracao
interface ActionAnalyzerConfig<TCtx = unknown> {
/** Engine pra observar (read-only). Obrigatorio. */
engine: IAnalyzableEngine<TCtx>;
/**
* Grafo de dependencias.
* Se fornecido: usa direto (consumer controla propagators).
* Se omitido: cria internamente com DEFAULT_PROPAGATORS.
*/
graph?: DependencyGraph;
/**
* Propagators pro grafo.
* So usado se `graph` nao foi fornecido.
* Default: DEFAULT_PROPAGATORS (capabilities, leaf, maxDepth).
*/
propagators?: Record<string, Propagator<any>>;
/**
* Manifest de composition control (controle de dependencias 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, compileManifestLicenca
MIT
Para arquitetura interna, detalhes de implementacao e decisoes de design, veja ARCHITECTURE.md.
