@mnemonica/tactica 0.1.15 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -134,8 +134,11 @@ Tactica writes everything under the `--output` directory (default `.tactica/`):
134
134
  | `index.ts` | default mode | Re-exports everything from `types.ts` and `registry.ts`. |
135
135
  | `index.d.ts` | with `--module-augmentation` | Single global-augmentation file (legacy mode). |
136
136
  | `definitions.json` | always | One entry per discovered type: `{ name, location, kind: 'define'\|'decorate', parent, strictChain, blockErrors }`. Consumed by `mnemographica`'s Go to Definition. |
137
- | `usages.json` | always | One entry per type, value is an array of `{ location, kind, code }` records — where each type is instantiated, referenced, accessed, or looked up. Consumed by `mnemographica`'s Find References. |
137
+ | `usages.json` | always | One entry per type, value is an array of `{ location, kind, code, holderScopeId?, constructorText? }` records — where each type is instantiated, referenced, accessed, or looked up. `holderScopeId` points into `scopes.json` (the innermost scope holding the usage); `constructorText` (instantiations only) is the constructor expression text actually used. Consumed by `mnemographica`'s Find References. |
138
138
  | `flow.json` | always | Native instance-flow patterns (property reads/writes, method calls, destructuring, returns, spreads, etc.) per type. |
139
+ | `instrumentation.json` | always | v2 envelope. `points`: framework lifecycle crossroads (interceptors, guards, pipes, filters, middleware) detected via **plugin-supplied vocabulary** — heritage declarations, decorator sites, provider-token registrations, `consumer.apply()` wiring. Syntactic only — no dive dependency; with no plugins loaded, `points` is `[]`. `creationGraph`: the inside-out walk from every instantiation site out to the starters — see "Creation graph" below. |
140
+ | `modules.json` | always | Module-scope graph: every module's `exportedBindings`/`importedBindings` (functions, classes, consts, types — not only mnemonica types), project-internal `dependencies`, `builtinSpecifiers` (Node builtins are skipped entirely — both `'path'` and `'node:path'` forms), `unresolvedSpecifiers`, circular-import `cycles`, and cross-module mnemonica-type `edges`. Resolution uses `ts.resolveModuleName` with the project's compilerOptions (tsconfig `paths`, extensionless imports, index files) — no type checker. Bindings resolved into `node_modules` are marked `external: true` and never enter `dependencies`. |
141
+ | `scopes.json` | always | Local-scope graph: function/method/arrow scopes only (no block scopes) plus one module scope per file; variables with `typePath` (mnemonica type when known), `isParameter`, `isMutable`, and `reassignments` — each reassignment of a mutable binding is a flow-termination point. |
139
142
  | `eds.json` | when EDS enabled | Execution-flow patterns (`wrap`, `current`, `getFlow`, `attachHooks` lifecycle wiring). Consumed by tools that visualize execution chains. |
140
143
 
141
144
  ### Default mode (types.ts + registry.ts)
@@ -474,7 +477,7 @@ When inference fails the property's type falls back to `unknown` (or `any` in so
474
477
 
475
478
  ```ts
476
479
  class MnemonicaAnalyzer {
477
- constructor(program?: ts.Program);
480
+ constructor(program?: ts.Program, plugins?: TacticaPlugin[]);
478
481
 
479
482
  analyzeFile(sourceFile: ts.SourceFile): AnalyzeResult;
480
483
  analyzeSource(sourceCode: string, fileName?: string): AnalyzeResult;
@@ -487,6 +490,7 @@ class MnemonicaAnalyzer {
487
490
  getUsages(): Map<string, UsageInfo[]>;
488
491
  getEDSUsages(): Map<string, EDSInfo[]>;
489
492
  getFlowUsages(): Map<string, FlowInfo[]>;
493
+ getInstrumentationPoints(): InstrumentationPoint[];
490
494
  }
491
495
  ```
492
496
 
@@ -534,6 +538,61 @@ class TypesGenerator {
534
538
  }
535
539
  ```
536
540
 
541
+ ### `ModuleGraphBuilder`
542
+
543
+ Module-scope walker. Tracks every export/import binding of every module —
544
+ generically, not only mnemonica types — and resolves specifiers with
545
+ `ts.resolveModuleName` driven by the program's compilerOptions
546
+ (no `getTypeChecker()`).
547
+
548
+ ```ts
549
+ class ModuleGraphBuilder {
550
+ constructor(program?: ts.Program, compilerOptions?: ts.CompilerOptions);
551
+
552
+ addFile(sourceFile: ts.SourceFile): ModuleInfo; // call per file (definitions pass)
553
+ build(definedTypesByFile?: Map<string, string[]>): ModuleGraph;
554
+ }
555
+ ```
556
+
557
+ `ModuleGraph` = `{ modules: Map<absPath, ModuleInfo>, edges: CrossModuleUsage[], cycles: string[][] }`.
558
+ Each `ModuleBinding` carries `{ name, kind: 'function'|'class'|'const'|'type'|'unknown',
559
+ sourceModule (resolved absolute path), importKind?, importAlias?, isReExport, external? }`.
560
+ Node.js builtins (`'path'`, `'node:fs'`, …) are skipped entirely and recorded
561
+ only in the module's `builtinSpecifiers`; bindings resolved into `node_modules`
562
+ carry `external: true` and never enter `dependencies`. Re-export chains
563
+ (barrels, `export * from`) are chased to the origin module when producing
564
+ `edges`; circular imports are recorded in `cycles`, never treated as errors.
565
+ Unused-export detection is internal only — nothing about it is emitted.
566
+
567
+ ### `LocalScopeWalker`
568
+
569
+ Local-scope walker. Tracks function/method/arrow scopes only (no block
570
+ scopes), plus one synthetic module scope per file. Variables carry
571
+ `isParameter`, `isMutable` (const vs let/var; `this` parameters are
572
+ immutable), and `reassignments` — every reassignment site of a mutable
573
+ binding is a flow-termination point where downstream walking stops.
574
+
575
+ ```ts
576
+ class LocalScopeWalker {
577
+ addFile(sourceFile: ts.SourceFile): void; // call per file
578
+ build(resolver?: ScopeTypeResolver): ScopeAnalysis;
579
+ findHolderScopeId(location: string): string | undefined;
580
+
581
+ static attachHolderScopeIds(usages: Map<string, UsageInfo[]>, walker: LocalScopeWalker): void;
582
+ }
583
+
584
+ interface ScopeTypeResolver {
585
+ resolveByName(name: string): string | undefined; // bare name → mnemonica fullPath
586
+ hasPath(fullPath: string): boolean;
587
+ }
588
+ ```
589
+
590
+ `ScopeAnalysis` = `{ scopes: Map<scopeId, ScopeInfo>, variables: Map<'scopeId#name', ScopeVariable> }`.
591
+ Scope ids: module scope = file path; function scopes = `file:line:col`.
592
+ Labels: functions by name, methods as `Class.method`, arrows by the binding
593
+ they are assigned to, anonymous holders as `file:line`. Never relies on
594
+ `node.parent` (program files can be unbound).
595
+
537
596
  ### `TypesWriter`
538
597
 
539
598
  ```ts
@@ -548,6 +607,9 @@ class TypesWriter {
548
607
  writeUsagesFile (map: Map<string, UsageInfo[]>): string; // → outputDir/usages.json
549
608
  writeEDSFile (map: Map<string, EDSInfo[]>): string; // → outputDir/eds.json
550
609
  writeFlowFile (map: Map<string, FlowInfo[]>): string; // → outputDir/flow.json
610
+ writeInstrumentationFile(points: InstrumentationPoint[], creationGraph?: CreationGraph): string; // → outputDir/instrumentation.json
611
+ writeModulesFile (graph: ModuleGraph): string; // → outputDir/modules.json
612
+ writeScopesFile (analysis: ScopeAnalysis): string; // → outputDir/scopes.json
551
613
 
552
614
  write(generated: GeneratedTypes): string; // legacy alias for writeTypesFile
553
615
  clean(): void;
@@ -557,7 +619,7 @@ class TypesWriter {
557
619
 
558
620
  ### Types
559
621
 
560
- `TacticaConfig`, `TypeNode`, `TypeGraph`, `PropertyInfo`, `ConstructorParamInfo`, `AnalyzeResult`, `AnalyzeError`, `GeneratedTypes`, `DefinitionInfo`, `UsageInfo`, `UsagesJson`, `DefinitionsJson`, `EDSInfo`, `EDSJson`, `EDSKind`, `FlowInfo`, `FlowJson`, `FlowKind` — all exported from `@mnemonica/tactica`. See [`src/types.ts`](src/types.ts) for the full schema.
622
+ `TacticaConfig`, `TypeNode`, `TypeGraph`, `PropertyInfo`, `ConstructorParamInfo`, `AnalyzeResult`, `AnalyzeError`, `GeneratedTypes`, `DefinitionInfo`, `UsageInfo`, `UsagesJson`, `DefinitionsJson`, `EDSInfo`, `EDSJson`, `EDSKind`, `FlowInfo`, `FlowJson`, `FlowKind`, `InstrumentationKind`, `InstrumentationScope`, `InstrumentationPoint`, `InstrumentationJson`, `ModuleBindingKind`, `ModuleImportKind`, `ModuleBinding`, `ModuleInfo`, `CrossModuleUsage`, `ModuleGraph`, `ModulesJson`, `ScopeKind`, `ScopeInfo`, `ScopeVariable`, `ScopeAnalysis`, `ScopesJson`, `CreationGraphNode`, `CreationGraphEdge`, `CreationAnchor`, `CreationGraph`, `TacticaPlugin`, `InstrumentationVocabulary` — all exported from `@mnemonica/tactica`. See [`src/types.ts`](src/types.ts) for the full schema. The `mergeTacticaPlugins(plugins)` helper merges plugin vocabulary the same way the analyzer does.
561
623
 
562
624
  ## EDS (Execution Data Storage) Tracking
563
625
 
@@ -567,7 +629,7 @@ When enabled, tactica detects execution-flow patterns alongside type definitions
567
629
  |---|---|---|
568
630
  | `wrap(fn)`, `wrapConstructorArg(fn, parent)`, `upgradeConstructorArg(arg, inst)`, `wrapInstanceMethods(obj)` | `wrap` | Wrap a function / constructor argument / instance methods for context propagation and tracing (`@mnemonica/dive`) |
569
631
  | `current()`, `getErrorInstance(err)`, `getFlow(target?)` | `contextConsume` | Read the current context, the error-pinned instance, or the recorded flow (`@mnemonica/dive`) |
570
- | `attachHooks(collection)` | `hookAttach` | Wire a TypesCollection to dive's lifecycle tracing (`@mnemonica/nestjs`) |
632
+ | `attachHooks(collection)` | `hookAttach` | Wire a TypesCollection to dive's lifecycle tracing (`@mnemonica/otel`) |
571
633
 
572
634
  `eds.json` structure:
573
635
 
@@ -588,12 +650,110 @@ When enabled, tactica detects execution-flow patterns alongside type definitions
588
650
  }
589
651
  ```
590
652
 
653
+ `wrap` entries additionally carry graph-join fields (all optional, additive):
654
+
655
+ - `label` — the label string literal of `wrap(fn, …, 'label')` when statically visible.
656
+ - `callbackScopeId` — the scopeId (scopes.json) of the wrapped callback's own scope; the callback is what the wrapper runs, so graph consumers join on it first.
657
+ - `instanceArg` — the identifier passed as the instance/context argument (`user` in `wrap(fn, user)`), when it is one.
658
+ - `scopeId` — the scopeId of the scope holding the wrap call site (fallback join).
659
+ - `wrapsTypePath` — the mnemonica fullPath of the instance argument, resolved through the scope-variable chain (innermost binding wins; an untyped local shadows a typed outer one rather than being guessed).
660
+ - `via` — the location of the enclosing wrap site when the call is nested inside another wrapped body (or returns a function): the wrappers-graph generation chain is built from it.
661
+
662
+ ## Instrumentation Points
663
+
664
+ Tactica statically detects framework lifecycle crossroads — interceptors, guards, pipes, exception filters, middleware — purely syntactically (no type checker), and always emits `.tactica/instrumentation.json`. **Tactica core ships no framework vocabulary**: detection is driven entirely by plugins, so with no plugins loaded `points` is `[]`. Framework adapters ship a plugin; projects enable it through a `.tactica.js` (or `tactica.config.js`) config file next to `tsconfig.json`:
665
+
666
+ ```js
667
+ // .tactica.js
668
+ module.exports = {
669
+ plugins: [ '@mnemonica/example-adapter/tactica' ],
670
+ };
671
+ ```
672
+
673
+ Plugin entries are module specifiers (required relative to the config file) or inline plugin objects. A plugin is a plain object:
674
+
675
+ ```ts
676
+ interface TacticaPlugin {
677
+ name?: string;
678
+ // `implements X` heritage matches: interface identifier -> kind
679
+ instrumentationInterfaces?: Record<string, InstrumentationKind>;
680
+ // `@X(Impl)` decorator sites: decorator identifier -> kind
681
+ useDecorators?: Record<string, InstrumentationKind>;
682
+ // `{ provide: TOKEN, useClass: Impl }` registrations: token identifier -> kind
683
+ appTokens?: Record<string, InstrumentationKind>;
684
+ // opt in to shape-based `consumer.apply(Mw).forRoutes(...)` detection
685
+ middlewareWiring?: boolean;
686
+ }
687
+ ```
688
+
689
+ Programmatic callers pass plugins directly: `new MnemonicaAnalyzer(program, plugins)` or `run({ …, plugins })`; config-file plugins append after programmatic ones, and later plugins override earlier ones on the same identifier key. Detection covers:
690
+
691
+ - **Heritage** — `class X implements <plugin-listed interface>` (matched by interface identifier name). Bare declarations carry scope `module` (attachment statically unknown).
692
+ - **Decorator sites** — plugin-listed decorators on classes or methods, including inline instances (`@Register(new Impl(…))`). One point per referenced class; scope is `controller:<Name>` or `method:<Class>.<method>`.
693
+ - **Global providers** — `{ provide: <plugin-listed token>, useClass: X }` object literals → scope `global`. `useExisting`/`useFactory` without `useClass` are skipped.
694
+ - **Middleware wiring** — `consumer.apply(Mw).forRoutes(...)` inside a class's `configure()` method → scope `module`, targets from `forRoutes` arguments when statically readable. Requires `middlewareWiring: true` from any loaded plugin.
695
+
696
+ When a referenced class is declared in the analyzed project, the point's `location`/`code` resolve to the class declaration; external classes keep the registration site. Points are deduped by `(kind, className, location, scope)` with `targets` merged — a class seen via both heritage and a decorator yields separate entries per scope, with the bare declaration carrying scope `module`.
697
+
698
+ `instrumentation.json` structure (v2):
699
+
700
+ ```json
701
+ {
702
+ "version": 2,
703
+ "generatedAt": "2026-09-04T…",
704
+ "points": [
705
+ {
706
+ "kind": "pipe",
707
+ "className": "PayloadPipe",
708
+ "location": "/project/src/user.controller.ts:49:2",
709
+ "code": "@RegisterPipe(new PayloadPipe({ transform: true }))",
710
+ "scope": "method:UserController.createUser",
711
+ "targets": ["UserController"]
712
+ }
713
+ ],
714
+ "creationGraph": {
715
+ "nodes": [
716
+ {
717
+ "scopeId": "/project/src/main.ts",
718
+ "name": "/project/src/main.ts",
719
+ "kind": "module",
720
+ "filePath": "/project/src/main.ts",
721
+ "location": "/project/src/main.ts:1:1",
722
+ "starter": true
723
+ }
724
+ ],
725
+ "edges": [
726
+ { "caller": "/project/src/main.ts", "callee": "/project/src/user.service.ts:26:2" }
727
+ ],
728
+ "anchors": [
729
+ {
730
+ "location": "/project/src/user.service.ts:29:24",
731
+ "holderScopeId": "/project/src/user.service.ts:26:2",
732
+ "typePath": "UserEntity.UserResponse",
733
+ "constructorText": "user.UserResponse",
734
+ "variable": "userResponse"
735
+ }
736
+ ]
737
+ }
738
+ }
739
+ ```
740
+
741
+ ## Creation graph (inside-out walk)
742
+
743
+ The `creationGraph` key is the third phase of the instrumentation walker: it starts at the certain points — every `usages.json` `instantiation` entry (each carries a `holderScopeId` into `scopes.json`) — and walks **outward**: who invokes or references the holder function, crossing files through `modules.json` (re-export barrels chased to the origin module), until no callers remain. Terminals are the **starters** (application entry points); the walk never assumes a linear type Trie (mnemonica `strictChain: false` permits cycles and out-of-order construction), so traversal is cycle-guarded and every anchor records the constructor expression actually used.
744
+
745
+ - **Nodes** are scopes (`module` / `function` / `method` / `arrow`), labeled like `scopes.json`; `starter: true` marks nodes with no discovered callers.
746
+ - **Edges** point `caller → callee`, where the callee is closer to the creation site.
747
+ - **Anchors** pin each creation site to its holder scope: `typePath`, the `constructorText` used, `rooted: true` for module-scope creations (a legitimate root or a developer error — labeled, not policed), and a `variable`/`terminatedAt` pair from a documented heuristic: the variable declared in the holder scope on the same line as the creation (typePath must match when known), with `terminatedAt` being that variable's first reassignment site — the flow-termination point where the walk stops following the binding.
748
+
749
+ Deliberate approximations (name-based, no type checker): namespace imports count any reference to the alias once the namespace's module exposes the holder's export; method holders bind to their **class** name (that is what callers reference); any non-declaration identifier counts as a reference. Export wiring alone (`export { f }`, `export default f`) is not a caller — an importer's usage creates the edge.
750
+
591
751
  ## How It Works
592
752
 
593
753
  1. **Parse** — load `tsconfig.json`, build a `ts.Program`, walk each source file's AST.
594
754
  2. **Detect** — find `define()` and `@decorate()` calls, plus `lookup` lookups, `new` expressions, and EDS / flow patterns.
595
755
  3. **Graph** — build a Trie of types in `TypeGraphImpl`, with parent links via the chain of `.define()` calls and `@decorate(Parent)` references.
596
- 4. **Generate** — emit `types.ts`, `registry.ts`, `index.ts` (default mode) or `index.d.ts` (legacy mode), plus `definitions.json`, `usages.json`, `flow.json`, and optionally `eds.json`.
756
+ 4. **Generate** — emit `types.ts`, `registry.ts`, `index.ts` (default mode) or `index.d.ts` (legacy mode), plus `definitions.json`, `usages.json` (with `holderScopeId`), `flow.json`, `instrumentation.json`, `modules.json`, `scopes.json`, `hierarchy.json`/`hierarchy.txt`, and optionally `eds.json`.
597
757
  5. **Write** — files land in the output directory (default `.tactica/`).
598
758
 
599
759
  ```
package/lib/analyzer.d.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  import * as ts from 'typescript';
2
- import { AnalyzeResult, DefinitionInfo, UsageInfo, EDSInfo, FlowInfo } from './types';
2
+ import { AnalyzeResult, DefinitionInfo, UsageInfo, EDSInfo, FlowInfo, InstrumentationPoint } from './types';
3
3
  import { TypeGraphImpl } from './graph';
4
+ import { TacticaPlugin } from './plugins';
4
5
  /**
5
6
  * AST Analyzer for finding Mnemonica define() and decorate() calls
7
+ *
8
+ * Framework-blind by construction: instrumentation detection vocabulary
9
+ * (interface names, decorator names, provider tokens, middleware wiring)
10
+ * comes entirely from plugins — with none loaded, zero points are collected.
6
11
  */
7
12
  export declare class MnemonicaAnalyzer {
8
13
  private errors;
@@ -22,7 +27,10 @@ export declare class MnemonicaAnalyzer {
22
27
  private collectionVariables;
23
28
  private collectionInfo;
24
29
  private collectionCounter;
25
- constructor(program?: ts.Program);
30
+ private instrumentationClassDecls;
31
+ private instrumentationSites;
32
+ private instrumentationVocabulary;
33
+ constructor(program?: ts.Program, plugins?: TacticaPlugin[]);
26
34
  /**
27
35
  * Reset usage-related state for a fresh pass.
28
36
  * Call before the usage-collection pass to avoid duplicates from definition pass.
@@ -56,6 +64,17 @@ export declare class MnemonicaAnalyzer {
56
64
  * Get collected flow usages
57
65
  */
58
66
  getFlowUsages(): Map<string, FlowInfo[]>;
67
+ /**
68
+ * Get collected instrumentation points.
69
+ * Registration sites referencing a class declared in the same project
70
+ * resolve to the class declaration's location/code; external classes
71
+ * (e.g., a framework-builtin implementation from node_modules) keep
72
+ * the registration site.
73
+ * Deduped by kind+className+location+scope with targets merged — a
74
+ * class detected by heritage AND by a decorator site yields separate
75
+ * entries with distinct scopes (see InstrumentationPoint in types.ts).
76
+ */
77
+ getInstrumentationPoints(): InstrumentationPoint[];
59
78
  /**
60
79
  * Add a topologica type to the analyzer for usage tracking.
61
80
  * This allows the analyzer to recognize topologica types when collecting usages.
@@ -462,4 +481,42 @@ export declare class MnemonicaAnalyzer {
462
481
  * Extract constructor parameters from a constructor expression.
463
482
  */
464
483
  private extractConstructorParamsFromConstructor;
484
+ /**
485
+ * Collect framework instrumentation points. Purely syntactic: heritage
486
+ * clauses, decorator application sites, provider-token object literals
487
+ * and consumer.apply().forRoutes() wiring. The vocabulary comes from
488
+ * plugins; identifier text is matched as-is — no import resolution,
489
+ * the type checker stays unused.
490
+ */
491
+ private collectInstrumentation;
492
+ /**
493
+ * Record a named class declaration for instrumentation site resolution
494
+ * and detect heritage-based kinds (`implements <plugin interface>`)
495
+ */
496
+ private collectInstrumentationClass;
497
+ /**
498
+ * Detect decorator application sites: plugin-listed decorators applied
499
+ * with class arguments on a class or one of its methods. One site per
500
+ * referenced class identifier.
501
+ */
502
+ private collectInstrumentationDecorator;
503
+ /**
504
+ * Detect global registrations: object literals shaped like
505
+ * `{ provide: <plugin-listed token>, useClass: X }`.
506
+ * useExisting/useFactory without a useClass identifier are not
507
+ * statically obvious — skipped rather than guessed.
508
+ */
509
+ private collectInstrumentationProvider;
510
+ /**
511
+ * Detect middleware wiring: `consumer.apply(Mw1, Mw2).forRoutes(...)`
512
+ * inside a class's configure() method. Targets come from forRoutes
513
+ * arguments when statically readable (string routes or controller
514
+ * identifiers), else []. Shape-based, so a plugin must opt in via
515
+ * `middlewareWiring: true`.
516
+ */
517
+ private collectInstrumentationMiddleware;
518
+ /**
519
+ * Walk up the parent chain looking for an enclosing configure() method
520
+ */
521
+ private isInsideConfigureMethod;
465
522
  }