@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 +1 -1
- package/docs/lookup-pattern.md +109 -0
- package/lib/analyzer.d.ts +45 -0
- package/lib/analyzer.js +209 -15
- package/package.json +2 -1
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
|
|
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,
|