@chemx/create-chemx 26.9.20-408 → 26.9.21-8

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 (64) 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/extended-text-patterns.js +123 -0
  5. package/cli/audit/rules-registry.js +5 -0
  6. package/cli/blast-radius.spec.js +113 -0
  7. package/cli/commands/cmd-audit.js +201 -0
  8. package/cli/commands/cmd-router.js +190 -0
  9. package/cli/commands-schema.js +198 -0
  10. package/cli/create.js +72 -0
  11. package/cli/create.spec.js +200 -0
  12. package/cli/embeddings/vectorizer.js +68 -0
  13. package/cli/embeddings.spec.js +92 -0
  14. package/cli/generator-framework.spec.js +243 -0
  15. package/cli/generator-templates/archetypes/collection-archetypes.js +45 -17
  16. package/cli/generator-templates/archetypes/domain-archetypes.js +2 -2
  17. package/cli/generator-templates/archetypes/index.js +46 -7
  18. package/cli/generator-templates/controller.js +157 -3
  19. package/cli/generator-templates/hooks.js +27 -3
  20. package/cli/generator-templates/react.js +3 -3
  21. package/cli/generator-templates/specs.js +48 -2
  22. package/cli/generator-templates/svelte.js +89 -4
  23. package/cli/generator-templates/types.js +14 -5
  24. package/cli/generator-templates/views.js +28 -3
  25. package/cli/generator-templates/vue.js +90 -4
  26. package/cli/generator-templates.spec.js +11 -0
  27. package/cli/generator.js +82 -48
  28. package/cli/generator.spec.js +57 -0
  29. package/cli/help.js +37 -233
  30. package/cli/help.spec.js +56 -0
  31. package/cli/index.js +95 -381
  32. package/cli/installer-templates.js +1 -1
  33. package/cli/installer.js +17 -9
  34. package/cli/license.js +4 -3
  35. package/cli/mcp/antigravity.js +21 -6
  36. package/cli/mcp/index.js +10 -1
  37. package/cli/mcp/manifests.js +51 -5
  38. package/cli/mcp/server.js +4 -0
  39. package/cli/mcp/tools-search.js +64 -7
  40. package/cli/mcp/tools-team.js +16 -1
  41. package/cli/mcp/tools.js +7 -3
  42. package/cli/mutators.js +32 -10
  43. package/cli/patcher.js +13 -3
  44. package/cli/patcher.spec.js +23 -0
  45. package/cli/pillars-schema.js +150 -0
  46. package/cli/pillars-wizard.js +158 -0
  47. package/cli/pillars.spec.js +138 -0
  48. package/cli/project-detector.js +54 -13
  49. package/cli/scaffold.js +4 -20
  50. package/cli/search-commands-graph.js +82 -0
  51. package/cli/search-commands-semantic.js +98 -0
  52. package/cli/search-commands.js +4 -0
  53. package/cli/search-db.js +66 -4
  54. package/cli/search-queries-graph.js +109 -0
  55. package/cli/search-queries-semantic.js +137 -0
  56. package/cli/search-queries.js +4 -0
  57. package/cli/search-schema.js +41 -0
  58. package/cli/search.js +88 -4
  59. package/cli/team/team-commands.js +134 -7
  60. package/cli/team/team-triage.js +109 -7
  61. package/cli/team/team.spec.js +129 -2
  62. package/cli/ui-e2e-verification.spec.js +2 -1
  63. package/package.json +3 -3
  64. package/scripts/publish-both.mjs +6 -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 }}
@@ -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);
@@ -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,113 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert";
3
+ import { openIndexDb } from "./search-schema.js";
4
+ import { upsertFileIndex } from "./search-db.js";
5
+ import { calculateBlastRadius } from "./search-queries.js";
6
+
7
+ test("calculateBlastRadius: traverses downstream multi-tier dependencies", () => {
8
+ const db = openIndexDb();
9
+ if (!db) return;
10
+
11
+ // Set up mock components in DB:
12
+ // a-test-btn (atom) <- m-test-grp (molecule) <- o-test-nav (organism)
13
+ upsertFileIndex(db, {
14
+ path: "src/ui/atoms/a-test-btn/a-test-btn.vue",
15
+ mtime: Date.now(),
16
+ size: 100,
17
+ tier: "atom",
18
+ lines: 20,
19
+ chars: 400,
20
+ symbols: [{ name: "AtomTestBtn", kind: "const", isExport: true, startLine: 1, endLine: 20 }],
21
+ imports: []
22
+ });
23
+
24
+ upsertFileIndex(db, {
25
+ path: "src/ui/molecules/m-test-grp/m-test-grp.vue",
26
+ mtime: Date.now(),
27
+ size: 200,
28
+ tier: "molecule",
29
+ lines: 40,
30
+ chars: 800,
31
+ symbols: [{ name: "MoleculeTestGrp", kind: "const", isExport: true, startLine: 1, endLine: 40 }],
32
+ imports: [
33
+ { importedSymbol: "AtomTestBtn", sourceModule: "../../atoms/a-test-btn/a-test-btn.vue", line: 2 }
34
+ ]
35
+ });
36
+
37
+ upsertFileIndex(db, {
38
+ path: "src/ui/organisms/o-test-nav/o-test-nav.vue",
39
+ mtime: Date.now(),
40
+ size: 300,
41
+ tier: "organism",
42
+ lines: 60,
43
+ chars: 1200,
44
+ symbols: [{ name: "OrganismTestNav", kind: "const", isExport: true, startLine: 1, endLine: 60 }],
45
+ imports: [
46
+ { importedSymbol: "MoleculeTestGrp", sourceModule: "../../molecules/m-test-grp/m-test-grp.vue", line: 3 }
47
+ ]
48
+ });
49
+
50
+ upsertFileIndex(db, {
51
+ path: "src/ui/molecules/m-test-grp/m-test-grp.spec.ts",
52
+ mtime: Date.now(),
53
+ size: 150,
54
+ tier: "utility",
55
+ lines: 30,
56
+ chars: 600,
57
+ symbols: [],
58
+ imports: [
59
+ { importedSymbol: "MoleculeTestGrp", sourceModule: "./m-test-grp.vue", line: 1 }
60
+ ]
61
+ });
62
+
63
+ const blast = calculateBlastRadius(db, "a-test-btn");
64
+
65
+ assert.strictEqual(blast.target, "a-test-btn");
66
+ assert.ok(blast.totalImpactCount >= 3, "Should detect at least 3 downstream files");
67
+ assert.strictEqual(blast.depth >= 2, true, "Should reach depth of at least 2");
68
+
69
+ // Verify direct consumers (m-test-grp)
70
+ const hasDirect = blast.directConsumers.some((c) => c.path.includes("m-test-grp.vue"));
71
+ assert.strictEqual(hasDirect, true, "m-test-grp.vue should be a direct consumer");
72
+
73
+ // Verify transitive consumers (o-test-nav)
74
+ const hasTransitive = blast.transitiveConsumers.some((c) => c.path.includes("o-test-nav.vue"));
75
+ assert.strictEqual(hasTransitive, true, "o-test-nav.vue should be a transitive consumer");
76
+
77
+ // Verify impacted test file detection
78
+ const hasTest = blast.impactedTests.some((t) => t.path.includes("m-test-grp.spec.ts"));
79
+ assert.strictEqual(hasTest, true, "m-test-grp.spec.ts should be flagged as impacted test");
80
+ });
81
+
82
+ test("calculateBlastRadius: safely terminates on circular dependency cycles", () => {
83
+ const db = openIndexDb();
84
+ if (!db) return;
85
+
86
+ // Cycle: circ-a -> circ-b -> circ-a
87
+ upsertFileIndex(db, {
88
+ path: "src/ui/circ-a.ts",
89
+ mtime: Date.now(),
90
+ size: 100,
91
+ tier: "utility",
92
+ lines: 10,
93
+ chars: 200,
94
+ symbols: [{ name: "CircA", kind: "const", isExport: true }],
95
+ imports: [{ importedSymbol: "CircB", sourceModule: "./circ-b", line: 1 }]
96
+ });
97
+
98
+ upsertFileIndex(db, {
99
+ path: "src/ui/circ-b.ts",
100
+ mtime: Date.now(),
101
+ size: 100,
102
+ tier: "utility",
103
+ lines: 10,
104
+ chars: 200,
105
+ symbols: [{ name: "CircB", kind: "const", isExport: true }],
106
+ imports: [{ importedSymbol: "CircA", sourceModule: "./circ-a", line: 1 }]
107
+ });
108
+
109
+ // Must not loop indefinitely or throw
110
+ const blast = calculateBlastRadius(db, "src/ui/circ-a.ts", { maxDepth: 4 });
111
+ assert.ok(blast.totalImpactCount > 0);
112
+ assert.ok(blast.depth <= 4);
113
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * cmd-audit.js — Audit command orchestrator.
3
+ * Single responsibility: run the full AST audit pipeline given CLI args.
4
+ * Extracted from cli/index.js per Directive 1.A (Monolith Decomposition).
5
+ */
6
+
7
+ import { printHelp } from '../help.js';
8
+
9
+ /**
10
+ * @param {string|null} customDir
11
+ * @param {boolean} isCli
12
+ * @param {string[]} rawArgs - process.argv slice already resolved by the caller
13
+ * @param {() => object} loadProjectConfig
14
+ */
15
+ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) => {
16
+ const {
17
+ runAudit: executeAstAudit,
18
+ formatTerminalReport,
19
+ generateMarkdownReport,
20
+ saveAuditSnapshot,
21
+ copyToClipboard,
22
+ buildMasterPrompt,
23
+ groupViolationsBySeverity
24
+ } = await import('../audit.js');
25
+ const {
26
+ runInteractiveAuditNavigator,
27
+ showConversionMenu,
28
+ handleShareToDiscussions
29
+ } = await import('../navigator.js');
30
+ const { renderDashboardBanner } = await import('../navigator-banner.js');
31
+ const {
32
+ isGradeBelowMinimum,
33
+ evaluateAuditFailure,
34
+ isNonInteractiveSession
35
+ } = await import('../audit/rules-predicates.js');
36
+ const { runAuditPreflight, resolveGitAuditScope } = await import('../audit-preflight.js');
37
+ const { syncSearchIndex, resolveTargetDir, syncViolationsIndex, recordAuditSnapshot } = await import('../search.js');
38
+ const { autoGenerateTasksFromAudit } = await import('../team/index.js');
39
+ const { runScaffold } = await import('../scaffold.js');
40
+
41
+ const projectConfig = loadProjectConfig();
42
+ const isJson = rawArgs.includes('--json');
43
+ const isMarkdown = rawArgs.includes('--markdown') || rawArgs.includes('--md');
44
+ const isUnroll = rawArgs.includes('--unroll') || rawArgs.includes('--all');
45
+ const isShare = rawArgs.includes('--share') || rawArgs.includes('--post');
46
+ const isStrict = rawArgs.includes('--strict');
47
+ const isPromptOnFail = rawArgs.includes('--prompt-on-fail');
48
+ const isCopyPrompt = rawArgs.includes('--copy-prompt');
49
+
50
+ const dirFlag = rawArgs.find((arg) => arg.startsWith('--dir='));
51
+ const outputFlag = rawArgs.find((arg) => arg.startsWith('--output=') || arg.startsWith('-o='));
52
+ const outputFile = outputFlag ? outputFlag.split('=')[1] : null;
53
+
54
+ const minGradeFlag = rawArgs.find((arg) => arg.startsWith('--min-grade='));
55
+ const minGrade = (minGradeFlag ? minGradeFlag.split('=')[1] : (process.env.CHEMX_MIN_GRADE || projectConfig.minGrade || '')).toUpperCase();
56
+
57
+ const minScoreFlag = rawArgs.find((arg) => arg.startsWith('--min-score='));
58
+ const minScoreRaw = minScoreFlag ? minScoreFlag.split('=')[1] : (process.env.CHEMX_MIN_SCORE || projectConfig.minScore || null);
59
+ const minScore = minScoreRaw !== null && minScoreRaw !== undefined ? parseInt(String(minScoreRaw), 10) : null;
60
+
61
+ const modelFlag = rawArgs.find((arg) => arg.startsWith('--model='));
62
+ const model = modelFlag ? modelFlag.split('=')[1] : (projectConfig.model || 'blended');
63
+ const costFlag = rawArgs.find((arg) => arg.startsWith('--cost-per-million='));
64
+ const costPerMillion = costFlag ? parseFloat(costFlag.split('=')[1]) : null;
65
+
66
+ const isNonInteractive = isNonInteractiveSession(rawArgs);
67
+ const isInteractive = !isNonInteractive && Boolean(process.stdin.isTTY && process.stdout.isTTY);
68
+
69
+ let targetDir = resolveTargetDir(customDir, dirFlag);
70
+ let isFast = rawArgs.includes('--fast') || rawArgs.includes('--quick');
71
+ let fileList = null;
72
+
73
+ const hasGitFlag = rawArgs.includes('--git') || rawArgs.includes('--changed');
74
+ if (hasGitFlag) {
75
+ const gitScope = resolveGitAuditScope(process.cwd());
76
+ if (gitScope.ok) fileList = gitScope.files;
77
+ }
78
+
79
+ const shouldRunPreflight = isCli && isInteractive && !isJson && !isMarkdown && !isShare;
80
+ if (shouldRunPreflight) {
81
+ const preflight = await runAuditPreflight(rawArgs, {
82
+ customDir,
83
+ defaultDir: targetDir,
84
+ cwd: process.cwd()
85
+ });
86
+ targetDir = preflight.targetDir;
87
+ isFast = preflight.fast;
88
+ fileList = preflight.fileList;
89
+ }
90
+
91
+ const auditOptions = { outputFile, model, costPerMillion, fast: isFast, fileList };
92
+ const report = executeAstAudit(targetDir, auditOptions);
93
+ saveAuditSnapshot(report);
94
+ const syncRes = syncSearchIndex(targetDir, process.cwd());
95
+
96
+ if (syncRes?.db) {
97
+ syncViolationsIndex(syncRes.db, report.violations);
98
+ recordAuditSnapshot(syncRes.db, report);
99
+ if (rawArgs.includes('--triage')) {
100
+ const createdTasks = autoGenerateTasksFromAudit(syncRes.db, { cwd: process.cwd(), targetDir });
101
+ if (isCli && !isJson) {
102
+ process.stdout.write(`\x1b[32m✔\x1b[0m Auto-triage generated ${createdTasks.length} team task(s) from audit violations.\n`);
103
+ }
104
+ }
105
+ }
106
+
107
+ // Stage 1: Atomic failure predicates
108
+ const isSevereViolation = (v) => v.severity === 'CRITICAL' || v.severity === 'HIGH';
109
+ const hasCriticalOrHigh = report.violations.some(isSevereViolation);
110
+ const hasViolations = report.violations.length > 0;
111
+ const isStrictFail = isStrict && hasViolations;
112
+ const isGradeFail = isGradeBelowMinimum(report.health.grade, minGrade);
113
+ const hasMinScore = minScore !== null && !isNaN(minScore);
114
+ const isScoreFail = hasMinScore && report.health.score < minScore;
115
+ const hasThreshold = Boolean(minGrade) || hasMinScore;
116
+ const isDefaultFail = !hasThreshold && !isStrict && hasCriticalOrHigh;
117
+
118
+ // Stage 2: Unified failure decision
119
+ const hasFailingViolations = evaluateAuditFailure([isStrictFail, isDefaultFail, isGradeFail, isScoreFail]);
120
+
121
+ if (isJson) {
122
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
123
+ if (isCli) process.exit(hasFailingViolations ? 1 : 0);
124
+ return report;
125
+ }
126
+
127
+ if (isMarkdown && !outputFile) {
128
+ const md = generateMarkdownReport(report);
129
+ process.stdout.write(md + '\n');
130
+ if (isCli) process.exit(hasFailingViolations ? 1 : 0);
131
+ return report;
132
+ }
133
+
134
+ if (isCli) {
135
+ if (isShare) {
136
+ await handleShareToDiscussions(report);
137
+ process.exit(0);
138
+ }
139
+
140
+ if (isInteractive && !isUnroll) {
141
+ const handleReAudit = () => {
142
+ const refreshed = executeAstAudit(targetDir, auditOptions);
143
+ saveAuditSnapshot(refreshed);
144
+ return refreshed;
145
+ };
146
+ await runInteractiveAuditNavigator(
147
+ report,
148
+ () => runScaffold(undefined, rawArgs, (d, cli) => runAudit(d, cli, rawArgs, loadProjectConfig)),
149
+ handleReAudit
150
+ );
151
+ } else {
152
+ if (isUnroll) {
153
+ process.stdout.write(formatTerminalReport(report));
154
+ if (isInteractive) {
155
+ await showConversionMenu(() => runScaffold(undefined, rawArgs, (d, cli) => runAudit(d, cli, rawArgs, loadProjectConfig)));
156
+ }
157
+ } else {
158
+ const { critical, high, medium, low } = groupViolationsBySeverity(report.violations);
159
+ const highMediumCount = high.length + medium.length;
160
+ renderDashboardBanner(
161
+ report.health,
162
+ report.metrics,
163
+ report.violations,
164
+ critical,
165
+ highMediumCount,
166
+ low,
167
+ report.contextAnalysis,
168
+ report.aiSlop,
169
+ { clear: false }
170
+ );
171
+
172
+ if (hasFailingViolations && !isInteractive) {
173
+ process.stdout.write('\n\x1b[1m\x1b[31m✕ [Chemical X] Architectural health verification failed:\x1b[0m\n');
174
+ if (isGradeFail) process.stdout.write(` \x1b[31m•\x1b[0m Grade ${report.health.grade} is below required minimum tier ${minGrade}\n`);
175
+ if (isScoreFail) process.stdout.write(` \x1b[31m•\x1b[0m Score ${report.health.score}/100 is below required minimum score ${minScore}/100\n`);
176
+ if (isDefaultFail) process.stdout.write(` \x1b[31m•\x1b[0m Unresolved hazards: ${critical.length} Critical, ${high.length} High (run 'chemx audit --unroll' to inspect)\n`);
177
+ if (isStrictFail) process.stdout.write(` \x1b[31m•\x1b[0m Strict mode: ${report.violations.length} total violation(s) detected\n`);
178
+ process.stdout.write(' \x1b[36m💡 Convert hazards into team tasks: chemx team task triage\x1b[0m\n');
179
+ }
180
+ }
181
+ }
182
+
183
+ if (hasFailingViolations && (isPromptOnFail || isCopyPrompt)) {
184
+ const prompt = buildMasterPrompt(report);
185
+ if (prompt) {
186
+ const copied = copyToClipboard(prompt);
187
+ if (copied) {
188
+ process.stdout.write('\n\x1b[1m\x1b[32m✔ AI Agent refactoring prompt copied to clipboard!\x1b[0m\n');
189
+ process.stdout.write('\x1b[36mPaste directly into Cursor, Claude, or Windsurf to resolve architectural hazards.\x1b[0m\n\n');
190
+ } else if (isCopyPrompt) {
191
+ process.stdout.write('\n\x1b[33m⚠ Could not access system clipboard.\x1b[0m\n\n');
192
+ }
193
+ }
194
+ }
195
+
196
+ process.exit(hasFailingViolations ? 1 : 0);
197
+ }
198
+
199
+ process.stdout.write(formatTerminalReport(report));
200
+ return report;
201
+ };