@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.
- package/README.md +146 -0
- package/blueprints/_package.json +3 -1
- package/blueprints/workflows/chemx-audit.yml +1 -1
- package/cli/audit/extended-text-patterns.js +123 -0
- package/cli/audit/rules-registry.js +5 -0
- package/cli/blast-radius.spec.js +113 -0
- package/cli/commands/cmd-audit.js +201 -0
- package/cli/commands/cmd-router.js +190 -0
- package/cli/commands-schema.js +198 -0
- package/cli/create.js +72 -0
- package/cli/create.spec.js +200 -0
- package/cli/embeddings/vectorizer.js +68 -0
- package/cli/embeddings.spec.js +92 -0
- package/cli/generator-framework.spec.js +243 -0
- package/cli/generator-templates/archetypes/collection-archetypes.js +45 -17
- package/cli/generator-templates/archetypes/domain-archetypes.js +2 -2
- package/cli/generator-templates/archetypes/index.js +46 -7
- package/cli/generator-templates/controller.js +157 -3
- package/cli/generator-templates/hooks.js +27 -3
- package/cli/generator-templates/react.js +3 -3
- package/cli/generator-templates/specs.js +48 -2
- package/cli/generator-templates/svelte.js +89 -4
- package/cli/generator-templates/types.js +14 -5
- package/cli/generator-templates/views.js +28 -3
- package/cli/generator-templates/vue.js +90 -4
- package/cli/generator-templates.spec.js +11 -0
- package/cli/generator.js +82 -48
- package/cli/generator.spec.js +57 -0
- package/cli/help.js +37 -233
- package/cli/help.spec.js +56 -0
- package/cli/index.js +95 -381
- package/cli/installer-templates.js +1 -1
- package/cli/installer.js +17 -9
- package/cli/license.js +4 -3
- package/cli/mcp/antigravity.js +21 -6
- package/cli/mcp/index.js +10 -1
- package/cli/mcp/manifests.js +51 -5
- package/cli/mcp/server.js +4 -0
- package/cli/mcp/tools-search.js +64 -7
- package/cli/mcp/tools-team.js +16 -1
- package/cli/mcp/tools.js +7 -3
- package/cli/mutators.js +32 -10
- package/cli/patcher.js +13 -3
- package/cli/patcher.spec.js +23 -0
- package/cli/pillars-schema.js +150 -0
- package/cli/pillars-wizard.js +158 -0
- package/cli/pillars.spec.js +138 -0
- package/cli/project-detector.js +54 -13
- package/cli/scaffold.js +4 -20
- package/cli/search-commands-graph.js +82 -0
- package/cli/search-commands-semantic.js +98 -0
- package/cli/search-commands.js +4 -0
- package/cli/search-db.js +66 -4
- package/cli/search-queries-graph.js +109 -0
- package/cli/search-queries-semantic.js +137 -0
- package/cli/search-queries.js +4 -0
- package/cli/search-schema.js +41 -0
- package/cli/search.js +88 -4
- package/cli/team/team-commands.js +134 -7
- package/cli/team/team-triage.js +109 -7
- package/cli/team/team.spec.js +129 -2
- package/cli/ui-e2e-verification.spec.js +2 -1
- package/package.json +3 -3
- 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]
|
package/blueprints/_package.json
CHANGED
|
@@ -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
|
+
};
|