@mnemonica/tactica 0.3.5 → 0.3.7

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
@@ -473,7 +473,7 @@ When inference fails the property's type falls back to `unknown` (or `any` in so
473
473
 
474
474
  ### Referenced types in constructor signatures
475
475
 
476
- 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. 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).
476
+ 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).
477
477
 
478
478
  **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:
479
479
 
@@ -939,6 +939,8 @@ Mnemonica accepts the `exposeInstanceMethods` option at runtime, but tactica's `
939
939
 
940
940
  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
941
 
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 (cross-file only when the const is imported by the declaring file); every other `typeof` source degrades the field to `unknown`.
943
+
942
944
  ### Same-file interface merging is last-wins
943
945
 
944
946
  Two same-named interfaces in ONE module are legal TypeScript declaration merging, and tactica does not treat them as ambiguity (the fatal class is same-named declarations across several files, unanchored by an import). Resolution keeps a single declaration per name per file, and it is the last one — members from earlier declarations are not combined:
package/lib/analyzer.d.ts CHANGED
@@ -37,6 +37,7 @@ export declare class MnemonicaAnalyzer {
37
37
  private referencedTypeNamespaces;
38
38
  private referencedTypeNamespaceStars;
39
39
  private referencedTypeResolutionCache;
40
+ private referencedTypeConstArrays;
40
41
  private referencedTypeCompilerOptions;
41
42
  private currentReferencedTypeFile;
42
43
  private expandingReferencedAliases;
@@ -117,6 +118,40 @@ export declare class MnemonicaAnalyzer {
117
118
  * interface) for the file currently being visited.
118
119
  */
119
120
  private trackReferencedTypeDeclaration;
121
+ /**
122
+ * Record consts initialized with an array literal (optionally wrapped in
123
+ * `as const` / `satisfies`), so a `typeof statusList[number]` field type
124
+ * expands to the element literal union — the generated file carries no
125
+ * imports, so emitting the bare `typeof statusList` query would be an
126
+ * unresolvable name downstream. First binding wins: a nested shadow
127
+ * must not replace the module-level const the typeof refers to.
128
+ */
129
+ private trackReferencedTypeConstArray;
130
+ /**
131
+ * Find the array literal behind a module const referenced through
132
+ * `typeof`: the declaring file's own consts first (the F13 case is a
133
+ * NON-exported const in the same module as the expanded class), then —
134
+ * when the file imports the name — the imported module's consts.
135
+ * External modules are never analyzed, so those yield nothing.
136
+ */
137
+ private findReferencedConstArray;
138
+ /**
139
+ * 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, boolean, or null. Spreads, identifiers, and nested
142
+ * arrays mean the union is not statically visible and yield undefined,
143
+ * so the caller degrades the field to `unknown` rather than guessing.
144
+ */
145
+ private literalTypesOfArray;
146
+ /**
147
+ * Emit-type for `typeof name` when `name` is a tracked const array: the
148
+ * union of its element literal types (`'active' | 'closed'`). Every
149
+ * other typeof source — non-array consts, functions, classes, names not
150
+ * tracked at all — yields undefined, so the caller degrades the field
151
+ * to `unknown`: a bare `typeof name` emitted into types.ts has no
152
+ * import to resolve against downstream.
153
+ */
154
+ private typeOfConstArrayUnion;
120
155
  /**
121
156
  * Record the importing file's named/namespace/default import bindings so
122
157
  * referenced-type names resolve through the file's own import statements
@@ -162,8 +197,28 @@ export declare class MnemonicaAnalyzer {
162
197
  /**
163
198
  * Properties of a referenced class/interface/alias-of-literal declaration,
164
199
  * shared by `this:`-parameter expansion and inline type emission.
200
+ * Inherited members are included: the extends chain is walked
201
+ * (depth-capped, cycle-guarded) and parent fields merge first, the
202
+ * declaration's own fields overriding on name clash.
165
203
  */
166
204
  private referencedDeclarationProperties;
205
+ private referencedDeclarationPropertiesInner;
206
+ /**
207
+ * Property signatures of interface/alias type-literal members, into
208
+ * the given map.
209
+ */
210
+ private collectTypeElementProperties;
211
+ /**
212
+ * Resolve the heritage clause of a class (`extends Base`) or interface
213
+ * (`extends A, B`) to referenced-type declarations through the SAME
214
+ * import-aware machinery as plain references (the declaring file's own
215
+ * imports first, then its locals, then the unique program-wide
216
+ * declaration). Unresolvable or external bases yield nothing — their
217
+ * inherited fields simply stay absent, same as before this walk
218
+ * existed. Mixin calls (`extends mixin(X)`) and namespace access are
219
+ * not followed.
220
+ */
221
+ private resolveHeritageDeclarations;
167
222
  /**
168
223
  * Expand a referenced-type declaration to a self-contained type string
169
224
  * for emission into generated files: type aliases through inferType,