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 +1 -1
- package/.ai/ONBOARDING.md +2 -1
- package/.ai/PROTOTYPE-CHAIN.md +122 -0
- package/.ai/TACTICA-DEEP-DIVE.md +52 -49
- package/.ai/TACTICA-RULES.md +8 -8
- package/.ai/rules-skill/lookup-typed.md +17 -19
- package/AGENTS.md +19 -13
- package/CONTRIBUTING.md +1 -1
- package/FOR_HUMANS.md +51 -20
- package/README.md +14 -12
- package/SKILL.md +4 -4
- package/build/api/types/createInstanceModificator.js +1 -1
- package/build/descriptors/types/index.js +1 -1
- package/build/index.d.ts +6 -7
- package/build/index.js +6 -13
- package/build/types/index.d.ts +25 -2
- package/build/types/index.js +1 -1
- package/build/utils/parent.d.ts +4 -1
- package/build/utils/parent.js +5 -6
- package/docs/UTILS.md +1 -1
- package/docs/ai-learning-trajectory.md +8 -8
- package/docs/async-constructors.md +18 -0
- package/docs/prototype-chain.md +127 -0
- package/docs/purpose.md +4 -4
- package/docs/tactica-pattern.md +10 -10
- package/docs/typed-lookup.md +19 -15
- package/module/index.js +0 -1
- package/package.json +2 -1
- package/src/api/types/createInstanceModificator.ts +13 -0
- package/src/descriptors/types/index.ts +1 -1
- package/src/index.ts +35 -53
- package/src/types/index.ts +88 -2
- package/src/utils/parent.ts +13 -2
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 +
|
|
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** —
|
|
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`.
|
package/.ai/TACTICA-DEEP-DIVE.md
CHANGED
|
@@ -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
|
|
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/
|
|
194
|
+
// In mnemonica core (src/index.ts)
|
|
195
195
|
export interface TypeRegistry {
|
|
196
|
-
|
|
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':
|
|
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
|
|
213
|
+
### 4.4 Why an Empty `TypeRegistry`?
|
|
214
214
|
|
|
215
|
-
The
|
|
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
|
-
|
|
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':
|
|
228
|
+
'RequestData': TypeConstructor<RequestData>;
|
|
233
229
|
}
|
|
234
230
|
}
|
|
235
231
|
|
|
236
|
-
|
|
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 {
|
|
276
|
-
const RequestData =
|
|
277
|
-
//
|
|
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`
|
|
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
|
|
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
|
|
286
|
+
export function lookup<const K extends keyof TypeRegistry>(
|
|
287
|
+
this: unknown,
|
|
291
288
|
TypeNestedPath: K
|
|
292
|
-
): TypeRegistry[K]
|
|
293
|
-
|
|
294
|
-
|
|
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, `
|
|
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 =
|
|
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 `
|
|
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
|
|
376
|
+
### 7.2 lookup
|
|
374
377
|
|
|
375
378
|
```typescript
|
|
376
|
-
import {
|
|
379
|
+
import { lookup } from 'mnemonica';
|
|
377
380
|
|
|
378
|
-
const 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 {
|
|
437
|
+
import { lookup } from 'mnemonica';
|
|
435
438
|
|
|
436
|
-
const 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 `
|
|
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 `
|
|
482
|
+
**Fix:** Use `lookup` — it gives you both the runtime constructor AND the type in one call.
|
|
480
483
|
|
|
481
|
-
### 9.3 "
|
|
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 =
|
|
489
|
+
const RequestData = lookup('RequestData');
|
|
487
490
|
const requestData = new RequestData({ ... });
|
|
488
491
|
});
|
|
489
492
|
```
|
|
490
493
|
|
|
491
|
-
**Why wrong:** `
|
|
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 =
|
|
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 =
|
|
510
|
+
const TypedRequestData = lookup('RequestData');
|
|
508
511
|
|
|
509
512
|
app.decorate('RequestData', RequestData); // direct import
|
|
510
|
-
const requestData = new TypedRequestData({ ... }); //
|
|
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 `
|
|
518
|
+
**Fix:** Use `lookup` for everything. The returned constructor is the same object:
|
|
516
519
|
|
|
517
520
|
```typescript
|
|
518
521
|
// ✅ CORRECT
|
|
519
|
-
const 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 =
|
|
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 =
|
|
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 `
|
|
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. **
|
|
579
|
-
4. **The same object is returned either way** — `import { T }` and `
|
|
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 `
|
|
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 `
|
|
586
|
+
If you find yourself writing `as unknown as` with mnemonica types, **you have taken a wrong turn.** Stop. Use `lookup`. Trust the registry.
|
package/.ai/TACTICA-RULES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# mnemonica + `
|
|
1
|
+
# mnemonica + `lookup`: rules and the underlying pattern
|
|
2
2
|
|
|
3
|
-
> **The real rule:** use `
|
|
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 `
|
|
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 {
|
|
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 `
|
|
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 =
|
|
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) — `
|
|
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.** `
|
|
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
|
|
5
|
-
Use when the user asks about
|
|
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
|
|
11
|
+
# Type-Safe Lookup with `lookup()`
|
|
12
12
|
|
|
13
|
-
## lookup()
|
|
13
|
+
## `lookup()` with and without `TypeRegistry` augmentation
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
|
-
import { lookup
|
|
16
|
+
import { lookup } from 'mnemonica';
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// Without augmentation: returns TypeClass | undefined
|
|
19
19
|
const SomeType = lookup('SomeType');
|
|
20
20
|
const instance = new SomeType({ ... }); // no type safety
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
const 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`
|
|
30
|
-
|
|
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
|
-
|
|
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':
|
|
44
|
-
'Parent.SubType':
|
|
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 `
|
|
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 |
|
|
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
|
|
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 =
|
|
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 / `
|
|
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 `
|
|
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`
|
|
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
|
-
|
|
102
|
+
// Intentionally empty. Augment via declaration merging.
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
export
|
|
105
|
+
export function lookup<const K extends keyof TypeRegistry>(
|
|
106
|
+
this: unknown,
|
|
106
107
|
TypeNestedPath: K
|
|
107
|
-
): TypeRegistry[K]
|
|
108
|
-
|
|
109
|
-
|
|
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':
|
|
120
|
-
'Parent.SubType':
|
|
125
|
+
'UserType': TypeConstructor<UserTypeInstance>;
|
|
126
|
+
'Parent.SubType': TypeConstructor<SubTypeInstance>;
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
```
|
|
124
130
|
|
|
125
|
-
Runtime behavior is identical
|
|
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 `
|
|
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
|
|
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
|