@mnemonica/tactica 0.4.9 → 0.5.0

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
@@ -88,9 +88,6 @@ const writer = new TypesWriter('.tactica');
88
88
  // Default mode — exportable type aliases in types.ts + a TypeRegistry augmentation in registry.ts.
89
89
  writer.writeTypesFile(generator.generateTypesFile());
90
90
  writer.writeTo('registry.ts', generator.generateTypeRegistry().content);
91
-
92
- // Legacy mode — single global augmentation file index.d.ts.
93
- // writer.writeGlobalAugmentation(generator.generateGlobalAugmentation());
94
91
  ```
95
92
 
96
93
  ### Project setup
@@ -122,7 +119,6 @@ You should commit `.tactica/` if you want type information to flow through CI wi
122
119
  | `--include <patterns>` | `-i` | Comma-separated include patterns |
123
120
  | `--exclude <patterns>` | `-e` | Comma-separated exclude patterns |
124
121
  | `--topologica <dirs>` | `-t` | Comma-separated Topologica directories to scan |
125
- | `--module-augmentation` | `-m` | Legacy mode — emit a single global-augmentation `index.d.ts` instead of `types.ts` + `registry.ts` |
126
122
  | `--esm` | | Append `.js` extensions to relative imports in generated files (for ESM / NodeNext module resolution) |
127
123
  | `--eds` | | Force-enable EDS (Execution Data Storage) tracking |
128
124
  | `--no-eds` | | Force-disable EDS tracking |
@@ -142,7 +138,6 @@ Tactica writes everything under the `--output` directory (default `.tactica/`).
142
138
  | `types.ts` | default mode | Exportable type aliases — one per discovered type. Uses `ProtoFlat<Parent, Self>` for nested types. |
143
139
  | `registry.ts` | default mode | `declare module 'mnemonica' { interface TypeRegistry { … } }` augmentation — powers `lookup<K>()`. |
144
140
  | `index.ts` | default mode | Re-exports everything from `types.ts` and `registry.ts`. |
145
- | `index.d.ts` | with `--module-augmentation` | Single global-augmentation file (legacy mode). |
146
141
  | `definitions.json` | always | One entry per discovered type: `{ name, location, kind: 'define'\|'decorate', parent, strictChain, blockErrors }`, plus `fields` (the type's own fields in declaration order, `{ name, type, optional }`) and `args` (the constructor's parameters in declaration order, `{ name, type, optional?, kind? }` — a rest parameter carries `kind: 'rest'` and no `optional`). Consumed by `mnemographica`'s Go to Definition. |
147
142
  | `usages.json` | always | One entry per type, value is an array of `{ location, kind, code, holderScopeId?, constructorText? }` records — where each type is instantiated, referenced, accessed, or looked up. `holderScopeId` points into `scopes.json` (the innermost scope holding the usage); `constructorText` (instantiations only) is the constructor expression text actually used. Consumed by `mnemographica`'s Find References. |
148
143
  | `flow.json` | always | Native instance-flow patterns (property reads/writes, method calls, destructuring, returns, spreads, etc.) per type. |
@@ -155,32 +150,49 @@ Tactica writes everything under the `--output` directory (default `.tactica/`).
155
150
  ### Default mode (types.ts + registry.ts)
156
151
 
157
152
  ```ts
158
- // .tactica/types.ts
159
- export type UserType = {
160
- name: string;
161
- email: string;
162
- AdminType: new (data: { role: string }) => UserType_AdminType;
163
- };
153
+ // .tactica/registry.ts
154
+ import type { App } from '../src/app';
155
+ type FromBuilder = RegistryOf<typeof App>;
164
156
 
165
- export type UserType_AdminType = ProtoFlat<UserType, {
166
- role: string;
167
- AdminType: undefined;
168
- }>;
169
- ```
157
+ import type { Gadget, Gadget_Part } from './types';
170
158
 
171
- ```ts
172
- // .tactica/registry.ts
173
159
  declare module 'mnemonica' {
174
- interface TypeRegistry {
175
- 'UserType': new (data: { name: string; email: string }) => UserType;
176
- 'UserType.AdminType': new (data: { role: string }) => UserType_AdminType;
160
+ interface TypeRegistry extends FromBuilder {
161
+ 'Gadget': RegistryEntry<Gadget, 'Gadget', object, [g: string]>;
162
+ 'Gadget.Part': RegistryEntry<Gadget_Part, 'Gadget.Part', Gadget, [data: { size: number }]>;
177
163
  }
178
164
  }
165
+
166
+ import type { RegistryEntry, RegistryOf, TypeRegistry } from 'mnemonica';
167
+ export type { TypeRegistry };
168
+ ```
169
+
170
+ ```ts
171
+ // .tactica/types.ts
172
+ import type { ProtoFlat, LookedUpConstructor, LookedUpInstance } from 'mnemonica';
173
+ import type { TypeRegistry } from './registry';
174
+
175
+ export type User = LookedUpInstance<TypeRegistry, 'User'>; // derived from App
176
+ export type User_Admin = LookedUpInstance<TypeRegistry, 'User.Admin'>;
177
+
178
+ export type Gadget = { // extracted
179
+ g: string;
180
+ Part: LookedUpConstructor<TypeRegistry, 'Gadget.Part', true>;
181
+ };
182
+
183
+ export type Gadget_Part = ProtoFlat<Gadget, {
184
+ size: number;
185
+ Part?: undefined;
186
+ }>;
179
187
  ```
180
188
 
189
+ Types held by an exported builder value (`App` above) are **derived**: the registry extends the value's own `RegistryOf<…>` and the aliases read the instances back, so tactica's type and the builder's type are the same type. Everything else is **extracted** from source, written in core's own vocabulary (`RegistryEntry`, `LookedUpConstructor`), so extracted and derived instances and constructors are assignable to each other in both directions. See [Derived and extracted types](#derived-and-extracted-types).
190
+
191
+ A child's own name and its siblings' names are emitted as optional `undefined` (`Part?: undefined`) on the child's instance type: at runtime `part.Part` is `undefined` and `new part.Part()` throws, and the marker keeps the parent's constructor field from being inherited through `ProtoFlat`.
192
+
181
193
  **Recommended for new projects.** Explicit imports, better tree-shaking, no global namespace pollution.
182
194
 
183
- **Async constructors.** When the construct handler is an `async function` (or async arrow), `new` resolves to a Promise at runtime, so the emitted constructor shapes resolve to `Promise<InstanceType>`: the subtype property on the parent emits `{ new (…): Promise<X>; (…): Promise<X> }` (both twins) and the registry entry emits `new (…) => Promise<X>`. The instance type `X` itself is unchanged, so `await new` yields `X`. Async CLASSES (a class constructor returning a Promise) are not detected — the syntactic shape carries no reliable signal without a type checker — and stay typed as the plain instance type; type them yourself in userland if you need `Promise<X>` there.
195
+ **Async constructors.** When the construct handler is an `async function` (or async arrow), `new` resolves to a Promise at runtime. An extracted async type's registry entry is `AsyncRegistryEntry<X, 'Path', Parent, [args]>` (core's async counterpart of `RegistryEntry`), so `new` on it, and on the subtype field of its parent, is typed `Promise<X>`, and `await` yields `X`, with its own subtypes. Derived async types get the same typing from core's builder. A class constructor that returns a Promise cannot be written in typed code (TypeScript rejects it with TS2409), so async classes stay typed as the plain instance.
184
196
 
185
197
  **Avoid arrow handlers — use the ESLint plugin.** Construct handlers must be regular functions or classes so the instance can be substituted as `this`. Core rejects sync arrows at `define()` time, but async arrows cannot be detected at runtime and never receive the instance — the failure surfaces far from the cause. The early guard is [`eslint-plugin-no-arrow-this`](https://www.npmjs.com/package/eslint-plugin-no-arrow-this), which flags `this` inside arrow functions before the code runs:
186
198
 
@@ -198,16 +210,6 @@ Recommended in every mnemonica project.
198
210
 
199
211
  **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 (`serial` in `WidgetRoot`) 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.
200
212
 
201
- ### Legacy global-augmentation mode (`--module-augmentation`)
202
-
203
- Emits one `.tactica/index.d.ts` whose contents live inside `declare global { … }`. Reference it from your code with:
204
-
205
- ```ts
206
- /// <reference types="./.tactica/index" />
207
- ```
208
-
209
- or by adding `./.tactica` to `compilerOptions.typeRoots`. Kept for backwards compatibility with older tactica integrations.
210
-
211
213
  ## Type-Safe Lookup with `lookup()`
212
214
 
213
215
  Mnemonica core exposes `lookup<K>()` against an empty `TypeRegistry` interface. Tactica's `registry.ts` augments that interface with all of your discovered types:
@@ -216,8 +218,8 @@ Mnemonica core exposes `lookup<K>()` against an empty `TypeRegistry` interface.
216
218
  // .tactica/registry.ts (generated)
217
219
  declare module 'mnemonica' {
218
220
  interface TypeRegistry {
219
- 'UserType': new (data: { name: string; email: string }) => UserType;
220
- 'UserType.AdminType': new (data: { role: string }) => UserType_AdminType;
221
+ 'UserType': RegistryEntry<UserType, 'UserType', object, [data: { name: string; email: string }]>;
222
+ 'UserType.AdminType': RegistryEntry<UserType_AdminType, 'UserType.AdminType', UserType, [data: { role: string }]>;
221
223
  }
222
224
  }
223
225
  ```
@@ -237,6 +239,35 @@ const admin = new user.AdminType({ role: 'admin' });
237
239
 
238
240
  No `as unknown as` casts required. See [`docs/lookup-pattern.md`](docs/lookup-pattern.md) for the consumer-side guide.
239
241
 
242
+ ## Derived and extracted types
243
+
244
+ A project may use both of mnemonica's typing paths: builder values and tactica. For the same runtime type both give TypeScript the same type, on TypeScript 6 and 7. Tactica takes every type a builder value already carries and extracts only what the builder cannot type.
245
+
246
+ **Derived** (the type comes from the value itself):
247
+
248
+ - builder chains held in an exported value on the default collection: `export const App = mnemonica.define(…).define(…)`, and `export const Sessions = App.lookup('User').define('Session', …)`. Every exported value is included, so a subtype defined in a later statement is in the registry; `registry.ts` extends the intersection of their `RegistryOf<typeof X>`;
249
+ - exported free `define` results: `export const Widget = define('Widget', …)` enters as `typeof Widget`.
250
+
251
+ Aliases in `types.ts` read derived instances back with `LookedUpInstance<TypeRegistry, 'Path'>`.
252
+
253
+ **Extracted** (tactica reads the source):
254
+
255
+ - inline or unexported `define`, `@decorate` (and typeomatica's `@Strict` beside it), topologica directory types, Option B collections;
256
+ - a handler that uses a type from the generated `.tactica` anywhere in its signature (`this`, a parameter, the return type), and every exported value whose chain passes through such a handler. Deriving those would be circular: the value's type would depend on the registry it is part of.
257
+
258
+ ```ts
259
+ // extracted: the handler takes a generated type, so typeof Notes depends on TypeRegistry
260
+ export const Notes = Sessions.lookup('User').define('Note', function (this: { text: string }, author: User) {
261
+ this.text = author.name;
262
+ });
263
+ ```
264
+
265
+ Here tactica extracts `User.Note`, while code holding `Notes` keeps the builder's own type for it; the two cross both ways.
266
+
267
+ Custom collections without Option B are never in the global `TypeRegistry`: the free `lookup()` resolves only the default collection at runtime.
268
+
269
+ Hierarchy, definitions, usages and the creation graph come from tactica for every type, derived or extracted.
270
+
240
271
  ## What Gets Analyzed
241
272
 
242
273
  ### `define()` calls
@@ -313,6 +344,25 @@ const AdminType = define(UserType, 'AdminType', function (this: { role: string }
313
344
  const AdminCtor = lookup(App, 'UserType.AdminType');
314
345
  ```
315
346
 
347
+ ### Looped defines with computed names
348
+
349
+ A define whose name ranges over a const object's keys is recognized when the set of names is closed: a helper parameter typed `keyof typeof Data` (directly or through a local alias), or a `for…of Object.keys(Data)` variable used as the name.
350
+
351
+ ```ts
352
+ const Data = { Prepare: { step: 1 }, Produce: { step: 2 } };
353
+ type PhaseName = keyof typeof Data;
354
+
355
+ const definePhase = (name: PhaseName) => {
356
+ const result = Run.define(name, holder<typeof Data[typeof name]>());
357
+ return result;
358
+ };
359
+ for (const key of Object.keys(Data)) {
360
+ if (isPhaseName(key)) Phase.set(key, definePhase(key));
361
+ }
362
+ ```
363
+
364
+ `Run.Prepare` and `Run.Produce` enter `hierarchy.json` and `definitions.json` under `Run`, in declaration order, sharing the define site's location. They get no `registry.ts` or `types.ts` entries in any collection: their TypeScript types are the builder value's (`Phase.get(…)`). A name that is not a closed const-object key set (a plain `string`, a computed key, an unresolved alias) is still reported with "Could not extract type name" and skipped.
365
+
316
366
  ### Custom collections via `createTypesCollection()`
317
367
 
318
368
  Types defined on a collection created with `createTypesCollection()` are tracked by the analyzer. By default they are **not** emitted in `.tactica/types.ts` and are **not** added to the global `mnemonica.TypeRegistry` augmentation because they live in an isolated runtime collection. Use the collection variable directly in your own code to get typed constructors.
@@ -426,24 +476,29 @@ const sub = new gadget.EnhancedGadget();
426
476
  ### `Object.assign(this, data)` pattern
427
477
 
428
478
  ```ts
429
- const UserType = define('UserType', function (this: any, data: { name: string }) {
430
- Object.assign(this, data); // properties inferred from `data` type
479
+ // a handler without a `this` annotation
480
+ const UserType = define('UserType', function (data: { name: string }) {
481
+ Object.assign(this, data); // properties inferred from `data`'s type
431
482
  });
432
483
  ```
433
484
 
485
+ In a handler with a `this` annotation the annotation is the type (see [Property Type Inference](#property-type-inference)); `Object.assign` then adds nothing beyond it.
486
+
434
487
  ### `TypeConstructor` casting (from mnemonica)
435
488
 
436
489
  ```ts
437
490
  import { define } from 'mnemonica';
438
491
  import type { TypeConstructor } from 'mnemonica';
439
492
 
440
- const MyFn = function (this: any) {
493
+ const MyFn = function (this: { field: number }) {
441
494
  this.field = 123;
442
495
  } as TypeConstructor<{ field: number }>;
443
496
 
444
- const MyFnType = define('MyFnType', MyFn);
497
+ const MyFnType = define('MyFnType', MyFn); // MyFnType: { field: number }
445
498
  ```
446
499
 
500
+ The analyzer follows `MyFn` to its function expression and extracts it exactly like an inline handler. The `TypeConstructor` type argument is not used as a substitute for the function body; the annotated-`this` rule applies, so `this: { field: number }` gives `{ field: number }` (and `this: any` would give `any`). Const-held function and class expressions, function and class declarations, and imported handlers are followed through their identifiers, including through `lazy()`, `Type.define()`, and chained `define()` calls. An identifier whose declaration cannot be resolved keeps the previous empty extraction.
501
+
447
502
  (In older mnemonica documentation this helper was called `ConstructorFunction`; the analyzer still recognizes that name in source code for back-compat.)
448
503
 
449
504
  ### Typeomatica integration
@@ -490,7 +545,21 @@ npx tactica --topologica ./src/ai-types,./custom/topologica
490
545
 
491
546
  ## Property Type Inference
492
547
 
493
- The analyzer infers property types from constructor bodies and class members. Supported expression patterns:
548
+ **An annotated `this` is the type.** A handler is used as a constructor, so its `this` annotation states the instance, the way a class is typed by its declared fields. Assignments beyond the annotation add nothing:
549
+
550
+ ```ts
551
+ interface PackData { data?: Record<string, unknown> }
552
+
553
+ define('Pack', function (this: PackData, files: PackFiles, meta: PackMeta) {
554
+ this.header = files.header; // not in PackData: not typed
555
+ this.data = files.extra;
556
+ });
557
+ // Pack is PackData: { data?: Record<string, unknown> }
558
+ ```
559
+
560
+ Declare the fields in the annotation to have them typed. `this: any` is respected the same way: the instance type is `any`. Two cases keep reading the body: a handler without a `this` annotation, and a handler whose `this` is a type from the generated `.tactica` (the [call-site ergonomics](#define-calls) pattern), since that annotation is tactica's own output.
561
+
562
+ For handlers without a `this` annotation, the analyzer infers property types from constructor bodies and class members. Supported expression patterns:
494
563
 
495
564
  | Pattern | Inferred type |
496
565
  |---|---|
@@ -709,7 +778,6 @@ class TypesGenerator {
709
778
 
710
779
  generateTypesFile(): GeneratedTypes; // default mode → types.ts
711
780
  generateTypeRegistry(): GeneratedTypes; // default mode → registry.ts
712
- generateGlobalAugmentation(): GeneratedTypes; // legacy mode → index.d.ts
713
781
  generateSingleType(node: TypeNode): string;
714
782
  }
715
783
  ```
@@ -776,7 +844,6 @@ class TypesWriter {
776
844
  constructor(outputDir?: string);
777
845
 
778
846
  writeTypesFile(generated: GeneratedTypes): string; // → outputDir/types.ts
779
- writeGlobalAugmentation(generated: GeneratedTypes): string; // → outputDir/index.d.ts
780
847
  writeTo(filename: string, content: string): string; // → outputDir/<filename>
781
848
 
782
849
  writeDefinitionsFile(map: Map<string, DefinitionInfo>, graph?: TypeGraph): string; // → outputDir/definitions.json (graph adds fields/args)
@@ -936,7 +1003,7 @@ Deliberate approximations (name-based, no type checker): namespace imports count
936
1003
  1. **Parse** — load `tsconfig.json`, build a `ts.Program`, walk each source file's AST.
937
1004
  2. **Detect** — find `define()` and `@decorate()` calls, plus `lookup` lookups, `new` expressions, and EDS / flow patterns.
938
1005
  3. **Graph** — build a Trie of types in `TypeGraphImpl`, with parent links via the chain of `.define()` calls and `@decorate(Parent)` references.
939
- 4. **Generate** — emit `types.ts`, `registry.ts`, `index.ts` (default mode) or `index.d.ts` (legacy mode), plus `definitions.json`, `usages.json` (with `holderScopeId`), `flow.json`, `instrumentation.json`, `control.json`, `modules.json`, `scopes.json`, `hierarchy.json`/`hierarchy.txt`, `collections.json`, and optionally `eds.json`.
1006
+ 4. **Generate** — emit `types.ts`, `registry.ts`, `index.ts`, plus `definitions.json`, `usages.json` (with `holderScopeId`), `flow.json`, `instrumentation.json`, `control.json`, `modules.json`, `scopes.json`, `hierarchy.json`/`hierarchy.txt`, `collections.json`, and optionally `eds.json`.
940
1007
  5. **Write** — files land in the output directory (default `.tactica/`).
941
1008
 
942
1009
  ```
@@ -976,7 +1043,7 @@ Multi-level property access in constructors falls back to `unknown`:
976
1043
 
977
1044
  ```ts
978
1045
  // ⚠️ this.name's inferred type is unknown
979
- const UserType = define('UserType', function (this: any, data: { profile: { name: string } }) {
1046
+ const UserType = define('UserType', function (data: { profile: { name: string } }) {
980
1047
  this.name = data.profile.name;
981
1048
  });
982
1049
  ```
@@ -1011,7 +1078,7 @@ Custom collection types are isolated in the analyzer by their collection identit
1011
1078
 
1012
1079
  ## Troubleshooting
1013
1080
 
1014
- **Generated types are missing properties** — make sure the constructor function has either an explicit `this:` type annotation, or that the data parameter has an inline / aliased type (`{ id: string }` or `Foo` where `type Foo = { id: string }`). Property inference walks one level of indirection.
1081
+ **Generated types are missing properties** — with a `this:` annotation, the annotation is the type: declare the missing fields there. Without one, make sure the data parameter has an inline / aliased type (`{ id: string }` or `Foo` where `type Foo = { id: string }`); property inference walks one level of indirection. For a derived type (an exported builder value), the fields are whatever the builder's own type carries.
1015
1082
 
1016
1083
  **`new user.AdminType()` shows red squiggles** — confirm `.tactica/types.ts` is in your `tsconfig.json` `include`. If you use `lookup`, also import `.tactica/registry.ts` somewhere.
1017
1084
 
package/SKILL.md CHANGED
@@ -50,27 +50,42 @@ augmented interface instead of the global `TypeRegistry`.
50
50
 
51
51
  ## What it types
52
52
 
53
+ - **Derived types:** an exported builder value on the default collection
54
+ (`export const App = mnemonica.define(…).define(…)`,
55
+ `export const Sessions = App.lookup('User').define(…)`) or an exported
56
+ free `define` result is not re-extracted: `registry.ts` extends its
57
+ `RegistryOf<typeof X>` (or uses `typeof X`), and `types.ts` reads the
58
+ instances back with `LookedUpInstance<TypeRegistry, 'Path'>`. The
59
+ builder and tactica then give the same type for the same runtime type.
60
+ - **Extracted types:** everything else (inline or unexported `define`,
61
+ `@decorate`, handlers that use a generated `.tactica` type, topologica,
62
+ Option B) is written out in core's vocabulary: registry entries are
63
+ `RegistryEntry<X, 'Path', Parent, [args]>`, subtype fields
64
+ `LookedUpConstructor<TypeRegistry, 'Path', true>`, so extracted and
65
+ derived instances are assignable both ways.
53
66
  - One `export type` per discovered type, named by dotted path with `_`
54
- separators; nested types are `ProtoFlat<Parent, Self>`, so ancestor
55
- fields are present on descendants by construction.
56
- - Every parent instance type carries its subtype constructors:
57
- `Child: { new (args): Child; (args): Child }` — the construct form
58
- and the chain-tip call form.
59
- - `registry.ts` augments mnemonica's `TypeRegistry` so the free
60
- `lookup('Parent.Child')` returns the typed constructor.
67
+ separators; nested extracted types are `ProtoFlat<Parent, Self>`, so
68
+ ancestor fields are present on descendants by construction.
69
+ - **An annotated `this` is the type:** `function (this: Shape, …)` gives
70
+ `Shape`, and assignments beyond it add nothing (`this: any` gives
71
+ `any`). Only a handler without a `this` annotation is typed from its
72
+ body.
73
+ - Handlers held in a const (`const h = function …; define('X', h)`) are
74
+ followed like inline ones.
75
+ - Looped defines over a const object's keys enter hierarchy and
76
+ definitions only; their types come from the builder value.
61
77
 
62
78
  ## Async constructors
63
79
 
64
- When a construct handler is an `async function` (or async arrow),
65
- tactica emits `Promise<X>` for both constructor shapes — the subtype
66
- property and the registry entry — because `new` resolves to a Promise
67
- at runtime. The instance type `X` itself is unchanged, so
68
- `await new` yields `X`.
80
+ When a construct handler is an `async function` (or async arrow), `new`
81
+ resolves to a Promise at runtime. Extracted async types are registered as
82
+ `AsyncRegistryEntry<X, …>` (mnemonica ≥ 1.4.7), so construction is typed
83
+ `Promise<X>` and `await` yields `X` with its own subtypes; derived async
84
+ types get the same typing from mnemonica's builder.
69
85
 
70
- Async **classes** (a class constructor returning a Promise) are NOT
71
- detected — the syntactic shape carries no reliable signal without a
72
- type checker — and stay typed as the plain instance type. Type them in
73
- userland when `Promise<X>` is needed there.
86
+ A class constructor returning a Promise cannot be written in typed code
87
+ (TypeScript rejects it, TS2409), so async **classes** stay typed as the
88
+ plain instance.
74
89
 
75
90
  ## Avoid arrow handlers — use the ESLint plugin
76
91
 
@@ -107,16 +122,16 @@ written by contributors who have not internalized the rule.
107
122
  import { mnemonica } from 'mnemonica';
108
123
 
109
124
  export const App = mnemonica
110
- .define('Widget', function (this: Widget, data: { id: string }) {
125
+ .define('Widget', function (this: { id: string }, data: { id: string }) {
111
126
  this.id = data.id;
112
127
  })
113
- .define('Gadget', async function (this: Gadget, data: { serial: number }) {
128
+ .define('Gadget', async function (this: { serial: number }, data: { serial: number }) {
114
129
  this.serial = await register(data.serial);
115
130
  return this;
116
131
  });
117
132
  ```
118
133
 
119
- After `npx tactica`, `lookup('App.Gadget')` is typed
120
- `new (data: { serial: number }) => Promise<Gadget>` — a forgotten
121
- `await` is now a type error at the call site instead of a silent
122
- `undefined` at runtime.
134
+ After `npx tactica`, `App` is derived, and the free `lookup('Widget')` is
135
+ typed exactly as `App.lookup('Widget')`. `new widget.Gadget({ serial: 1 })`
136
+ is a `Promise` — a forgotten `await` is a type error at the call site
137
+ instead of a silent `undefined` at runtime.
package/lib/analyzer.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { AnalyzeResult, DefinitionInfo, UsageInfo, EDSInfo, FlowInfo, InstrumentationPoint, ResolutionError, CollectionManifestEntry } from './types';
2
+ import { AnalyzeResult, DefinitionInfo, UsageInfo, EDSInfo, FlowInfo, InstrumentationPoint, ResolutionError, CollectionManifestEntry, BuilderValue } from './types';
3
3
  import { TypeGraphImpl } from './graph';
4
4
  import { TacticaPlugin } from './plugins';
5
5
  /**
@@ -18,9 +18,13 @@ export declare class MnemonicaAnalyzer {
18
18
  private flowUsages;
19
19
  private edsScopeByNode;
20
20
  private functionBindings;
21
+ private constructorBindings;
21
22
  private nestedWrapVia;
22
23
  private wrapEntryByNode;
23
24
  private variableToTypeMap;
25
+ private builderCandidates;
26
+ private excludedTypePaths;
27
+ private generatedTypeImports;
24
28
  private moduleObjectVariables;
25
29
  private mnemonicaNamedImports;
26
30
  private createTypesCollectionVariables;
@@ -69,6 +73,7 @@ export declare class MnemonicaAnalyzer {
69
73
  * Get the type graph
70
74
  */
71
75
  getGraph(): TypeGraphImpl;
76
+ getBuilderValues(): BuilderValue[];
72
77
  /**
73
78
  * Get collected definitions
74
79
  */
@@ -119,6 +124,7 @@ export declare class MnemonicaAnalyzer {
119
124
  * Visit a node in the AST
120
125
  */
121
126
  private visitNode;
127
+ private trackConstructorBinding;
122
128
  /**
123
129
  * Track imports from 'mnemonica' so aliases of the module object and
124
130
  * createTypesCollection are recognized without relying on the type checker.
@@ -470,6 +476,7 @@ export declare class MnemonicaAnalyzer {
470
476
  * Process a define() call
471
477
  */
472
478
  private processDefineCall;
479
+ private processDefineContext;
473
480
  /**
474
481
  * Process a lazy() call
475
482
  */
@@ -510,6 +517,11 @@ export declare class MnemonicaAnalyzer {
510
517
  * Extract config options from lazy() call
511
518
  */
512
519
  private extractLazyConfig;
520
+ private hasGeneratedTypeReference;
521
+ private hasSelfTypeAnnotation;
522
+ private recordBuilderCandidate;
523
+ private markBuilderExport;
524
+ private trackBuilderExportAssignment;
513
525
  /**
514
526
  * Track variable assignments that capture define() results
515
527
  * e.g., const User = define('UserEntity', ...) maps "User" -> "UserEntity"
@@ -612,6 +624,17 @@ export declare class MnemonicaAnalyzer {
612
624
  * define(() => class TypeName {})
613
625
  */
614
626
  private extractTypeName;
627
+ /**
628
+ * Resolve a computed define name only when it ranges over a closed const
629
+ * object key set. Arbitrary strings and unresolved unions stay on the
630
+ * existing warning-and-skip path.
631
+ */
632
+ private extractComputedTypeNames;
633
+ private resolveLoopKeyNames;
634
+ private resolveObjectKeysCall;
635
+ private resolveParameterKeyNames;
636
+ private resolveKeyofType;
637
+ private resolveConstObjectKeys;
615
638
  /**
616
639
  * Extract the full define() call context: type name, parent type, and collection.
617
640
  * Handles direct calls, property-access calls, chained calls, and the
@@ -670,10 +693,14 @@ export declare class MnemonicaAnalyzer {
670
693
  * Get property chain from nested access
671
694
  */
672
695
  private getPropertyChain;
696
+ private findConstructorInModule;
697
+ private resolveHeldConstructor;
698
+ private unwrapConstructorAssertions;
699
+ private resolveConstructorExpression;
673
700
  /**
674
701
  * Determine the constructor expression for either a define() or lazy() call.
675
702
  * For define() this is the construct handler; for lazy() it is the value
676
- * returned by the lazy getter.
703
+ * returned by the lazy getter. Identifiers resolve to their declarations.
677
704
  */
678
705
  private extractConstructorExpression;
679
706
  /**
@@ -720,6 +747,8 @@ export declare class MnemonicaAnalyzer {
720
747
  * Infer method type from method declaration
721
748
  */
722
749
  private inferMethodType;
750
+ private hasGeneratedThisAnnotation;
751
+ private hasAnyThisAnnotation;
723
752
  /**
724
753
  * Extract properties from `this` parameter type annotation
725
754
  * Handles patterns like: function(this: SomeType, data: SomeType) { }