mnemonica 1.0.6 → 1.0.8

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/.ai/AGENTS.md CHANGED
@@ -132,7 +132,7 @@ Based on observed agent behavior:
132
132
  | [`rules-skill/philosophy.md`](./rules-skill/philosophy.md) | HoTT concepts applied to mnemonica's self-reflection model |
133
133
  | [`rules-skill/ecosystem.md`](./rules-skill/ecosystem.md) | PACT framework: personas, collaboration modes, integration points |
134
134
  | [`rules-skill/contributing.md`](./rules-skill/contributing.md) | Behavioral guidelines for AI contributors |
135
- | [`TACTICA-DEEP-DIVE.md`](./TACTICA-DEEP-DIVE.md) | Comprehensive tactica + lookupTyped technical guide |
135
+ | [`TACTICA-DEEP-DIVE.md`](./TACTICA-DEEP-DIVE.md) | Comprehensive tactica + lookup technical guide |
136
136
  | [`../docs/async-constructors.md`](../docs/async-constructors.md) | Async constructors: super() return values, native class mixing, chains |
137
137
 
138
138
  - Main README: [`../README.md`](../README.md)
package/.ai/ONBOARDING.md CHANGED
@@ -103,7 +103,7 @@ src/
103
103
  Key components:
104
104
  - **TypeProxy** — wraps type constructors
105
105
  - **InstanceCreator** — orchestrates construction lifecycle
106
- - **Mnemosyne** — handles instance method access via Proxy
106
+ - **Mnemosyne** — root memory proxy; stores construction context and resolves subtype lookups
107
107
 
108
108
  ---
109
109
 
@@ -159,6 +159,7 @@ Read [`../docs/async-constructors.md`](../docs/async-constructors.md) for the `s
159
159
  |------|------|
160
160
  | Coding standards, type rules | [`CODE.md`](./CODE.md) |
161
161
  | Design patterns, constraints | [`ARCHITECT.md`](./ARCHITECT.md) |
162
+ | Prototype chain internals | [`PROTOTYPE-CHAIN.md`](./PROTOTYPE-CHAIN.md) |
162
163
  | Debugging commands, issues | [`DEBUG.md`](./DEBUG.md) |
163
164
  | Async constructor deep dive | [`../docs/async-constructors.md`](../docs/async-constructors.md) |
164
165
  | tactica type-safe lookup | [`TACTICA-RULES.md`](./TACTICA-RULES.md) |
@@ -0,0 +1,122 @@
1
+ # Prototype Chain Architecture
2
+
3
+ ## Why this document exists
4
+
5
+ mnemonica instances do not have a normal JavaScript prototype chain. Between every parent instance and child instance there are two intermediate objects. If you are changing construction, props storage, `instanceof`, or subtype lookup, you need to know what those objects are and why they exist.
6
+
7
+ ## High-level shape
8
+
9
+ For a chain `user → admin → superadmin` the instance-level chain is:
10
+
11
+ ```
12
+ superadmin
13
+ └── SuperAdminType.prototype
14
+ └── SuperAdminMemory
15
+ └── admin
16
+ └── AdminType.prototype
17
+ └── AdminMemory
18
+ └── user
19
+ └── UserType.prototype
20
+ └── UserMemory
21
+ └── root Mnemosyne Proxy
22
+ └── Mnemonica instance
23
+ └── Mnemonica.prototype
24
+ └── uranus
25
+ ```
26
+
27
+ `SuperAdminMemory`, `AdminMemory`, and `UserMemory` are the **memory layers**. Only `UserMemory`’s parent is the real `Mnemosyne` constructor result wrapped in a Proxy. The deeper memory layers are plain objects that play the same role.
28
+
29
+ ## The three levels contributed by each type
30
+
31
+ Every mnemonica type adds three levels to the chain:
32
+
33
+ 1. **Instance object** — the object returned by `new parent.ChildType(...)`. Holds only the own properties assigned by the user constructor.
34
+ 2. **`ModificatorType.prototype`** — the user-prototype layer. Holds methods and getters captured at `define()` time. It gets a fresh `constructor` property pointing to the per-construction `ModificatorType` function.
35
+ 3. **Memory layer** — a plain object whose `[[Prototype]]` is the parent instance. It is the `WeakMap` key for internal props and has a `constructor` getter returning `ModificatorType`.
36
+
37
+ ## Files and functions involved
38
+
39
+ - `src/api/types/compileNewModificatorFunctionBody.ts` builds the `ModificatorType`, the actual function or class used for `new`. For function constructors it preserves `new.target`; for class constructors it generates a class that extends the user class so `super(...)` works.
40
+ - `src/api/types/createInstanceModificator.ts` is the default `ModificationConstructor`. It creates the memory layer, wires `_addProps`, copies the captured user prototype onto `ModificatorType.prototype`, and rewires `ModificatorType.prototype` to inherit from the memory layer.
41
+ - `src/api/types/Props.ts` implements `_addProps` and `getProps`. Internal props are stored in a module-level `WeakMap` keyed by the memory-layer object.
42
+ - `src/api/types/Mnemosyne.ts` builds the root memory proxy. Only the root type goes through `createMnemosyne`; all deeper memory layers are plain objects produced by `createInstanceModificator`.
43
+ - `src/api/types/InstanceCreator.ts` orchestrates the pipeline and passes `existentInstance` (the parent instance) into the `ModificationConstructor`.
44
+ - `src/api/types/TypeProxy.ts` is the constructor-like object returned by `define()`. Its `construct` trap creates the root Mnemosyne proxy and then invokes `InstanceCreator`.
45
+
46
+ ## Capturing the user prototype
47
+
48
+ At `define()` time the `TypeDescriptor` stores `proto` — a snapshot of the user constructor’s `.prototype`. During construction that snapshot is copied onto the fresh `ModificatorType.prototype`. The user constructor’s original `.prototype` is restored after construction, so it is never part of the instance chain.
49
+
50
+ This is why the same constructor function can be reused across multiple type definitions without bleeding prototype state.
51
+
52
+ ## Internal props storage
53
+
54
+ `_addProps` receives the memory-layer object and stores a `value` object in the module `WeakMap` keyed by that object. The stored object contains getters for:
55
+
56
+ - `__type__`
57
+ - `__parent__`
58
+ - `__args__`
59
+ - `__timestamp__`
60
+ - `__creator__`
61
+ - `__collection__`
62
+ - `__subtypes__`
63
+ - `__proto_proto__`
64
+ - `__stack__`
65
+
66
+ `getProps(instance)` walks the prototype chain from the instance until it finds the first object with a `WeakMap` entry. That is always the instance’s own memory layer. `parent(instance)` reads `__parent__` from that props object; it is the `existentInstance` passed to `InstanceCreator`.
67
+
68
+ ## `_setSelf` and async construction
69
+
70
+ `_setSelf(instance)` is called at the end of successful construction. It adds one more getter to the props object:
71
+
72
+ ```js
73
+ __self__: () => instance
74
+ ```
75
+
76
+ Then it stores the props object in the `WeakMap` keyed by the instance itself. This solves the async-constructor problem.
77
+
78
+ When a constructor returns a Promise, the initial value of `new Constructor()` is that Promise. `makeAwaiter` waits for resolution, then checks:
79
+
80
+ ```js
81
+ if (props.__self__ !== self.inheritedInstance) {
82
+ self.postProcessing(type);
83
+ }
84
+ ```
85
+
86
+ If `__self__` is missing or does not match the resolved instance, post-processing has not run yet, so it runs validation and hooks. If it matches, post-processing has already happened and is skipped. The constructor can therefore return a Promise, and mnemonica finalizes the instance only after the Promise resolves — without double-initializing or losing the construction context.
87
+
88
+ ## Subtype lookup
89
+
90
+ Only the root has a Proxy. When you access `admin.SomeSubType`, the property lookup walks:
91
+
92
+ ```
93
+ admin → AdminType.prototype → AdminMemory → user → UserType.prototype → UserMemory → root Mnemosyne Proxy
94
+ ```
95
+
96
+ The Proxy’s `get` trap calls `prepareSubtypeForConstruction(prop, receiver)`. It uses `_getProps` to find the memory layer of `receiver` by walking from `Reflect.getPrototypeOf(receiver)`, reads `__subtypes__`, and returns a `SubTypeProxy` that closes over the subtype `TypeDef` and the parent instance.
97
+
98
+ This means the root Proxy serves the entire branch below it.
99
+
100
+ ## Why the root Proxy is kept
101
+
102
+ The root Proxy exists so that **subtypes defined after an instance is created are still visible on that instance**.
103
+
104
+ If subtype constructors were attached to the memory layer at construction time, an instance would only know about the subtypes that existed when it was built. Any `ParentType.define('NewSubType', ...)` call afterward would not appear on existing instances.
105
+
106
+ The Proxy avoids that by doing a live lookup in the type’s `__subtypes__` Map on every property access. Instances do not carry a snapshot of the Trie; they delegate to the current type graph. That decouples instance lifetime from type-graph evolution and is the main reason a Proxy is necessary. The other Proxy-based mechanisms in the codebase could be replaced with simpler constructs, but this live lookup is hard to achieve without a Proxy.
107
+
108
+ ## Classes vs functions
109
+
110
+ The final chain shape is identical for class and function definitions. The difference is only in how `ModificatorType` runs the user constructor:
111
+
112
+ - **Function:** the wrapper temporarily swaps `ConstructHandler.prototype`, calls `new ConstructHandler(...)`, then restores it.
113
+ - **Class:** the generated class `extends ConstructHandler`, calls `super(...)`, then runs the post-construction handler.
114
+
115
+ In both cases the resulting prototype chain is rewired to `instance → ModificatorType.prototype → memoryLayer → parent`.
116
+
117
+ ## Common mistakes
118
+
119
+ - **Assuming internal props are own properties of the instance.** They are stored in a `WeakMap` keyed by the memory layer.
120
+ - **Assuming `instance.constructor` is the user’s original function.** It points to the per-construction `ModificatorType`, which delegates behavior to the user handler.
121
+ - **Thinking the `Mnemosyne` constructor is used for every instance.** It is used once for the root; subtype memory layers are plain objects.
122
+ - **Expecting the user constructor’s `.prototype` to be in the chain.** It is snapshotted and restored; only copies of its descriptors end up on `ModificatorType.prototype`.
@@ -12,7 +12,7 @@
12
12
  3. [How tactica Bridges the Gap](#3-how-tactica-bridges-the-gap)
13
13
  4. [Declaration Merging: The TypeScript Mechanism](#4-declaration-merging-the-typescript-mechanism)
14
14
  5. [tsconfig.json Setup](#5-tsconfigjson-setup)
15
- 6. [How lookupTyped Works](#6-how-lookuptyped-works)
15
+ 6. [How lookup Works](#6-how-lookuptyped-works)
16
16
  7. [Why Direct Import Fails](#7-why-direct-import-fails)
17
17
  8. [The Epiphany: Zero-Cast Chaining](#8-the-epiphany-zero-cast-chaining)
18
18
  9. [Common Mistakes](#9-common-mistakes)
@@ -188,12 +188,12 @@ declare module 'some-lib' {
188
188
 
189
189
  ### 4.3 How tactica Uses It
190
190
 
191
- mnemonica core defines:
191
+ mnemonica core defines an empty `TypeRegistry`:
192
192
 
193
193
  ```typescript
194
- // In mnemonica core (src/types/index.ts)
194
+ // In mnemonica core (src/index.ts)
195
195
  export interface TypeRegistry {
196
- [key: string]: never; // Empty by default, prevents accidental usage
196
+ // Intentionally empty. Augment via declaration merging.
197
197
  }
198
198
  ```
199
199
 
@@ -203,37 +203,33 @@ tactica generates:
203
203
  // In .tactica/registry.ts
204
204
  declare module 'mnemonica' {
205
205
  interface TypeRegistry {
206
- 'RequestData': new (...args: unknown[]) => RequestData;
206
+ 'RequestData': TypeConstructor<RequestData>;
207
207
  }
208
208
  }
209
209
  ```
210
210
 
211
211
  After this augmentation, `TypeRegistry` contains the `'RequestData'` key. Any code that imports from `'mnemonica'` sees the augmented `TypeRegistry`.
212
212
 
213
- ### 4.4 Why `[key: string]: never`?
213
+ ### 4.4 Why an Empty `TypeRegistry`?
214
214
 
215
- The `[key: string]: never` pattern in the base `TypeRegistry` is a TypeScript trick:
215
+ The base `TypeRegistry` is intentionally empty:
216
216
 
217
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'
218
+ export interface TypeRegistry {}
223
219
  ```
224
220
 
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.
221
+ Without augmentation, `lookup('Something')` falls back to the broad `TypeClass | undefined` return type. This keeps the library usable without a registry while still allowing full type inference once you augment it.
226
222
 
227
223
  Once augmented:
228
224
 
229
225
  ```typescript
230
226
  declare module 'mnemonica' {
231
227
  interface TypeRegistry {
232
- 'RequestData': new (...args: unknown[]) => RequestData;
228
+ 'RequestData': TypeConstructor<RequestData>;
233
229
  }
234
230
  }
235
231
 
236
- type Test = TypeRegistry['RequestData']; // OK — returns the constructor type
232
+ const RequestData = lookup('RequestData'); // typed constructor
237
233
  ```
238
234
 
239
235
  ---
@@ -272,30 +268,37 @@ For declaration merging to work, the `.tactica/registry.ts` file must be include
272
268
  TypeScript only processes files in the `include` array. If `.tactica/` is not included:
273
269
 
274
270
  ```typescript
275
- import { lookupTyped } from 'mnemonica';
276
- const RequestData = lookupTyped('RequestData');
277
- // Error: 'RequestData' is not assignable to parameter of type 'never'
271
+ import { lookup } from 'mnemonica';
272
+ const RequestData = lookup('RequestData');
273
+ // RequestData is typed as TypeClass | undefined instead of the concrete constructor
278
274
  ```
279
275
 
280
- Because without the augmentation, `TypeRegistry` still has `[key: string]: never`.
276
+ Because without the augmentation, `TypeRegistry` is empty and `lookup()` falls back to its unaugmented overload.
281
277
 
282
278
  ---
283
279
 
284
- ## 6. How lookupTyped Works
280
+ ## 6. How lookup Works
285
281
 
286
282
  ### 6.1 Runtime Behavior
287
283
 
288
284
  ```typescript
289
285
  // In mnemonica core (src/index.ts)
290
- export const lookupTyped = function <const K extends keyof TypeRegistry>(
286
+ export function lookup<const K extends keyof TypeRegistry>(
287
+ this: unknown,
291
288
  TypeNestedPath: K
292
- ): TypeRegistry[K] {
293
- // Runtime delegates to lookup()
294
- return types.lookup(TypeNestedPath as string) as TypeRegistry[K];
289
+ ): TypeRegistry[K];
290
+ export function lookup(
291
+ this: unknown,
292
+ TypeNestedPath: string
293
+ ): TypeClass | undefined {
294
+ // Runtime delegates to types.lookup()
295
+ const types = checkThis(this) ? defaultTypes : this || defaultTypes;
296
+ return types.lookup(TypeNestedPath);
297
+ }
295
298
  };
296
299
  ```
297
300
 
298
- At runtime, `lookupTyped('RequestData')`:
301
+ At runtime, `lookup('RequestData')`:
299
302
  1. Calls `types.lookup('RequestData')`
300
303
  2. Searches the default types collection
301
304
  3. Returns the constructor function
@@ -307,7 +310,7 @@ At runtime, `lookupTyped('RequestData')`:
307
310
  At compile time, TypeScript sees:
308
311
 
309
312
  ```typescript
310
- const RequestData = lookupTyped('RequestData');
313
+ const RequestData = lookup('RequestData');
311
314
  ```
312
315
 
313
316
  TypeScript infers:
@@ -350,7 +353,7 @@ It checks the constructor signature → `new (...args: unknown[]) => RequestData
350
353
 
351
354
  ## 7. Why Direct Import Fails
352
355
 
353
- Let's trace what happens with direct import vs `lookupTyped`.
356
+ Let's trace what happens with direct import vs `lookup`.
354
357
 
355
358
  ### 7.1 Direct Import
356
359
 
@@ -370,12 +373,12 @@ const routeData = new requestData.RouteData({ ... });
370
373
 
371
374
  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
375
 
373
- ### 7.2 lookupTyped
376
+ ### 7.2 lookup
374
377
 
375
378
  ```typescript
376
- import { lookupTyped } from 'mnemonica';
379
+ import { lookup } from 'mnemonica';
377
380
 
378
- const RequestData = lookupTyped('RequestData');
381
+ const RequestData = lookup('RequestData');
379
382
  // ↑
380
383
  // TypeScript sees: TypeRegistry['RequestData']
381
384
  // Which is: new (...args: unknown[]) => RequestData
@@ -431,9 +434,9 @@ Creates:
431
434
 
432
435
  ```typescript
433
436
  // src/server.ts or any file
434
- import { lookupTyped } from 'mnemonica';
437
+ import { lookup } from 'mnemonica';
435
438
 
436
- const RequestData = lookupTyped('RequestData');
439
+ const RequestData = lookup('RequestData');
437
440
  ```
438
441
 
439
442
  ### Step 4: Chain (Zero Casts)
@@ -463,7 +466,7 @@ const requestData = new RequestData({ ... }) as unknown as RequestDataT;
463
466
 
464
467
  **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
468
 
466
- **Fix:** Use `lookupTyped`.
469
+ **Fix:** Use `lookup`.
467
470
 
468
471
  ### 9.2 "I'll import the generated types too"
469
472
 
@@ -476,23 +479,23 @@ const requestData = new RequestData({ ... }) as unknown as RequestDataT;
476
479
 
477
480
  **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
481
 
479
- **Fix:** Use `lookupTyped` — it gives you both the runtime constructor AND the type in one call.
482
+ **Fix:** Use `lookup` — it gives you both the runtime constructor AND the type in one call.
480
483
 
481
- ### 9.3 "lookupTyped only works in route handlers"
484
+ ### 9.3 "lookup only works in route handlers"
482
485
 
483
486
  ```typescript
484
487
  // ❌ WRONG (unnecessary)
485
488
  app.get('/test', async () => {
486
- const RequestData = lookupTyped('RequestData');
489
+ const RequestData = lookup('RequestData');
487
490
  const requestData = new RequestData({ ... });
488
491
  });
489
492
  ```
490
493
 
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:
494
+ **Why wrong:** `lookup` is a runtime lookup, but it's deterministic and cached. Calling it at module level is perfectly fine and more efficient:
492
495
 
493
496
  ```typescript
494
497
  // ✅ CORRECT
495
- const RequestData = lookupTyped('RequestData');
498
+ const RequestData = lookup('RequestData');
496
499
 
497
500
  app.get('/test', async () => {
498
501
  const requestData = new RequestData({ ... });
@@ -504,19 +507,19 @@ app.get('/test', async () => {
504
507
  ```typescript
505
508
  // ❌ WRONG (mixing patterns)
506
509
  import { RequestData } from './collections/requestTypes.js';
507
- const TypedRequestData = lookupTyped('RequestData');
510
+ const TypedRequestData = lookup('RequestData');
508
511
 
509
512
  app.decorate('RequestData', RequestData); // direct import
510
- const requestData = new TypedRequestData({ ... }); // lookupTyped
513
+ const requestData = new TypedRequestData({ ... }); // lookup
511
514
  ```
512
515
 
513
516
  **Why wrong:** You're maintaining two references to the same object.
514
517
 
515
- **Fix:** Use `lookupTyped` for everything. The returned constructor is the same object:
518
+ **Fix:** Use `lookup` for everything. The returned constructor is the same object:
516
519
 
517
520
  ```typescript
518
521
  // ✅ CORRECT
519
- const RequestData = lookupTyped('RequestData');
522
+ const RequestData = lookup('RequestData');
520
523
 
521
524
  app.decorate('RequestData', RequestData);
522
525
  const requestData = new RequestData({ ... });
@@ -527,7 +530,7 @@ const requestData = new RequestData({ ... });
527
530
  ```typescript
528
531
  // You add a new property to the define() call
529
532
  // But forget to run tactica
530
- const RequestData = lookupTyped('RequestData');
533
+ const RequestData = lookup('RequestData');
531
534
  const requestData = new RequestData({ newField: 'value' });
532
535
  // Error: Object literal may only specify known properties
533
536
  ```
@@ -544,10 +547,10 @@ npm run tactica
544
547
 
545
548
  | I want to... | Do this | Don't do this |
546
549
  |---|---|---|
547
- | Get a typed constructor | `const T = lookupTyped('T')` | `import { T } from './collections/T.js'` |
550
+ | Get a typed constructor | `const T = lookup('T')` | `import { T } from './collections/T.js'` |
548
551
  | Create an instance | `new T({ ... })` | `new T({ ... }) as unknown as TT` |
549
552
  | 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 |
553
+ | Decorate Fastify | `app.decorate('T', T)` with `lookup` | Direct import + separate lookup |
551
554
  | Add a new property | Modify `define()` → run `tactica` | Modify `define()` + manual cast |
552
555
  | Fix "Property does not exist" | Run `tactica` to regenerate | Add `as any` or `as unknown` |
553
556
 
@@ -575,9 +578,9 @@ This pattern works because:
575
578
 
576
579
  1. **mnemonica creates a Trie at runtime** — sub-constructors exist, instances can spawn children
577
580
  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
581
+ 3. **lookup retrieves the typed constructor** — compile-time type safety + runtime correctness
582
+ 4. **The same object is returned either way** — `import { T }` and `lookup('T')` are identical at runtime
580
583
 
581
- **The only difference is TypeScript's knowledge.** Use `lookupTyped` and let TypeScript help you.
584
+ **The only difference is TypeScript's knowledge.** Use `lookup` and let TypeScript help you.
582
585
 
583
- If you find yourself writing `as unknown as` with mnemonica types, **you have taken a wrong turn.** Stop. Use `lookupTyped`. Trust the registry.
586
+ If you find yourself writing `as unknown as` with mnemonica types, **you have taken a wrong turn.** Stop. Use `lookup`. Trust the registry.
@@ -1,6 +1,6 @@
1
- # mnemonica + `lookupTyped`: rules and the underlying pattern
1
+ # mnemonica + `lookup`: rules and the underlying pattern
2
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).
3
+ > **The real rule:** use `lookup()` 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
4
 
5
5
  ---
6
6
 
@@ -33,13 +33,13 @@ The temptation is to cast around the gap:
33
33
  const requestData = new RequestData({ ... }) as unknown as RequestDataT;
34
34
  ```
35
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.
36
+ That bypasses the type system. The right answer is to teach TypeScript what the Trie looks like — which is exactly what `lookup()` + a `TypeRegistry` augmentation does.
37
37
 
38
38
  ---
39
39
 
40
40
  ## The mechanism in one paragraph
41
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.
42
+ `mnemonica` exports an empty `interface TypeRegistry {}`. `lookup<const 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 `lookup('Path.To.Type')` a fully typed return value. Zero casts.
43
43
 
44
44
  After augmentation, TypeScript knows:
45
45
  - The constructor arguments for each type
@@ -62,7 +62,7 @@ When asked to work with a mnemonica project:
62
62
 
63
63
  2. **If `.tactica/` exists, check `tsconfig.json` includes it.** If missing, add `.tactica/**/*.ts` to `include`.
64
64
 
65
- 3. **Check that `lookupTyped` is being used.** Direct imports from collection files + casts are the smell that augmentation is missing or unused.
65
+ 3. **Check that `lookup` is being used.** Direct imports from collection files + casts are the smell that augmentation is missing or unused.
66
66
 
67
67
  4. **Never accept `as unknown as` as a solution.** It is a symptom of the augmentation being missing, not a fix.
68
68
 
@@ -72,7 +72,7 @@ When asked to work with a mnemonica project:
72
72
 
73
73
  | Situation | Wrong | Right |
74
74
  |---|---|---|
75
- | Get a constructor | `import { T } from './collections/T.js'` | `const T = lookupTyped('T')` |
75
+ | Get a constructor | `import { T } from './collections/T.js'` | `const T = lookup('T')` |
76
76
  | Create instance | `new T({ ... }) as unknown as TT` | `new T({ ... })` |
77
77
  | Chain to child | `new (instance as any).Child({ ... })` | `new instance.Child({ ... })` |
78
78
  | Type not found | Add `as any` | Augment `TypeRegistry` (manually or run `tactica`) |
@@ -96,8 +96,8 @@ If you are not using tactica, `npm run tactica` is not required; the hand-writte
96
96
 
97
97
  ## Deeper reading
98
98
 
99
- - [`../docs/typed-lookup.md`](../docs/typed-lookup.md) — `lookupTyped` with or without tactica (canonical reference, side-by-side)
99
+ - [`../docs/typed-lookup.md`](../docs/typed-lookup.md) — `lookup` with or without tactica (canonical reference, side-by-side)
100
100
  - [`../docs/tactica-pattern.md`](../docs/tactica-pattern.md) — human-facing explanation of declaration merging
101
101
  - [`./TACTICA-DEEP-DIVE.md`](./TACTICA-DEEP-DIVE.md) — comprehensive technical guide
102
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.
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.** `lookup` + augmented `TypeRegistry` teaches TypeScript what mnemonica already knows at runtime — and the augmentation can come from anywhere.
@@ -1,47 +1,45 @@
1
1
  ---
2
2
  name: mnemonica-lookup-typed
3
3
  description: |
4
- Type-safe lookup with lookupTyped() and TypeRegistry augmentation.
5
- Use when the user asks about lookupTyped, TypeRegistry, tactica-generated types,
4
+ Type-safe lookup with lookup() and TypeRegistry augmentation.
5
+ Use when the user asks about lookup, TypeRegistry, tactica-generated types,
6
6
  declaration merging, or type-safe constructor retrieval in mnemonica.
7
7
  metadata:
8
8
  tags: [mnemonica, lookup, types, tactica, declaration-merging]
9
9
  ---
10
10
 
11
- # Type-Safe Lookup with lookupTyped()
11
+ # Type-Safe Lookup with `lookup()`
12
12
 
13
- ## lookup() vs lookupTyped()
13
+ ## `lookup()` with and without `TypeRegistry` augmentation
14
14
 
15
15
  ```typescript
16
- import { lookup, lookupTyped } from 'mnemonica';
16
+ import { lookup } from 'mnemonica';
17
17
 
18
- // Runtime only, returns unknown type
18
+ // Without augmentation: returns TypeClass | undefined
19
19
  const SomeType = lookup('SomeType');
20
20
  const instance = new SomeType({ ... }); // no type safety
21
21
 
22
- // Compile-time type safety via TypeRegistry augmentation
23
- const SomeType = lookupTyped('SomeType');
22
+ // With augmentation: returns the typed constructor
23
+ const SomeType = lookup('SomeType');
24
24
  const instance = new SomeType({ ... }); // TypeScript knows the signature
25
25
  ```
26
26
 
27
27
  ## TypeRegistry Pattern
28
28
 
29
- The default `TypeRegistry` uses `[key: string]: TypeConstructor<never>` to
30
- enforce augmentation via declaration merging. Without augmentation, any key
31
- lookup produces a compile-time error because `TypeConstructor<never>` is not
32
- assignable to concrete types.
29
+ The default `TypeRegistry` is an empty interface. Application code or
30
+ `@mnemonica/tactica` augments it through TypeScript declaration merging.
33
31
 
34
32
  ```typescript
35
33
  // In mnemonica core
36
34
  export interface TypeRegistry {
37
- [key: string]: TypeConstructor<never>;
35
+ // Intentionally empty. Augment via declaration merging.
38
36
  }
39
37
 
40
38
  // Generated by tactica in .tactica/registry.ts
41
39
  declare module 'mnemonica' {
42
40
  interface TypeRegistry {
43
- 'UserType': new (...args: unknown[]) => UserTypeInstance;
44
- 'Parent.SubType': new (...args: unknown[]) => SubTypeInstance;
41
+ 'UserType': TypeConstructor<UserTypeInstance>;
42
+ 'Parent.SubType': TypeConstructor<SubTypeInstance>;
45
43
  }
46
44
  }
47
45
  ```
@@ -52,9 +50,9 @@ The axis that actually matters is whether `TypeRegistry` has been augmented for
52
50
 
53
51
  | Feature | Unaugmented `TypeRegistry` | Augmented `TypeRegistry` |
54
52
  |---------|---------------------------|--------------------------|
55
- | Constructor retrieval | `lookup('Name')` returns `unknown` | `lookupTyped('Name')` returns typed constructor |
53
+ | Constructor retrieval | `lookup('Name')` returns `TypeClass \| undefined` | `lookup('Name')` returns typed constructor |
56
54
  | Type safety | Runtime only | Compile-time + runtime |
57
- | Registry default | `[key: string]: TypeConstructor<never>` | Per-key constructor signatures |
55
+ | Registry default | Empty interface | Per-key constructor signatures |
58
56
  | Instance properties | `any` / `unknown` | Fully typed |
59
57
 
60
58
  Two ways to augment: hand-written `.d.ts` (small projects, learning) or [`@mnemonica/tactica`](https://www.npmjs.com/package/@mnemonica/tactica) (auto-generated, recommended for non-trivial projects). See [`../../docs/typed-lookup.md`](../../docs/typed-lookup.md) for both paths side by side.
@@ -65,7 +63,7 @@ Two ways to augment: hand-written `.d.ts` (small projects, learning) or [`@mnemo
65
63
  ```typescript
66
64
  const UserType = lookup('UserType');
67
65
  const user = new UserType({ name: 'John' });
68
- // user is typed as unknown — no IntelliSense for properties
66
+ // user is typed as object — no IntelliSense for properties
69
67
  ```
70
68
 
71
69
  **After** (TypeRegistry augmented — either hand-written or tactica-generated; runtime is identical)
@@ -78,7 +76,7 @@ declare module 'mnemonica' {
78
76
  }
79
77
 
80
78
  // In application code
81
- const UserType = lookupTyped('UserType');
79
+ const UserType = lookup('UserType');
82
80
  const user = new UserType({ name: 'John' });
83
81
  // user is fully typed as UserTypeInstance
84
82
  ```
package/AGENTS.md CHANGED
@@ -53,7 +53,7 @@ Load the docs that match your change type. The wrong context produces broken cod
53
53
  | Involves async constructors | + [`.ai/rules-skill/async-constructors.md`](./.ai/rules-skill/async-constructors.md) + [`.ai/async_init.md`](./.ai/async_init.md) |
54
54
  | Involves TypeScript types | + [`.ai/rules-skill/type-system.md`](./.ai/rules-skill/type-system.md) |
55
55
  | Involves proxy internals | + [`.ai/rules-skill/proxy-architecture.md`](./.ai/rules-skill/proxy-architecture.md) |
56
- | Uses tactica / `lookupTyped` | + [`.ai/TACTICA-RULES.md`](./.ai/TACTICA-RULES.md) |
56
+ | Uses tactica / `lookup` | + [`.ai/TACTICA-RULES.md`](./.ai/TACTICA-RULES.md) |
57
57
  | Docs-only change | README section you're touching only |
58
58
 
59
59
  **This file + `.ai/ONBOARDING.md` are the always-required baseline for any `src/` edit.**
@@ -92,22 +92,28 @@ The core API is `define(TypeName, constructHandler, config?)` in `src/index.ts`.
92
92
  - `.lookup()` - find types by path
93
93
  - `.registerHook()` - register lifecycle hooks
94
94
 
95
- ### The `lookupTyped()` Function
95
+ ### The `lookup()` Function
96
96
 
97
- For user-facing semantics, see [`README.md`](./README.md) and [`.ai/TACTICA-RULES.md`](./.ai/TACTICA-RULES.md). The contributor-relevant detail is the implementation pattern: `TypeRegistry` exposes a `[key: string]: never` index so that any lookup against an unaugmented registry is a compile-time error.
97
+ For user-facing semantics, see [`README.md`](./README.md) and [`.ai/TACTICA-RULES.md`](./.ai/TACTICA-RULES.md). The contributor-relevant detail is the implementation pattern: `TypeRegistry` starts empty, and `lookup()` uses overloads so augmented keys return the typed constructor while unaugmented keys fall back to `TypeClass | undefined`.
98
98
 
99
99
  ```typescript
100
100
  // In mnemonica core (src/index.ts)
101
101
  export interface TypeRegistry {
102
- [key: string]: never; // forces augmentation before keys resolve to real types
102
+ // Intentionally empty. Augment via declaration merging.
103
103
  }
104
104
 
105
- export const lookupTyped = function <const K extends keyof TypeRegistry>(
105
+ export function lookup<const K extends keyof TypeRegistry>(
106
+ this: unknown,
106
107
  TypeNestedPath: K
107
- ): TypeRegistry[K] {
108
- // Runtime delegates to lookup(); type safety is compile-time only.
109
- return types.lookup(TypeNestedPath as string) as TypeRegistry[K];
110
- };
108
+ ): TypeRegistry[K];
109
+ export function lookup(
110
+ this: unknown,
111
+ TypeNestedPath: string
112
+ ): TypeClass | undefined {
113
+ // Runtime delegates to types.lookup(); type safety is compile-time only.
114
+ const types = checkThis(this) ? defaultTypes : this || defaultTypes;
115
+ return types.lookup(TypeNestedPath);
116
+ }
111
117
  ```
112
118
 
113
119
  Tactica generates the augmentation:
@@ -116,15 +122,15 @@ Tactica generates the augmentation:
116
122
  // In .tactica/registry.ts (generated)
117
123
  declare module 'mnemonica' {
118
124
  interface TypeRegistry {
119
- 'UserType': new (...args: unknown[]) => UserTypeInstance;
120
- 'Parent.SubType': new (...args: unknown[]) => SubTypeInstance;
125
+ 'UserType': TypeConstructor<UserTypeInstance>;
126
+ 'Parent.SubType': TypeConstructor<SubTypeInstance>;
121
127
  }
122
128
  }
123
129
  ```
124
130
 
125
- Runtime behavior is identical to `lookup()`; the only difference is the compile-time constraint on the key.
131
+ Runtime behavior is identical whether `TypeRegistry` is augmented or not; the only difference is the compile-time return type.
126
132
 
127
- > **Roadmap.** Nested `lookupTyped()` (a type-safe `.lookupTyped()` method
133
+ > **Roadmap.** Nested `lookup()` (a type-safe `.lookup()` method
128
134
  > on constructors that preserves the prototype chain) is designed but not
129
135
  > yet shipped.
130
136
 
package/CONTRIBUTING.md CHANGED
@@ -51,7 +51,7 @@ behavior that the type system tolerates, and vice versa.
51
51
  Conventional commits are preferred:
52
52
 
53
53
  ```
54
- feat: add lookupTyped overload for nested registry
54
+ feat: add lookup overload for nested registry
55
55
  fix(InstanceCreator): preserve __args__ across async chain
56
56
  docs: clarify instance method opt-in pattern
57
57
  chore(ci): bump setup-node to v4