mnemonica 0.9.99787 → 1.0.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.
Files changed (129) hide show
  1. package/.ai/AGENTS.md +138 -0
  2. package/.ai/ARCHITECT.md +110 -0
  3. package/.ai/CODE.md +111 -0
  4. package/.ai/DEBUG.md +179 -0
  5. package/.ai/ONBOARDING.md +171 -0
  6. package/.ai/TACTICA-DEEP-DIVE.md +583 -0
  7. package/.ai/TACTICA-RULES.md +103 -0
  8. package/.ai/ask/AGENTS.md +115 -0
  9. package/.ai/async_init.md +94 -0
  10. package/.ai/orchestrator/AGENTS.md +158 -0
  11. package/.ai/rules/CODING.md +229 -0
  12. package/.ai/rules/CONTEXT-CONDENSING.md +50 -0
  13. package/.ai/rules/REMINDERS.md +63 -0
  14. package/.ai/rules-skill/async-constructors.md +206 -0
  15. package/.ai/rules-skill/code-style.md +95 -0
  16. package/.ai/rules-skill/contributing.md +192 -0
  17. package/.ai/rules-skill/define-patterns.md +96 -0
  18. package/.ai/rules-skill/ecosystem.md +140 -0
  19. package/.ai/rules-skill/error-system.md +56 -0
  20. package/.ai/rules-skill/hooks.md +46 -0
  21. package/.ai/rules-skill/instance-methods.md +60 -0
  22. package/.ai/rules-skill/lookup-typed.md +84 -0
  23. package/.ai/rules-skill/philosophy.md +231 -0
  24. package/.ai/rules-skill/proxy-architecture.md +63 -0
  25. package/.ai/rules-skill/testing.md +66 -0
  26. package/.ai/rules-skill/type-system.md +114 -0
  27. package/.ai/task-templates/new-feature.md +46 -0
  28. package/AGENTS.md +250 -0
  29. package/CONTRIBUTING.md +112 -0
  30. package/FOR_HUMANS.md +1391 -0
  31. package/README.md +306 -999
  32. package/SKILL.md +109 -0
  33. package/build/api/errors/exceptionConstructor.js +3 -2
  34. package/build/api/errors/index.d.ts +5 -1
  35. package/build/api/errors/index.js +1 -1
  36. package/build/api/errors/throwModificationError.d.ts +1 -19
  37. package/build/api/errors/throwModificationError.js +5 -3
  38. package/build/api/hooks/HookInvocation.d.ts +15 -0
  39. package/build/api/hooks/HookInvocation.js +38 -0
  40. package/build/api/hooks/flowCheckers.d.ts +3 -2
  41. package/build/api/hooks/flowCheckers.js +1 -1
  42. package/build/api/hooks/invokeHook.d.ts +2 -3
  43. package/build/api/hooks/invokeHook.js +12 -21
  44. package/build/api/hooks/registerHook.d.ts +2 -1
  45. package/build/api/hooks/registerHook.js +1 -1
  46. package/build/api/index.d.ts +5 -7
  47. package/build/api/types/InstanceCreator.d.ts +9 -30
  48. package/build/api/types/InstanceCreator.js +38 -18
  49. package/build/api/types/InstanceModificator.d.ts +2 -1
  50. package/build/api/types/InstanceModificator.js +1 -1
  51. package/build/api/types/Mnemosyne.d.ts +8 -24
  52. package/build/api/types/Mnemosyne.js +15 -8
  53. package/build/api/types/Props.d.ts +2 -2
  54. package/build/api/types/Props.js +7 -3
  55. package/build/api/types/TypeProxy.d.ts +17 -14
  56. package/build/api/types/TypeProxy.js +5 -6
  57. package/build/api/types/compileNewModificatorFunctionBody.d.ts +13 -1
  58. package/build/api/types/compileNewModificatorFunctionBody.js +1 -1
  59. package/build/api/types/createInstanceModificator.d.ts +3 -2
  60. package/build/api/types/createInstanceModificator.js +1 -1
  61. package/build/api/types/index.d.ts +3 -2
  62. package/build/api/types/index.js +91 -73
  63. package/build/api/utils/index.d.ts +5 -2
  64. package/build/api/utils/index.js +1 -1
  65. package/build/constants/index.js +1 -1
  66. package/build/descriptors/errors/index.js +1 -1
  67. package/build/descriptors/index.d.ts +1 -1
  68. package/build/descriptors/index.js +1 -1
  69. package/build/descriptors/types/index.js +20 -17
  70. package/build/index.d.ts +3 -3
  71. package/build/index.js +7 -4
  72. package/build/types/index.d.ts +100 -67
  73. package/build/types/index.js +1 -1
  74. package/build/utils/collectConstructors.js +1 -1
  75. package/build/utils/defineStackCleaner.js +1 -1
  76. package/build/utils/extract.js +1 -1
  77. package/build/utils/hop.js +1 -1
  78. package/build/utils/index.js +3 -2
  79. package/build/utils/merge.js +1 -1
  80. package/build/utils/parent.js +1 -1
  81. package/build/utils/parse.js +9 -4
  82. package/build/utils/pick.js +1 -1
  83. package/build/utils/toJSON.js +1 -1
  84. package/docs/ai-learning-trajectory.md +142 -0
  85. package/docs/async-constructors.md +273 -0
  86. package/docs/purpose.md +676 -0
  87. package/docs/tactica-pattern.md +147 -0
  88. package/docs/typed-lookup.md +157 -0
  89. package/docs/typeomatica.md +622 -0
  90. package/examples/AsyncNewTest.js +12 -0
  91. package/examples/ClassReName.js +23 -0
  92. package/examples/README.md +40 -0
  93. package/examples/v8bug.js +64 -0
  94. package/module/index.js +19 -11
  95. package/package.json +27 -8
  96. package/src/api/errors/exceptionConstructor.ts +271 -0
  97. package/src/api/errors/index.ts +138 -0
  98. package/src/api/errors/throwModificationError.ts +272 -0
  99. package/src/api/hooks/HookInvocation.ts +69 -0
  100. package/src/api/hooks/flowCheckers.ts +27 -0
  101. package/src/api/hooks/index.ts +6 -0
  102. package/src/api/hooks/invokeHook.ts +67 -0
  103. package/src/api/hooks/registerHook.ts +35 -0
  104. package/src/api/index.ts +32 -0
  105. package/src/api/types/InstanceCreator.ts +463 -0
  106. package/src/api/types/InstanceModificator.ts +35 -0
  107. package/src/api/types/Mnemosyne.ts +445 -0
  108. package/src/api/types/Props.ts +262 -0
  109. package/src/api/types/TypeProxy.ts +215 -0
  110. package/src/api/types/compileNewModificatorFunctionBody.ts +215 -0
  111. package/src/api/types/createInstanceModificator.ts +70 -0
  112. package/src/api/types/index.ts +556 -0
  113. package/src/api/utils/index.ts +313 -0
  114. package/src/constants/index.ts +141 -0
  115. package/src/descriptors/errors/index.ts +26 -0
  116. package/src/descriptors/index.ts +13 -0
  117. package/src/descriptors/types/index.ts +319 -0
  118. package/src/index.ts +319 -0
  119. package/src/types/index.ts +538 -0
  120. package/src/utils/collectConstructors.ts +83 -0
  121. package/src/utils/defineStackCleaner.ts +13 -0
  122. package/src/utils/extract.ts +30 -0
  123. package/src/utils/hop.ts +6 -0
  124. package/src/utils/index.ts +55 -0
  125. package/src/utils/merge.ts +29 -0
  126. package/src/utils/parent.ts +42 -0
  127. package/src/utils/parse.ts +85 -0
  128. package/src/utils/pick.ts +36 -0
  129. package/src/utils/toJSON.ts +44 -0
@@ -0,0 +1,583 @@
1
+ # The mnemonica + tactica Type-Safe Pattern
2
+
3
+ > **Project-agnostic reference.** This document applies to every project using mnemonica with tactica.
4
+ > Read it when you are tempted to write `as unknown as` with mnemonica types.
5
+
6
+ ---
7
+
8
+ ## Table of Contents
9
+
10
+ 1. [The Runtime Reality: mnemonica Is a Trie](#1-the-runtime-reality-mnemonica-is-a-trie)
11
+ 2. [The TypeScript Gap](#2-the-typescript-gap)
12
+ 3. [How tactica Bridges the Gap](#3-how-tactica-bridges-the-gap)
13
+ 4. [Declaration Merging: The TypeScript Mechanism](#4-declaration-merging-the-typescript-mechanism)
14
+ 5. [tsconfig.json Setup](#5-tsconfigjson-setup)
15
+ 6. [How lookupTyped Works](#6-how-lookuptyped-works)
16
+ 7. [Why Direct Import Fails](#7-why-direct-import-fails)
17
+ 8. [The Epiphany: Zero-Cast Chaining](#8-the-epiphany-zero-cast-chaining)
18
+ 9. [Common Mistakes](#9-common-mistakes)
19
+ 10. [Cheat Sheet](#10-cheat-sheet)
20
+
21
+ ---
22
+
23
+ ## 1. The Runtime Reality: mnemonica Is a Trie
24
+
25
+ At runtime, mnemonica creates a **Trie data structure** of constructors.
26
+
27
+ ```typescript
28
+ // You define a type
29
+ const RequestData = define('RequestData', function (this: {...}, data) {
30
+ Object.assign(this, data);
31
+ });
32
+
33
+ // You define a child type
34
+ const RouteData = RequestData.define('RouteData', function (this: {...}, data) {
35
+ Object.assign(this, data);
36
+ });
37
+ ```
38
+
39
+ What exists at runtime:
40
+
41
+ ```
42
+ RequestData (constructor)
43
+ ├── RouteData (sub-constructor, accessible as RequestData.RouteData)
44
+ │ ├── PageData (sub-constructor)
45
+ │ │ ├── RenderData (sub-constructor)
46
+ │ │ │ └── ResponseData (sub-constructor)
47
+ ```
48
+
49
+ When you call `new RequestData(data)`, mnemonica:
50
+ 1. Creates an instance with the prototype chain set
51
+ 2. Attaches every sub-constructor as a property on the instance
52
+ 3. The instance itself can spawn children: `new requestData.RouteData(data)`
53
+
54
+ **This is runtime behavior.** It works in JavaScript without any TypeScript involvement.
55
+
56
+ ---
57
+
58
+ ## 2. The TypeScript Gap
59
+
60
+ TypeScript does **not** know about the Trie structure by default.
61
+
62
+ When you write:
63
+
64
+ ```typescript
65
+ import { RequestData } from './collections/requestTypes.js';
66
+ const requestData = new RequestData({ method: 'GET', url: '/' });
67
+ ```
68
+
69
+ TypeScript sees `RequestData` as a constructor function returned by `define()`. It knows the return type is `RequestData` (the instance type), but it does **not** know that:
70
+ - `RequestData` has a `.RouteData` property
71
+ - The instance `requestData` has a `.RouteData` sub-constructor
72
+ - `.RouteData` returns an instance with `.PageData`
73
+ - And so on through the chain
74
+
75
+ So when you write:
76
+
77
+ ```typescript
78
+ const routeData = new requestData.RouteData({ pagePath: '/' });
79
+ ```
80
+
81
+ TypeScript complains: `Property 'RouteData' does not exist on type 'RequestData'`.
82
+
83
+ This is the gap: **mnemonica knows about the Trie, but TypeScript doesn't.**
84
+
85
+ ---
86
+
87
+ ## 3. How tactica Bridges the Gap
88
+
89
+ tactica is an AST analyzer. It scans your codebase for `define()` calls and understands:
90
+ - The type name (`'RequestData'`)
91
+ - The parent type (if any)
92
+ - The `this` properties
93
+ - The constructor parameters
94
+ - The full inheritance chain
95
+
96
+ It generates two files in `.tactica/`:
97
+
98
+ ### 3.1 `.tactica/types.ts`
99
+
100
+ Contains the instance types:
101
+
102
+ ```typescript
103
+ export type RequestData = {
104
+ method: string;
105
+ url: string;
106
+ query: Record<string, unknown>;
107
+ // ...
108
+ };
109
+
110
+ export type RequestData_RouteData = ProtoFlat<RequestData, {
111
+ pagePath: string;
112
+ isMain: boolean;
113
+ deep: string;
114
+ }>;
115
+ ```
116
+
117
+ `ProtoFlat<A, B>` merges the properties of parent `A` and child `B`.
118
+
119
+ ### 3.2 `.tactica/registry.ts`
120
+
121
+ Contains the **TypeRegistry augmentation**:
122
+
123
+ ```typescript
124
+ declare module 'mnemonica' {
125
+ interface TypeRegistry {
126
+ 'RequestData': new (...args: unknown[]) => RequestData;
127
+ 'RequestData_RouteData': new (...args: unknown[]) => RequestData_RouteData;
128
+ 'RequestData_RouteData_PageData': new (...args: unknown[]) => RequestData_RouteData_PageData;
129
+ // ... every type in every chain
130
+ }
131
+ }
132
+ ```
133
+
134
+ **This is the critical file.** It teaches TypeScript about the constructors.
135
+
136
+ ---
137
+
138
+ ## 4. Declaration Merging: The TypeScript Mechanism
139
+
140
+ To understand why `.tactica/registry.ts` works, you need to understand **TypeScript declaration merging**.
141
+
142
+ ### 4.1 What Is Declaration Merging?
143
+
144
+ TypeScript allows multiple declarations with the same name to be **merged** into a single definition:
145
+
146
+ ```typescript
147
+ // File A.ts
148
+ interface Person {
149
+ name: string;
150
+ }
151
+
152
+ // File B.ts
153
+ interface Person {
154
+ age: number;
155
+ }
156
+
157
+ // Result: Person has both name and age
158
+ const p: Person = { name: 'Alice', age: 30 }; // OK
159
+ ```
160
+
161
+ This works for:
162
+ - Interfaces (merged member-wise)
163
+ - Namespaces (merged member-wise)
164
+ - Classes with namespaces
165
+ - **Module augmentations** ← this is what tactica uses
166
+
167
+ ### 4.2 Module Augmentation
168
+
169
+ You can augment an existing module's exports using `declare module`:
170
+
171
+ ```typescript
172
+ // Original module
173
+ // node_modules/some-lib/index.d.ts
174
+ export interface Config {
175
+ timeout: number;
176
+ }
177
+
178
+ // Your augmentation
179
+ // src/augmentation.ts
180
+ declare module 'some-lib' {
181
+ interface Config {
182
+ retries: number;
183
+ }
184
+ }
185
+
186
+ // Now Config has both timeout and retries
187
+ ```
188
+
189
+ ### 4.3 How tactica Uses It
190
+
191
+ mnemonica core defines:
192
+
193
+ ```typescript
194
+ // In mnemonica core (src/types/index.ts)
195
+ export interface TypeRegistry {
196
+ [key: string]: never; // Empty by default, prevents accidental usage
197
+ }
198
+ ```
199
+
200
+ tactica generates:
201
+
202
+ ```typescript
203
+ // In .tactica/registry.ts
204
+ declare module 'mnemonica' {
205
+ interface TypeRegistry {
206
+ 'RequestData': new (...args: unknown[]) => RequestData;
207
+ }
208
+ }
209
+ ```
210
+
211
+ After this augmentation, `TypeRegistry` contains the `'RequestData'` key. Any code that imports from `'mnemonica'` sees the augmented `TypeRegistry`.
212
+
213
+ ### 4.4 Why `[key: string]: never`?
214
+
215
+ The `[key: string]: never` pattern in the base `TypeRegistry` is a TypeScript trick:
216
+
217
+ ```typescript
218
+ interface TypeRegistry {
219
+ [key: string]: never;
220
+ }
221
+
222
+ type Test = TypeRegistry['anything']; // Error: Type 'anything' is not assignable to type 'never'
223
+ ```
224
+
225
+ This means **you cannot use `TypeRegistry` without augmentation**. It forces you to run tactica and generate the registry. Without it, any `lookupTyped('Something')` would be a compile error.
226
+
227
+ Once augmented:
228
+
229
+ ```typescript
230
+ declare module 'mnemonica' {
231
+ interface TypeRegistry {
232
+ 'RequestData': new (...args: unknown[]) => RequestData;
233
+ }
234
+ }
235
+
236
+ type Test = TypeRegistry['RequestData']; // OK — returns the constructor type
237
+ ```
238
+
239
+ ---
240
+
241
+ ## 5. tsconfig.json Setup
242
+
243
+ For declaration merging to work, the `.tactica/registry.ts` file must be included in your TypeScript compilation.
244
+
245
+ ### 5.1 Required Configuration
246
+
247
+ ```json
248
+ {
249
+ "compilerOptions": {
250
+ "module": "NodeNext",
251
+ "moduleResolution": "NodeNext",
252
+ "strict": true,
253
+ "esModuleInterop": true
254
+ },
255
+ "include": [
256
+ "src/**/*",
257
+ ".tactica/**/*"
258
+ ]
259
+ }
260
+ ```
261
+
262
+ **Critical points:**
263
+
264
+ 1. **`.tactica` must be in `include`** — Otherwise TypeScript never sees the augmentation.
265
+
266
+ 2. **`strict: true`** — Ensures the type system is fully active.
267
+
268
+ 3. **`moduleResolution: "NodeNext"`** — Required for modern ESM resolution with `.js` extensions.
269
+
270
+ ### 5.2 Why `include` Matters
271
+
272
+ TypeScript only processes files in the `include` array. If `.tactica/` is not included:
273
+
274
+ ```typescript
275
+ import { lookupTyped } from 'mnemonica';
276
+ const RequestData = lookupTyped('RequestData');
277
+ // Error: 'RequestData' is not assignable to parameter of type 'never'
278
+ ```
279
+
280
+ Because without the augmentation, `TypeRegistry` still has `[key: string]: never`.
281
+
282
+ ---
283
+
284
+ ## 6. How lookupTyped Works
285
+
286
+ ### 6.1 Runtime Behavior
287
+
288
+ ```typescript
289
+ // In mnemonica core (src/index.ts)
290
+ export const lookupTyped = function <const K extends keyof TypeRegistry>(
291
+ TypeNestedPath: K
292
+ ): TypeRegistry[K] {
293
+ // Runtime delegates to lookup()
294
+ return types.lookup(TypeNestedPath as string) as TypeRegistry[K];
295
+ };
296
+ ```
297
+
298
+ At runtime, `lookupTyped('RequestData')`:
299
+ 1. Calls `types.lookup('RequestData')`
300
+ 2. Searches the default types collection
301
+ 3. Returns the constructor function
302
+
303
+ **This is the same constructor as the direct import.** No difference at runtime.
304
+
305
+ ### 6.2 Compile-Time Behavior
306
+
307
+ At compile time, TypeScript sees:
308
+
309
+ ```typescript
310
+ const RequestData = lookupTyped('RequestData');
311
+ ```
312
+
313
+ TypeScript infers:
314
+ - `K` = `'RequestData'` (literal type, thanks to `const`)
315
+ - `keyof TypeRegistry` = all registered type names
316
+ - `TypeRegistry['RequestData']` = `new (...args: unknown[]) => RequestData`
317
+
318
+ So `RequestData` has type `new (...args: unknown[]) => RequestData`.
319
+
320
+ When you then write:
321
+
322
+ ```typescript
323
+ const requestData = new RequestData({ method: 'GET', url: '/' });
324
+ ```
325
+
326
+ TypeScript knows `requestData` is of type `RequestData` (the instance type).
327
+
328
+ ### 6.3 The Constructor Has Sub-Constructors
329
+
330
+ But here's the key: the `.tactica/types.ts` file also defines the instance type to have sub-constructors:
331
+
332
+ ```typescript
333
+ // This is part of how ProtoFlat and the generated types work
334
+ // The RequestData instance type KNOWS it has .RouteData
335
+ ```
336
+
337
+ When TypeScript sees:
338
+
339
+ ```typescript
340
+ const routeData = new requestData.RouteData({ pagePath: '/' });
341
+ ```
342
+
343
+ It checks the type of `requestData` → `RequestData`.
344
+ It checks if `RequestData` has `.RouteData` → Yes, in the generated type.
345
+ It checks the constructor signature → `new (...args: unknown[]) => RequestData_RouteData`.
346
+
347
+ **No cast needed.** TypeScript believes you because the type system was told the truth.
348
+
349
+ ---
350
+
351
+ ## 7. Why Direct Import Fails
352
+
353
+ Let's trace what happens with direct import vs `lookupTyped`.
354
+
355
+ ### 7.1 Direct Import
356
+
357
+ ```typescript
358
+ import { RequestData } from './collections/requestTypes.js';
359
+ // ↑
360
+ // TypeScript sees: typeof RequestData (the constructor)
361
+ // It knows: new (...args: unknown[]) => RequestData
362
+ // It does NOT know: RequestData.RouteData exists
363
+
364
+ const requestData = new RequestData({ ... });
365
+ // TypeScript: requestData is RequestData
366
+
367
+ const routeData = new requestData.RouteData({ ... });
368
+ // TypeScript ERROR: Property 'RouteData' does not exist on type 'RequestData'
369
+ ```
370
+
371
+ The direct import returns the constructor, but TypeScript's type for that constructor doesn't include `.RouteData`. The `define()` function's return type is not augmented with sub-constructor information.
372
+
373
+ ### 7.2 lookupTyped
374
+
375
+ ```typescript
376
+ import { lookupTyped } from 'mnemonica';
377
+
378
+ const RequestData = lookupTyped('RequestData');
379
+ // ↑
380
+ // TypeScript sees: TypeRegistry['RequestData']
381
+ // Which is: new (...args: unknown[]) => RequestData
382
+ // PLUS the instance type RequestData knows about .RouteData
383
+
384
+ const requestData = new RequestData({ ... });
385
+ // TypeScript: requestData is RequestData
386
+
387
+ const routeData = new requestData.RouteData({ ... });
388
+ // TypeScript: OK! RouteData is a known property of RequestData instance
389
+ ```
390
+
391
+ The difference is not in the runtime object. The difference is in **TypeScript's compile-time knowledge**.
392
+
393
+ ---
394
+
395
+ ## 8. The Epiphany: Zero-Cast Chaining
396
+
397
+ The complete pattern, end to end:
398
+
399
+ ### Step 1: Define
400
+
401
+ ```typescript
402
+ // src/collections/requestTypes.ts
403
+ import { define } from 'mnemonica';
404
+
405
+ export const RequestData = define('RequestData', function (
406
+ this: { method: string; url: string },
407
+ data: { method: string; url: string }
408
+ ) {
409
+ Object.assign(this, data);
410
+ });
411
+
412
+ export const RouteData = RequestData.define('RouteData', function (
413
+ this: { pagePath: string },
414
+ data: { pagePath: string }
415
+ ) {
416
+ Object.assign(this, data);
417
+ });
418
+ ```
419
+
420
+ ### Step 2: Generate
421
+
422
+ ```bash
423
+ npx tactica --esm --verbose
424
+ ```
425
+
426
+ Creates:
427
+ - `.tactica/types.ts` — instance types
428
+ - `.tactica/registry.ts` — TypeRegistry augmentation
429
+
430
+ ### Step 3: Lookup
431
+
432
+ ```typescript
433
+ // src/server.ts or any file
434
+ import { lookupTyped } from 'mnemonica';
435
+
436
+ const RequestData = lookupTyped('RequestData');
437
+ ```
438
+
439
+ ### Step 4: Chain (Zero Casts)
440
+
441
+ ```typescript
442
+ const requestData = new RequestData({ method: 'GET', url: '/' });
443
+ const routeData = new requestData.RouteData({ pagePath: '/' });
444
+ const pageData = new routeData.PageData({ header: {...}, content: '' });
445
+ // ... and so on
446
+ ```
447
+
448
+ **Every `.SubType` access is fully typed.** TypeScript knows:
449
+ - The constructor signature
450
+ - The instance properties
451
+ - The next sub-constructor in the chain
452
+
453
+ ---
454
+
455
+ ## 9. Common Mistakes
456
+
457
+ ### 9.1 "I'll just cast it"
458
+
459
+ ```typescript
460
+ // ❌ WRONG
461
+ const requestData = new RequestData({ ... }) as unknown as RequestDataT;
462
+ ```
463
+
464
+ **Why wrong:** You are fighting the type system instead of using it. Every cast is a bug waiting to happen. If the type changes, the cast still compiles but may break at runtime.
465
+
466
+ **Fix:** Use `lookupTyped`.
467
+
468
+ ### 9.2 "I'll import the generated types too"
469
+
470
+ ```typescript
471
+ // ❌ WRONG
472
+ import { RequestData } from './collections/requestTypes.js';
473
+ import type { RequestData as RequestDataT } from '../../.tactica/types.js';
474
+ const requestData = new RequestData({ ... }) as unknown as RequestDataT;
475
+ ```
476
+
477
+ **Why wrong:** You are importing both the runtime constructor AND the generated type, then bridging them with a cast. This is twice the work and still unsafe.
478
+
479
+ **Fix:** Use `lookupTyped` — it gives you both the runtime constructor AND the type in one call.
480
+
481
+ ### 9.3 "lookupTyped only works in route handlers"
482
+
483
+ ```typescript
484
+ // ❌ WRONG (unnecessary)
485
+ app.get('/test', async () => {
486
+ const RequestData = lookupTyped('RequestData');
487
+ const requestData = new RequestData({ ... });
488
+ });
489
+ ```
490
+
491
+ **Why wrong:** `lookupTyped` is a runtime lookup, but it's deterministic and cached. Calling it at module level is perfectly fine and more efficient:
492
+
493
+ ```typescript
494
+ // ✅ CORRECT
495
+ const RequestData = lookupTyped('RequestData');
496
+
497
+ app.get('/test', async () => {
498
+ const requestData = new RequestData({ ... });
499
+ });
500
+ ```
501
+
502
+ ### 9.4 "I need to import the constructor for decoration"
503
+
504
+ ```typescript
505
+ // ❌ WRONG (mixing patterns)
506
+ import { RequestData } from './collections/requestTypes.js';
507
+ const TypedRequestData = lookupTyped('RequestData');
508
+
509
+ app.decorate('RequestData', RequestData); // direct import
510
+ const requestData = new TypedRequestData({ ... }); // lookupTyped
511
+ ```
512
+
513
+ **Why wrong:** You're maintaining two references to the same object.
514
+
515
+ **Fix:** Use `lookupTyped` for everything. The returned constructor is the same object:
516
+
517
+ ```typescript
518
+ // ✅ CORRECT
519
+ const RequestData = lookupTyped('RequestData');
520
+
521
+ app.decorate('RequestData', RequestData);
522
+ const requestData = new RequestData({ ... });
523
+ ```
524
+
525
+ ### 9.5 Forgetting to regenerate
526
+
527
+ ```typescript
528
+ // You add a new property to the define() call
529
+ // But forget to run tactica
530
+ const RequestData = lookupTyped('RequestData');
531
+ const requestData = new RequestData({ newField: 'value' });
532
+ // Error: Object literal may only specify known properties
533
+ ```
534
+
535
+ **Fix:** Always regenerate after modifying `define()` calls:
536
+
537
+ ```bash
538
+ npm run tactica
539
+ ```
540
+
541
+ ---
542
+
543
+ ## 10. Cheat Sheet
544
+
545
+ | I want to... | Do this | Don't do this |
546
+ |---|---|---|
547
+ | Get a typed constructor | `const T = lookupTyped('T')` | `import { T } from './collections/T.js'` |
548
+ | Create an instance | `new T({ ... })` | `new T({ ... }) as unknown as TT` |
549
+ | Chain to a child type | `new instance.Child({ ... })` | `new (instance as any).Child({ ... })` |
550
+ | Decorate Fastify | `app.decorate('T', T)` with `lookupTyped` | Direct import + separate lookupTyped |
551
+ | Add a new property | Modify `define()` → run `tactica` | Modify `define()` + manual cast |
552
+ | Fix "Property does not exist" | Run `tactica` to regenerate | Add `as any` or `as unknown` |
553
+
554
+ ### Build Commands
555
+
556
+ ```bash
557
+ # Regenerate types after modifying define() calls
558
+ npm run tactica
559
+
560
+ # Verify everything compiles
561
+ npx tsc --noEmit
562
+
563
+ # Verify linting
564
+ npx eslint src/
565
+
566
+ # Run tests
567
+ npx vitest run
568
+ ```
569
+
570
+ ---
571
+
572
+ ## Trust
573
+
574
+ This pattern works because:
575
+
576
+ 1. **mnemonica creates a Trie at runtime** — sub-constructors exist, instances can spawn children
577
+ 2. **tactica teaches TypeScript about the Trie** — via declaration merging of `TypeRegistry`
578
+ 3. **lookupTyped retrieves the typed constructor** — compile-time type safety + runtime correctness
579
+ 4. **The same object is returned either way** — `import { T }` and `lookupTyped('T')` are identical at runtime
580
+
581
+ **The only difference is TypeScript's knowledge.** Use `lookupTyped` and let TypeScript help you.
582
+
583
+ If you find yourself writing `as unknown as` with mnemonica types, **you have taken a wrong turn.** Stop. Use `lookupTyped`. Trust the registry.
@@ -0,0 +1,103 @@
1
+ # mnemonica + `lookupTyped`: rules and the underlying pattern
2
+
3
+ > **The real rule:** use `lookupTyped()` plus an augmented `TypeRegistry` — never `import { X } from './collections/...'` plus `as unknown as` casts. The augmentation can be hand-written or generated by [`@mnemonica/tactica`](https://www.npmjs.com/package/@mnemonica/tactica). Tactica is the productivity tool; the **augmentation** is the requirement. For the full mechanism and a side-by-side example, see [`../docs/typed-lookup.md`](../docs/typed-lookup.md).
4
+
5
+ ---
6
+
7
+ ## What this file is
8
+
9
+ The anti-patterns that show up when working with a mnemonica project — and what to do instead. The rules apply regardless of whether you use tactica or hand-augment `TypeRegistry`; the runtime is identical either way.
10
+
11
+ ---
12
+
13
+ ## Why the type-safe path matters
14
+
15
+ mnemonica creates a Trie of constructors at runtime:
16
+
17
+ ```
18
+ RequestData
19
+ └── RouteData
20
+ └── PageData
21
+ └── RenderData
22
+ └── ResponseData
23
+ ```
24
+
25
+ At runtime, this works perfectly — `new requestData.RouteData({ ... })` just works.
26
+
27
+ But TypeScript does not see this Trie. The `define()` function returns a constructor TypeScript understands as a single function — not as something that grows `.RouteData`, `.PageData`, etc. on each instance.
28
+
29
+ The temptation is to cast around the gap:
30
+
31
+ ```typescript
32
+ // ❌ never
33
+ const requestData = new RequestData({ ... }) as unknown as RequestDataT;
34
+ ```
35
+
36
+ That bypasses the type system. The right answer is to teach TypeScript what the Trie looks like — which is exactly what `lookupTyped()` + a `TypeRegistry` augmentation does.
37
+
38
+ ---
39
+
40
+ ## The mechanism in one paragraph
41
+
42
+ `mnemonica` exports `interface TypeRegistry { [key: string]: TypeConstructor<never> }`. `lookupTyped<K extends keyof TypeRegistry>(path: K)` returns `TypeRegistry[K]`. So a `declare module 'mnemonica' { interface TypeRegistry { ... } }` augmentation in your project — by hand, or generated by tactica — gives `lookupTyped('Path.To.Type')` a fully typed return value. Zero casts.
43
+
44
+ After augmentation, TypeScript knows:
45
+ - The constructor arguments for each type
46
+ - The instance properties
47
+ - The sub-constructors on each instance (`.RouteData`, `.PageData`, etc.)
48
+
49
+ See [`../docs/typed-lookup.md`](../docs/typed-lookup.md) for both paths with examples.
50
+
51
+ ---
52
+
53
+ ## When to insist on what
54
+
55
+ When asked to work with a mnemonica project:
56
+
57
+ 1. **Check whether `TypeRegistry` is augmented somewhere.** This can be:
58
+ - A `.tactica/` directory with `registry.ts` (tactica-generated), OR
59
+ - A `.d.ts` or `.ts` file containing `declare module 'mnemonica' { interface TypeRegistry { ... } }` (hand-written)
60
+
61
+ If neither exists, the project has no type-safe path. Ask the user before writing code: do they want tactica set up, or to hand-augment? Either is fine — but pick one before writing types.
62
+
63
+ 2. **If `.tactica/` exists, check `tsconfig.json` includes it.** If missing, add `.tactica/**/*.ts` to `include`.
64
+
65
+ 3. **Check that `lookupTyped` is being used.** Direct imports from collection files + casts are the smell that augmentation is missing or unused.
66
+
67
+ 4. **Never accept `as unknown as` as a solution.** It is a symptom of the augmentation being missing, not a fix.
68
+
69
+ ---
70
+
71
+ ## Quick reference
72
+
73
+ | Situation | Wrong | Right |
74
+ |---|---|---|
75
+ | Get a constructor | `import { T } from './collections/T.js'` | `const T = lookupTyped('T')` |
76
+ | Create instance | `new T({ ... }) as unknown as TT` | `new T({ ... })` |
77
+ | Chain to child | `new (instance as any).Child({ ... })` | `new instance.Child({ ... })` |
78
+ | Type not found | Add `as any` | Augment `TypeRegistry` (manually or run `tactica`) |
79
+
80
+ ---
81
+
82
+ ## Build verification
83
+
84
+ After any change:
85
+
86
+ ```bash
87
+ npm run tactica # only if using tactica — regenerate .tactica/
88
+ npx tsc --noEmit # verify zero TS errors
89
+ npx eslint src/ # verify zero lint errors
90
+ npm test # verify all tests pass
91
+ ```
92
+
93
+ If you are not using tactica, `npm run tactica` is not required; the hand-written augmentation is the source of truth.
94
+
95
+ ---
96
+
97
+ ## Deeper reading
98
+
99
+ - [`../docs/typed-lookup.md`](../docs/typed-lookup.md) — `lookupTyped` with or without tactica (canonical reference, side-by-side)
100
+ - [`../docs/tactica-pattern.md`](../docs/tactica-pattern.md) — human-facing explanation of declaration merging
101
+ - [`./TACTICA-DEEP-DIVE.md`](./TACTICA-DEEP-DIVE.md) — comprehensive technical guide
102
+
103
+ The key insight: **the runtime constructor is identical whether you import it directly or look it up. The only difference is TypeScript's compile-time knowledge.** `lookupTyped` + augmented `TypeRegistry` teaches TypeScript what mnemonica already knows at runtime — and the augmentation can come from anywhere.