@chemx/starter-kit 26.9.20-390 → 26.9.21-227

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 (97) hide show
  1. package/README.md +146 -0
  2. package/blueprints/_package.json +3 -1
  3. package/blueprints/workflows/chemx-audit.yml +1 -1
  4. package/cli/audit/autofix.js +17 -2
  5. package/cli/audit/clone-detector.js +64 -0
  6. package/cli/audit/clone-detector.spec.js +46 -0
  7. package/cli/audit/extended-text-patterns.js +123 -0
  8. package/cli/audit/reporter-clones.js +33 -0
  9. package/cli/audit/reporter-hotspot-graph.js +34 -0
  10. package/cli/audit/rules-registry.js +5 -0
  11. package/cli/audit-stages.spec.js +30 -0
  12. package/cli/audit.js +3 -0
  13. package/cli/blast-radius.spec.js +113 -0
  14. package/cli/commands/cmd-audit.js +241 -0
  15. package/cli/commands/cmd-router.js +202 -0
  16. package/cli/commands-schema.js +198 -0
  17. package/cli/create.js +72 -0
  18. package/cli/create.spec.js +200 -0
  19. package/cli/embeddings/vectorizer.js +68 -0
  20. package/cli/embeddings.spec.js +92 -0
  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 +315 -0
  25. package/cli/generator-templates/archetypes/collection-archetypes.js +53 -25
  26. package/cli/generator-templates/archetypes/domain-archetypes.js +2 -2
  27. package/cli/generator-templates/archetypes/index.js +76 -7
  28. package/cli/generator-templates/archetypes/input-archetypes.js +2 -2
  29. package/cli/generator-templates/archetypes/layout-archetypes.js +2 -2
  30. package/cli/generator-templates/archetypes/minimal-archetypes.js +131 -0
  31. package/cli/generator-templates/archetypes/status-archetypes.js +4 -4
  32. package/cli/generator-templates/archetypes/vector-matcher.js +50 -0
  33. package/cli/generator-templates/archetypes/vector-matcher.spec.js +25 -0
  34. package/cli/generator-templates/compact.js +130 -0
  35. package/cli/generator-templates/controller-converters.js +93 -0
  36. package/cli/generator-templates/controller.js +95 -3
  37. package/cli/generator-templates/hooks.js +27 -3
  38. package/cli/generator-templates/react.js +3 -3
  39. package/cli/generator-templates/specs.js +48 -2
  40. package/cli/generator-templates/svelte.js +89 -4
  41. package/cli/generator-templates/types.js +14 -5
  42. package/cli/generator-templates/views.js +28 -3
  43. package/cli/generator-templates/vue.js +90 -4
  44. package/cli/generator-templates.js +1 -0
  45. package/cli/generator-templates.spec.js +17 -1
  46. package/cli/generator.js +169 -48
  47. package/cli/generator.spec.js +57 -0
  48. package/cli/help.js +37 -233
  49. package/cli/help.spec.js +56 -0
  50. package/cli/index.js +98 -372
  51. package/cli/installer-templates.js +1 -1
  52. package/cli/installer.js +17 -9
  53. package/cli/license.js +4 -3
  54. package/cli/mcp/antigravity.js +22 -6
  55. package/cli/mcp/index.js +10 -1
  56. package/cli/mcp/manifests.js +51 -5
  57. package/cli/mcp/server.js +4 -0
  58. package/cli/mcp/tools-search.js +70 -7
  59. package/cli/mcp/tools-team.js +52 -11
  60. package/cli/mcp/tools.js +43 -3
  61. package/cli/mutators.js +42 -33
  62. package/cli/patcher.js +13 -3
  63. package/cli/patcher.spec.js +23 -0
  64. package/cli/pillars-schema.js +150 -0
  65. package/cli/pillars-wizard.js +158 -0
  66. package/cli/pillars.spec.js +138 -0
  67. package/cli/project-detector.js +101 -13
  68. package/cli/scaffold.js +4 -20
  69. package/cli/search-commands-graph.js +82 -0
  70. package/cli/search-commands-semantic.js +98 -0
  71. package/cli/search-commands.js +4 -0
  72. package/cli/search-db.js +66 -4
  73. package/cli/search-queries-graph.js +109 -0
  74. package/cli/search-queries-hotspot-graph.js +65 -0
  75. package/cli/search-queries-hotspot-graph.spec.js +66 -0
  76. package/cli/search-queries-semantic.js +137 -0
  77. package/cli/search-queries-similar.js +84 -0
  78. package/cli/search-queries-similar.spec.js +43 -0
  79. package/cli/search-queries.js +4 -0
  80. package/cli/search-schema.js +46 -5
  81. package/cli/search.js +88 -4
  82. package/cli/team/team-commands.js +191 -16
  83. package/cli/team/team-triage.js +175 -16
  84. package/cli/team/team.spec.js +232 -2
  85. package/cli/trend.js +84 -0
  86. package/cli/trend.spec.js +51 -0
  87. package/cli/ui-e2e-verification.spec.js +5 -2
  88. package/cli/ui-filetree.spec.js +5 -5
  89. package/cli/ui-html.js +26 -34
  90. package/cli/ui-index.html +659 -0
  91. package/cli/ui-kanban.spec.js +12 -12
  92. package/cli/ui-styles.js +195 -81
  93. package/cli/ui-vbulletin.spec.js +12 -13
  94. package/cli/ui.spec.js +6 -4
  95. package/package.json +5 -3
  96. package/scripts/check-framework-generation.mjs +195 -0
  97. package/scripts/publish-both.mjs +10 -6
package/README.md CHANGED
@@ -18,6 +18,47 @@ Access the interactive book, prompt generator, and asset vault at [https://chemi
18
18
 
19
19
  ---
20
20
 
21
+ ## Quickstart: Scaffolding a New Project
22
+
23
+ Create a brand new molecular architecture project in seconds using your preferred package manager:
24
+
25
+ ```bash
26
+ # npm (interactive or pass project directory)
27
+ npm create chemx my-molecular-app
28
+
29
+ # Automated / Agent / Headless mode (skips prompts, scaffolds Community Edition immediately)
30
+ npm create chemx my-molecular-app -- --yes
31
+
32
+ # npx
33
+ npx create-chemx my-molecular-app --yes
34
+
35
+ # pnpm / yarn / bun
36
+ pnpm create chemx my-molecular-app
37
+ yarn create chemx my-molecular-app
38
+ bun create chemx my-molecular-app
39
+ ```
40
+
41
+ ### Headless & Autonomous Agent Mode
42
+ When running in unattended environments (CI/CD pipelines, Cursor Agent, Windsurf, Claude Code, Antigravity), pass `--yes` (or `-y`, `--ci`, `--headless`) to bypass interactive terminal menus and immediately scaffold the free Community Edition with recommended architectural pillars:
43
+
44
+ ```bash
45
+ npx create-chemx my-molecular-app --yes
46
+ ```
47
+
48
+ ### Package Architecture: Scaffolder vs Command Engine
49
+ Chemical X provides two coordinated packages:
50
+ - **`create-chemx`**: Dedicated zero-dependency project scaffolder (`npm create chemx`). Directly provisions project templates, test suites, and architectural configurations.
51
+ - **`chemx`**: The full Molecular Architecture CLI & AST Query Engine. Manages audits, verifications, AST query lookups, code patching, and multi-agent swarm task coordination.
52
+
53
+ ```bash
54
+ # Install chemx CLI globally or in your project:
55
+ npm install -g chemx
56
+ # or run on-demand:
57
+ npx chemx --help
58
+ ```
59
+
60
+ ---
61
+
21
62
  ## Structure
22
63
 
23
64
  ```
@@ -138,6 +179,106 @@ $ npx chemx verify --dir=blueprints
138
179
 
139
180
  ---
140
181
 
182
+ ## CLI Command Reference & Workflow
183
+
184
+ The `chemx` command suite is specifically tailored for token conservation, instant feedback, and zero terminal clutter:
185
+
186
+ ### 1. Verification & Quality
187
+ ```bash
188
+ # Full verification pipeline (AST Audit + Typecheck + Tests) -> ~45 token status card
189
+ chemx verify
190
+ chemx verify --json
191
+
192
+ # Run 7-Pillar static AST audit
193
+ chemx audit
194
+ chemx audit --unroll # Inspect individual hazard lines and explanations
195
+ chemx audit --strict # Fail on any violation, including minor style warnings
196
+ chemx audit --json # Machine-readable format for agent pipelines
197
+
198
+ # Silent TypeScript compilation check (suppresses passing noise, returns error lines)
199
+ chemx typecheck
200
+ chemx typecheck --json
201
+
202
+ # Silent test runner (suppresses passing tests, extracts only failing assertions & stack diffs)
203
+ chemx test
204
+ chemx test --json
205
+
206
+ # Silent build audit (categorizes diagnostics into TypeScript, Rollup, and style budgets)
207
+ chemx build
208
+ chemx build --json
209
+ ```
210
+
211
+ ### 2. AST Query Engine & Surgical Inspection
212
+ ```bash
213
+ # Hybrid search (BM25 keyword + cosine vector similarity via Reciprocal Rank Fusion)
214
+ chemx q "useAttentionCardController" --hybrid --json
215
+
216
+ # Transitive blast radius analysis before refactoring foundational capsules
217
+ chemx q "a-button" --blast-radius --json
218
+
219
+ # Inspect component props, hooks, and types without reading entire files
220
+ chemx q "m-task-list" --inspect
221
+
222
+ # Surgical token-optimized file reader (AST outlines, stripped comments, specific symbols)
223
+ chemx read src/components/m-card.vue --symbol=useCardController
224
+ chemx read src/components/m-card.vue --outline
225
+ chemx read src/components/m-card.vue --start=10 --end=40
226
+
227
+ # Surgical file patching without full-file rewrites
228
+ chemx patch <file> --target="oldCode" --replacement="newCode"
229
+ ```
230
+
231
+ ### 3. Crystalline Capsule Generator
232
+ Scaffold production-ready component capsules matching strict zero-raw-DOM standards:
233
+ ```bash
234
+ # Molecule capsule (component, controller, glass styling, types, spec)
235
+ chemx m-task-card
236
+
237
+ # Atom foundation (the only tier permitted raw HTML elements)
238
+ chemx a-status-pill
239
+
240
+ # Organism module (complex grouping of molecules and atoms)
241
+ chemx o-workspace-header
242
+
243
+ # Pure reactive hook / domain composable
244
+ chemx use-task-filter
245
+ ```
246
+
247
+ ### 4. Database-Driven Multi-Agent Swarm Coordination
248
+ Coordinate multi-agent swarms using the local SQLite store (`.chemx/index.db`) without multi-thousand-token markdown specification bloat:
249
+ ```bash
250
+ # Auto-triage: convert AST audit hazards directly into assignable team tasks
251
+ chemx team task triage
252
+
253
+ # List tasks assigned to a specific agent
254
+ chemx team task list --agent=@agent-alpha
255
+
256
+ # Claim an open task
257
+ chemx team task claim 1 --as=@agent-alpha
258
+
259
+ # Mark task complete (automatically re-audits target file on disk to guarantee zero hazards)
260
+ chemx team task done 1 --as=@agent-alpha
261
+
262
+ # Inspect multi-agent swarm status and lock queues
263
+ chemx team status
264
+ ```
265
+
266
+ ---
267
+
268
+ ## The 7 Molecular Architecture Pillars
269
+
270
+ Chemical X enforces seven core architectural directives configured via `chemx pillars`:
271
+
272
+ 1. **Strict Molecular Line Budgets (< 100 Lines)**: Single-purpose files. Approaching 100 lines is a decomposition trigger. Eliminates context rot and cuts token ingestion costs.
273
+ 2. **Strict Component Tiers & Zero-Raw-DOM**: Raw HTML elements (`<button>`, `<input>`, `<div>`) are strictly isolated inside foundational **Atoms** (`a-*`). Molecules, Organisms, Templates, and Views assemble atoms and never contain raw tags.
274
+ 3. **Table-of-Contents Views**: Top-level page views are clean, 10–20 line declarative blueprints assembling self-contained molecules and organisms via named slots (`#header`, `#default`, `#modals`).
275
+ 4. **Molecular Composable Contracts**: Composables return plain destructurable objects with a strict 3-to-5 property limit (State + Status + Actions). Domain types use discriminated unions (zero impossible states).
276
+ 5. **Silent Verification Pipeline**: Verification tools suppress passing checkmarks and compiler banners, returning token-compact summaries (~45 tokens) to protect AI agent context windows.
277
+ 6. **AST Codebase Query Engine**: In-band AST symbol graph lookups, blast radius calculations, and outline extraction eliminate blind full-file context dumps.
278
+ 7. **Database-First Swarm Coordination**: Task backlogs, file locks, and agent communications live in local SQLite (`.chemx/index.db`) rather than monolithic markdown specifications.
279
+
280
+ ---
281
+
141
282
  ## Model Context Protocol (MCP) Server
142
283
 
143
284
  Chemical X includes a high-performance, zero-dependency JSON-RPC 2.0 Stdio MCP server that connects directly to AI agent hosts (Cursor, Claude Desktop, Windsurf, Antigravity, VS Code).
@@ -215,6 +356,11 @@ npx chemx --version
215
356
  # create-chemx v26.9.17-606
216
357
  ```
217
358
 
359
+ ### Package Pairing & npm Registry Propagation
360
+
361
+ - **Package Pairing**: `@chemx/create-chemx` and `chemx` publish together under synchronized minute-precision CalVer timestamps (`YY.M.D-minute`). For consistent behavior, install or pin the matching release versions.
362
+ - **npm Registry Propagation Note**: Newly published releases on npm can take 2–5 minutes to propagate across all edge CDN mirrors after `npm view` lists them. If `npm install` intermittently fails with a 404 on an exact newly published version, wait a few minutes and retry.
363
+
218
364
  Because autonomous coding agent models update frequently, this package is continuously integrated and deployed via automated CI whenever new agent directives, AST checks, or framework rules are tuned. Rapid version iterations reflect active, daily alignment rather than breaking SemVer shifts.
219
365
 
220
366
  > [!NOTE]
@@ -10,8 +10,10 @@
10
10
  "verify": "chemx verify"
11
11
  },
12
12
  "devDependencies": {
13
+ "@vue/test-utils": "^2.4.6",
13
14
  "create-chemx": "latest",
14
15
  "typescript": "^5.4.0",
15
- "vitest": "^1.6.0"
16
+ "vitest": "^1.6.0",
17
+ "vue": "^3.4.0"
16
18
  }
17
19
  }
@@ -47,7 +47,7 @@ jobs:
47
47
  path: AUDIT_REPORT.md
48
48
 
49
49
  - name: Post Architectural Audit Comment on PR
50
- if: always() && github.event_name == 'pull_request' && hashFiles('AUDIT_REPORT.md') != ''
50
+ if: always() && github.event_name == 'pull_request' && hashFiles('AUDIT_REPORT.md') != '' && vars.CHEMX_ENABLE_PR_COMMENTS == 'true'
51
51
  continue-on-error: true
52
52
  env:
53
53
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -63,7 +63,9 @@ export const autofixContent = (content, options = {}) => {
63
63
  }
64
64
 
65
65
  // 3. AI lazy placeholder comment removal
66
- if (!shouldDropLine && shouldFix('AI_SLOP_LAZY_PLACEHOLDER') && TRUNCATION_REGEX.test(line)) {
66
+ const hasLazyTruncation = TRUNCATION_REGEX.test(line);
67
+ const isLazyTruncationEligible = !shouldDropLine && shouldFix('AI_SLOP_LAZY_PLACEHOLDER');
68
+ if (isLazyTruncationEligible && hasLazyTruncation) {
67
69
  fixes.push({
68
70
  line: lineNumber,
69
71
  rule: 'AI_SLOP_LAZY_PLACEHOLDER',
@@ -72,7 +74,9 @@ export const autofixContent = (content, options = {}) => {
72
74
  shouldDropLine = true;
73
75
  }
74
76
 
75
- if (!shouldDropLine && shouldFix('TYPOGRAPHY_EM_DASH') && line.includes('\u2014')) {
77
+ const hasEmDash = line.includes('\u2014');
78
+ const isEmDashEligible = !shouldDropLine && shouldFix('TYPOGRAPHY_EM_DASH');
79
+ if (isEmDashEligible && hasEmDash) {
76
80
  line = line.replace(/\u2014/g, '-');
77
81
  fixes.push({
78
82
  line: lineNumber,
@@ -81,6 +85,17 @@ export const autofixContent = (content, options = {}) => {
81
85
  });
82
86
  }
83
87
 
88
+ const hasTimeoutMacro = /setTimeout\([^,]+,\s*0\)/.test(line);
89
+ const isTimeoutEligible = !shouldDropLine && shouldFix('MACRO_TASK_OVER_MICRO_TASK');
90
+ if (isTimeoutEligible && hasTimeoutMacro) {
91
+ line = line.replace(/setTimeout\(([^,]+),\s*0\)/g, 'queueMicrotask($1)');
92
+ fixes.push({
93
+ line: lineNumber,
94
+ rule: 'MACRO_TASK_OVER_MICRO_TASK',
95
+ action: 'Replaced setTimeout(fn, 0) with queueMicrotask(fn)'
96
+ });
97
+ }
98
+
84
99
  if (!shouldDropLine) {
85
100
  resultLines.push(line);
86
101
  }
@@ -0,0 +1,64 @@
1
+ import path from 'node:path';
2
+ import { deserializeVector, cosineSimilarity } from '../embeddings/vectorizer.js';
3
+
4
+ const isSpecOrTest = (filePath) => {
5
+ if (!filePath || typeof filePath !== 'string') return false;
6
+ return filePath.includes('.spec.') || filePath.includes('.test.') || filePath.includes('__tests__');
7
+ };
8
+
9
+ export const detectSemanticClones = (db, options = {}) => {
10
+ const isValidDb = Boolean(db);
11
+ if (!isValidDb) return { count: 0, threshold: 0.85, pairs: [] };
12
+
13
+ const threshold = typeof options.threshold === 'number' ? options.threshold : 0.85;
14
+ const limit = options.limit || 50;
15
+
16
+ const rows = db.prepare(
17
+ "SELECT file_path, target_name, vector FROM embeddings WHERE target_type = 'file'"
18
+ ).all();
19
+
20
+ const entries = rows
21
+ .filter((r) => !isSpecOrTest(r.file_path))
22
+ .map((r) => ({
23
+ filePath: r.file_path,
24
+ name: r.target_name,
25
+ vec: deserializeVector(r.vector)
26
+ }));
27
+
28
+ const pairs = [];
29
+
30
+ for (let i = 0; i < entries.length; i++) {
31
+ for (let j = i + 1; j < entries.length; j++) {
32
+ const a = entries[i];
33
+ const b = entries[j];
34
+
35
+ const isSameFile = a.filePath === b.filePath;
36
+ if (isSameFile) continue;
37
+
38
+ const sim = cosineSimilarity(a.vec, b.vec);
39
+ const isClone = sim >= threshold;
40
+
41
+ if (isClone) {
42
+ pairs.push({
43
+ fileA: a.filePath,
44
+ fileB: b.filePath,
45
+ nameA: a.name,
46
+ nameB: b.name,
47
+ similarity: Number(sim.toFixed(4)),
48
+ isExact: sim >= 0.95
49
+ });
50
+ }
51
+ }
52
+ }
53
+
54
+ pairs.sort((p1, p2) => p2.similarity - p1.similarity);
55
+ const cappedPairs = pairs.slice(0, limit);
56
+
57
+ return {
58
+ count: pairs.length,
59
+ threshold,
60
+ exactClones: cappedPairs.filter((p) => p.isExact),
61
+ nearClones: cappedPairs.filter((p) => !p.isExact),
62
+ pairs: cappedPairs
63
+ };
64
+ };
@@ -0,0 +1,46 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { generateEmbedding, serializeVector } from '../embeddings/vectorizer.js';
5
+ import { detectSemanticClones } from './clone-detector.js';
6
+ import { formatCloneReport } from './reporter-clones.js';
7
+
8
+ test('clone-detector: detects high-similarity clones while ignoring spec companion files', () => {
9
+ const db = new DatabaseSync(':memory:');
10
+ db.exec(`
11
+ CREATE TABLE embeddings (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ file_path TEXT NOT NULL,
14
+ target_type TEXT NOT NULL,
15
+ target_name TEXT NOT NULL,
16
+ vector BLOB NOT NULL
17
+ );
18
+ `);
19
+
20
+ const insert = db.prepare('INSERT INTO embeddings (file_path, target_type, target_name, vector) VALUES (?, ?, ?, ?)');
21
+ const vecA = serializeVector(generateEmbedding('modal window popup backdrop blur close cancel confirm'));
22
+ const vecB = serializeVector(generateEmbedding('modal dialog popup backdrop blur cancel confirm close'));
23
+ const vecTest = serializeVector(generateEmbedding('modal window popup backdrop blur close cancel confirm spec test'));
24
+ const vecDiff = serializeVector(generateEmbedding('database sqlite migration column index'));
25
+
26
+ insert.run('src/ui/m-modal-a/m-modal-a.vue', 'file', 'm-modal-a.vue', vecA);
27
+ insert.run('src/ui/m-modal-b/m-modal-b.vue', 'file', 'm-modal-b.vue', vecB);
28
+ insert.run('src/ui/m-modal-a/m-modal-a.spec.ts', 'file', 'm-modal-a.spec.ts', vecTest);
29
+ insert.run('src/db/storage.ts', 'file', 'storage.ts', vecDiff);
30
+
31
+ const res = detectSemanticClones(db, { threshold: 0.8 });
32
+ assert.equal(res.count, 1);
33
+ assert.equal(res.pairs[0].fileA, 'src/ui/m-modal-a/m-modal-a.vue');
34
+ assert.equal(res.pairs[0].fileB, 'src/ui/m-modal-b/m-modal-b.vue');
35
+ assert.ok(res.pairs[0].similarity > 0.85);
36
+
37
+ const report = formatCloneReport(res);
38
+ assert.match(report, /Semantic Clone Detection/);
39
+ assert.match(report, /m-modal-a\.vue <-> src\/ui\/m-modal-b\/m-modal-b\.vue/);
40
+ });
41
+
42
+ test('clone-detector: handles empty or invalid db gracefully', () => {
43
+ assert.equal(detectSemanticClones(null).count, 0);
44
+ const emptyReport = formatCloneReport({ count: 0, threshold: 0.85, pairs: [] });
45
+ assert.match(emptyReport, /Zero duplicate clone hazards detected/);
46
+ });
@@ -66,8 +66,131 @@ export const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) =
66
66
  });
67
67
  };
68
68
 
69
+ /**
70
+ * Pillar 3: Return-shape diff check to ensure view only references identifiers
71
+ * present in the co-located controller's return object.
72
+ */
73
+ export const checkControllerViewContract = (filePath, relativePath, content, violations) => {
74
+ const baseName = path.basename(filePath);
75
+ const ext = path.extname(filePath);
76
+
77
+ const isMoleculePath = relativePath.includes('molecules') || baseName.startsWith('m-') || baseName.startsWith('o-');
78
+ const isComponentExt = isComponentExtension(ext);
79
+ const isTestOrSpecFile = baseName.includes('.spec.') || baseName.includes('.test.');
80
+
81
+ const isComponent = isMoleculePath && isComponentExt && !isTestOrSpecFile;
82
+ if (!isComponent) return;
83
+
84
+ const controllerImportMatch = content.match(/from\s+['"]\.\/([a-zA-Z0-9_-]+\.controller)(?:\.ts)?['"]/);
85
+ if (!controllerImportMatch) return;
86
+
87
+ const controllerRel = controllerImportMatch[1];
88
+ const dir = path.dirname(filePath);
89
+ const controllerFullPath = path.join(dir, `${controllerRel}.ts`);
90
+
91
+ const [controllerContent, readErr] = toResultSync(() => {
92
+ if (fs.existsSync(controllerFullPath)) {
93
+ return fs.readFileSync(controllerFullPath, 'utf-8');
94
+ }
95
+ return null;
96
+ });
97
+
98
+ if (readErr || !controllerContent) return;
99
+
100
+ // Locate the last `return {` in the controller (handles early-return guards).
101
+ // Use a brace-depth-aware scan to find the matching closing brace so that
102
+ // nested object literals (e.g. `filter: { current, set }`) do not truncate
103
+ // the extraction prematurely — the non-greedy `[\s\S]*?` regex stops at the
104
+ // first `}` it sees, which is wrong for controllers with nested return props.
105
+ const returnStartIdx = controllerContent.lastIndexOf('return {');
106
+ if (returnStartIdx === -1) return;
107
+
108
+ const openIdx = controllerContent.indexOf('{', returnStartIdx);
109
+ if (openIdx === -1) return;
110
+
111
+ let depth = 0;
112
+ let closeIdx = -1;
113
+ for (let i = openIdx; i < controllerContent.length; i++) {
114
+ if (controllerContent[i] === '{') depth++;
115
+ else if (controllerContent[i] === '}') {
116
+ depth--;
117
+ if (depth === 0) { closeIdx = i; break; }
118
+ }
119
+ }
120
+ if (closeIdx === -1) return;
121
+
122
+ const returnBody = controllerContent.slice(openIdx + 1, closeIdx);
123
+ const returnedKeys = new Set();
124
+
125
+ // Walk the return body character-by-character to extract only TOP-LEVEL keys
126
+ // (depth-0 identifiers immediately followed by `:` or end-of-entry `,`/`}`).
127
+ // This correctly ignores property names nested inside sub-objects.
128
+ let scanDepth = 0;
129
+ let currentToken = '';
130
+ const flushToken = () => {
131
+ const trimmed = currentToken.trim();
132
+ currentToken = '';
133
+ if (!trimmed) return;
134
+ const getterMatch = trimmed.match(/^get\s+([a-zA-Z0-9_]+)\s*\(/);
135
+ if (getterMatch) { returnedKeys.add(getterMatch[1]); return; }
136
+ const keyMatch = trimmed.match(/^([a-zA-Z0-9_]+)/);
137
+ if (keyMatch) returnedKeys.add(keyMatch[1]);
138
+ };
139
+ for (let i = 0; i < returnBody.length; i++) {
140
+ const ch = returnBody[i];
141
+ if (ch === '{' || ch === '[' || ch === '(') { scanDepth++; currentToken += ch; }
142
+ else if (ch === '}' || ch === ']' || ch === ')') { scanDepth--; currentToken += ch; }
143
+ else if (ch === ',' && scanDepth === 0) { flushToken(); }
144
+ else { currentToken += ch; }
145
+ }
146
+ flushToken();
147
+
148
+ const destructuredMatches = content.matchAll(/(?:const|let)\s*\{([\s\S]*?)\}\s*=\s*(?:\{[^}]*\}\s*,\s*|\{\s*\.\.\.props\s*,\s*\.\.\.)?(?:use|create)[A-Z0-9]\w*Controller/g);
149
+ const destructuredKeys = new Set();
150
+ for (const match of destructuredMatches) {
151
+ const keysStr = match[1];
152
+ const rawKeys = keysStr.split(',').map((k) => k.trim()).filter(Boolean);
153
+ for (const rawKey of rawKeys) {
154
+ const cleanKey = rawKey.split(':')[0].trim();
155
+ if (cleanKey && !cleanKey.startsWith('...')) {
156
+ destructuredKeys.add(cleanKey);
157
+ }
158
+ }
159
+ }
160
+
161
+ const controllerVarMatch = content.match(/(?:const|let)\s+([a-zA-Z0-9_]+)\s*=\s*(?:use|create)[A-Z0-9]\w*Controller/);
162
+ if (controllerVarMatch) {
163
+ const varName = controllerVarMatch[1];
164
+ const propAccesses = content.matchAll(new RegExp(`\\b${varName}\\.([a-zA-Z0-9_]+)`, 'g'));
165
+ for (const pa of propAccesses) {
166
+ destructuredKeys.add(pa[1]);
167
+ }
168
+ }
169
+
170
+ const missing = [];
171
+ for (const key of destructuredKeys) {
172
+ if (!returnedKeys.has(key)) {
173
+ missing.push(key);
174
+ }
175
+ }
176
+
177
+ if (missing.length > 0) {
178
+ violations.push({
179
+ filePath: relativePath,
180
+ line: 1,
181
+ column: 1,
182
+ hazard: `View references undefined controller exports: ${missing.join(', ')} (controller returns: ${[...returnedKeys].join(', ')})`,
183
+ rule: 'CONTROLLER_VIEW_MISMATCH',
184
+ severity: 'CRITICAL',
185
+ pillar: 'Reactivity & Composable Contracts',
186
+ directive: 'Ensure view destructuring matches properties returned by the co-located controller'
187
+ });
188
+ }
189
+ };
190
+
69
191
  export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
70
192
  checkMoleculeCoLocatedTest(filePath, relativePath, violations);
193
+ checkControllerViewContract(filePath, relativePath, content, violations);
71
194
 
72
195
  const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath);
73
196
  const ext = path.extname(filePath);
@@ -0,0 +1,33 @@
1
+ import { ANSI } from '../theme.js';
2
+
3
+ export const formatCloneReport = (cloneResult) => {
4
+ const { count, threshold, exactClones = [], nearClones = [], pairs = [] } = cloneResult;
5
+ const hasPairs = Array.isArray(pairs) && pairs.length > 0;
6
+ if (!hasPairs) {
7
+ return ` ${ANSI.LIME}✔ Zero duplicate clone hazards detected (threshold: ${threshold})${ANSI.RESET}\n`;
8
+ }
9
+
10
+ const lines = [
11
+ ` ${ANSI.BOLD}${ANSI.PINK}Semantic Clone Detection (${count} candidate pairs >= ${threshold})${ANSI.RESET}`
12
+ ];
13
+
14
+ const hasExact = exactClones.length > 0;
15
+ if (hasExact) {
16
+ lines.push(` ${ANSI.RED}${ANSI.BOLD}Hazard: ${exactClones.length} high-similarity duplicate(s) (>= 0.95)${ANSI.RESET}`);
17
+ for (const p of exactClones) {
18
+ const simPct = Math.round(p.similarity * 100);
19
+ lines.push(` ${ANSI.RED}[${simPct}%]${ANSI.RESET} ${p.fileA} <-> ${p.fileB}`);
20
+ }
21
+ }
22
+
23
+ const hasNear = nearClones.length > 0;
24
+ if (hasNear) {
25
+ lines.push(` ${ANSI.GOLD}Harmonization Candidates: ${nearClones.length} structural clone(s)${ANSI.RESET}`);
26
+ for (const p of nearClones) {
27
+ const simPct = Math.round(p.similarity * 100);
28
+ lines.push(` ${ANSI.GOLD}[${simPct}%]${ANSI.RESET} ${p.fileA} <-> ${p.fileB}`);
29
+ }
30
+ }
31
+
32
+ return lines.join('\n') + '\n';
33
+ };
@@ -0,0 +1,34 @@
1
+ import { ANSI } from '../theme.js';
2
+
3
+ export const formatHotspotGraphReport = (graphResult) => {
4
+ const { count, hotspots = [] } = graphResult;
5
+ const hasHotspots = Array.isArray(hotspots) && hotspots.length > 0;
6
+ if (!hasHotspots) {
7
+ return ` ${ANSI.LIME}✔ Zero cascading violation hotspots detected.${ANSI.RESET}\n`;
8
+ }
9
+
10
+ const lines = [
11
+ ` ${ANSI.BOLD}${ANSI.PINK}Cascading Violation Hotspot Graph (${count} files with propagating debt)${ANSI.RESET}`
12
+ ];
13
+
14
+ for (let idx = 0; idx < hotspots.length; idx++) {
15
+ const h = hotspots[idx];
16
+ const rank = idx + 1;
17
+ const blast = h.blastRadius;
18
+ const critStr = h.criticalCount > 0 ? `${ANSI.RED}${h.criticalCount} crit${ANSI.RESET}, ` : '';
19
+ const highStr = h.highCount > 0 ? `${ANSI.GOLD}${h.highCount} high${ANSI.RESET}, ` : '';
20
+ const medStr = `${h.medCount} med`;
21
+
22
+ lines.push(
23
+ ` ${ANSI.BOLD}#${rank} ${ANSI.CYAN}${h.filePath}${ANSI.RESET} [${h.tier}, ${h.lines} LOC]`
24
+ );
25
+ lines.push(
26
+ ` ${ANSI.RED}Cascading Risk: ${h.cascadingRisk} pts${ANSI.RESET} (Violations: ${critStr}${highStr}${medStr})`
27
+ );
28
+ lines.push(
29
+ ` ${ANSI.DIM}Blast Radius: ${blast.totalImpact} consumers (${blast.directConsumers} direct, ${blast.impactedTests} tests across ${blast.depth} hops)${ANSI.RESET}`
30
+ );
31
+ }
32
+
33
+ return lines.join('\n') + '\n';
34
+ };
@@ -121,6 +121,11 @@ export const RULE_REGISTRY = {
121
121
  severity: 'HIGH',
122
122
  directive: 'Limit hook return values to 3 to 5 properties (State + Status + Actions)'
123
123
  },
124
+ CONTROLLER_VIEW_MISMATCH: {
125
+ pillar: PILLARS.PILLAR_3,
126
+ severity: 'CRITICAL',
127
+ directive: 'Ensure view destructuring matches properties returned by the co-located controller'
128
+ },
124
129
  TIMER_DISCIPLINE: {
125
130
  pillar: PILLARS.PILLAR_6,
126
131
  severity: 'CRITICAL',
@@ -0,0 +1,30 @@
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 { runAudit } from './audit.js';
6
+
7
+ test('audit: options.stage tags the report as draft or strict', () => {
8
+ const tmpDir = path.resolve(process.cwd(), 'scratch/test-audit-stage');
9
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
10
+ fs.mkdirSync(tmpDir, { recursive: true });
11
+
12
+ // Sample file with an inline multi-clause boolean (MEDIUM severity)
13
+ const code = `export const check = (a: number, b: number, c: number) => {
14
+ if (a > 0 && b > 0 && c > 0) return true;
15
+ return false;
16
+ };`;
17
+ fs.writeFileSync(path.join(tmpDir, 'sample.ts'), code, 'utf-8');
18
+
19
+ const draftReport = runAudit(tmpDir, { stage: 'draft' });
20
+ assert.strictEqual(draftReport.stage, 'draft');
21
+ assert.ok(draftReport.totalViolations > 0);
22
+
23
+ const hasCritical = draftReport.violations.some((v) => v.severity === 'CRITICAL');
24
+ assert.strictEqual(hasCritical, false, 'Should have 0 critical violations');
25
+
26
+ const strictReport = runAudit(tmpDir, { stage: 'strict' });
27
+ assert.strictEqual(strictReport.stage, 'strict');
28
+
29
+ fs.rmSync(tmpDir, { recursive: true, force: true });
30
+ });
package/cli/audit.js CHANGED
@@ -226,8 +226,11 @@ export const runAudit = (targetDir = 'src', options = {}) => {
226
226
  const patterns = patternRegistry.resolveHarmonizationCandidates(hotspots);
227
227
  const roadmap = buildRemediationRoadmap({ hotspots, violations, patterns });
228
228
 
229
+ const stage = options.stage || (options.relax ? 'draft' : 'strict');
230
+
229
231
  const report = {
230
232
  targetDir,
233
+ stage,
231
234
  options,
232
235
  scannedFiles,
233
236
  totalViolations: violations.length,