@mnemonica/tactica 0.3.7 → 0.3.9
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 +24 -4
- package/lib/analyzer.d.ts +84 -4
- package/lib/analyzer.js +505 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,6 +169,8 @@ declare module 'mnemonica' {
|
|
|
169
169
|
|
|
170
170
|
**Recommended for new projects.** Explicit imports, better tree-shaking, no global namespace pollution.
|
|
171
171
|
|
|
172
|
+
**Instance-type inheritance.** Nested instance types flatten the WHOLE ancestor chain: each nested type is `ProtoFlat<ParentInstance, Self>` (mnemonica's `ProtoFlat` keeps every parent key except the ones `Self` overrides), and the parent instance type is itself a `ProtoFlat` over its own parent — so the ROOT's own constructor-arg fields (`uuid` in `PaymentRoot`) are directly present on every descendant instance type, at any depth, alongside intermediate ancestors' fields. If a root field appears missing on a nested type, the usual cause is a stale `.tactica` — regenerate; the composition itself is transitive by construction.
|
|
173
|
+
|
|
172
174
|
### Legacy global-augmentation mode (`--module-augmentation`)
|
|
173
175
|
|
|
174
176
|
Emits one `.tactica/index.d.ts` whose contents live inside `declare global { … }`. Reference it from your code with:
|
|
@@ -222,7 +224,9 @@ const AdminType = UserType.define('AdminType', function (this: { role: string })
|
|
|
222
224
|
});
|
|
223
225
|
```
|
|
224
226
|
|
|
225
|
-
Chained calls (`define('A').define('B')`) and nested calls via variable references (`const A = define('A', …); A.define('B', …)`) are both supported.
|
|
227
|
+
Chained calls (`define('A').define('B')`) and nested calls via variable references (`const A = define('A', …); A.define('B', …)`) are both supported. A const holding a multi-hop initializer binds the LAST hop — `define()` returns the defined type's constructor, so `const Branch = Root.define('A', …).define('B', …)` makes `Branch` denote `Root.A.B` and `Branch.define('C', …)` nests C under B, exactly as the runtime does.
|
|
228
|
+
|
|
229
|
+
**Call-site ergonomics (recommended pattern):** annotate the root constructor's `this` with the generated instance type alias — `define('Widget', function (this: TWidgetInstance, data: …) {…})`, with `TWidgetInstance` imported from the generated types file. The common form is an intersection — `type TWidgetInstance = TWidgetArgs & TGeneratedRootInstance`. Tactica tolerates the self-reference: the annotation is never emitted into the generated type (so no circularity can form), and it is **ergonomic-only** — its members are never expanded, so the root's fields must be carried by the constructor body: `Object.assign(this, args)` (the identifier form extracts every field from the named args type) or explicit `this.x = …` assignments. Property extraction falls back to the body / data parameter whenever the alias is not analyzable (it lives in the excluded `.tactica/`). With the generated `types.ts`/`registry.ts` in place, `new (lookup('Widget'))(args)` — and `new Widget(args)` where the const merges with the generated type — is directly assignable to the instance type at call sites, zero casts. Regenerate `.tactica` after adding the annotation.
|
|
226
230
|
|
|
227
231
|
### `lazy()` definitions
|
|
228
232
|
|
|
@@ -237,6 +241,18 @@ const AdminType = UserType.lazy(() => class AdminType {
|
|
|
237
241
|
|
|
238
242
|
All forms are recognized: free `lazy('Name', getter)`, method `Type.lazy(...)`, and chained `define('A').lazy('B', getter)`. The getter is followed, and the returned constructor is analyzed like a direct `define()` handler — properties and constructor parameters are extracted and emitted in `types.ts` / `registry.ts` like any other type.
|
|
239
243
|
|
|
244
|
+
### Construction shapes beyond plain `new`
|
|
245
|
+
|
|
246
|
+
These usage shapes are recognized and attributed within the existing output contract (the `instantiation` usage kind and value-scope bindings — no new output fields):
|
|
247
|
+
|
|
248
|
+
- **Chain construction** — `new Root(...).Child(...)` and the single-await form `await new Root(...).A(...).B(...)`: the chain-tip call records an `instantiation` of the tip type (`Root.A.B`), supplying the creation-graph anchor, and the result variable binds to the TIP's type, not the root's. `await` is transparent everywhere on this page — `await call(...)`, `await apply(...)`, `await entry.fork(...)`, and async `bind` recognition all work identically.
|
|
249
|
+
- **`instance.fork(...)` / `instance.clone()`** (call forms) and **`instance.clone`** (the property form — core types it `readonly clone: this`) — runtime returns `this`, so the result variable binds to the source instance's type. Fork re-runs construction (hooks fire, a distinct instance on a distinct line), so these record an **`instantiation` usage** at the site — byte-indistinguishable from `new` until the deferred mechanism-kind revision; the result binding and the generic `methodCall`/`propertyRead` flow entries stay.
|
|
250
|
+
- **`utils.merge(a, b, ...)`** (also the direct named import, and the curried `utils.fork(instance)(...)`) — the result binds to **arg 0's type**: the runtime answer is a's lineage over b's context, and a's fullPath is the honest approximation within the contract. Merge is `fork(a)` over b's context, so it records an `instantiation` for a's type too; the curried `utils.fork` records at the invocation site, and the inner `utils.fork(instance)` binding line records its own entry — one per distinct source line.
|
|
251
|
+
- **`call(entity, Ctor, ...)` / `apply(entity, Ctor, args)`** — the mnemonica construction exports (import-aware: only actual `'mnemonica'` imports or members of a tracked module-object alias match; userland `call`/`apply` never do). The Ctor argument (arg 1) records an `instantiation`, and the result variable binds to the Ctor's type (runtime `InstanceResult<Merge<E, T>>` approximated by T). **Decorated classes resolve as the Ctor** through the same graph tiers — `@decorate() class C` is a known type, so `call(parent, C)` records and binds; an undecorated plain class has no graph entry, so the call binds nothing and records no usage (never a bare name). The free-call `decorate(Class)` form is not tracked (tactica recognizes the `@decorate` decorator syntax only).
|
|
252
|
+
- **`bind(entity, Ctor)`** — records no usage (it constructs nothing); the bound variable binds to the Ctor's type. Invoking the bound function later (`f(...)`, including `await f(...)`) is not followed.
|
|
253
|
+
|
|
254
|
+
Two boundaries stay deliberate: bindings surface only for references lexically AFTER the binding statement (a binding made inside a constructor body is not yet visible to that same handler's property extraction), and the mechanism distinction (`new` vs fork vs call) is not carried in the outputs — fork/clone/merge record as plain `instantiation` entries, byte-indistinguishable from `new`, until the deferred mechanism-kind contract revision lands. Consumers counting constructions should expect fork sites among them.
|
|
255
|
+
|
|
240
256
|
### Builder pattern on the imported module object
|
|
241
257
|
|
|
242
258
|
The analyzer also recognizes the chainable `mnemonica` module object and aliases of it:
|
|
@@ -464,11 +480,15 @@ The analyzer infers property types from constructor bodies and class members. Su
|
|
|
464
480
|
| `this.x = a + b` (where `a, b: number`) | `number` |
|
|
465
481
|
| `` this.x = `${a} ${b}` `` | `string` |
|
|
466
482
|
| `this.x = data.field` | type of `data.field` from the parameter annotation |
|
|
483
|
+
| `this.x = data.field` where `data: SomeNamedType` (alias/interface/class) | type of the field from the resolved declaration — import-aware, inherited members included; a bare `this.x = data` keeps the full expanded shape |
|
|
467
484
|
| `this.x = data.field \|\| []` | type of the fallback expression |
|
|
468
485
|
| `this.x = data.field ? a : b` | type of the truthy branch |
|
|
469
|
-
| `Object.assign(this, data)` | all fields of `data`'s type annotation |
|
|
486
|
+
| `Object.assign(this, data)` | all fields of `data`'s type annotation — inline literal or named alias/interface/class, resolved import-aware (F14 machinery) |
|
|
487
|
+
| `this.x = boundVar` where `boundVar` holds a construction result (`new`/`lookup()`/chain/fork/merge/call) | the bound type's instance type (value-scope binding) — bindings surface lexically AFTER the binding statement; within the same constructor body a fresh binding is not yet visible to the handler's own extraction |
|
|
470
488
|
| async / sync constructor functions | same rules |
|
|
471
489
|
|
|
490
|
+
An existing annotation always beats a weaker inference: `unknown` (and `unknown`-bearing guesses like `Array<unknown>`) never overwrites a known type — `Record<string, unknown>` IS a known type — and an overwrite never drops an optionality modifier.
|
|
491
|
+
|
|
472
492
|
When inference fails the property's type falls back to `unknown` (or `any` in some Topologica paths) — safe, and you can refine manually.
|
|
473
493
|
|
|
474
494
|
### Referenced types in constructor signatures
|
|
@@ -784,7 +804,7 @@ When enabled, tactica detects execution-flow patterns alongside type definitions
|
|
|
784
804
|
- `scopeId` — the scopeId of the scope holding the wrap call site (fallback join).
|
|
785
805
|
- `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).
|
|
786
806
|
- `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.
|
|
787
|
-
- `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(…)`,
|
|
807
|
+
- `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.
|
|
788
808
|
|
|
789
809
|
## Instrumentation Points
|
|
790
810
|
|
|
@@ -939,7 +959,7 @@ Mnemonica accepts the `exposeInstanceMethods` option at runtime, but tactica's `
|
|
|
939
959
|
|
|
940
960
|
The analyzer does not use `ts.Program.getTypeChecker()` for resolution. Plain TypeScript references in constructor signatures resolve through the importing file's import statements first, then its local declarations, and only then a unique same-named declaration across the scanned files — so a reference whose name is genuinely absent (or lives behind unresolvable specifiers) resolves to `unknown`, while a name several of your own files declare with no import to disambiguate fails hard (see above). References to mnemonica graph types follow the path-aware lookup law described above and fail hard instead of degrading to `unknown`.
|
|
941
961
|
|
|
942
|
-
Two expansion boundaries stay name-based and deliberate: **heritage** — `extends` chains are walked through the same import-aware machinery (depth-capped, cycle-guarded), but bases that resolve to an external/node_modules declaration or to a non-identifier expression (mixin calls like `extends mixin(X)`) are not followed — their inherited fields simply stay absent, same as before the walk existed. And **`typeof` fields** — only module-level const arrays with statically visible literal elements expand to the literal union (
|
|
962
|
+
Two expansion boundaries stay name-based and deliberate: **heritage** — `extends` chains are walked through the same import-aware machinery (depth-capped, cycle-guarded), but bases that resolve to an external/node_modules declaration or to a non-identifier expression (mixin calls like `extends mixin(X)`) are not followed — their inherited fields simply stay absent, same as before the walk existed. And **`typeof` fields** — only module-level const arrays with statically visible literal elements expand to the literal union, with the index suffix consumed entirely (`typeof statusList[number]` emits exactly `'active' | 'not_active' | 'hold'`, never `'hold'[number]`). Any assertion spelling is tracked (`[…] as const`, `satisfies`, `<const>[…]`), and signed numeric literals keep their sign (`-1 | 1`). Cross-file only when the const is imported by the declaring file. Every other `typeof` source — and any indexed-access target that fails to resolve — degrades the whole expression to `unknown`: an index suffix glued onto a fallback type (`unknown[number]`) is invalid TypeScript and is never emitted.
|
|
943
963
|
|
|
944
964
|
### Same-file interface merging is last-wins
|
|
945
965
|
|
package/lib/analyzer.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export declare class MnemonicaAnalyzer {
|
|
|
22
22
|
private wrapEntryByNode;
|
|
23
23
|
private variableToTypeMap;
|
|
24
24
|
private moduleObjectVariables;
|
|
25
|
+
private mnemonicaNamedImports;
|
|
25
26
|
private createTypesCollectionVariables;
|
|
26
27
|
private collectionVariables;
|
|
27
28
|
private collectionInfo;
|
|
@@ -137,10 +138,11 @@ export declare class MnemonicaAnalyzer {
|
|
|
137
138
|
private findReferencedConstArray;
|
|
138
139
|
/**
|
|
139
140
|
* Element literal types of a tracked const array: every element must be
|
|
140
|
-
* a plain literal (optionally wrapped in `as const` / `satisfies`
|
|
141
|
-
* string, numeric
|
|
142
|
-
*
|
|
143
|
-
*
|
|
141
|
+
* a plain literal (optionally wrapped in `as const` / `satisfies` /
|
|
142
|
+
* `<const>` assertions) — string, numeric (unary `-`/`+` preserved),
|
|
143
|
+
* boolean, or null. Spreads, identifiers, and nested arrays mean the
|
|
144
|
+
* union is not statically visible and yield undefined, so the caller
|
|
145
|
+
* degrades the field to `unknown` rather than guessing.
|
|
144
146
|
*/
|
|
145
147
|
private literalTypesOfArray;
|
|
146
148
|
/**
|
|
@@ -467,6 +469,13 @@ export declare class MnemonicaAnalyzer {
|
|
|
467
469
|
* For chained calls like const X = define('A').define('B'), we map X -> A (the root type)
|
|
468
470
|
*/
|
|
469
471
|
private trackVariableAssignment;
|
|
472
|
+
/**
|
|
473
|
+
* A `.define(...)` hop wrapped by another `.define(...)` call is not
|
|
474
|
+
* the value its const ends up holding — the OUTERMOST hop of the
|
|
475
|
+
* initializer chain is (define() returns the defined type's
|
|
476
|
+
* constructor). Only the outermost hop may bind the variable.
|
|
477
|
+
*/
|
|
478
|
+
private isDeeperDefineHop;
|
|
470
479
|
/**
|
|
471
480
|
* Mirror a variable -> mnemonica fullPath binding into the per-file
|
|
472
481
|
* value-scope map (graph identity law: `typeof X` and bare references
|
|
@@ -483,6 +492,59 @@ export declare class MnemonicaAnalyzer {
|
|
|
483
492
|
* e.g., const user = new UserType() maps "user" -> "UserType"
|
|
484
493
|
*/
|
|
485
494
|
private trackNewAssignment;
|
|
495
|
+
/**
|
|
496
|
+
* Bind the nearest enclosing `const/let/var X = …` to a mnemonica
|
|
497
|
+
* fullPath — the shared result-variable walker behind new/lookup/
|
|
498
|
+
* chain/fork/merge/call tracking (value scope: downstream references
|
|
499
|
+
* and `this.x = x` assignments resolve through the same binding).
|
|
500
|
+
*/
|
|
501
|
+
private bindResultVariable;
|
|
502
|
+
/**
|
|
503
|
+
* Record an `instantiation` usage for a construction-shape call
|
|
504
|
+
* (chain tip / call / apply / fork / clone / merge —
|
|
505
|
+
* byte-indistinguishable from `new` until the deferred
|
|
506
|
+
* mechanism-kind revision). `constructorText` defaults to the callee
|
|
507
|
+
* expression text so the site stays readable without new fields;
|
|
508
|
+
* call/apply override it with the Ctor argument text.
|
|
509
|
+
*/
|
|
510
|
+
private recordConstructionUsage;
|
|
511
|
+
/**
|
|
512
|
+
* Resolve the type a construction-chain tip call constructs:
|
|
513
|
+
* `new R(...).A(...)` constructs R.A; `await new R(...).A(...).B(...)`
|
|
514
|
+
* constructs R.A.B. The receiver is the nested chain (NewExpression
|
|
515
|
+
* base, then tip calls); exact fullPath first, and only when the root
|
|
516
|
+
* itself is unknown does the prop-name fallback law apply (so plain
|
|
517
|
+
* method calls on fresh instances never record a construction).
|
|
518
|
+
*/
|
|
519
|
+
private resolveChainTipTypePath;
|
|
520
|
+
/**
|
|
521
|
+
* True when `expr` denotes a construction function imported from
|
|
522
|
+
* 'mnemonica' — the named-import form (`import { call } from
|
|
523
|
+
* 'mnemonica'`, aliases included) or a member of a tracked
|
|
524
|
+
* module-object alias (`mnemonica.call`). Userland call/apply/bind
|
|
525
|
+
* functions never match.
|
|
526
|
+
*/
|
|
527
|
+
private isMnemonicaConstructionFn;
|
|
528
|
+
/**
|
|
529
|
+
* mnemonica call/apply(entity, Ctor, ...) / bind(entity, Ctor):
|
|
530
|
+
* resolve the Ctor argument (arg 1) to a graph fullPath through the
|
|
531
|
+
* same tiers as the `new` branch (value scope for identifiers,
|
|
532
|
+
* chain resolution for property accesses).
|
|
533
|
+
*/
|
|
534
|
+
private resolveConstructionFnTypePath;
|
|
535
|
+
/**
|
|
536
|
+
* instance.fork(...) / instance.clone(...) on a tracked variable —
|
|
537
|
+
* runtime returns `this`, so the result carries the source type.
|
|
538
|
+
*/
|
|
539
|
+
private resolveForkLikeTypePath;
|
|
540
|
+
/**
|
|
541
|
+
* Free utils forms: utils.merge(a, b, ...) (also the direct named
|
|
542
|
+
* import `merge(a, b)`) and the curried utils.fork(instance)(...).
|
|
543
|
+
* The result binds to arg 0's type — runtime returns a's lineage over
|
|
544
|
+
* b's context; a's fullPath is the honest approximation within the
|
|
545
|
+
* output contract (documented in README).
|
|
546
|
+
*/
|
|
547
|
+
private resolveUtilsFnTypePath;
|
|
486
548
|
/**
|
|
487
549
|
* Process a @decorate() decorator
|
|
488
550
|
*/
|
|
@@ -666,6 +728,24 @@ export declare class MnemonicaAnalyzer {
|
|
|
666
728
|
* undefined.
|
|
667
729
|
*/
|
|
668
730
|
private resolveParameterAnnotationTypePath;
|
|
731
|
+
/**
|
|
732
|
+
* F20 cheap tier: the wrap argument is an identifier declared with an
|
|
733
|
+
* EXPLICIT type annotation (`let updateCommitted: LedgerUpdate;`
|
|
734
|
+
* assigned later in a flow the analyzer does not track). The
|
|
735
|
+
* annotation resolves through the same graph tiers as parameter
|
|
736
|
+
* annotations. Deliberately NOT flow-sensitive: an UNANNOTATED
|
|
737
|
+
* let/var still buckets unknown, and a const with an analyzable
|
|
738
|
+
* initializer stays the recommended discipline. The lookup walks the
|
|
739
|
+
* enclosing statement containers innermost-out, so a shadowing inner
|
|
740
|
+
* declaration wins.
|
|
741
|
+
*/
|
|
742
|
+
private resolveVariableAnnotationTypePath;
|
|
743
|
+
/**
|
|
744
|
+
* First variable declaration carrying an explicit bare-identifier type
|
|
745
|
+
* annotation for `name` in the given statement list, resolved through
|
|
746
|
+
* the graph law.
|
|
747
|
+
*/
|
|
748
|
+
private findAnnotatedVariableTypePath;
|
|
669
749
|
/**
|
|
670
750
|
* Resolve a wrap() argument to its function node without the type
|
|
671
751
|
* checker: direct function expressions/arrows, or same-file bindings
|