@mnemonica/tactica 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Mythographica
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,475 @@
1
+ # @mnemonica/tactica
2
+
3
+ **TypeScript Language Service Plugin for Mnemonica**
4
+
5
+ Tactica generates type definitions for Mnemonica's dynamic nested constructors, enabling TypeScript to understand runtime type hierarchies created through `define()` and `decorate()` calls.
6
+
7
+ ## The Problem
8
+
9
+ Mnemonica enables powerful instance-level inheritance:
10
+
11
+ ```typescript
12
+ const UserType = define('UserType', function (this: { name: string }) {
13
+ this.name = '';
14
+ });
15
+
16
+ const AdminType = UserType.define('AdminType', function (this: { role: string }) {
17
+ this.role = 'admin';
18
+ });
19
+
20
+ const user = new UserType();
21
+ const admin = new user.AdminType(); // Works at runtime!
22
+ ```
23
+
24
+ But TypeScript doesn't know that `user.AdminType` exists because `UserType.define()` is a runtime operation.
25
+
26
+ ## The Solution
27
+
28
+ Tactica analyzes your TypeScript source files and generates declaration files that tell TypeScript about the nested constructor hierarchy.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install --save-dev @mnemonica/tactica
34
+ ```
35
+
36
+ ## Usage
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
+ ```
66
+
67
+ ### 2. As a CLI Tool
68
+
69
+ Generate types once:
70
+
71
+ ```bash
72
+ npx tactica
73
+ ```
74
+
75
+ Watch mode for development:
76
+
77
+ ```bash
78
+ npx tactica --watch
79
+ ```
80
+
81
+ With custom options:
82
+
83
+ ```bash
84
+ npx tactica --project ./src/tsconfig.json --output ./types/mnemonica
85
+ ```
86
+
87
+ ### 3. As a Module
88
+
89
+ ```typescript
90
+ import { MnemonicaAnalyzer, TypesGenerator, TypesWriter } from '@mnemonica/tactica';
91
+ import * as ts from 'typescript';
92
+
93
+ const program = ts.createProgram(['./src/index.ts'], {});
94
+ const analyzer = new MnemonicaAnalyzer(program);
95
+
96
+ for (const sourceFile of program.getSourceFiles()) {
97
+ if (!sourceFile.isDeclarationFile) {
98
+ analyzer.analyzeFile(sourceFile);
99
+ }
100
+ }
101
+
102
+ const generator = new TypesGenerator(analyzer.getGraph());
103
+
104
+ // Generate types.ts (exportable type aliases - default mode)
105
+ const generatedTypes = generator.generateTypesFile();
106
+ const writer = new TypesWriter('.tactica');
107
+ writer.writeTypesFile(generatedTypes);
108
+
109
+ // Or generate index.d.ts (global augmentation - legacy mode)
110
+ const generatedGlobal = generator.generateGlobalAugmentation();
111
+ writer.writeGlobalAugmentation(generatedGlobal);
112
+ ```
113
+
114
+ ## Configuration Options
115
+
116
+ ### Plugin Options (tsconfig.json)
117
+
118
+ | Option | Type | Default | Description |
119
+ |--------|------|---------|-------------|
120
+ | `outputDir` | string | `.tactica` | Directory for generated types |
121
+ | `include` | string[] | `['**/*.ts']` | File patterns to include |
122
+ | `exclude` | string[] | `['**/*.d.ts']` | File patterns to exclude |
123
+ | `verbose` | boolean | `false` | Enable verbose logging |
124
+
125
+ ### CLI Options
126
+
127
+ | Option | Short | Description |
128
+ |--------|-------|-------------|
129
+ | `--watch` | `-w` | Watch mode - regenerate on file changes |
130
+ | `--project` | `-p` | Path to tsconfig.json |
131
+ | `--output` | `-o` | Output directory (default: `.tactica`) |
132
+ | `--include` | `-i` | Include patterns (comma-separated) |
133
+ | `--exclude` | `-e` | Exclude patterns (comma-separated) |
134
+ | `--module-augmentation` | `-m` | Generate global augmentation (legacy mode) |
135
+ | `--verbose` | `-v` | Enable verbose logging |
136
+ | `--help` | `-h` | Show help message |
137
+
138
+ **Examples:**
139
+
140
+ ```bash
141
+ # Default mode - generates .tactica/types.ts
142
+ npx tactica
143
+
144
+ # Global augmentation mode - generates .tactica/index.d.ts
145
+ npx tactica --module-augmentation
146
+
147
+ # Watch mode with custom output directory
148
+ npx tactica --watch --output ./custom-types
149
+
150
+ # Exclude test files
151
+ npx tactica --exclude "*.test.ts,*.spec.ts"
152
+ ```
153
+
154
+ ## Generated Output
155
+
156
+ By default, Tactica generates `.tactica/types.ts` with exported type aliases:
157
+
158
+ ```typescript
159
+ // Generated by @mnemonica/tactica - DO NOT EDIT
160
+ export type UserTypeInstance = {
161
+ name: string;
162
+ email: string;
163
+ AdminType: TypeConstructor<AdminTypeInstance>;
164
+ }
165
+
166
+ export type AdminTypeInstance = UserTypeInstance & {
167
+ role: string;
168
+ permissions: string[];
169
+ }
170
+ ```
171
+
172
+ ### Output Modes
173
+
174
+ **Default mode** (`npx tactica`):
175
+ - Generates `.tactica/types.ts` - Exportable type aliases
176
+ - Import types explicitly: `import type { UserTypeInstance } from './.tactica/types'`
177
+ - Include in tsconfig.json: `"include": ["src/**/*.ts", ".tactica/types.ts"]`
178
+ - **Recommended for new projects** - explicit imports, better tree-shaking
179
+
180
+ **Global mode** (`npx tactica --module-augmentation`):
181
+ - Generates `.tactica/index.d.ts` - Global type declarations
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" />`
185
+
186
+ **Choosing a mode:**
187
+ - Use **Default mode** for new projects - explicit imports are clearer and work better with tree-shaking
188
+ - Use **Global mode** if you want types available without imports (legacy behavior)
189
+
190
+ ## What Gets Analyzed
191
+
192
+ ### 1. define() Calls
193
+
194
+ ```typescript
195
+ // Root type
196
+ const UserType = define('UserType', function (this: { name: string }) {
197
+ this.name = '';
198
+ });
199
+
200
+ // Nested type
201
+ const AdminType = UserType.define('AdminType', function (this: { role: string }) {
202
+ this.role = 'admin';
203
+ });
204
+ ```
205
+
206
+ ### 2. @decorate() Decorator
207
+
208
+ ```typescript
209
+ @decorate()
210
+ class User {
211
+ name: string = '';
212
+ }
213
+
214
+ @decorate(User)
215
+ class Admin {
216
+ role: string = 'admin';
217
+ }
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
+
237
+ @decorate(Order)
238
+ class AugmentedOrder {
239
+ addition: string = 'extra';
240
+ }
241
+
242
+ // Cast to OrderInstance to access AugmentedOrder constructor
243
+ const order = new Order() as OrderInstance;
244
+ const augmented = new order.AugmentedOrder(); // ✅ Works!
245
+
246
+ // The 'augmented' variable is automatically typed as AugmentedOrderInstance
247
+ console.log(augmented.orderId); // From Order
248
+ console.log(augmented.addition); // From AugmentedOrder
249
+ ```
250
+
251
+ Generated types (like `OrderInstance`, `AugmentedOrderInstance`) are automatically available globally - no imports needed!
252
+
253
+ #### @decorate() with Options
254
+
255
+ ```typescript
256
+ @decorate({
257
+ blockErrors: true,
258
+ strictChain: false,
259
+ exposeInstanceMethods: true
260
+ })
261
+ class ConfigurableClass {
262
+ value: string = '';
263
+ }
264
+ ```
265
+
266
+ ### 3. Object.assign Pattern
267
+
268
+ ```typescript
269
+ const UserType = define('UserType', function (this: any, data: any) {
270
+ Object.assign(this, data);
271
+ });
272
+ ```
273
+
274
+ ### 4. Typeomatica Integration
275
+
276
+ Tactica works seamlessly with Typeomatica patterns:
277
+
278
+ ```typescript
279
+ import { decorate } from 'mnemonica';
280
+ import { Strict, BaseClass } from 'typeomatica';
281
+
282
+ // @Strict decorator alongside @decorate
283
+ @decorate()
284
+ @Strict({ someProp: 123 })
285
+ class StrictDecorated {
286
+ someProp!: number;
287
+ }
288
+
289
+ // BaseClass with Object.setPrototypeOf
290
+ @decorate()
291
+ class MyBaseClass {
292
+ base_field = 555;
293
+ }
294
+
295
+ Object.setPrototypeOf(MyBaseClass.prototype, new BaseClass({ strict: true }));
296
+ ```
297
+
298
+ ### 5. ConstructorFunction Pattern
299
+
300
+ ```typescript
301
+ import { define, ConstructorFunction } from 'mnemonica';
302
+
303
+ const MyFn = function (this: any) {
304
+ this.field = 123;
305
+ } as ConstructorFunction<{ field: number }>;
306
+
307
+ const MyFnType = define('MyFnType', MyFn);
308
+ ```
309
+
310
+ ## CLI Features
311
+
312
+ ### Tree Output
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
326
+ ```
327
+
328
+ ### Code Coverage
329
+
330
+ Run tests with coverage:
331
+
332
+ ```bash
333
+ npm run test:coverage
334
+ ```
335
+
336
+ ## Integration with Your Workflow
337
+
338
+ ### .gitignore
339
+
340
+ Tactica automatically adds `.tactica/` to your `.gitignore` if not already present.
341
+
342
+ ### IDE Support
343
+
344
+ With the Language Service Plugin:
345
+ - **VS Code**: Automatic type updates on file save
346
+ - **WebStorm**: Works with TypeScript service
347
+ - **Vim/Neovim**: Works with coc.nvim, nvim-lspconfig
348
+
349
+ ## API Reference
350
+
351
+ ### MnemonicaAnalyzer
352
+
353
+ ```typescript
354
+ class MnemonicaAnalyzer {
355
+ constructor(program?: ts.Program);
356
+ analyzeFile(sourceFile: ts.SourceFile): AnalyzeResult;
357
+ analyzeSource(sourceCode: string, fileName?: string): AnalyzeResult;
358
+ getGraph(): TypeGraphImpl;
359
+ }
360
+ ```
361
+
362
+ ### TypeGraphImpl
363
+
364
+ ```typescript
365
+ class TypeGraphImpl implements TypeGraph {
366
+ roots: Map<string, TypeNode>;
367
+ allTypes: Map<string, TypeNode>;
368
+ addRoot(node: TypeNode): void;
369
+ addChild(parent: TypeNode, child: TypeNode): void;
370
+ findType(fullPath: string): TypeNode | undefined;
371
+ getAllTypes(): TypeNode[];
372
+ *bfs(): Generator<TypeNode>;
373
+ *dfs(node?: TypeNode): Generator<TypeNode>;
374
+ }
375
+ ```
376
+
377
+ ### TypesGenerator
378
+
379
+ ```typescript
380
+ class TypesGenerator {
381
+ constructor(graph: TypeGraphImpl);
382
+ generate(): GeneratedTypes;
383
+ generateSingleType(node: TypeNode): string;
384
+ }
385
+ ```
386
+
387
+ ### TypesWriter
388
+
389
+ ```typescript
390
+ class TypesWriter {
391
+ constructor(outputDir?: string);
392
+ write(generated: GeneratedTypes): string;
393
+ writeTo(filename: string, content: string): string;
394
+ clean(): void;
395
+ getOutputDir(): string;
396
+ }
397
+ ```
398
+
399
+ ## How It Works
400
+
401
+ 1. **Parse**: TypeScript AST is parsed to find `define()` and `decorate()` calls
402
+ 2. **Analyze**: The analyzer extracts type names, properties, and hierarchy
403
+ 3. **Graph**: A Trie (tree) structure represents the type hierarchy
404
+ 4. **Generate**: TypeScript declarations are generated from the graph
405
+ 5. **Output**: Files are written to `.tactica/` directory
406
+
407
+ ```
408
+ Type Hierarchy (Trie)
409
+ ├── UserType
410
+ │ ├── properties: { name: string }
411
+ │ └── AdminType
412
+ │ ├── properties: { role: string }
413
+ │ └── SuperAdminType
414
+ │ └── properties: { permissions: string[] }
415
+ └── OrderType
416
+ └── properties: { items: Item[] }
417
+ ```
418
+
419
+ ## Troubleshooting
420
+
421
+ ### Types not updating
422
+
423
+ 1. Check that the plugin is loaded in `tsconfig.json`
424
+ 2. Restart TypeScript service (VS Code: Command Palette → "TypeScript: Restart TS Server")
425
+ 3. Verify file patterns in `include`/`exclude` config
426
+
427
+ ### Generated types have errors
428
+
429
+ 1. Ensure all mnemonica types have explicit type annotations
430
+ 2. Check that `define()` calls use string literals for type names
431
+ 3. Verify property types are valid TypeScript
432
+
433
+ ### Plugin not working
434
+
435
+ ```bash
436
+ # Test with CLI first
437
+ npx tactica --verbose
438
+
439
+ # Check for parsing errors
440
+ npx tactica --verbose 2>&1 | grep -i error
441
+ ```
442
+
443
+ ## Related Projects
444
+
445
+ - [mnemonica](https://github.com/wentout/mnemonica) - Instance inheritance system
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`
448
+
449
+ ## Testing
450
+
451
+ Tactica includes comprehensive test coverage:
452
+
453
+ ```bash
454
+ # Run all tests
455
+ npm test
456
+
457
+ # Run with coverage
458
+ npm run test:coverage
459
+ ```
460
+
461
+ Test suites include:
462
+ - **Analyzer tests** - Core AST parsing functionality
463
+ - **Generator tests** - TypeScript declaration generation
464
+ - **Writer tests** - File I/O operations
465
+ - **Integration tests** - End-to-end workflows
466
+ - **Example tests** - Patterns from `tactica-test/` project
467
+ - **Typeomatica tests** - Combined mnemonica + typeomatica patterns
468
+
469
+ ## License
470
+
471
+ MIT
472
+
473
+ ## Contributing
474
+
475
+ Contributions welcome! Please read the [Contributing Guide](CONTRIBUTING.md) for details.
@@ -0,0 +1,75 @@
1
+ import * as ts from 'typescript';
2
+ import { AnalyzeResult } from './types';
3
+ import { TypeGraphImpl } from './graph';
4
+ /**
5
+ * AST Analyzer for finding Mnemonica define() and decorate() calls
6
+ */
7
+ export declare class MnemonicaAnalyzer {
8
+ private errors;
9
+ private graph;
10
+ constructor(program?: ts.Program);
11
+ /**
12
+ * Analyze a source file for Mnemonica type definitions
13
+ */
14
+ analyzeFile(sourceFile: ts.SourceFile): AnalyzeResult;
15
+ /**
16
+ * Analyze source code string
17
+ */
18
+ analyzeSource(sourceCode: string, fileName?: string): AnalyzeResult;
19
+ /**
20
+ * Get the type graph
21
+ */
22
+ getGraph(): TypeGraphImpl;
23
+ /**
24
+ * Visit a node in the AST
25
+ */
26
+ private visitNode;
27
+ /**
28
+ * Check if a node is a define() call
29
+ */
30
+ private isDefineCall;
31
+ /**
32
+ * Check if a node is a @decorate() decorator
33
+ */
34
+ private isDecorateDecorator;
35
+ /**
36
+ * Process a define() call
37
+ */
38
+ private processDefineCall;
39
+ /**
40
+ * Process a @decorate() decorator
41
+ */
42
+ private processDecorateDecorator;
43
+ /**
44
+ * Extract type name from define() call arguments
45
+ */
46
+ private extractTypeName;
47
+ /**
48
+ * Find parent type node for nested define calls
49
+ */
50
+ private findParentType;
51
+ /**
52
+ * Get property chain from nested access
53
+ */
54
+ private getPropertyChain;
55
+ /**
56
+ * Extract properties from constructor function
57
+ */
58
+ private extractProperties;
59
+ /**
60
+ * Extract property assignment from statement
61
+ */
62
+ private extractPropertyFromStatement;
63
+ /**
64
+ * Extract properties from class declaration
65
+ */
66
+ private extractClassProperties;
67
+ /**
68
+ * Infer TypeScript type from type node
69
+ */
70
+ private inferType;
71
+ /**
72
+ * Infer type from initializer
73
+ */
74
+ private inferTypeFromInitializer;
75
+ }