@mnemonica/tactica 0.3.9 → 0.3.10
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 +3 -2
- package/lib/analyzer.d.ts +22 -0
- package/lib/analyzer.js +106 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -483,6 +483,7 @@ The analyzer infers property types from constructor bodies and class members. Su
|
|
|
483
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 |
|
|
484
484
|
| `this.x = data.field \|\| []` | type of the fallback expression |
|
|
485
485
|
| `this.x = data.field ? a : b` | type of the truthy branch |
|
|
486
|
+
| `this.x = (<const>['a', 'b'])[0]` / `this.x = ([…] as const)[1]` / `this.x = trackedList[2]` | the element's literal type (`'a'`) — const-asserted literal arrays either spelling, or a tracked module const array (`const trackedList = <const>[…]`); non-numeric/out-of-range/spread/non-literal elements and general `<T>` assertions stay `unknown` |
|
|
486
487
|
| `Object.assign(this, data)` | all fields of `data`'s type annotation — inline literal or named alias/interface/class, resolved import-aware (F14 machinery) |
|
|
487
488
|
| `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 |
|
|
488
489
|
| async / sync constructor functions | same rules |
|
|
@@ -493,7 +494,7 @@ When inference fails the property's type falls back to `unknown` (or `any` in so
|
|
|
493
494
|
|
|
494
495
|
### Referenced types in constructor signatures
|
|
495
496
|
|
|
496
|
-
A constructor parameter that references a named type (`record: SharedShape`) resolves **through the importing file's own import statements** — relative and tsconfig-paths imports are followed to the declaration (chasing re-export barrels), and imported classes/interfaces/type aliases with extractable fields are expanded inline, **inherited members included**: `extends` chains are walked (depth-bounded, cycle-guarded) and parent fields merge with the declaration's own fields overriding on name clash. A field typed by a `typeof` query over a module-level const array (`status?: typeof statusList[number]`) expands to the element literal union (`status?: 'active' | 'closed'`); any other `typeof` source degrades that field to `unknown` — a bare `typeof name` is never emitted, since the generated file has no imports to resolve it against. An import is never shadowed by a same-named declaration elsewhere in the program. A reference that resolves not at all (a ghost name, declared nowhere) emits `unknown` — generated `.tactica/types.ts` carries no import statements of its own, so a bare unresolvable name is never emitted. A reference that resolves *ambiguously* — several of your project's files declare the name and no import chooses between them — is **fatal** like the mnemonica-graph identity law below: tactica prints the reference site and every declaration site, exits non-zero, and writes no output. An import anchoring the reference is never ambiguity (the import wins, duplicate declarations or not), and package-declared same-named types never count — a user-local declaration always wins over a node_modules `.d.ts`. Two same-named interfaces in one file are legal TypeScript merging, not ambiguity (see Known Limitations for the last-wins caveat).
|
|
497
|
+
A constructor parameter that references a named type (`record: SharedShape`) resolves **through the importing file's own import statements** — relative and tsconfig-paths imports are followed to the declaration (chasing re-export barrels), and imported classes/interfaces/type aliases with extractable fields are expanded inline, **inherited members included**: `extends` chains are walked (depth-bounded, cycle-guarded) and parent fields merge with the declaration's own fields overriding on name clash. A field typed by a `typeof` query over a module-level const array (`status?: typeof statusList[number]`) expands to the element literal union (`status?: 'active' | 'closed'`); the value-level twin — element access over a const-asserted literal array (`(<const>['active', 'not_active', 'hold'])[0]`, `([…] as const)[1]`, or a tracked module const used as `x[0]`) — infers the element's literal type the same way; any other `typeof` source degrades that field to `unknown` — a bare `typeof name` is never emitted, since the generated file has no imports to resolve it against. An import is never shadowed by a same-named declaration elsewhere in the program. A reference that resolves not at all (a ghost name, declared nowhere) emits `unknown` — generated `.tactica/types.ts` carries no import statements of its own, so a bare unresolvable name is never emitted. A reference that resolves *ambiguously* — several of your project's files declare the name and no import chooses between them — is **fatal** like the mnemonica-graph identity law below: tactica prints the reference site and every declaration site, exits non-zero, and writes no output. An import anchoring the reference is never ambiguity (the import wins, duplicate declarations or not), and package-declared same-named types never count — a user-local declaration always wins over a node_modules `.d.ts`. Two same-named interfaces in one file are legal TypeScript merging, not ambiguity (see Known Limitations for the last-wins caveat).
|
|
497
498
|
|
|
498
499
|
**Ambiguous plain-TypeScript reference — fails, and how to fix it.** Two plain interfaces named `SharedShape` exist in different files, and a third file references a bare `SharedShape` with nothing anchoring it:
|
|
499
500
|
|
|
@@ -959,7 +960,7 @@ Mnemonica accepts the `exposeInstanceMethods` option at runtime, but tactica's `
|
|
|
959
960
|
|
|
960
961
|
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`.
|
|
961
962
|
|
|
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.
|
|
963
|
+
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]`). The value-level twin — element access over a const-asserted literal array (`(<const>[…])[0]`, `([…] as const)[1]`, tracked `x[0]`) — infers the element literal under the same rules; non-numeric, out-of-range, spread, or non-literal elements and general `<T>` assertions (as opposed to `<const>`) stay `unknown`. 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.
|
|
963
964
|
|
|
964
965
|
### Same-file interface merging is last-wins
|
|
965
966
|
|
package/lib/analyzer.d.ts
CHANGED
|
@@ -145,6 +145,28 @@ export declare class MnemonicaAnalyzer {
|
|
|
145
145
|
* degrades the field to `unknown` rather than guessing.
|
|
146
146
|
*/
|
|
147
147
|
private literalTypesOfArray;
|
|
148
|
+
/**
|
|
149
|
+
* The literal type of one array element: a plain literal (optionally
|
|
150
|
+
* wrapped in `as const` / `satisfies` / assertion expressions) —
|
|
151
|
+
* string, numeric (unary `-`/`+` preserved), boolean, or null.
|
|
152
|
+
* Anything else yields undefined.
|
|
153
|
+
*/
|
|
154
|
+
private literalTypeOfExpression;
|
|
155
|
+
/**
|
|
156
|
+
* F22: the const-assertion check shared by the value-level and
|
|
157
|
+
* declaration-level paths — `expr as const` and `<const>expr` parse
|
|
158
|
+
* identically (a TypeReferenceNode named 'const'). General `<T>expr`
|
|
159
|
+
* assertions never match.
|
|
160
|
+
*/
|
|
161
|
+
private isConstAssertionType;
|
|
162
|
+
/**
|
|
163
|
+
* The array literal behind a value-level element access: inline
|
|
164
|
+
* (`(<const>[…])[0]`, `([…] as const)[1]`), parenthesized, or a
|
|
165
|
+
* tracked module const array (`const x = <const>[…]` / `x[0]`, F17
|
|
166
|
+
* tracking). Only const assertions are unwrapped — general
|
|
167
|
+
* assertions stay unknown (F22 scope boundary).
|
|
168
|
+
*/
|
|
169
|
+
private constArrayLiteralOf;
|
|
148
170
|
/**
|
|
149
171
|
* Emit-type for `typeof name` when `name` is a tracked const array: the
|
|
150
172
|
* union of its element literal types (`'active' | 'closed'`). Every
|