@chemx/starter-kit 26.9.20-408 → 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.
- package/README.md +146 -0
- package/blueprints/_package.json +3 -1
- package/blueprints/workflows/chemx-audit.yml +1 -1
- package/cli/audit/autofix.js +17 -2
- package/cli/audit/clone-detector.js +64 -0
- package/cli/audit/clone-detector.spec.js +46 -0
- package/cli/audit/extended-text-patterns.js +123 -0
- package/cli/audit/reporter-clones.js +33 -0
- package/cli/audit/reporter-hotspot-graph.js +34 -0
- package/cli/audit/rules-registry.js +5 -0
- package/cli/audit-stages.spec.js +30 -0
- package/cli/audit.js +3 -0
- package/cli/blast-radius.spec.js +113 -0
- package/cli/commands/cmd-audit.js +241 -0
- package/cli/commands/cmd-router.js +202 -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/exploder.js +258 -0
- package/cli/exploder.spec.js +99 -0
- package/cli/generator-compact.spec.js +104 -0
- package/cli/generator-framework.spec.js +315 -0
- package/cli/generator-templates/archetypes/collection-archetypes.js +53 -25
- package/cli/generator-templates/archetypes/domain-archetypes.js +2 -2
- package/cli/generator-templates/archetypes/index.js +76 -7
- package/cli/generator-templates/archetypes/input-archetypes.js +2 -2
- package/cli/generator-templates/archetypes/layout-archetypes.js +2 -2
- package/cli/generator-templates/archetypes/minimal-archetypes.js +131 -0
- package/cli/generator-templates/archetypes/status-archetypes.js +4 -4
- package/cli/generator-templates/archetypes/vector-matcher.js +50 -0
- package/cli/generator-templates/archetypes/vector-matcher.spec.js +25 -0
- package/cli/generator-templates/compact.js +130 -0
- package/cli/generator-templates/controller-converters.js +93 -0
- package/cli/generator-templates/controller.js +95 -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.js +1 -0
- package/cli/generator-templates.spec.js +17 -1
- package/cli/generator.js +169 -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 +98 -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 +22 -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 +70 -7
- package/cli/mcp/tools-team.js +16 -1
- package/cli/mcp/tools.js +30 -3
- package/cli/mutators.js +42 -33
- 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 +101 -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-hotspot-graph.js +65 -0
- package/cli/search-queries-hotspot-graph.spec.js +66 -0
- package/cli/search-queries-semantic.js +137 -0
- package/cli/search-queries-similar.js +84 -0
- package/cli/search-queries-similar.spec.js +43 -0
- package/cli/search-queries.js +4 -0
- package/cli/search-schema.js +46 -5
- 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/trend.js +84 -0
- package/cli/trend.spec.js +51 -0
- package/cli/ui-e2e-verification.spec.js +5 -2
- package/cli/ui-filetree.spec.js +5 -5
- package/cli/ui-html.js +26 -34
- package/cli/ui-index.html +659 -0
- package/cli/ui-kanban.spec.js +12 -12
- package/cli/ui-styles.js +195 -81
- package/cli/ui-vbulletin.spec.js +12 -13
- package/cli/ui.spec.js +6 -4
- package/package.json +5 -3
- package/scripts/check-framework-generation.mjs +195 -0
- 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]
|
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 }}
|
package/cli/audit/autofix.js
CHANGED
|
@@ -63,7 +63,9 @@ export const autofixContent = (content, options = {}) => {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// 3. AI lazy placeholder comment removal
|
|
66
|
-
|
|
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
|
-
|
|
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,
|