@mnemonica/tactica 0.3.10 → 0.3.11

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
@@ -805,7 +805,7 @@ When enabled, tactica detects execution-flow patterns alongside type definitions
805
805
  - `scopeId` — the scopeId of the scope holding the wrap call site (fallback join).
806
806
  - `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).
807
807
  - `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.
808
- - `scope` — the enclosing mnemonica type path. Lexical scoping (the owning `define()`/`lazy()` handler or `@decorate()`-ed class) wins; a wrap site outside any handler — the fire-and-forget-wrapper shape — is attributed through its instance/context argument (a tracked assignment like `const holder = new Holder(…)`, the enclosing function's parameter annotation resolved through the graph law, or a `let`/`var`/`const` declaration with an EXPLICIT type annotation like `let updateCommitted: LedgerUpdate;` — declaration-site typing only, no flow analysis), and nested sites (function-valued returns, lexically nested wraps) inherit the causing wrap site's scope down the `via` chain. An UNANNOTATED `let`/`var` still buckets under `unknown` (the analyzer does not track assignments flow-sensitively) — a `const` with an analyzable initializer stays the recommended discipline. Sites attributable to nothing stay under the `unknown` key with `scope` absent.
808
+ - `scope` — the enclosing mnemonica type path. Lexical scoping (the owning `define()`/`lazy()` handler or `@decorate()`-ed class) wins; a wrap site outside any handler — the fire-and-forget-wrapper shape — is attributed through its instance/context argument (a tracked assignment like `const holder = new Holder(…)`, the enclosing function's parameter annotation resolved through the graph law, or a `let`/`var`/`const` declaration with an EXPLICIT type annotation like `let updateCommitted: LedgerUpdate;` — declaration-site typing only, no flow analysis; the annotation may be the type name or the generated nested-type alias (`UpdatePay_SomeTerminal`, mapped by the underscore→dotted naming law)); the wrap CALLEE identifier also follows a `let`/`var` binding to its first statically-visible in-scope assignment — the catch-guard pattern (`let fn; try { fn = new T(…) } catch { return } wrap(fn, …)`) attributes as `T`, while closure-only assignments and never-assigned bindings stay `unknown` (function/class boundaries are not crossed). One discipline covers both wrap arguments: assignment evidence beats an explicit annotation claim (`let fn: T`, or a wrapper arriving as an annotated parameter) beats `unknown` — the annotation pins the type only when the assignment is not resolvable (a userland call return, say). Nested sites (function-valued returns, lexically nested wraps) inherit the causing wrap site's scope down the `via` chain. An UNANNOTATED `let`/`var` still buckets under `unknown` (the analyzer does not track assignments flow-sensitively) — a `const` with an analyzable initializer stays the recommended discipline. Sites attributable to nothing stay under the `unknown` key with `scope` absent.
809
809
 
810
810
  ## Instrumentation Points
811
811
 
@@ -0,0 +1,109 @@
1
+ # Consuming tactica Output: The lookup Pattern
2
+
3
+ > **For application developers using tactica-generated types.**
4
+ > If you generated `.tactica/types.ts` and `.tactica/registry.ts` but still write `as unknown as` casts, read this.
5
+
6
+ ---
7
+
8
+ ## The Mistake Everyone Makes
9
+
10
+ You run tactica. It generates the files. You import the types. You still cast.
11
+
12
+ ```typescript
13
+ // ❌ WRONG — you generated the types but didn't use them properly
14
+ import { RequestData } from './collections/requestTypes.js';
15
+ import type { RequestData as RequestDataT } from '../../.tactica/types.js';
16
+
17
+ const requestData = new RequestData({ ... }) as unknown as RequestDataT;
18
+ ```
19
+
20
+ This is the most common failure mode. The types exist. The registry exists. But you're bridging them with a cast instead of using `lookup`.
21
+
22
+ ---
23
+
24
+ ## The Correct Way
25
+
26
+ ```typescript
27
+ // ✅ CORRECT — lookup uses the TypeRegistry augmentation
28
+ import { lookup } from 'mnemonica';
29
+
30
+ const RequestData = lookup('RequestData');
31
+ const requestData = new RequestData({ ... });
32
+ const routeData = new requestData.RouteData({ ... });
33
+ ```
34
+
35
+ `lookup('RequestData')` returns the constructor **typed through the registry**. TypeScript knows:
36
+ - The constructor signature
37
+ - The instance properties
38
+ - That the instance has `.RouteData`, `.PageData`, etc.
39
+
40
+ No cast needed. The registry does the work.
41
+
42
+ ---
43
+
44
+ ## Why Importing the Type Is Not Enough
45
+
46
+ `tactica` generates two files:
47
+
48
+ 1. `.tactica/types.ts` — exports like `export type RequestData = {...}`
49
+ 2. `.tactica/registry.ts` — augments `mnemonica.TypeRegistry`
50
+
51
+ Importing from `types.ts` gives you the **instance shape**. It does NOT give you the **constructor type** with sub-constructors.
52
+
53
+ The registry augmentation is what teaches TypeScript that `lookup('RequestData')` returns a constructor whose instances have `.RouteData`. Without `lookup`, you're just importing shapes and casting between them.
54
+
55
+ ---
56
+
57
+ ## Module-Level Usage
58
+
59
+ `lookup` is a runtime lookup, but it's safe to call at module level:
60
+
61
+ ```typescript
62
+ // ✅ Safe and correct
63
+ const RequestData = lookup('RequestData');
64
+ const EngineRequest = lookup('EngineRequest');
65
+
66
+ app.get('/', async () => {
67
+ const requestData = new RequestData({ ... });
68
+ });
69
+ ```
70
+
71
+ The returned constructor is the same object every time. There is no per-request overhead.
72
+
73
+ ---
74
+
75
+ ## For Decoration (Fastify, Express, etc.)
76
+
77
+ Use the same `lookup` result for decoration:
78
+
79
+ ```typescript
80
+ const RequestData = lookup('RequestData');
81
+
82
+ app.decorate('RequestData', RequestData);
83
+
84
+ // Later in a route handler:
85
+ const requestData = new RequestData({ ... });
86
+ ```
87
+
88
+ No need to import the raw constructor separately. `lookup` gives you the runtime object AND the type.
89
+
90
+ ---
91
+
92
+ ## Checklist
93
+
94
+ After running `tactica`:
95
+
96
+ - [ ] `.tactica/` directory exists with `types.ts` and `registry.ts`
97
+ - [ ] `tsconfig.json` includes `.tactica/**/*` in `include`
98
+ - [ ] You use `lookup('TypeName')` instead of direct imports
99
+ - [ ] You do NOT write `as unknown as` with mnemonica types
100
+ - [ ] You run `tsc --noEmit` to verify types resolve
101
+
102
+ If any box is unchecked, the integration is incomplete.
103
+
104
+ ---
105
+
106
+ ## See Also
107
+
108
+ - **Canonical typing guide:** [`core/docs/typed-lookup.md`](https://github.com/wentout/mnemonica/blob/master/docs/typed-lookup.md) — builder mode, the one-line `RegistryOf` bridge, and the augmented mode this repo generates for
109
+ - **Library landing:** [`core/README.md`](https://github.com/wentout/mnemonica) — the thesis, the four data mistakes, the three type-system paths
package/lib/analyzer.d.ts CHANGED
@@ -725,6 +725,40 @@ export declare class MnemonicaAnalyzer {
725
725
  * Resolve type from EDS call argument (best effort)
726
726
  */
727
727
  private resolveEDSArgumentType;
728
+ /**
729
+ * let-in-try: find the RIGHT-HAND SIDE of the first statically-visible
730
+ * assignment to `name` in the scope that declares it. The declaring
731
+ * container is found innermost-out (blocks, case clauses, the source
732
+ * file — the F20 walk); the scan recurses into nested blocks (try/
733
+ * catch/finally, if/else, loops, switch cases) but NEVER crosses
734
+ * function or class boundaries — an assignment inside a closure does
735
+ * not attribute. Returns undefined when the binding is declared but
736
+ * never assigned in scope (and stops there: an inner declaration
737
+ * shadows any outer binding).
738
+ */
739
+ private followScopeAssignment;
740
+ /**
741
+ * True when the statement list contains a `let`/`var`/`const`
742
+ * declaration for `name` (any initializer form).
743
+ */
744
+ private statementsDeclareVariable;
745
+ /**
746
+ * First `name = rhs` assignment in the statement list, recursing
747
+ * into nested in-scope blocks. Function and class bodies are
748
+ * boundaries and are not entered.
749
+ */
750
+ private findAssignmentRhsInStatements;
751
+ /**
752
+ * `name = rhs` as a direct expression statement.
753
+ */
754
+ private directAssignmentRhs;
755
+ /**
756
+ * Statement lists of the nested blocks that stay INSIDE the current
757
+ * scope — try/catch/finally, if/else, loops, switch cases, nested
758
+ * blocks, labeled statements. Function-like and class bodies are
759
+ * scope boundaries and yield nothing.
760
+ */
761
+ private nestedScopeBlocks;
728
762
  /**
729
763
  * Resolve the enclosing mnemonica scope of an EDS call site by walking
730
764
  * up the parent chain: nearest define()/lazy() call whose handler holds
@@ -742,6 +776,17 @@ export declare class MnemonicaAnalyzer {
742
776
  * metadata heuristic, not the identity-law surface.
743
777
  */
744
778
  private resolveWrapInstanceTypePath;
779
+ /**
780
+ * F24: resolve a bare-identifier annotation to a graph fullPath. The
781
+ * annotation may name the type directly (`LedgerUpdate`) or carry
782
+ * the GENERATED instance alias of a nested type
783
+ * (`UpdatePay_SomeTerminal`, imported from the generated types file
784
+ * via tsconfig paths) — not a graph node NAME. The name is tried
785
+ * as-is first, then its underscore→dotted form (the generated alias
786
+ * naming law; the same mapping scopes.json uses for annotations).
787
+ * Ambiguity and absence yield undefined.
788
+ */
789
+ private resolveAnnotationTypePath;
745
790
  /**
746
791
  * Resolve a bare-identifier type annotation of the nearest enclosing
747
792
  * function's parameter through the mnemonica-graph tiers (value scope,