@chemx/starter-kit 26.9.21-8 → 26.9.24-393

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.
Files changed (138) hide show
  1. package/cli/audit/ast-visitors.js +10 -17
  2. package/cli/audit/autofix.js +17 -2
  3. package/cli/audit/clone-detector.js +64 -0
  4. package/cli/audit/clone-detector.spec.js +46 -0
  5. package/cli/audit/history.js +22 -6
  6. package/cli/audit/hook-shape-validator.js +312 -0
  7. package/cli/audit/prompts.js +13 -10
  8. package/cli/audit/prompts.spec.js +52 -1
  9. package/cli/audit/reporter-clones.js +33 -0
  10. package/cli/audit/reporter-hotspot-graph.js +34 -0
  11. package/cli/audit/rules-registry.js +6 -1
  12. package/cli/audit/rules.js +10 -2
  13. package/cli/audit/rules.spec.js +129 -0
  14. package/cli/audit-stages.spec.js +30 -0
  15. package/cli/audit.js +12 -3
  16. package/cli/commands/cmd-audit.js +56 -12
  17. package/cli/commands/cmd-project.js +100 -0
  18. package/cli/commands/cmd-router.js +52 -3
  19. package/cli/commands-schema.js +28 -0
  20. package/cli/create.spec.js +2 -2
  21. package/cli/exploder.js +258 -0
  22. package/cli/exploder.spec.js +99 -0
  23. package/cli/generator-compact.spec.js +104 -0
  24. package/cli/generator-framework.spec.js +72 -0
  25. package/cli/generator-templates/archetypes/collection-archetypes.js +9 -9
  26. package/cli/generator-templates/archetypes/index.js +34 -4
  27. package/cli/generator-templates/archetypes/input-archetypes.js +2 -2
  28. package/cli/generator-templates/archetypes/layout-archetypes.js +2 -2
  29. package/cli/generator-templates/archetypes/minimal-archetypes.js +131 -0
  30. package/cli/generator-templates/archetypes/status-archetypes.js +4 -4
  31. package/cli/generator-templates/archetypes/vector-matcher.js +50 -0
  32. package/cli/generator-templates/archetypes/vector-matcher.spec.js +25 -0
  33. package/cli/generator-templates/compact.js +130 -0
  34. package/cli/generator-templates/controller-converters.js +93 -0
  35. package/cli/generator-templates/controller.js +2 -64
  36. package/cli/generator-templates/react.js +1 -1
  37. package/cli/generator-templates/svelte.js +1 -1
  38. package/cli/generator-templates/types.js +2 -2
  39. package/cli/generator-templates/vue.js +1 -1
  40. package/cli/generator-templates.js +1 -0
  41. package/cli/generator-templates.spec.js +6 -1
  42. package/cli/generator.js +94 -7
  43. package/cli/help.js +1 -1
  44. package/cli/index.js +23 -8
  45. package/cli/installer-templates.js +1 -1
  46. package/cli/mcp/antigravity.js +5 -2
  47. package/cli/mcp/installer.js +7 -7
  48. package/cli/mcp/installer.spec.js +3 -3
  49. package/cli/mcp/manifests.js +4 -2
  50. package/cli/mcp/prompts.js +1 -1
  51. package/cli/mcp/server.js +2 -0
  52. package/cli/mcp/tools-audit.js +97 -0
  53. package/cli/mcp/tools-generate.js +62 -0
  54. package/cli/mcp/tools-patch.js +72 -0
  55. package/cli/mcp/tools-patterns.js +71 -0
  56. package/cli/mcp/tools-project.js +75 -0
  57. package/cli/mcp/tools-project.spec.js +51 -0
  58. package/cli/mcp/tools-q-modes.js +69 -0
  59. package/cli/mcp/tools-q.js +58 -0
  60. package/cli/mcp/tools-read.js +69 -0
  61. package/cli/mcp/tools-search-util.js +35 -0
  62. package/cli/mcp/tools-search.js +15 -294
  63. package/cli/mcp/tools-team-feed.js +57 -0
  64. package/cli/mcp/tools-team-locks.js +63 -0
  65. package/cli/mcp/tools-team-mailbox.js +32 -0
  66. package/cli/mcp/tools-team-tasks.js +85 -0
  67. package/cli/mcp/tools-team.js +37 -183
  68. package/cli/mcp/tools-verify.js +47 -0
  69. package/cli/mcp/tools-write.js +33 -0
  70. package/cli/mcp/tools.js +79 -372
  71. package/cli/mutators.js +10 -23
  72. package/cli/pillars-schema.js +3 -3
  73. package/cli/project-detector.js +47 -0
  74. package/cli/reader-shorthand.spec.js +60 -0
  75. package/cli/reader.js +67 -24
  76. package/cli/search-bulletproof.spec.js +104 -0
  77. package/cli/search-db.js +33 -5
  78. package/cli/search-queries-hotspot-graph.js +65 -0
  79. package/cli/search-queries-hotspot-graph.spec.js +66 -0
  80. package/cli/search-queries-similar.js +84 -0
  81. package/cli/search-queries-similar.spec.js +43 -0
  82. package/cli/search-schema.js +79 -11
  83. package/cli/search.js +13 -2
  84. package/cli/team/index.js +9 -0
  85. package/cli/team/team-commands.js +106 -64
  86. package/cli/team/team-concurrency-stress.spec.js +95 -0
  87. package/cli/team/team-db-lock-promotion.js +15 -11
  88. package/cli/team/team-db-mailbox.js +86 -0
  89. package/cli/team/team-db-task-helpers.js +80 -28
  90. package/cli/team/team-db-tasks.js +13 -5
  91. package/cli/team/team-flags.js +72 -0
  92. package/cli/team/team-format.js +87 -46
  93. package/cli/team/team-mailbox-empirical.spec.js +191 -0
  94. package/cli/team/team-mailbox.spec.js +93 -0
  95. package/cli/team/team-memory.js +84 -0
  96. package/cli/team/team-memory.spec.js +74 -0
  97. package/cli/team/team-projects-coordinator.js +100 -0
  98. package/cli/team/team-projects-format.js +45 -0
  99. package/cli/team/team-projects-memory.js +70 -0
  100. package/cli/team/team-projects.js +94 -0
  101. package/cli/team/team-projects.spec.js +116 -0
  102. package/cli/team/team-schema-memory.js +25 -0
  103. package/cli/team/team-schema-projects.js +50 -0
  104. package/cli/team/team-schema.js +33 -14
  105. package/cli/team/team-tasks-hierarchy.spec.js +176 -0
  106. package/cli/team/team-tokens.js +95 -0
  107. package/cli/team/team-triage.js +274 -52
  108. package/cli/team/team-verification-gate.spec.js +253 -0
  109. package/cli/team/team.spec.js +1 -1
  110. package/cli/tesseract-hud.js +59 -0
  111. package/cli/tesseract-manifesto.js +61 -0
  112. package/cli/tesseract-state.js +58 -0
  113. package/cli/tesseract.js +81 -0
  114. package/cli/tesseract.spec.js +55 -0
  115. package/cli/trend.js +84 -0
  116. package/cli/trend.spec.js +51 -0
  117. package/cli/ui-actions-tasks.js +13 -5
  118. package/cli/ui-actions.js +10 -2
  119. package/cli/ui-client-api.js +2 -2
  120. package/cli/ui-client-script.js +12 -15
  121. package/cli/ui-e2e-verification.spec.js +13 -3
  122. package/cli/ui-filetree.spec.js +5 -5
  123. package/cli/ui-handlers.js +10 -2
  124. package/cli/ui-html.js +26 -34
  125. package/cli/ui-index.html +905 -0
  126. package/cli/ui-kanban.spec.js +12 -12
  127. package/cli/ui-server.js +19 -3
  128. package/cli/ui-sse.js +57 -0
  129. package/cli/ui-sse.spec.js +74 -0
  130. package/cli/ui-styles.js +195 -81
  131. package/cli/ui-template-kanban.js +15 -1
  132. package/cli/ui-vbulletin.spec.js +12 -13
  133. package/cli/ui.spec.js +12 -5
  134. package/cli/verify.js +19 -8
  135. package/package.json +3 -1
  136. package/scripts/check-framework-generation.mjs +195 -0
  137. package/scripts/pre-commit.sh +2 -7
  138. package/scripts/publish-both.mjs +4 -0
@@ -63,7 +63,7 @@ describe('Scaffolding Entrypoint Invocations', () => {
63
63
  const result = spawnSync('node', [CREATE_BIN, '--help'], {
64
64
  encoding: 'utf-8',
65
65
  cwd: tmpBase,
66
- timeout: 5000
66
+ timeout: 15000
67
67
  });
68
68
 
69
69
  assert.equal(result.status, 0, 'Should exit with code 0 on --help');
@@ -78,7 +78,7 @@ describe('Scaffolding Entrypoint Invocations', () => {
78
78
  const result = spawnSync('node', [CREATE_BIN, '--version'], {
79
79
  encoding: 'utf-8',
80
80
  cwd: tmpBase,
81
- timeout: 5000
81
+ timeout: 15000
82
82
  });
83
83
 
84
84
  assert.equal(result.status, 0, 'Should exit with code 0 on --version');
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Chemical X Capsule Exploder
3
+ * Atomically unpacks a "liquid" single-file compact capsule into a standard crystalline multi-file directory capsule.
4
+ * Transactional: Uses .temp.bak and rolls back if post-unpack verification fails.
5
+ */
6
+
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import * as babelParser from '@babel/parser';
10
+ import { toPascalCase } from './generator-templates/naming.js';
11
+ import { buildComponentSpec, buildIndex } from './generator-templates/specs.js';
12
+ import { buildTypesIndex } from './generator-templates/types.js';
13
+ import { detectTestRunner } from './project-detector.js';
14
+
15
+ export const parseCompactFile = (content, ext = 'tsx') => {
16
+ const isTsx = ext === 'tsx' || ext === 'jsx';
17
+ const plugins = ['typescript'];
18
+ if (isTsx) plugins.push('jsx');
19
+
20
+ const ast = babelParser.parse(content, {
21
+ sourceType: 'module',
22
+ plugins
23
+ });
24
+
25
+ const lines = content.split('\n');
26
+ const getSlice = (node) => {
27
+ return lines.slice(node.loc.start.line - 1, node.loc.end.line).join('\n');
28
+ };
29
+
30
+ const typesProps = [];
31
+ const typesState = [];
32
+ let controllerHookCode = null;
33
+ let componentCode = null;
34
+ const reactImports = new Set();
35
+
36
+ for (const node of ast.program.body) {
37
+ if (node.type === 'ImportDeclaration') {
38
+ if (node.source.value === 'react') {
39
+ for (const spec of node.specifiers) {
40
+ if (spec.imported?.name) reactImports.add(spec.imported.name);
41
+ }
42
+ }
43
+ continue;
44
+ }
45
+
46
+ // Capture TypeScript interfaces and type aliases
47
+ const isExportedType = node.type === 'ExportNamedDeclaration' &&
48
+ (node.declaration?.type === 'TSInterfaceDeclaration' || node.declaration?.type === 'TSTypeAliasDeclaration');
49
+
50
+ if (isExportedType) {
51
+ const typeName = node.declaration.id?.name || '';
52
+ const slice = getSlice(node);
53
+ const isPropsOrEmits = typeName.includes('Props') || typeName.includes('Emits');
54
+ if (isPropsOrEmits) {
55
+ typesProps.push(slice);
56
+ } else {
57
+ typesState.push(slice);
58
+ }
59
+ continue;
60
+ }
61
+
62
+ // Capture Controller Hook
63
+ const isExportedVar = node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration';
64
+ if (isExportedVar) {
65
+ const decl = node.declaration.declarations[0];
66
+ const varName = decl?.id?.name || '';
67
+ const isControllerHook = varName.startsWith('use') && varName.endsWith('Controller');
68
+ if (isControllerHook) {
69
+ controllerHookCode = getSlice(node);
70
+ continue;
71
+ }
72
+ if (varName && !varName.startsWith('use')) {
73
+ componentCode = getSlice(node);
74
+ continue;
75
+ }
76
+ }
77
+
78
+ if (node.type === 'ExportDefaultDeclaration') {
79
+ continue;
80
+ }
81
+ }
82
+
83
+ return {
84
+ typesProps: typesProps.join('\n\n'),
85
+ typesState: typesState.join('\n\n'),
86
+ controllerHookCode,
87
+ componentCode,
88
+ reactImports: Array.from(reactImports)
89
+ };
90
+ };
91
+
92
+ export const explodeCapsule = (targetFilePath, options = {}) => {
93
+ const cwd = options.cwd || process.cwd();
94
+ const absPath = path.resolve(cwd, targetFilePath);
95
+
96
+ if (!fs.existsSync(absPath)) {
97
+ throw new Error(`Target file not found at ${absPath}`);
98
+ }
99
+
100
+ const stat = fs.statSync(absPath);
101
+ if (stat.isDirectory()) {
102
+ throw new Error(`Target ${targetFilePath} is already a directory capsule.`);
103
+ }
104
+
105
+ const fileName = path.basename(absPath);
106
+ const ext = path.extname(absPath).replace('.', '');
107
+ const capsuleName = fileName.replace(/\.[^.]+$/, '');
108
+ const parentDir = path.dirname(absPath);
109
+ const targetDir = path.join(parentDir, capsuleName);
110
+ const bakPath = `${absPath}.temp.bak`;
111
+
112
+ if (fs.existsSync(targetDir)) {
113
+ throw new Error(`Destination directory ${capsuleName} already exists.`);
114
+ }
115
+
116
+ const content = fs.readFileSync(absPath, 'utf-8');
117
+ const baseSlug = capsuleName.replace(/^[a-z]+-/, '');
118
+ const pascalName = toPascalCase(baseSlug);
119
+
120
+ if (options.dryRun) {
121
+ parseCompactFile(content, ext);
122
+ return {
123
+ success: true,
124
+ dryRun: true,
125
+ capsuleName,
126
+ targetDir: path.relative(cwd, targetDir),
127
+ files: [
128
+ `${capsuleName}.${ext}`,
129
+ `${capsuleName}.controller.ts`,
130
+ `${capsuleName}.spec.ts`,
131
+ 'index.ts',
132
+ 'types/props.d.ts',
133
+ 'types/state.d.ts',
134
+ 'types/index.ts',
135
+ 'types.d.ts'
136
+ ]
137
+ };
138
+ }
139
+
140
+ // 1. Transactional Backup Snapshot
141
+ fs.copyFileSync(absPath, bakPath);
142
+
143
+ try {
144
+ const parsed = parseCompactFile(content, ext);
145
+
146
+ // 2. Create Directory Capsule Structure
147
+ fs.mkdirSync(targetDir, { recursive: true });
148
+ const typesDir = path.join(targetDir, 'types');
149
+ fs.mkdirSync(typesDir, { recursive: true });
150
+
151
+ // 3. Types Files
152
+ let propsContent = parsed.typesProps || `export interface ${pascalName}Props {\n readonly className?: string;\n}\n`;
153
+ const usesReactNode = propsContent.includes('ReactNode') && !propsContent.includes("from 'react'");
154
+ if (usesReactNode) {
155
+ propsContent = `import type { ReactNode } from 'react';\n\n${propsContent}`;
156
+ }
157
+ const stateContent = parsed.typesState || `export interface ${pascalName}State {\n readonly isActive: boolean;\n}\n`;
158
+
159
+ fs.writeFileSync(path.join(typesDir, 'props.d.ts'), `${propsContent}\n`, 'utf-8');
160
+ fs.writeFileSync(path.join(typesDir, 'state.d.ts'), `${stateContent}\n`, 'utf-8');
161
+ fs.writeFileSync(path.join(typesDir, 'index.ts'), buildTypesIndex(['props', 'state']), 'utf-8');
162
+ fs.writeFileSync(path.join(targetDir, 'types.d.ts'), "export * from './types/index';\n", 'utf-8');
163
+
164
+ // 4. Controller File
165
+ const hookCode = parsed.controllerHookCode ||
166
+ `export const use${pascalName}Controller = () => {\n const [isActive, setIsActive] = useState<boolean>(false);\n return { isActive };\n};`;
167
+ const controllerImports = "import { useState, useMemo } from 'react';\n";
168
+ fs.writeFileSync(path.join(targetDir, `${capsuleName}.controller.ts`), `${controllerImports}\n${hookCode}\n`, 'utf-8');
169
+
170
+ // 5. Component File
171
+ const compImports = `import type { ${pascalName}Props } from './types';\nimport { use${pascalName}Controller } from './${capsuleName}.controller';\n\n`;
172
+ const compBody = parsed.componentCode || `export const ${pascalName} = ({ className = '' }: ${pascalName}Props) => {\n return <div className="${capsuleName}" />;\n};`;
173
+ const compContent = `${compImports}${compBody}\n\nexport default ${pascalName};\n`;
174
+ fs.writeFileSync(path.join(targetDir, `${capsuleName}.${ext}`), compContent, 'utf-8');
175
+
176
+ // 6. Index & Spec
177
+ const hasController = Boolean(parsed.controllerHookCode);
178
+ const runner = detectTestRunner(targetDir);
179
+ fs.writeFileSync(path.join(targetDir, 'index.ts'), buildIndex(capsuleName, pascalName, ext, hasController), 'utf-8');
180
+ fs.writeFileSync(path.join(targetDir, `${capsuleName}.spec.ts`), buildComponentSpec(capsuleName, pascalName, hasController, runner), 'utf-8');
181
+
182
+ // 7. Verify Unpacked Directory (Syntactic AST Parse Verification)
183
+ babelParser.parse(fs.readFileSync(path.join(targetDir, `${capsuleName}.${ext}`), 'utf-8'), {
184
+ sourceType: 'module',
185
+ plugins: ['typescript', 'jsx']
186
+ });
187
+
188
+ // 8. Success: Delete source file and backup
189
+ fs.unlinkSync(absPath);
190
+ if (fs.existsSync(bakPath)) fs.unlinkSync(bakPath);
191
+
192
+ return {
193
+ success: true,
194
+ capsuleName,
195
+ targetDir: path.relative(cwd, targetDir),
196
+ files: [
197
+ `${capsuleName}.${ext}`,
198
+ `${capsuleName}.controller.ts`,
199
+ `${capsuleName}.spec.ts`,
200
+ 'index.ts',
201
+ 'types/props.d.ts',
202
+ 'types/state.d.ts',
203
+ 'types/index.ts',
204
+ 'types.d.ts'
205
+ ]
206
+ };
207
+ } catch (err) {
208
+ // Transactional Rollback
209
+ if (fs.existsSync(targetDir)) {
210
+ fs.rmSync(targetDir, { recursive: true, force: true });
211
+ }
212
+ if (fs.existsSync(bakPath)) {
213
+ fs.copyFileSync(bakPath, absPath);
214
+ fs.unlinkSync(bakPath);
215
+ }
216
+ throw new Error(`Explode transaction aborted and rolled back: ${err.message}`);
217
+ }
218
+ };
219
+
220
+ export const runExplodeCli = async (rawArgs = [], isCli = true) => {
221
+ const isJson = rawArgs.includes('--json');
222
+ const isDryRun = rawArgs.includes('--dry-run') || rawArgs.includes('-n');
223
+ const target = rawArgs.find((a) => !a.startsWith('-'));
224
+
225
+ if (!target) {
226
+ const msg = 'Usage: npx chemx explode <file-path> [--dry-run] [--json]';
227
+ if (isJson) {
228
+ process.stdout.write(JSON.stringify({ error: msg, success: false }) + '\n');
229
+ } else {
230
+ process.stderr.write(`\x1b[31m✕ Error: ${msg}\x1b[0m\n`);
231
+ }
232
+ if (isCli) process.exit(1);
233
+ return { error: msg, success: false };
234
+ }
235
+
236
+ try {
237
+ const result = explodeCapsule(target, { dryRun: isDryRun });
238
+ if (isJson) {
239
+ process.stdout.write(JSON.stringify(result) + '\n');
240
+ } else {
241
+ process.stdout.write(`\n\x1b[1m\x1b[32m✔ Successfully exploded capsule:\x1b[0m \x1b[36m${result.targetDir}/\x1b[0m\n`);
242
+ for (const f of result.files) {
243
+ process.stdout.write(` \x1b[32m✔\x1b[0m ${f}\n`);
244
+ }
245
+ process.stdout.write('\n');
246
+ }
247
+ if (isCli) process.exit(0);
248
+ return result;
249
+ } catch (err) {
250
+ if (isJson) {
251
+ process.stdout.write(JSON.stringify({ error: err.message, success: false }) + '\n');
252
+ } else {
253
+ process.stderr.write(`\x1b[31m✕ Error: ${err.message}\x1b[0m\n`);
254
+ }
255
+ if (isCli) process.exit(1);
256
+ throw err;
257
+ }
258
+ };
@@ -0,0 +1,99 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { parseCompactFile, explodeCapsule } from './exploder.js';
6
+
7
+ const SAMPLE_COMPACT_CODE = `import type { ReactNode } from 'react';
8
+ import { useState } from 'react';
9
+
10
+ export interface CounterProps {
11
+ readonly className?: string;
12
+ readonly children?: ReactNode;
13
+ readonly initialCount?: number;
14
+ }
15
+
16
+ export interface CounterState {
17
+ readonly count: number;
18
+ }
19
+
20
+ export const useCounterController = (options: { initialCount?: number } = {}) => {
21
+ const [count, setCount] = useState<number>(options.initialCount || 0);
22
+ const increment = () => setCount((c) => c + 1);
23
+ return { count, increment };
24
+ };
25
+
26
+ export const Counter = ({ className = '', initialCount = 0 }: CounterProps) => {
27
+ const { count, increment } = useCounterController({ initialCount });
28
+ return (
29
+ <div className={\`m-counter \${className}\`.trim()}>
30
+ <button type="button" onClick={increment}>Count: {count}</button>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export default Counter;
36
+ `;
37
+
38
+ test('exploder: parseCompactFile extracts types, controller, and component sections', () => {
39
+ const parsed = parseCompactFile(SAMPLE_COMPACT_CODE, 'tsx');
40
+ assert.ok(parsed.typesProps.includes('CounterProps'), 'Must extract CounterProps');
41
+ assert.ok(parsed.typesState.includes('CounterState'), 'Must extract CounterState');
42
+ assert.ok(parsed.controllerHookCode.includes('useCounterController'), 'Must extract controller hook');
43
+ assert.ok(parsed.componentCode.includes('export const Counter'), 'Must extract component body');
44
+ });
45
+
46
+ test('exploder: explodeCapsule unpacks single file into multi-file capsule and removes .temp.bak', () => {
47
+ const tmpDir = path.resolve(process.cwd(), 'scratch/test-explode');
48
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
49
+ fs.mkdirSync(tmpDir, { recursive: true });
50
+
51
+ const compactFile = path.join(tmpDir, 'm-counter.tsx');
52
+ fs.writeFileSync(compactFile, SAMPLE_COMPACT_CODE, 'utf-8');
53
+
54
+ const result = explodeCapsule(compactFile, { cwd: tmpDir });
55
+ assert.strictEqual(result.success, true);
56
+ assert.strictEqual(result.capsuleName, 'm-counter');
57
+
58
+ // Verify single file is gone and bak is cleaned up
59
+ assert.strictEqual(fs.existsSync(compactFile), false, 'Original file should be replaced');
60
+ assert.strictEqual(fs.existsSync(`${compactFile}.temp.bak`), false, 'Temp bak must be removed on success');
61
+
62
+ // Verify unpacked capsule structure
63
+ const capsuleDir = path.join(tmpDir, 'm-counter');
64
+ assert.ok(fs.existsSync(capsuleDir), 'Directory capsule must exist');
65
+ assert.ok(fs.existsSync(path.join(capsuleDir, 'm-counter.tsx')), 'Component must exist');
66
+ assert.ok(fs.existsSync(path.join(capsuleDir, 'm-counter.controller.ts')), 'Controller must exist');
67
+ assert.ok(fs.existsSync(path.join(capsuleDir, 'm-counter.spec.ts')), 'Spec must exist');
68
+ assert.ok(fs.existsSync(path.join(capsuleDir, 'index.ts')), 'Index barrel must exist');
69
+ assert.ok(fs.existsSync(path.join(capsuleDir, 'types/props.d.ts')), 'Props type must exist');
70
+ assert.ok(fs.existsSync(path.join(capsuleDir, 'types/state.d.ts')), 'State type must exist');
71
+ assert.ok(fs.existsSync(path.join(capsuleDir, 'types.d.ts')), 'Types barrel must exist');
72
+
73
+ fs.rmSync(tmpDir, { recursive: true, force: true });
74
+ });
75
+
76
+ test('exploder: rolls back and restores from .temp.bak on failure', () => {
77
+ const tmpDir = path.resolve(process.cwd(), 'scratch/test-explode-fail');
78
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
79
+ fs.mkdirSync(tmpDir, { recursive: true });
80
+
81
+ // Invalid syntax that fails verification
82
+ const invalidCode = 'export const Bad = () => { return <div missing closing tag>; };';
83
+ const targetFile = path.join(tmpDir, 'm-bad.tsx');
84
+ fs.writeFileSync(targetFile, invalidCode, 'utf-8');
85
+
86
+ assert.throws(() => {
87
+ explodeCapsule(targetFile, { cwd: tmpDir });
88
+ }, /Explode transaction aborted/);
89
+
90
+ // Original file must be restored
91
+ assert.ok(fs.existsSync(targetFile), 'Original file must be restored on failure');
92
+ assert.strictEqual(fs.readFileSync(targetFile, 'utf-8'), invalidCode);
93
+ // Backup must be cleaned up
94
+ assert.strictEqual(fs.existsSync(`${targetFile}.temp.bak`), false, 'Temp bak must be cleaned up');
95
+ // Capsule dir must not exist
96
+ assert.strictEqual(fs.existsSync(path.join(tmpDir, 'm-bad')), false, 'Failed capsule dir must be removed');
97
+
98
+ fs.rmSync(tmpDir, { recursive: true, force: true });
99
+ });
@@ -0,0 +1,104 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { runGenerateWizard, createCapsuleFiles } from './generator.js';
6
+ import { detectStylingStack } from './project-detector.js';
7
+ import { resolveArchetype } from './generator-templates/archetypes/index.js';
8
+
9
+ test('detectStylingStack: detects tailwind, scss, or css appropriately', () => {
10
+ const tmpDir = path.resolve(process.cwd(), 'scratch/test-detect-styling');
11
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
12
+ fs.mkdirSync(tmpDir, { recursive: true });
13
+
14
+ // 1. Plain CSS fallback
15
+ const plain = detectStylingStack(tmpDir);
16
+ assert.strictEqual(plain.hasTailwind, false);
17
+ assert.strictEqual(plain.hasScss, false);
18
+ assert.strictEqual(plain.styleFlavor, 'css');
19
+
20
+ // 2. Tailwind via package.json
21
+ fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({
22
+ dependencies: { tailwindcss: '^4.0.0' }
23
+ }));
24
+ const twPkg = detectStylingStack(tmpDir);
25
+ assert.strictEqual(twPkg.hasTailwind, true);
26
+ assert.strictEqual(twPkg.styleFlavor, 'tailwind');
27
+
28
+ // 3. Tailwind via config file
29
+ fs.unlinkSync(path.join(tmpDir, 'package.json'));
30
+ fs.writeFileSync(path.join(tmpDir, 'tailwind.config.js'), 'export default {};');
31
+ const twCfg = detectStylingStack(tmpDir);
32
+ assert.strictEqual(twCfg.hasTailwind, true);
33
+ assert.strictEqual(twCfg.styleFlavor, 'tailwind');
34
+
35
+ fs.rmSync(tmpDir, { recursive: true, force: true });
36
+ });
37
+
38
+ test('archetypes: resolveArchetype resolves minimal, controls, and canvas archetypes', () => {
39
+ const minimal = resolveArchetype('m-card', '', 'minimal');
40
+ assert.strictEqual(minimal.id, 'minimal');
41
+
42
+ const bare = resolveArchetype('m-card', '', 'bare');
43
+ assert.strictEqual(bare.id, 'minimal');
44
+
45
+ const controls = resolveArchetype('m-dock', '', 'controls');
46
+ assert.strictEqual(controls.id, 'controls');
47
+
48
+ const canvas = resolveArchetype('m-stage', '', 'canvas');
49
+ assert.strictEqual(canvas.id, 'canvas');
50
+ });
51
+
52
+ test('generator: --compact generates single clean file with inlined types and controller', async () => {
53
+ const tmpDir = path.resolve(process.cwd(), 'scratch/test-gen-compact');
54
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
55
+
56
+ const res = await runGenerateWizard([
57
+ 'molecule',
58
+ 'time-controls',
59
+ '--compact',
60
+ '--template=minimal',
61
+ '-y',
62
+ `--dir=${tmpDir}`,
63
+ '--framework=react'
64
+ ]);
65
+
66
+ assert.strictEqual(res.success, true);
67
+ assert.strictEqual(res.compact, true);
68
+ assert.strictEqual(res.capsuleName, 'm-time-controls');
69
+
70
+ const singleFilePath = path.join(tmpDir, 'm-time-controls.tsx');
71
+ assert.ok(fs.existsSync(singleFilePath), 'Compact file must exist');
72
+
73
+ const content = fs.readFileSync(singleFilePath, 'utf-8');
74
+ assert.ok(content.includes('interface TimeControlsProps'), 'Must inline props');
75
+ assert.ok(content.includes('interface TimeControlsState'), 'Must inline state');
76
+ assert.ok(content.includes('useTimeControlsController'), 'Must inline controller hook');
77
+ assert.ok(content.includes('export const TimeControls'), 'Must export component');
78
+
79
+ const lineCount = content.split('\n').length;
80
+ assert.ok(lineCount < 80, `Compact file must be < 80 lines (got ${lineCount})`);
81
+
82
+ fs.rmSync(tmpDir, { recursive: true, force: true });
83
+ });
84
+
85
+ test('generator: skips scss creation when tailwind is detected or --css=none', async () => {
86
+ const tmpDir = path.resolve(process.cwd(), 'scratch/test-gen-tailwind');
87
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
88
+
89
+ const res = await runGenerateWizard([
90
+ 'molecule',
91
+ 'quick-card',
92
+ '--css=tailwind',
93
+ '-y',
94
+ `--dir=${tmpDir}`,
95
+ '--framework=react'
96
+ ]);
97
+
98
+ assert.strictEqual(res.success, true);
99
+ const capsuleDir = path.join(tmpDir, 'm-quick-card');
100
+ const scssFile = path.join(capsuleDir, '_m-quick-card.scss');
101
+ assert.strictEqual(fs.existsSync(scssFile), false, 'SCSS file must not exist for tailwind');
102
+
103
+ fs.rmSync(tmpDir, { recursive: true, force: true });
104
+ });
@@ -2,6 +2,7 @@ import test from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import ts from 'typescript';
5
6
  import { runGenerateWizard } from './generator.js';
6
7
  import { auditFile } from './audit.js';
7
8
 
@@ -115,6 +116,15 @@ test('generator integration: Vue capsule with --desc generates Vue reactivity an
115
116
  assert.ok(!controllerContent.includes("from 'react'"), 'Vue controller must NOT import from react');
116
117
  assert.ok(!controllerContent.includes('useState'), 'Vue controller must NOT use useState');
117
118
 
119
+ // Assert method collision is prevented
120
+ assert.ok(!controllerContent.includes('.filter.value'), 'Must not collide Array.filter method with filter.value');
121
+ assert.ok(controllerContent.includes('items.value.filter'), 'Must unwrap items.value before Array.filter');
122
+ assert.ok(controllerContent.includes("filter.value === 'active'"), 'Must unwrap filter state variable comparison');
123
+
124
+ // Assert controller parses with zero TypeScript syntax errors
125
+ const sf = ts.createSourceFile('test.ts', controllerContent, ts.ScriptTarget.Latest, true);
126
+ assert.strictEqual(sf.parseDiagnostics?.length || 0, 0, 'Generated Vue controller must have zero TypeScript syntax errors');
127
+
118
128
  // Assert no old generic placeholder properties
119
129
  assert.ok(!viewContent.includes('descriptor.className'));
120
130
  assert.ok(!viewContent.includes('descriptor.text'));
@@ -161,6 +171,10 @@ test('generator integration: Svelte capsule with --desc generates matching view
161
171
  assert.ok(!controllerContent.includes("from 'react'"), 'Svelte controller must NOT import from react');
162
172
  assert.ok(!controllerContent.includes('useState'), 'Svelte controller must NOT use useState');
163
173
 
174
+ // Assert Svelte controller parses with zero TypeScript syntax errors
175
+ const sfSvelte = ts.createSourceFile('test.ts', controllerContent, ts.ScriptTarget.Latest, true);
176
+ assert.strictEqual(sfSvelte.parseDiagnostics?.length || 0, 0, 'Generated Svelte controller must have zero TypeScript syntax errors');
177
+
164
178
  const returnedKeys = parseControllerReturns(controllerContent);
165
179
  const destructuredKeys = parseViewDestructured(viewContent);
166
180
 
@@ -241,3 +255,61 @@ test('generator integration: auto-detects framework from .chemx/config.json when
241
255
  fs.rmSync(tmpDir, { recursive: true, force: true });
242
256
  });
243
257
 
258
+ test('generator integration: mechanical contract diff and syntax check across all frameworks with --desc', async () => {
259
+ const frameworks = ['react', 'vue', 'svelte'];
260
+ const testDesc = 'add, toggle, and remove tasks with an input field';
261
+
262
+ for (const fw of frameworks) {
263
+ const tmpDir = path.resolve(process.cwd(), `scratch/test-contract-diff-${fw}`);
264
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
265
+
266
+ const res = await runGenerateWizard([
267
+ 'molecule',
268
+ 'task-list',
269
+ '-y',
270
+ `--dir=${tmpDir}`,
271
+ `--framework=${fw}`,
272
+ `--desc=${testDesc}`
273
+ ]);
274
+
275
+ assert.strictEqual(res.success, true, `Scaffolding must succeed for ${fw}`);
276
+ const capsuleDir = path.join(tmpDir, 'm-task-list');
277
+ const ext = fw === 'react' ? 'tsx' : fw;
278
+ const viewPath = path.join(capsuleDir, `m-task-list.${ext}`);
279
+ const controllerPath = path.join(capsuleDir, 'm-task-list.controller.ts');
280
+
281
+ assert.ok(fs.existsSync(viewPath), `View file must exist for ${fw}`);
282
+ assert.ok(fs.existsSync(controllerPath), `Controller file must exist for ${fw}`);
283
+
284
+ const viewContent = fs.readFileSync(viewPath, 'utf8');
285
+ const controllerContent = fs.readFileSync(controllerPath, 'utf8');
286
+
287
+ // 1. Mechanical check: TS syntax validation
288
+ const sf = ts.createSourceFile('test.ts', controllerContent, ts.ScriptTarget.Latest, true);
289
+ assert.strictEqual(
290
+ sf.parseDiagnostics?.length || 0,
291
+ 0,
292
+ `Controller for ${fw} must have zero TypeScript syntax errors`
293
+ );
294
+
295
+ // 2. Mechanical check: diff controller return shape against view destructure
296
+ const returnedKeys = parseControllerReturns(controllerContent);
297
+ const destructuredKeys = parseViewDestructured(viewContent);
298
+
299
+ assert.ok(destructuredKeys.size > 0, `View for ${fw} must destructure controller properties`);
300
+ for (const key of destructuredKeys) {
301
+ assert.ok(
302
+ returnedKeys.has(key),
303
+ `[${fw}] View property "${key}" must be present in controller return object: ${[...returnedKeys].join(', ')}`
304
+ );
305
+ }
306
+
307
+ // 3. Audit check: CONTROLLER_VIEW_MISMATCH must report 0 violations
308
+ const violations = auditFile(viewPath, `m-task-list.${ext}`);
309
+ const mismatchViolations = violations.filter((v) => v.rule === 'CONTROLLER_VIEW_MISMATCH');
310
+ assert.strictEqual(mismatchViolations.length, 0, `Audit must report 0 mismatch violations for ${fw}`);
311
+
312
+ fs.rmSync(tmpDir, { recursive: true, force: true });
313
+ }
314
+ });
315
+
@@ -7,7 +7,7 @@ export const COLLECTION_ARCHETYPES = [
7
7
  id: 'task-list',
8
8
  name: 'Collection & Task List',
9
9
  keywords: ['task', 'todo', 'tasklist', 'checklist', 'item', 'queue', 'list', 'add', 'toggle', 'remove', 'delete'],
10
- destructure: 'items, activeCount, setFilter, toggleItem, removeItem, addItem',
10
+ destructure: 'items, filter, activeCount, setFilter, toggleItem, removeItem, addItem',
11
11
  buildState: (name, pascal) => `export interface ${pascal}Item {
12
12
  readonly id: string;
13
13
  readonly title: string;
@@ -151,7 +151,7 @@ export const use${pascal}Controller = (options: { rows?: readonly ${pascal}Row[]
151
151
  }
152
152
  };
153
153
 
154
- return { sortColumn, sortDirection, page, sortBy, setPage };
154
+ return { rows: options.rows || [], sortColumn, sortDirection, page, sortBy, setPage };
155
155
  };
156
156
  `,
157
157
  buildReactBody: (name, pascal) => ` <div className="${name}__header">
@@ -189,7 +189,7 @@ export interface ${pascal}State {
189
189
  buildController: (name, pascal) => `import { useState } from 'react';
190
190
  import type { ${pascal}Node } from './types';
191
191
 
192
- export const use${pascal}Controller = (options: { onSelect?: (id: string) => void } = {}) => {
192
+ export const use${pascal}Controller = (options: { nodes?: readonly ${pascal}Node[]; onSelect?: (id: string) => void } = {}) => {
193
193
  const [expandedIds, setExpandedIds] = useState<readonly string[]>([]);
194
194
  const [selectedId, setSelectedId] = useState<string | null>(null);
195
195
 
@@ -202,7 +202,7 @@ export const use${pascal}Controller = (options: { onSelect?: (id: string) => voi
202
202
  options.onSelect?.(id);
203
203
  };
204
204
 
205
- return { expandedIds, selectedId, toggleNode, selectNode };
205
+ return { nodes: options.nodes || [], expandedIds, selectedId, toggleNode, selectNode };
206
206
  };
207
207
  `,
208
208
  buildReactBody: (name, pascal) => ` <div className="${name}__tree">
@@ -235,11 +235,11 @@ export interface ${pascal}State {
235
235
  `,
236
236
  buildController: (name, pascal) => `import { useState } from 'react';
237
237
 
238
- export const use${pascal}Controller = () => {
238
+ export const use${pascal}Controller = (options: { series?: readonly ${pascal}DataPoint[]; timeRange?: '1h' | '24h' | '7d' | '30d' } = {}) => {
239
239
  const [activeIndex, setActiveIndex] = useState<number | null>(null);
240
240
  const [timeRange, setTimeRange] = useState<'1h' | '24h' | '7d' | '30d'>('24h');
241
241
 
242
- return { activeIndex, timeRange, setActiveIndex, setTimeRange };
242
+ return { series: options.series || [], activeIndex, timeRange, setActiveIndex, setTimeRange };
243
243
  };
244
244
  `,
245
245
  buildReactBody: (name, pascal) => ` <div className="${name}__toolbar">
@@ -274,9 +274,9 @@ export interface ${pascal}Column {
274
274
  }
275
275
  `,
276
276
  buildController: (name, pascal) => `import { useState } from 'react';
277
- import type { ${pascal}Card } from './types';
277
+ import type { ${pascal}Card, ${pascal}Column } from './types';
278
278
 
279
- export const use${pascal}Controller = (options: { cards?: readonly ${pascal}Card[]; onCardMove?: (id: string, colId: string) => void } = {}) => {
279
+ export const use${pascal}Controller = (options: { columns?: readonly ${pascal}Column[]; cards?: readonly ${pascal}Card[]; onCardMove?: (id: string, colId: string) => void } = {}) => {
280
280
  const [cards, setCards] = useState<readonly ${pascal}Card[]>(options.cards || []);
281
281
  const [activeCardId, setActiveCardId] = useState<string | null>(null);
282
282
 
@@ -286,7 +286,7 @@ export const use${pascal}Controller = (options: { cards?: readonly ${pascal}Card
286
286
  options.onCardMove?.(cardId, toColumnId);
287
287
  };
288
288
 
289
- return { cards, activeCardId, setActiveCardId, moveCard };
289
+ return { columns: options.columns || [], cards, activeCardId, setActiveCardId, moveCard };
290
290
  };
291
291
  `,
292
292
  buildReactBody: (name, pascal) => ` <div className="${name}__columns">