@mnemonica/tactica 0.1.0 → 0.1.1
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 +340 -262
- package/lib/analyzer.d.ts +195 -7
- package/lib/analyzer.js +1584 -19
- package/lib/analyzer.js.map +1 -1
- package/lib/cli.d.ts +6 -0
- package/lib/cli.js +202 -6
- package/lib/cli.js.map +1 -1
- package/lib/generator.d.ts +41 -7
- package/lib/generator.js +243 -49
- package/lib/generator.js.map +1 -1
- package/lib/graph.d.ts +4 -0
- package/lib/graph.js +11 -0
- package/lib/graph.js.map +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.js +39 -9
- package/lib/index.js.map +1 -1
- package/lib/topologica-analyzer.d.ts +85 -0
- package/lib/topologica-analyzer.js +509 -0
- package/lib/topologica-analyzer.js.map +1 -0
- package/lib/types.d.ts +110 -0
- package/lib/writer.d.ts +25 -13
- package/lib/writer.js +94 -51
- package/lib/writer.js.map +1 -1
- package/package.json +20 -21
- package/lib/plugin.d.ts +0 -8
- package/lib/plugin.js +0 -128
- package/lib/plugin.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# @mnemonica/tactica
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Type definition generator for Mnemonica.**
|
|
4
4
|
|
|
5
|
-
Tactica generates type definitions for
|
|
5
|
+
Tactica is a CLI tool and Node library that statically analyzes TypeScript / JavaScript source code and generates type definitions, navigation metadata, and runtime-flow data for projects that use [mnemonica](https://github.com/wentout/mnemonica) — the instance-inheritance system that creates types dynamically at runtime through `define()` and `decorate()` calls.
|
|
6
|
+
|
|
7
|
+
Output lives in `.tactica/` and is consumed by:
|
|
8
|
+
|
|
9
|
+
- **`tsc`** — so TypeScript can understand `user.AdminType` after `UserType.define('AdminType', …)`.
|
|
10
|
+
- **`mnemonica.lookupTyped<K>()`** — type-safe runtime lookup via the generated `TypeRegistry` module augmentation.
|
|
11
|
+
- **[mnemographica](https://github.com/mythographica/mnemographica)** (VS Code extension) — for Go to Definition, Find References, and the type-hierarchy graph view.
|
|
6
12
|
|
|
7
13
|
## The Problem
|
|
8
14
|
|
|
9
|
-
Mnemonica enables
|
|
15
|
+
Mnemonica enables instance-level inheritance:
|
|
10
16
|
|
|
11
|
-
```
|
|
17
|
+
```ts
|
|
12
18
|
const UserType = define('UserType', function (this: { name: string }) {
|
|
13
19
|
this.name = '';
|
|
14
20
|
});
|
|
@@ -18,14 +24,14 @@ const AdminType = UserType.define('AdminType', function (this: { role: string })
|
|
|
18
24
|
});
|
|
19
25
|
|
|
20
26
|
const user = new UserType();
|
|
21
|
-
const admin = new user.AdminType(); //
|
|
27
|
+
const admin = new user.AdminType(); // works at runtime
|
|
22
28
|
```
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
TypeScript can't infer that `user.AdminType` exists, because `UserType.define()` is a runtime operation.
|
|
25
31
|
|
|
26
32
|
## The Solution
|
|
27
33
|
|
|
28
|
-
Tactica
|
|
34
|
+
Tactica parses your source, builds the type hierarchy, and emits `.tactica/types.ts` + `.tactica/registry.ts`. Once those files are part of your `tsc` compilation, TypeScript understands the full hierarchy — without runtime changes.
|
|
29
35
|
|
|
30
36
|
## Installation
|
|
31
37
|
|
|
@@ -33,60 +39,29 @@ Tactica analyzes your TypeScript source files and generates declaration files th
|
|
|
33
39
|
npm install --save-dev @mnemonica/tactica
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
### 1. As a TypeScript Language Service Plugin (Recommended)
|
|
39
|
-
|
|
40
|
-
Add to your `tsconfig.json`:
|
|
41
|
-
|
|
42
|
-
```json
|
|
43
|
-
{
|
|
44
|
-
"compilerOptions": {
|
|
45
|
-
"plugins": [
|
|
46
|
-
{
|
|
47
|
-
"name": "@mnemonica/tactica",
|
|
48
|
-
"outputDir": ".tactica",
|
|
49
|
-
"include": ["src/**/*.ts"],
|
|
50
|
-
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Then include the generated types in your project:
|
|
58
|
-
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"compilerOptions": {
|
|
62
|
-
"typeRoots": ["./node_modules/@types", "./.tactica"]
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
```
|
|
42
|
+
`mnemonica` is a peer dependency (>= 1.0.1).
|
|
66
43
|
|
|
67
|
-
|
|
44
|
+
## Usage
|
|
68
45
|
|
|
69
|
-
|
|
46
|
+
### CLI
|
|
70
47
|
|
|
71
48
|
```bash
|
|
49
|
+
# One-shot generation
|
|
72
50
|
npx tactica
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Watch mode for development:
|
|
76
51
|
|
|
77
|
-
|
|
52
|
+
# Watch mode (regenerate on file changes)
|
|
78
53
|
npx tactica --watch
|
|
79
|
-
```
|
|
80
54
|
|
|
81
|
-
|
|
55
|
+
# Custom output directory
|
|
56
|
+
npx tactica --output ./types/mnemonica
|
|
82
57
|
|
|
83
|
-
|
|
84
|
-
npx tactica --project ./src/tsconfig.json
|
|
58
|
+
# Custom tsconfig
|
|
59
|
+
npx tactica --project ./src/tsconfig.json
|
|
85
60
|
```
|
|
86
61
|
|
|
87
|
-
###
|
|
62
|
+
### Library
|
|
88
63
|
|
|
89
|
-
```
|
|
64
|
+
```ts
|
|
90
65
|
import { MnemonicaAnalyzer, TypesGenerator, TypesWriter } from '@mnemonica/tactica';
|
|
91
66
|
import * as ts from 'typescript';
|
|
92
67
|
|
|
@@ -100,112 +75,155 @@ for (const sourceFile of program.getSourceFiles()) {
|
|
|
100
75
|
}
|
|
101
76
|
|
|
102
77
|
const generator = new TypesGenerator(analyzer.getGraph());
|
|
78
|
+
const writer = new TypesWriter('.tactica');
|
|
103
79
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
writer.writeTypesFile(generatedTypes);
|
|
80
|
+
// Default mode — exportable type aliases in types.ts + a TypeRegistry augmentation in registry.ts.
|
|
81
|
+
writer.writeTypesFile(generator.generateTypesFile());
|
|
82
|
+
writer.writeTo('registry.ts', generator.generateTypeRegistry().content);
|
|
108
83
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
writer.writeGlobalAugmentation(generatedGlobal);
|
|
84
|
+
// Legacy mode — single global augmentation file index.d.ts.
|
|
85
|
+
// writer.writeGlobalAugmentation(generator.generateGlobalAugmentation());
|
|
112
86
|
```
|
|
113
87
|
|
|
114
|
-
|
|
88
|
+
### Project setup
|
|
115
89
|
|
|
116
|
-
|
|
90
|
+
After running `npx tactica`, add `.tactica` to your `tsconfig.json` so the generated files are part of the compilation:
|
|
117
91
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"compilerOptions": {
|
|
95
|
+
"paths": {
|
|
96
|
+
"~tactica/*": ["./.tactica/*"]
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"include": ["src/**/*.ts", ".tactica/**/*.ts"]
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Then use `lookupTyped` for type-safe access (see "Type-Safe Lookup" below).
|
|
124
104
|
|
|
125
|
-
|
|
105
|
+
You should commit `.tactica/` if you want type information to flow through CI without re-running tactica; alternatively, add `.tactica/` to `.gitignore` and regenerate as part of your build. Tactica does **not** modify `.gitignore` for you.
|
|
106
|
+
|
|
107
|
+
## CLI Options
|
|
126
108
|
|
|
127
109
|
| Option | Short | Description |
|
|
128
|
-
|
|
129
|
-
| `--watch` | `-w` | Watch mode
|
|
130
|
-
| `--project
|
|
131
|
-
| `--output
|
|
132
|
-
| `--include
|
|
133
|
-
| `--exclude
|
|
134
|
-
| `--
|
|
135
|
-
| `--
|
|
136
|
-
| `--
|
|
137
|
-
|
|
138
|
-
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `--watch` | `-w` | Watch mode — regenerate on file changes |
|
|
112
|
+
| `--project <path>` | `-p` | Path to `tsconfig.json` (default: nearest ancestor) |
|
|
113
|
+
| `--output <dir>` | `-o` | Output directory (default: `.tactica`) |
|
|
114
|
+
| `--include <patterns>` | `-i` | Comma-separated include patterns |
|
|
115
|
+
| `--exclude <patterns>` | `-e` | Comma-separated exclude patterns |
|
|
116
|
+
| `--topologica <dirs>` | `-t` | Comma-separated Topologica directories to scan |
|
|
117
|
+
| `--module-augmentation` | `-m` | Legacy mode — emit a single global-augmentation `index.d.ts` instead of `types.ts` + `registry.ts` |
|
|
118
|
+
| `--esm` | | Append `.js` extensions to relative imports in generated files (for ESM / NodeNext module resolution) |
|
|
119
|
+
| `--eds` | | Force-enable EDS (Execution Data Storage) tracking |
|
|
120
|
+
| `--no-eds` | | Force-disable EDS tracking |
|
|
121
|
+
| `--verbose` | `-v` | Verbose logging |
|
|
122
|
+
| `--help` | `-h` | Show help |
|
|
123
|
+
|
|
124
|
+
EDS tracking is **auto-enabled** when `@mnemonica/dive` is present in `package.json` dependencies; otherwise it is off. `--eds` / `--no-eds` override the auto-detection.
|
|
125
|
+
|
|
126
|
+
## Generated Files
|
|
127
|
+
|
|
128
|
+
Tactica writes everything under the `--output` directory (default `.tactica/`):
|
|
129
|
+
|
|
130
|
+
| File | When | Purpose |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `types.ts` | default mode | Exportable type aliases — one per discovered type. Uses `ProtoFlat<Parent, Self>` for nested types. |
|
|
133
|
+
| `registry.ts` | default mode | `declare module 'mnemonica' { interface TypeRegistry { … } }` augmentation — powers `lookupTyped<K>()`. |
|
|
134
|
+
| `index.ts` | default mode | Re-exports everything from `types.ts` and `registry.ts`. |
|
|
135
|
+
| `index.d.ts` | with `--module-augmentation` | Single global-augmentation file (legacy mode). |
|
|
136
|
+
| `definitions.json` | always | One entry per discovered type: `{ name, location, kind: 'define'\|'decorate', parent, strictChain, blockErrors }`. Consumed by `mnemographica`'s Go to Definition. |
|
|
137
|
+
| `usages.json` | always | One entry per type, value is an array of `{ location, kind, code }` records — where each type is instantiated, referenced, accessed, or looked up. Consumed by `mnemographica`'s Find References. |
|
|
138
|
+
| `flow.json` | always | Native instance-flow patterns (property reads/writes, method calls, destructuring, returns, spreads, etc.) per type. |
|
|
139
|
+
| `eds.json` | when EDS enabled | Execution-flow patterns (`wrap`, `link`, `getLastContext`, hook attachment, framework adapters). Consumed by tools that visualize execution chains. |
|
|
140
|
+
|
|
141
|
+
### Default mode (types.ts + registry.ts)
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// .tactica/types.ts
|
|
145
|
+
export type UserType = {
|
|
146
|
+
name: string;
|
|
147
|
+
email: string;
|
|
148
|
+
AdminType: new (data: { role: string }) => UserType_AdminType;
|
|
149
|
+
};
|
|
139
150
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
export type UserType_AdminType = ProtoFlat<UserType, {
|
|
152
|
+
role: string;
|
|
153
|
+
AdminType: undefined;
|
|
154
|
+
}>;
|
|
155
|
+
```
|
|
143
156
|
|
|
144
|
-
|
|
145
|
-
|
|
157
|
+
```ts
|
|
158
|
+
// .tactica/registry.ts
|
|
159
|
+
declare module 'mnemonica' {
|
|
160
|
+
interface TypeRegistry {
|
|
161
|
+
'UserType': new (data: { name: string; email: string }) => UserType;
|
|
162
|
+
'UserType.AdminType': new (data: { role: string }) => UserType_AdminType;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Recommended for new projects.** Explicit imports, better tree-shaking, no global namespace pollution.
|
|
146
168
|
|
|
147
|
-
|
|
148
|
-
npx tactica --watch --output ./custom-types
|
|
169
|
+
### Legacy global-augmentation mode (`--module-augmentation`)
|
|
149
170
|
|
|
150
|
-
|
|
151
|
-
|
|
171
|
+
Emits one `.tactica/index.d.ts` whose contents live inside `declare global { … }`. Reference it from your code with:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
/// <reference types="./.tactica/index" />
|
|
152
175
|
```
|
|
153
176
|
|
|
154
|
-
|
|
177
|
+
or by adding `./.tactica` to `compilerOptions.typeRoots`. Kept for backwards compatibility with older tactica integrations.
|
|
155
178
|
|
|
156
|
-
|
|
179
|
+
## Type-Safe Lookup with `lookupTyped()`
|
|
157
180
|
|
|
158
|
-
|
|
159
|
-
// Generated by @mnemonica/tactica - DO NOT EDIT
|
|
160
|
-
export type UserTypeInstance = {
|
|
161
|
-
name: string;
|
|
162
|
-
email: string;
|
|
163
|
-
AdminType: TypeConstructor<AdminTypeInstance>;
|
|
164
|
-
}
|
|
181
|
+
Mnemonica core exposes `lookupTyped<K>()` against an empty `TypeRegistry` interface. Tactica's `registry.ts` augments that interface with all of your discovered types:
|
|
165
182
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
183
|
+
```ts
|
|
184
|
+
// .tactica/registry.ts (generated)
|
|
185
|
+
declare module 'mnemonica' {
|
|
186
|
+
interface TypeRegistry {
|
|
187
|
+
'UserType': new (data: { name: string; email: string }) => UserType;
|
|
188
|
+
'UserType.AdminType': new (data: { role: string }) => UserType_AdminType;
|
|
189
|
+
}
|
|
169
190
|
}
|
|
170
191
|
```
|
|
171
192
|
|
|
172
|
-
|
|
193
|
+
Once `registry.ts` is in your `tsc` compilation, the lookup is fully typed:
|
|
173
194
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
- Include in tsconfig.json: `"include": ["src/**/*.ts", ".tactica/types.ts"]`
|
|
178
|
-
- **Recommended for new projects** - explicit imports, better tree-shaking
|
|
195
|
+
```ts
|
|
196
|
+
import { lookupTyped } from 'mnemonica';
|
|
197
|
+
import './.tactica/registry'; // or via tsconfig "include"
|
|
179
198
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
- Types are available without imports (via `declare global`)
|
|
183
|
-
- Add to tsconfig.json: `"typeRoots": ["./node_modules/@types", "./.tactica"]`
|
|
184
|
-
- Use triple-slash reference: `/// <reference types="./.tactica/index" />`
|
|
199
|
+
const UserType = lookupTyped('UserType');
|
|
200
|
+
const AdminType = lookupTyped('UserType.AdminType');
|
|
185
201
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
const user = new UserType({ name: 'John', email: 'j@x.dev' });
|
|
203
|
+
const admin = new user.AdminType({ role: 'admin' });
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
No `as unknown as` casts required. See [`docs/lookupTyped-pattern.md`](docs/lookupTyped-pattern.md) for the consumer-side guide.
|
|
189
207
|
|
|
190
208
|
## What Gets Analyzed
|
|
191
209
|
|
|
192
|
-
###
|
|
210
|
+
### `define()` calls
|
|
193
211
|
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
const UserType = define('UserType', function (this: { name: string }) {
|
|
212
|
+
```ts
|
|
213
|
+
const UserType = define('UserType', function (this: { name: string }) {
|
|
197
214
|
this.name = '';
|
|
198
215
|
});
|
|
199
216
|
|
|
200
|
-
// Nested type
|
|
201
217
|
const AdminType = UserType.define('AdminType', function (this: { role: string }) {
|
|
202
218
|
this.role = 'admin';
|
|
203
219
|
});
|
|
204
220
|
```
|
|
205
221
|
|
|
206
|
-
|
|
222
|
+
Chained calls (`define('A').define('B')`) and nested calls via variable references (`const A = define('A', …); A.define('B', …)`) are both supported.
|
|
223
|
+
|
|
224
|
+
### `@decorate()` decorator
|
|
207
225
|
|
|
208
|
-
```
|
|
226
|
+
```ts
|
|
209
227
|
@decorate()
|
|
210
228
|
class User {
|
|
211
229
|
name: string = '';
|
|
@@ -215,78 +233,57 @@ class User {
|
|
|
215
233
|
class Admin {
|
|
216
234
|
role: string = 'admin';
|
|
217
235
|
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
#### Why Type Casting is Necessary for @decorate()
|
|
221
|
-
|
|
222
|
-
When using `@decorate()` on classes, TypeScript cannot automatically infer that instances have nested type constructors (like `user.Admin`). This is because:
|
|
223
|
-
|
|
224
|
-
1. **`define()` types work automatically**: When you use `define()`, the returned constructor has the correct type signature with nested constructors.
|
|
225
|
-
|
|
226
|
-
2. **`@decorate()` classes need casting**: When you decorate a class, TypeScript sees the class itself, not the augmented type that mnemonica creates at runtime.
|
|
227
|
-
|
|
228
|
-
**The Solution:** Cast to the instance type to access nested constructors:
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
@decorate()
|
|
232
|
-
class Order {
|
|
233
|
-
orderId: string = '';
|
|
234
|
-
total: number = 0;
|
|
235
|
-
}
|
|
236
236
|
|
|
237
|
-
@decorate(
|
|
238
|
-
class
|
|
239
|
-
|
|
237
|
+
@decorate({ blockErrors: true, strictChain: false })
|
|
238
|
+
class Configurable {
|
|
239
|
+
value: string = '';
|
|
240
240
|
}
|
|
241
|
+
```
|
|
241
242
|
|
|
242
|
-
|
|
243
|
-
const order = new Order() as OrderInstance;
|
|
244
|
-
const augmented = new order.AugmentedOrder(); // ✅ Works!
|
|
243
|
+
Decorate-class instances need a cast to the generated instance type to access nested constructors at the type level:
|
|
245
244
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
```ts
|
|
246
|
+
const order = new Order() as Order_AugmentedOrder; // … or whatever your generated alias is
|
|
247
|
+
const sub = new order.AugmentedOrder();
|
|
249
248
|
```
|
|
250
249
|
|
|
251
|
-
|
|
250
|
+
### `Object.assign(this, data)` pattern
|
|
252
251
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
blockErrors: true,
|
|
258
|
-
strictChain: false,
|
|
259
|
-
exposeInstanceMethods: true
|
|
260
|
-
})
|
|
261
|
-
class ConfigurableClass {
|
|
262
|
-
value: string = '';
|
|
263
|
-
}
|
|
252
|
+
```ts
|
|
253
|
+
const UserType = define('UserType', function (this: any, data: { name: string }) {
|
|
254
|
+
Object.assign(this, data); // properties inferred from `data` type
|
|
255
|
+
});
|
|
264
256
|
```
|
|
265
257
|
|
|
266
|
-
###
|
|
258
|
+
### `TypeConstructor` casting (from mnemonica)
|
|
267
259
|
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
260
|
+
```ts
|
|
261
|
+
import { define } from 'mnemonica';
|
|
262
|
+
import type { TypeConstructor } from 'mnemonica';
|
|
263
|
+
|
|
264
|
+
const MyFn = function (this: any) {
|
|
265
|
+
this.field = 123;
|
|
266
|
+
} as TypeConstructor<{ field: number }>;
|
|
267
|
+
|
|
268
|
+
const MyFnType = define('MyFnType', MyFn);
|
|
272
269
|
```
|
|
273
270
|
|
|
274
|
-
|
|
271
|
+
(In older mnemonica documentation this helper was called `ConstructorFunction`; the analyzer still recognizes that name in source code for back-compat.)
|
|
275
272
|
|
|
276
|
-
|
|
273
|
+
### Typeomatica integration
|
|
277
274
|
|
|
278
|
-
|
|
275
|
+
Tactica recognizes [typeomatica](https://github.com/wentout/typeomatica) patterns alongside mnemonica:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
279
278
|
import { decorate } from 'mnemonica';
|
|
280
279
|
import { Strict, BaseClass } from 'typeomatica';
|
|
281
280
|
|
|
282
|
-
// @Strict decorator alongside @decorate
|
|
283
281
|
@decorate()
|
|
284
282
|
@Strict({ someProp: 123 })
|
|
285
283
|
class StrictDecorated {
|
|
286
284
|
someProp!: number;
|
|
287
285
|
}
|
|
288
286
|
|
|
289
|
-
// BaseClass with Object.setPrototypeOf
|
|
290
287
|
@decorate()
|
|
291
288
|
class MyBaseClass {
|
|
292
289
|
base_field = 555;
|
|
@@ -295,114 +292,184 @@ class MyBaseClass {
|
|
|
295
292
|
Object.setPrototypeOf(MyBaseClass.prototype, new BaseClass({ strict: true }));
|
|
296
293
|
```
|
|
297
294
|
|
|
298
|
-
###
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
import { define, ConstructorFunction } from 'mnemonica';
|
|
295
|
+
### Topologica directory structures
|
|
302
296
|
|
|
303
|
-
|
|
304
|
-
this.field = 123;
|
|
305
|
-
} as ConstructorFunction<{ field: number }>;
|
|
297
|
+
[Topologica](https://github.com/mythographica/topologica)-style directory hierarchies are scanned for handler files, and a type tree is built from the directory layout:
|
|
306
298
|
|
|
307
|
-
const MyFnType = define('MyFnType', MyFn);
|
|
308
299
|
```
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
The CLI displays type hierarchy as a tree:
|
|
315
|
-
|
|
316
|
-
```bash
|
|
317
|
-
$ npx tactica
|
|
318
|
-
|
|
319
|
-
Type Hierarchy (Trie):
|
|
320
|
-
└── UserTypeInstance
|
|
321
|
-
└── AdminTypeInstance
|
|
322
|
-
└── SuperAdminTypeInstance
|
|
323
|
-
└── ProductTypeInstance
|
|
324
|
-
├── DigitalProductTypeInstance
|
|
325
|
-
└── PhysicalProductTypeInstance
|
|
300
|
+
ai-types/
|
|
301
|
+
├── Sentience/
|
|
302
|
+
│ ├── index.ts // exports SentienceHandler(this, data)
|
|
303
|
+
│ └── Consciousness/
|
|
304
|
+
│ └── index.ts // exports ConsciousnessHandler(this, data)
|
|
326
305
|
```
|
|
327
306
|
|
|
328
|
-
|
|
307
|
+
Auto-discovered directories (relative to `tsconfig.json`): `./ai-types`, `./types`, `./topologica-types`, plus the same names under `./src/`.
|
|
329
308
|
|
|
330
|
-
|
|
309
|
+
Add custom directories with `--topologica`:
|
|
331
310
|
|
|
332
311
|
```bash
|
|
333
|
-
|
|
312
|
+
npx tactica --topologica ./src/ai-types,./custom/topologica
|
|
334
313
|
```
|
|
335
314
|
|
|
336
|
-
##
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
315
|
+
## Property Type Inference
|
|
316
|
+
|
|
317
|
+
The analyzer infers property types from constructor bodies and class members. Supported expression patterns:
|
|
318
|
+
|
|
319
|
+
| Pattern | Inferred type |
|
|
320
|
+
|---|---|
|
|
321
|
+
| `this.x = 'foo'` | `string` |
|
|
322
|
+
| `this.x = 123` | `number` |
|
|
323
|
+
| `this.x = true` | `boolean` |
|
|
324
|
+
| `this.x = []` / `new Array()` | `Array<…>` |
|
|
325
|
+
| `this.x = {}` | `object` |
|
|
326
|
+
| `this.x = new Date()` | `Date` |
|
|
327
|
+
| `this.x = new Map()` / `new Set()` | `Map<…>` / `Set<…>` |
|
|
328
|
+
| `this.x = Date.now()` | `number` |
|
|
329
|
+
| `this.x = parseInt(…)` / `parseFloat(…)` | `number` |
|
|
330
|
+
| `this.x = String(…)` / `Number(…)` / `Boolean(…)` | `string` / `number` / `boolean` |
|
|
331
|
+
| `this.x = a + b` (where `a, b: number`) | `number` |
|
|
332
|
+
| `` this.x = `${a} ${b}` `` | `string` |
|
|
333
|
+
| `this.x = data.field` | type of `data.field` from the parameter annotation |
|
|
334
|
+
| `this.x = data.field \|\| []` | type of the fallback expression |
|
|
335
|
+
| `this.x = data.field ? a : b` | type of the truthy branch |
|
|
336
|
+
| `Object.assign(this, data)` | all fields of `data`'s type annotation |
|
|
337
|
+
| async / sync constructor functions | same rules |
|
|
338
|
+
|
|
339
|
+
When inference fails the property's type falls back to `unknown` (or `any` in some Topologica paths) — safe, and you can refine manually.
|
|
348
340
|
|
|
349
341
|
## API Reference
|
|
350
342
|
|
|
351
|
-
### MnemonicaAnalyzer
|
|
343
|
+
### `MnemonicaAnalyzer`
|
|
352
344
|
|
|
353
|
-
```
|
|
345
|
+
```ts
|
|
354
346
|
class MnemonicaAnalyzer {
|
|
355
347
|
constructor(program?: ts.Program);
|
|
348
|
+
|
|
356
349
|
analyzeFile(sourceFile: ts.SourceFile): AnalyzeResult;
|
|
357
350
|
analyzeSource(sourceCode: string, fileName?: string): AnalyzeResult;
|
|
351
|
+
|
|
352
|
+
resetUsages(): void; // call between definition pass and usage pass
|
|
353
|
+
addTopologicaType(fullPath: string, node: TypeNode): void;
|
|
354
|
+
|
|
358
355
|
getGraph(): TypeGraphImpl;
|
|
356
|
+
getDefinitions(): Map<string, DefinitionInfo>;
|
|
357
|
+
getUsages(): Map<string, UsageInfo[]>;
|
|
358
|
+
getEDSUsages(): Map<string, EDSInfo[]>;
|
|
359
|
+
getFlowUsages(): Map<string, FlowInfo[]>;
|
|
359
360
|
}
|
|
360
361
|
```
|
|
361
362
|
|
|
362
|
-
###
|
|
363
|
+
### `TopologicaAnalyzer`
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
class TopologicaAnalyzer {
|
|
367
|
+
analyzeDirectory(directoryPath: string): { types: Map<string, TypeNode>; errors: string[] };
|
|
368
|
+
getGraph(): TypeGraphImpl;
|
|
369
|
+
getErrors(): string[];
|
|
370
|
+
}
|
|
371
|
+
```
|
|
363
372
|
|
|
364
|
-
|
|
373
|
+
### `TypeGraphImpl`
|
|
374
|
+
|
|
375
|
+
```ts
|
|
365
376
|
class TypeGraphImpl implements TypeGraph {
|
|
366
|
-
roots:
|
|
377
|
+
roots: Map<string, TypeNode>;
|
|
367
378
|
allTypes: Map<string, TypeNode>;
|
|
379
|
+
|
|
368
380
|
addRoot(node: TypeNode): void;
|
|
369
381
|
addChild(parent: TypeNode, child: TypeNode): void;
|
|
370
382
|
findType(fullPath: string): TypeNode | undefined;
|
|
383
|
+
findTypeByName(name: string): TypeNode | undefined;
|
|
371
384
|
getAllTypes(): TypeNode[];
|
|
385
|
+
clear(): void;
|
|
386
|
+
|
|
372
387
|
*bfs(): Generator<TypeNode>;
|
|
373
388
|
*dfs(node?: TypeNode): Generator<TypeNode>;
|
|
389
|
+
|
|
390
|
+
static createNode(name, parent, sourceFile, line, column): TypeNode;
|
|
374
391
|
}
|
|
375
392
|
```
|
|
376
393
|
|
|
377
|
-
### TypesGenerator
|
|
394
|
+
### `TypesGenerator`
|
|
378
395
|
|
|
379
|
-
```
|
|
396
|
+
```ts
|
|
380
397
|
class TypesGenerator {
|
|
381
|
-
constructor(graph: TypeGraphImpl);
|
|
382
|
-
|
|
398
|
+
constructor(graph: TypeGraphImpl, esm?: boolean);
|
|
399
|
+
|
|
400
|
+
generateTypesFile(): GeneratedTypes; // default mode → types.ts
|
|
401
|
+
generateTypeRegistry(): GeneratedTypes; // default mode → registry.ts
|
|
402
|
+
generateGlobalAugmentation(): GeneratedTypes; // legacy mode → index.d.ts
|
|
383
403
|
generateSingleType(node: TypeNode): string;
|
|
384
404
|
}
|
|
385
405
|
```
|
|
386
406
|
|
|
387
|
-
### TypesWriter
|
|
407
|
+
### `TypesWriter`
|
|
388
408
|
|
|
389
|
-
```
|
|
409
|
+
```ts
|
|
390
410
|
class TypesWriter {
|
|
391
411
|
constructor(outputDir?: string);
|
|
392
|
-
|
|
393
|
-
|
|
412
|
+
|
|
413
|
+
writeTypesFile(generated: GeneratedTypes): string; // → outputDir/types.ts
|
|
414
|
+
writeGlobalAugmentation(generated: GeneratedTypes): string; // → outputDir/index.d.ts
|
|
415
|
+
writeTo(filename: string, content: string): string; // → outputDir/<filename>
|
|
416
|
+
|
|
417
|
+
writeDefinitionsFile(map: Map<string, DefinitionInfo>): string; // → outputDir/definitions.json
|
|
418
|
+
writeUsagesFile (map: Map<string, UsageInfo[]>): string; // → outputDir/usages.json
|
|
419
|
+
writeEDSFile (map: Map<string, EDSInfo[]>): string; // → outputDir/eds.json
|
|
420
|
+
writeFlowFile (map: Map<string, FlowInfo[]>): string; // → outputDir/flow.json
|
|
421
|
+
|
|
422
|
+
write(generated: GeneratedTypes): string; // legacy alias for writeTypesFile
|
|
394
423
|
clean(): void;
|
|
395
424
|
getOutputDir(): string;
|
|
396
425
|
}
|
|
397
426
|
```
|
|
398
427
|
|
|
428
|
+
### Types
|
|
429
|
+
|
|
430
|
+
`TacticaConfig`, `TypeNode`, `TypeGraph`, `PropertyInfo`, `ConstructorParamInfo`, `AnalyzeResult`, `AnalyzeError`, `GeneratedTypes`, `DefinitionInfo`, `UsageInfo`, `UsagesJson`, `DefinitionsJson`, `EDSInfo`, `EDSJson`, `EDSKind`, `FlowInfo`, `FlowJson`, `FlowKind` — all exported from `@mnemonica/tactica`. See [`src/types.ts`](src/types.ts) for the full schema.
|
|
431
|
+
|
|
432
|
+
## EDS (Execution Data Storage) Tracking
|
|
433
|
+
|
|
434
|
+
When enabled, tactica detects execution-flow patterns alongside type definitions. Each detection emits a record into `.tactica/eds.json`.
|
|
435
|
+
|
|
436
|
+
| Function | EDS kind | Description |
|
|
437
|
+
|---|---|---|
|
|
438
|
+
| `wrap(fn)`, `wrapArgs(fn)`, `wrapInstanceMethods(obj)` | `wrap` | Wrap a function / arguments / instance methods for context propagation |
|
|
439
|
+
| `link(parent, child)`, `runWithInstance(inst, fn)` | `link` | Link two instances in the EDS chain |
|
|
440
|
+
| `getLastContext()`, `getErrorInstance(err)` | `contextConsume` | Read the current / error-attached EDS context |
|
|
441
|
+
| `enrichError(err, inst)` | `errorEnrich` | Attach an instance to an error |
|
|
442
|
+
| `attachHooks(types)` | `hookAttach` | Install hooks on the given types |
|
|
443
|
+
| `createDiveInterceptor()` | `adapterUse` | NestJS interceptor adapter |
|
|
444
|
+
| `createDivePlugin()` | `adapterUse` | Fastify plugin adapter |
|
|
445
|
+
| `createDiveMiddleware()` | `adapterUse` | Express middleware adapter |
|
|
446
|
+
|
|
447
|
+
`eds.json` structure:
|
|
448
|
+
|
|
449
|
+
```json
|
|
450
|
+
{
|
|
451
|
+
"version": "1.0",
|
|
452
|
+
"generatedAt": "2026-05-22T…",
|
|
453
|
+
"eds": {
|
|
454
|
+
"UserEntity": [
|
|
455
|
+
{
|
|
456
|
+
"location": "/project/src/queue.ts:45:12",
|
|
457
|
+
"kind": "wrap",
|
|
458
|
+
"code": "wrap(process)",
|
|
459
|
+
"targetType": "UserEntity"
|
|
460
|
+
}
|
|
461
|
+
]
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
399
466
|
## How It Works
|
|
400
467
|
|
|
401
|
-
1. **Parse
|
|
402
|
-
2. **
|
|
403
|
-
3. **Graph
|
|
404
|
-
4. **Generate
|
|
405
|
-
5. **
|
|
468
|
+
1. **Parse** — load `tsconfig.json`, build a `ts.Program`, walk each source file's AST.
|
|
469
|
+
2. **Detect** — find `define()` and `@decorate()` calls, plus `lookupTyped` lookups, `new` expressions, and EDS / flow patterns.
|
|
470
|
+
3. **Graph** — build a Trie of types in `TypeGraphImpl`, with parent links via the chain of `.define()` calls and `@decorate(Parent)` references.
|
|
471
|
+
4. **Generate** — emit `types.ts`, `registry.ts`, `index.ts` (default mode) or `index.d.ts` (legacy mode), plus `definitions.json`, `usages.json`, `flow.json`, and optionally `eds.json`.
|
|
472
|
+
5. **Write** — files land in the output directory (default `.tactica/`).
|
|
406
473
|
|
|
407
474
|
```
|
|
408
475
|
Type Hierarchy (Trie)
|
|
@@ -416,60 +483,71 @@ Type Hierarchy (Trie)
|
|
|
416
483
|
└── properties: { items: Item[] }
|
|
417
484
|
```
|
|
418
485
|
|
|
419
|
-
##
|
|
486
|
+
## Known Limitations
|
|
420
487
|
|
|
421
|
-
###
|
|
488
|
+
### Rest / spread parameters with tuple types
|
|
422
489
|
|
|
423
|
-
|
|
424
|
-
2. Restart TypeScript service (VS Code: Command Palette → "TypeScript: Restart TS Server")
|
|
425
|
-
3. Verify file patterns in `include`/`exclude` config
|
|
490
|
+
The analyzer cannot extract property types from rest parameters that use tuple types:
|
|
426
491
|
|
|
427
|
-
|
|
492
|
+
```ts
|
|
493
|
+
// ❌ Property types resolve to unknown
|
|
494
|
+
export const UserEntity = define('UserEntity', function (
|
|
495
|
+
this: UserEntityInstance,
|
|
496
|
+
...args: [{ id: string; email: string; name: string }, ...unknown[]]
|
|
497
|
+
) {
|
|
498
|
+
const data = args[0];
|
|
499
|
+
Object.assign(this, data);
|
|
500
|
+
});
|
|
501
|
+
```
|
|
428
502
|
|
|
429
|
-
|
|
430
|
-
2. Check that `define()` calls use string literals for type names
|
|
431
|
-
3. Verify property types are valid TypeScript
|
|
503
|
+
**Workaround:** use a direct parameter with the same type annotation.
|
|
432
504
|
|
|
433
|
-
###
|
|
505
|
+
### Deep nested property access
|
|
434
506
|
|
|
435
|
-
|
|
436
|
-
# Test with CLI first
|
|
437
|
-
npx tactica --verbose
|
|
507
|
+
Multi-level property access in constructors falls back to `unknown`:
|
|
438
508
|
|
|
439
|
-
|
|
440
|
-
|
|
509
|
+
```ts
|
|
510
|
+
// ⚠️ this.name's inferred type is unknown
|
|
511
|
+
const UserType = define('UserType', function (this: any, data: { profile: { name: string } }) {
|
|
512
|
+
this.name = data.profile.name;
|
|
513
|
+
});
|
|
441
514
|
```
|
|
442
515
|
|
|
443
|
-
|
|
516
|
+
**Workaround:** flatten the data structure or use a direct parameter.
|
|
444
517
|
|
|
445
|
-
|
|
446
|
-
- [@mnemonica/topologica](https://github.com/mythographica/topologica) - Filesystem-based type discovery
|
|
447
|
-
- [typeomatica](https://github.com/wentout/typeomatica) - Runtime type checking with `@Strict` decorator and `BaseClass`
|
|
518
|
+
### `exposeInstanceMethods` is not parsed
|
|
448
519
|
|
|
449
|
-
|
|
520
|
+
Mnemonica accepts the `exposeInstanceMethods` option at runtime, but tactica's `extractConfig()` only parses `strictChain` and `blockErrors` from `define()` options today. The runtime behavior is unaffected.
|
|
450
521
|
|
|
451
|
-
|
|
522
|
+
### Single-pass analysis without binding
|
|
452
523
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
524
|
+
The analyzer does not use `ts.Program.getTypeChecker()` for resolution, so cross-file type references in unusual shapes may resolve to `unknown`.
|
|
525
|
+
|
|
526
|
+
## Troubleshooting
|
|
527
|
+
|
|
528
|
+
**Generated types are missing properties** — make sure the constructor function has either an explicit `this:` type annotation, or that the data parameter has an inline / aliased type (`{ id: string }` or `Foo` where `type Foo = { id: string }`). Property inference walks one level of indirection.
|
|
529
|
+
|
|
530
|
+
**`new user.AdminType()` shows red squiggles** — confirm `.tactica/types.ts` is in your `tsconfig.json` `include`. If you use `lookupTyped`, also import `.tactica/registry.ts` somewhere.
|
|
531
|
+
|
|
532
|
+
**`mnemographica` (VS Code extension) shows no graph** — confirm `.tactica/definitions.json` and `.tactica/types.ts` exist; run `npx tactica --verbose` to see what was discovered.
|
|
456
533
|
|
|
457
|
-
|
|
458
|
-
|
|
534
|
+
**`@mnemonica/dive` users**: EDS data should appear in `.tactica/eds.json`. If it's missing, run with `--eds --verbose` to force-enable and check the log.
|
|
535
|
+
|
|
536
|
+
## Testing
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
npm test # mocha + chai, all suites
|
|
540
|
+
npm run test:coverage # nyc text report
|
|
541
|
+
npm run test:coverage:html # nyc html report
|
|
459
542
|
```
|
|
460
543
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
-
|
|
464
|
-
-
|
|
465
|
-
-
|
|
466
|
-
-
|
|
467
|
-
- **Typeomatica tests** - Combined mnemonica + typeomatica patterns
|
|
544
|
+
## Related Projects
|
|
545
|
+
|
|
546
|
+
- [mnemonica](https://github.com/wentout/mnemonica) — the instance-inheritance runtime that this generator targets
|
|
547
|
+
- [typeomatica](https://github.com/wentout/typeomatica) — companion runtime type guards (`@Strict`, `BaseClass`)
|
|
548
|
+
- [@mnemonica/topologica](https://github.com/mythographica/topologica) — filesystem-based type discovery (consumed by tactica's `--topologica` mode)
|
|
549
|
+
- [mnemographica](https://github.com/mythographica/mnemographica) — VS Code extension that consumes tactica's `.tactica/` output for IDE features
|
|
468
550
|
|
|
469
551
|
## License
|
|
470
552
|
|
|
471
553
|
MIT
|
|
472
|
-
|
|
473
|
-
## Contributing
|
|
474
|
-
|
|
475
|
-
Contributions welcome! Please read the [Contributing Guide](CONTRIBUTING.md) for details.
|