@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 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