@mnemonica/tactica 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -171,7 +171,23 @@ declare module 'mnemonica' {
171
171
 
172
172
  **Recommended for new projects.** Explicit imports, better tree-shaking, no global namespace pollution.
173
173
 
174
- **Instance-type inheritance.** Nested instance types flatten the WHOLE ancestor chain: each nested type is `ProtoFlat<ParentInstance, Self>` (mnemonica's `ProtoFlat` keeps every parent key except the ones `Self` overrides), and the parent instance type is itself a `ProtoFlat` over its own parent — so the ROOT's own constructor-arg fields (`uuid` in `PaymentRoot`) are directly present on every descendant instance type, at any depth, alongside intermediate ancestors' fields. If a root field appears missing on a nested type, the usual cause is a stale `.tactica` — regenerate; the composition itself is transitive by construction.
174
+ **Async constructors.** When the construct handler is an `async function` (or async arrow), `new` resolves to a Promise at runtime, so the emitted constructor shapes resolve to `Promise<InstanceType>`: the subtype property on the parent emits `{ new (…): Promise<X>; (…): Promise<X> }` (both twins) and the registry entry emits `new (…) => Promise<X>`. The instance type `X` itself is unchanged, so `await new` yields `X`. Async CLASSES (a class constructor returning a Promise) are not detected — the syntactic shape carries no reliable signal without a type checker — and stay typed as the plain instance type; type them yourself in userland if you need `Promise<X>` there.
175
+
176
+ **Avoid arrow handlers — use the ESLint plugin.** Construct handlers must be regular functions or classes so the instance can be substituted as `this`. Core rejects sync arrows at `define()` time, but async arrows cannot be detected at runtime and never receive the instance — the failure surfaces far from the cause. The early guard is [`eslint-plugin-no-arrow-this`](https://www.npmjs.com/package/eslint-plugin-no-arrow-this), which flags `this` inside arrow functions before the code runs:
177
+
178
+ ```javascript
179
+ plugins: ['eslint-plugin-no-arrow-this'],
180
+ rules: {
181
+ // default: warn on `this` inside ANY arrow function
182
+ 'no-arrow-this/no-arrow-this': 'warn',
183
+ // or restrict to the global/window capture case:
184
+ // 'no-arrow-this/no-arrow-this': ['warn', { onlyGlobals: true }],
185
+ },
186
+ ```
187
+
188
+ Recommended in every mnemonica project.
189
+
190
+ **Instance-type inheritance.** Nested instance types flatten the WHOLE ancestor chain: each nested type is `ProtoFlat<ParentInstance, Self>` (mnemonica's `ProtoFlat` keeps every parent key except the ones `Self` overrides), and the parent instance type is itself a `ProtoFlat` over its own parent — so the ROOT's own constructor-arg fields (`serial` in `WidgetRoot`) are directly present on every descendant instance type, at any depth, alongside intermediate ancestors' fields. If a root field appears missing on a nested type, the usual cause is a stale `.tactica` — regenerate; the composition itself is transitive by construction.
175
191
 
176
192
  ### Legacy global-augmentation mode (`--module-augmentation`)
177
193
 
@@ -394,8 +410,8 @@ class Configurable {
394
410
  Decorate-class instances need a cast to the generated instance type to access nested constructors at the type level:
395
411
 
396
412
  ```ts
397
- const order = new Order() as Order_AugmentedOrder; // … or whatever your generated alias is
398
- const sub = new order.AugmentedOrder();
413
+ const gadget = new Gadget() as Gadget_EnhancedGadget; // … or whatever your generated alias is
414
+ const sub = new gadget.EnhancedGadget();
399
415
  ```
400
416
 
401
417
  ### `Object.assign(this, data)` pattern
@@ -920,8 +936,8 @@ Type Hierarchy (Trie)
920
936
  │ ├── properties: { role: string }
921
937
  │ └── SuperAdminType
922
938
  │ └── properties: { permissions: string[] }
923
- └── OrderType
924
- └── properties: { items: Item[] }
939
+ └── JobType
940
+ └── properties: { tasks: Task[] }
925
941
  ```
926
942
 
927
943
  ## Known Limitations
package/SKILL.md ADDED
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: mnemonica-tactica
3
+ description: |
4
+ Static type generator for mnemonica projects. Use when a project uses
5
+ mnemonica and needs typed lookup(), TypeRegistry augmentation, or
6
+ generated instance types; when `define()`/`lazy()`/`@decorate()` calls
7
+ should become visible to TypeScript; or when the user mentions tactica,
8
+ .tactica output, types.ts / registry.ts generation, or type-safe
9
+ mnemonica navigation.
10
+ metadata:
11
+ tags: [mnemonica, typescript, codegen, type-system, ast, nodejs]
12
+ ---
13
+
14
+ # tactica — usage skill for agents
15
+
16
+ tactica reads TypeScript/JavaScript source that uses mnemonica and
17
+ generates the types TypeScript cannot infer from runtime calls:
18
+ `.tactica/types.ts` (instance types), `.tactica/registry.ts` (module
19
+ augmentation of mnemonica's `TypeRegistry`), plus JSON metadata
20
+ (definitions, usages, hierarchy, scopes, modules, flow, instrumentation)
21
+ consumed by editor extensions and other tools.
22
+
23
+ ## When to use
24
+
25
+ - The project depends on `mnemonica` and calls `define()`, `lazy()`, or
26
+ uses `@decorate()`
27
+ - Code calls `lookup('Some.Type')` and you want the result typed
28
+ - A constructor or subtype type-errors and the fix belongs upstream
29
+ (regenerate `.tactica` before reaching for a cast)
30
+
31
+ ## How to run
32
+
33
+ ```bash
34
+ npx tactica # nearest tsconfig, writes .tactica/
35
+ npx tactica -p tsconfig.json # explicit project
36
+ npx tactica --esm # NodeNext ESM: .js import extensions
37
+ npx tactica -w # watch mode
38
+ ```
39
+
40
+ The generated `types.ts` + `registry.ts` are imported by application
41
+ code; the JSON files are consumed by tooling (graph views, go-to
42
+ definition). Custom types collections are tracked too — with a
43
+ user-declared registry interface (Option B) they emit their own
44
+ augmented interface instead of the global `TypeRegistry`.
45
+
46
+ ## What it types
47
+
48
+ - One `export type` per discovered type, named by dotted path with `_`
49
+ separators; nested types are `ProtoFlat<Parent, Self>`, so ancestor
50
+ fields are present on descendants by construction.
51
+ - Every parent instance type carries its subtype constructors:
52
+ `Child: { new (args): Child; (args): Child }` — the construct form
53
+ and the chain-tip call form.
54
+ - `registry.ts` augments mnemonica's `TypeRegistry` so the free
55
+ `lookup('Parent.Child')` returns the typed constructor.
56
+
57
+ ## Async constructors
58
+
59
+ When a construct handler is an `async function` (or async arrow),
60
+ tactica emits `Promise<X>` for both constructor shapes — the subtype
61
+ property and the registry entry — because `new` resolves to a Promise
62
+ at runtime. The instance type `X` itself is unchanged, so
63
+ `await new` yields `X`.
64
+
65
+ Async **classes** (a class constructor returning a Promise) are NOT
66
+ detected — the syntactic shape carries no reliable signal without a
67
+ type checker — and stay typed as the plain instance type. Type them in
68
+ userland when `Promise<X>` is needed there.
69
+
70
+ ## Avoid arrow handlers — use the ESLint plugin
71
+
72
+ mnemonica construct handlers must be regular functions or classes so the
73
+ instance can be substituted as `this`:
74
+
75
+ - Sync arrow handlers are rejected by mnemonica core at `define()` time
76
+ with a readable error.
77
+ - Async arrow handlers **cannot be detected at runtime** — they look
78
+ identical to `async function` — and they never receive the instance:
79
+ `this` inside an arrow is the outer lexical `this`, so fields land on
80
+ the wrong object and the construction fails later, far from the cause.
81
+
82
+ The early guard is the ESLint plugin `eslint-plugin-no-arrow-this`,
83
+ which flags `this` inside arrow functions before the code runs:
84
+
85
+ ```javascript
86
+ // eslint.config.js (flat) or .eslintrc
87
+ plugins: ['eslint-plugin-no-arrow-this'],
88
+ rules: {
89
+ // default: warn on `this` inside ANY arrow function
90
+ 'no-arrow-this/no-arrow-this': 'warn',
91
+ // or restrict to the global/window capture case:
92
+ // 'no-arrow-this/no-arrow-this': ['warn', { onlyGlobals: true }],
93
+ },
94
+ ```
95
+
96
+ Recommended in every mnemonica project, especially where handlers are
97
+ written by contributors who have not internalized the rule.
98
+
99
+ ## Example
100
+
101
+ ```typescript
102
+ import { mnemonica } from 'mnemonica';
103
+
104
+ export const App = mnemonica
105
+ .define('Widget', function (this: Widget, data: { id: string }) {
106
+ this.id = data.id;
107
+ })
108
+ .define('Gadget', async function (this: Gadget, data: { serial: number }) {
109
+ this.serial = await register(data.serial);
110
+ return this;
111
+ });
112
+ ```
113
+
114
+ After `npx tactica`, `lookup('App.Gadget')` is typed
115
+ `new (data: { serial: number }) => Promise<Gadget>` — a forgotten
116
+ `await` is now a type error at the call site instead of a silent
117
+ `undefined` at runtime.
package/lib/analyzer.d.ts CHANGED
@@ -665,6 +665,14 @@ export declare class MnemonicaAnalyzer {
665
665
  * returned by the lazy getter.
666
666
  */
667
667
  private extractConstructorExpression;
668
+ /**
669
+ * Detect an async constructor handler: the async modifier on a
670
+ * function expression or arrow. Async CLASSES (a class constructor
671
+ * returning a Promise) are deliberately NOT detected — the syntactic
672
+ * class shape gives no reliable signal without a type checker, and the
673
+ * owner decided they are typed by the user in userland.
674
+ */
675
+ private isAsyncConstructHandler;
668
676
  /**
669
677
  * Extract properties from constructor function
670
678
  */
@@ -925,6 +933,11 @@ export declare class MnemonicaAnalyzer {
925
933
  * Get type name from expression (identifier or property access)
926
934
  */
927
935
  private getTypeNameFromExpression;
936
+ /**
937
+ * The one candidate whose parent type is defined in `fileName`, or
938
+ * undefined when none or several qualify.
939
+ */
940
+ private subtypeOwnedByFile;
928
941
  /**
929
942
  * Resolve full type path from property access
930
943
  */