@chemx/starter-kit 26.9.23-1269 → 26.9.25-747
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 +68 -8
- package/blueprints/atoms/a-button.svelte +30 -0
- package/blueprints/atoms/a-button.tsx +36 -0
- package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.svelte +35 -0
- package/blueprints/molecule-capsule/m-sample-card.svelte +58 -0
- package/blueprints/molecule-capsule/m-sample-card.tsx +3 -3
- package/blueprints/molecule-capsule/m-sample-card.vue +60 -0
- package/blueprints/view-template.svelte +18 -0
- package/blueprints/view-template.vue +18 -0
- package/blueprints/workflows/chemx-audit.yml +36 -0
- package/cli/audit/ast-visitors.js +25 -13
- package/cli/audit/csharp-analyzer.js +80 -0
- package/cli/audit/extended-text-patterns.js +11 -2
- package/cli/audit/history.js +22 -6
- package/cli/audit/hook-shape-validator.js +1 -1
- package/cli/audit/pattern-detector.js +5 -3
- package/cli/audit/pattern-detector.spec.js +17 -0
- package/cli/audit/prompts.js +12 -9
- package/cli/audit/prompts.spec.js +52 -1
- package/cli/audit/reporter-grades.js +1 -0
- package/cli/audit/reporter-markdown.js +2 -0
- package/cli/audit/reporter-sections.js +3 -0
- package/cli/audit/rules-registry.js +27 -0
- package/cli/audit/rules.js +14 -3
- package/cli/audit/rules.spec.js +27 -0
- package/cli/audit/structural-weight-evaluator.js +121 -0
- package/cli/audit/structural-weight.js +92 -0
- package/cli/audit/structural-weight.spec.js +116 -0
- package/cli/audit-preflight-scope.js +4 -1
- package/cli/audit.js +9 -10
- package/cli/commands/cmd-audit.js +36 -32
- package/cli/commands/cmd-router.js +19 -1
- package/cli/commands-schema.js +14 -0
- package/cli/config/config.spec.js +51 -0
- package/cli/config/index.js +37 -0
- package/cli/config/loader.js +83 -0
- package/cli/config/profiles.js +56 -0
- package/cli/config.spec.js +1 -0
- package/cli/create.js +21 -8
- package/cli/create.spec.js +91 -0
- package/cli/generator-help.js +48 -0
- package/cli/generator-indexer.js +41 -0
- package/cli/generator.js +8 -43
- package/cli/generator.spec.js +29 -0
- package/cli/index.js +2 -0
- package/cli/languages.js +122 -0
- package/cli/languages.spec.js +44 -0
- package/cli/mcp/antigravity.js +2 -0
- package/cli/mcp/manifests.js +2 -1
- package/cli/mcp/tools-audit.js +2 -0
- package/cli/mcp/tools-patch.js +7 -5
- package/cli/mcp/tools-team-tasks.js +58 -11
- package/cli/mcp/tools.js +13 -2
- package/cli/polyglot.spec.js +111 -0
- package/cli/scaffold-frameworks.js +133 -0
- package/cli/scaffold.js +67 -37
- package/cli/search-ast.js +41 -9
- package/cli/search-db.js +2 -8
- package/cli/search-queries-semantic.js +28 -9
- package/cli/search-schema.js +54 -6
- package/cli/search-tokenizer.js +52 -0
- package/cli/search.js +6 -5
- package/cli/search.spec.js +51 -3
- package/cli/team/team-commands-vds.js +70 -0
- package/cli/team/team-commands.js +75 -4
- package/cli/team/team-db-lock-promotion.js +31 -16
- package/cli/team/team-db-locks.js +54 -43
- package/cli/team/team-db-task-helpers.js +68 -0
- package/cli/team/team-db-tasks.js +45 -51
- package/cli/team/team-db-transaction.js +52 -0
- package/cli/team/team-flags.js +4 -1
- package/cli/team/team-format.js +60 -0
- package/cli/team/team-multiprocess-concurrency.spec.js +193 -0
- package/cli/team/team-release-train.js +65 -0
- package/cli/team/team-release-train.spec.js +91 -0
- package/cli/team/team-schema-vds.js +39 -0
- package/cli/team/team-schema.js +21 -23
- package/cli/team/team-tasks-asana.spec.js +60 -0
- package/cli/team/team-tasks-hierarchy.spec.js +97 -1
- package/cli/team/team-triage.js +130 -43
- package/cli/team/team-vds.js +88 -0
- package/cli/team/team-vds.spec.js +125 -0
- package/cli/team/team-verification-gate.spec.js +27 -0
- package/cli/tesseract-hud.js +59 -0
- package/cli/tesseract-manifesto.js +61 -0
- package/cli/tesseract-state.js +58 -0
- package/cli/tesseract.js +81 -0
- package/cli/tesseract.spec.js +55 -0
- package/cli/ui-actions-vds.js +48 -0
- package/cli/ui-actions.js +1 -0
- package/cli/ui-client-api.js +8 -0
- package/cli/ui-client-script.js +33 -29
- package/cli/ui-dev-watcher.js +61 -0
- package/cli/ui-direct-routes.spec.js +55 -0
- package/cli/ui-handlers.js +7 -8
- package/cli/ui-index.html +416 -26
- package/cli/ui-kanban.spec.js +11 -0
- package/cli/ui-server-routes.js +14 -6
- package/cli/ui-server.js +26 -21
- package/cli/ui-sse.js +23 -10
- package/cli/ui-sse.spec.js +26 -0
- package/cli/ui-styles.js +20 -4
- package/cli/ui-template-kanban.js +23 -27
- package/cli/ui-template-task-sheet.js +87 -0
- package/cli/ui-template-vds-badges.js +34 -0
- package/cli/ui-template.js +13 -10
- package/cli/verify-helpers.js +173 -0
- package/cli/verify.js +132 -176
- package/cli/verify.spec.js +61 -0
- package/package.json +5 -1
- package/scripts/pre-commit.sh +4 -0
- package/docs/CHANGELOG.md +0 -332
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as t from '@babel/types';
|
|
2
2
|
import { RULE_REGISTRY } from './rules-registry.js';
|
|
3
3
|
|
|
4
|
-
const VERB_PREFIX_REGEX = /^(add|remove|toggle|set|get|fetch|load|save|delete|clear|reset|refresh|update|create|edit|send|sync|init|dispose|trigger|post|put|patch|apply|dismiss|select|deselect|filter|sort|search|open|close|show|hide|start|stop|cancel|execute|run|retry|submit|validate|mutate|next|prev|step|increment|decrement|insert|upsert|drop|enable|disable|register|unregister|login|logout|checkout|publish|archive|discard|confirm|undo|redo|claim|complete|acquire|release|download|upload|export|import|handle)([A-Z0-9_].*)?$/;
|
|
4
|
+
const VERB_PREFIX_REGEX = /^(add|remove|toggle|set|get|fetch|load|save|delete|clear|reset|refresh|update|create|edit|send|sync|init|setup|dispose|trigger|post|put|patch|apply|dismiss|select|deselect|filter|sort|search|open|close|show|hide|start|stop|cancel|execute|run|retry|submit|validate|mutate|next|prev|step|increment|decrement|insert|upsert|drop|enable|disable|register|unregister|login|logout|checkout|publish|archive|discard|confirm|undo|redo|claim|complete|acquire|release|download|upload|export|import|handle|play|pause|resume|seek|unlock|restore|advance|focus|notify|broadcast|copy|format)([A-Z0-9_].*)?$/;
|
|
5
5
|
|
|
6
6
|
const STATUS_HEURISTIC_REGEX = /^(is[A-Z]|has[A-Z]|can[A-Z]|should[A-Z])|.*(Error|Status)$/;
|
|
7
7
|
const STATUS_EXACT_NAMES = new Set(['status', 'error', 'loading', 'pending', 'ready']);
|
|
@@ -29,15 +29,17 @@ export const createPatternRegistry = () => {
|
|
|
29
29
|
});
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
const resolveHarmonizationCandidates = (hotspots = []) => {
|
|
32
|
+
const resolveHarmonizationCandidates = (hotspots = [], options = {}) => {
|
|
33
33
|
const hotspotFiles = new Set(hotspots.map((h) => h.filePath));
|
|
34
34
|
const candidates = [];
|
|
35
|
+
const minFiles = options.ruleOfThree ? 3 : 2;
|
|
35
36
|
|
|
36
37
|
for (const [key, bucket] of buckets.entries()) {
|
|
37
38
|
const uniqueFiles = Array.from(new Set(bucket.occurrences.map((o) => o.filePath)));
|
|
38
|
-
if (uniqueFiles.length < 2) continue;
|
|
39
|
-
|
|
40
39
|
const hasHotspot = uniqueFiles.some((f) => hotspotFiles.has(f));
|
|
40
|
+
const isCandidateEligible = hasHotspot ? uniqueFiles.length >= 2 : uniqueFiles.length >= minFiles;
|
|
41
|
+
if (!isCandidateEligible) continue;
|
|
42
|
+
|
|
41
43
|
const impactScore = uniqueFiles.length * (hasHotspot ? 3 : 1.5) + bucket.occurrences.length;
|
|
42
44
|
|
|
43
45
|
let label = 'Recurring Pattern';
|
|
@@ -94,3 +94,20 @@ test('categorizeUiStructure: maps semantic AST topologies to specialized capsule
|
|
|
94
94
|
assert.equal(fallbackRes.suggestedCapsule, 'm-feature-card');
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
test('Rule of Three: requires pattern across 3+ files unless a hotspot is involved', () => {
|
|
98
|
+
const registry = createPatternRegistry();
|
|
99
|
+
const template = `<template><v-sheet><x-btn>1</x-btn><x-btn>2</x-btn></v-sheet></template>`;
|
|
100
|
+
|
|
101
|
+
recordTemplatePatterns(registry, template, 'src/components/A.vue');
|
|
102
|
+
recordTemplatePatterns(registry, template, 'src/components/B.vue');
|
|
103
|
+
|
|
104
|
+
// With Rule of Three enabled and no hotspots, 2 files do not trigger premature abstraction
|
|
105
|
+
const strictCandidates = registry.resolveHarmonizationCandidates([], { ruleOfThree: true });
|
|
106
|
+
assert.equal(strictCandidates.length, 0, '2 occurrences without hotspot should not trigger premature abstraction');
|
|
107
|
+
|
|
108
|
+
// When a 3rd file adopts the pattern, Rule of Three threshold is satisfied
|
|
109
|
+
recordTemplatePatterns(registry, template, 'src/components/C.vue');
|
|
110
|
+
const satisfiedCandidates = registry.resolveHarmonizationCandidates([], { ruleOfThree: true });
|
|
111
|
+
assert.equal(satisfiedCandidates.length, 1, '3 occurrences should satisfy Rule of Three threshold');
|
|
112
|
+
});
|
|
113
|
+
|
package/cli/audit/prompts.js
CHANGED
|
@@ -36,7 +36,7 @@ const renderTreeLines = (node, depth = 0) => {
|
|
|
36
36
|
return lines;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
export const formatGroupedPromptViolations = (violations = []) => {
|
|
39
|
+
export const formatGroupedPromptViolations = (violations = [], options = {}) => {
|
|
40
40
|
const hasNoViolations = violations.length === 0;
|
|
41
41
|
if (hasNoViolations) return [];
|
|
42
42
|
|
|
@@ -49,11 +49,14 @@ export const formatGroupedPromptViolations = (violations = []) => {
|
|
|
49
49
|
lines.push(`${idx + 1}. ${sevColor}[${rg.rule}]${RESET} ${BOLD}(${countLabel})${RESET}`);
|
|
50
50
|
lines.push(` Hazard: ${rg.hazard}`);
|
|
51
51
|
lines.push(` Directive: ${CYAN}${rg.directive}${RESET}`);
|
|
52
|
-
lines.push(
|
|
52
|
+
lines.push(` Action: chemx team task list --rule=${rg.rule}`);
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
if (options.detailedLocations) {
|
|
55
|
+
lines.push(' Locations:');
|
|
56
|
+
const tree = buildPathTree(rg.violations);
|
|
57
|
+
const treeLines = renderTreeLines(tree, 0);
|
|
58
|
+
lines.push(...treeLines);
|
|
59
|
+
}
|
|
57
60
|
lines.push('');
|
|
58
61
|
});
|
|
59
62
|
|
|
@@ -62,10 +65,10 @@ export const formatGroupedPromptViolations = (violations = []) => {
|
|
|
62
65
|
|
|
63
66
|
export const buildAgentCommandsSection = () => [
|
|
64
67
|
'### AI AGENT DISCOVERY & REFACTORING COMMANDS:',
|
|
65
|
-
'-
|
|
66
|
-
'-
|
|
67
|
-
'-
|
|
68
|
-
'-
|
|
68
|
+
'- Claim Work: Run `chemx team task list --status=queued` and claim via `chemx team task claim <id> --as=@coder`.',
|
|
69
|
+
'- Inspect Target: Run `chemx read <target_path> --outline` to inspect AST signatures without token bloat.',
|
|
70
|
+
'- Verify & Auto-Resolve: Run `chemx verify`. Clean files auto-reconcile tasks to done in SQLite.',
|
|
71
|
+
'- MCP Invocations: If invoking via MCP, pass command string directly via `chemx({ command: "..." })`.'
|
|
69
72
|
].join('\n');
|
|
70
73
|
|
|
71
74
|
export const buildGradeFPrompt = (report, options = {}) => {
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
buildAiSlopPrompt,
|
|
11
11
|
buildHotspotsPrompt,
|
|
12
12
|
buildPillarPrompt,
|
|
13
|
-
buildMasterPrompt
|
|
13
|
+
buildMasterPrompt,
|
|
14
|
+
formatGroupedPromptViolations
|
|
14
15
|
} from './prompts.js';
|
|
15
16
|
|
|
16
17
|
test('deduplicateRolePreambles: handles invalid and empty inputs', () => {
|
|
@@ -314,3 +315,53 @@ test('deduplicateRefactoringCommands: collapses multiple occurrences to single b
|
|
|
314
315
|
assert.ok(result.includes('Section 2 content'));
|
|
315
316
|
assert.ok(result.includes('- Command B'));
|
|
316
317
|
});
|
|
318
|
+
|
|
319
|
+
test('formatGroupedPromptViolations: outputs token-compact task action by default', () => {
|
|
320
|
+
const violations = [
|
|
321
|
+
{
|
|
322
|
+
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
323
|
+
severity: 'CRITICAL',
|
|
324
|
+
hazard: 'Unsanitized HTML injection',
|
|
325
|
+
directive: 'Sanitize dynamic HTML via DOMPurify',
|
|
326
|
+
filePath: 'src/view-a.vue',
|
|
327
|
+
line: 10
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
331
|
+
severity: 'CRITICAL',
|
|
332
|
+
hazard: 'Unsanitized HTML injection',
|
|
333
|
+
directive: 'Sanitize dynamic HTML via DOMPurify',
|
|
334
|
+
filePath: 'src/view-b.vue',
|
|
335
|
+
line: 25
|
|
336
|
+
}
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
const lines = formatGroupedPromptViolations(violations);
|
|
340
|
+
const text = lines.join('\n');
|
|
341
|
+
|
|
342
|
+
assert.ok(text.includes('[SECURITY_RAW_HTML_INJECTION]'));
|
|
343
|
+
assert.ok(text.includes('(2 items)'));
|
|
344
|
+
assert.ok(text.includes('Action: chemx team task list --rule=SECURITY_RAW_HTML_INJECTION'));
|
|
345
|
+
// Does not dump file locations tree by default
|
|
346
|
+
assert.ok(!text.includes('Locations:'));
|
|
347
|
+
assert.ok(!text.includes('view-a.vue:10'));
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('formatGroupedPromptViolations: renders file tree when detailedLocations is enabled', () => {
|
|
351
|
+
const violations = [
|
|
352
|
+
{
|
|
353
|
+
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
354
|
+
severity: 'CRITICAL',
|
|
355
|
+
hazard: 'Unsanitized HTML injection',
|
|
356
|
+
directive: 'Sanitize dynamic HTML via DOMPurify',
|
|
357
|
+
filePath: 'src/view-a.vue',
|
|
358
|
+
line: 10
|
|
359
|
+
}
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
const lines = formatGroupedPromptViolations(violations, { detailedLocations: true });
|
|
363
|
+
const text = lines.join('\n');
|
|
364
|
+
|
|
365
|
+
assert.ok(text.includes('Locations:'));
|
|
366
|
+
assert.ok(text.includes('view-a.vue:10'));
|
|
367
|
+
});
|
|
@@ -267,6 +267,7 @@ export const formatGradeASection = (report) => {
|
|
|
267
267
|
lines.push(` ${GREEN}✔${RESET} Low Context Hazard & Token Burn Risk`);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
lines.push(`\n ${DIM}ℹ Notice: Architectural grade measures AST rules only. Run chemx verify for full typecheck and test verification.${RESET}`);
|
|
270
271
|
lines.push(`\n${GREEN}======================================================================${RESET}\n`);
|
|
271
272
|
return lines.join('\n');
|
|
272
273
|
};
|
|
@@ -39,6 +39,8 @@ export const generateMarkdownReport = (report) => {
|
|
|
39
39
|
lines.push('');
|
|
40
40
|
}
|
|
41
41
|
lines.push('> *Comprehensive static analysis report enforcing Chemical X Molecular Architecture standards.*');
|
|
42
|
+
lines.push('> ');
|
|
43
|
+
lines.push('> **Notice**: Architectural grades measure AST rule compliance only. Run `chemx verify` for full production readiness (typecheck + test suite).');
|
|
42
44
|
lines.push('');
|
|
43
45
|
lines.push('## 1. Executive Summary & Scorecard');
|
|
44
46
|
lines.push('');
|
|
@@ -72,6 +72,9 @@ export const formatScorecardSection = (report, themeColor = null) => {
|
|
|
72
72
|
lines.push(` Largest File: ${BOLD}${metrics.largestFile.filePath || 'None'}${RESET} (${metrics.largestFile.lineCount} lines)`);
|
|
73
73
|
lines.push(` Molecule Capsules: ${metrics.moleculeCount} found (${metrics.moleculeCompliantPct}% compliant < 100 lines of code)`);
|
|
74
74
|
lines.push(` Custom Hooks: ${metrics.hookCount} detected`);
|
|
75
|
+
lines.push(` ${sectionColor}----------------------------------------------------------------------${RESET}`);
|
|
76
|
+
lines.push(` ${DIM}ℹ Notice: Architectural grade measures AST rules only.${RESET}`);
|
|
77
|
+
lines.push(` ${DIM} Run ${RESET}${BOLD}chemx verify${RESET}${DIM} for full typecheck and test verification.${RESET}`);
|
|
75
78
|
lines.push(`${sectionColor}======================================================================${RESET}\n`);
|
|
76
79
|
|
|
77
80
|
return lines.join('\n');
|
|
@@ -215,5 +215,32 @@ export const RULE_REGISTRY = {
|
|
|
215
215
|
pillar: 'AI Slop & Code Authenticity',
|
|
216
216
|
severity: 'LOW',
|
|
217
217
|
directive: 'Inline redundant single-use passthrough assignments directly into return expressions'
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// Greybeard Pragmatic: Structural Weight & Bounded Context
|
|
221
|
+
STRUCTURAL_WEIGHT_EXCEEDED: {
|
|
222
|
+
pillar: PILLARS.PILLAR_1,
|
|
223
|
+
severity: 'HIGH',
|
|
224
|
+
directive: 'Reduce component responsibility density: extract domain hooks, decompose branching, or flatten render tree'
|
|
225
|
+
},
|
|
226
|
+
COMPLEXITY_CYCLOMATIC_HIGH: {
|
|
227
|
+
pillar: PILLARS.PILLAR_2,
|
|
228
|
+
severity: 'HIGH',
|
|
229
|
+
directive: 'Decompose high cyclomatic complexity into pure helper functions or table dispatch'
|
|
230
|
+
},
|
|
231
|
+
HOOK_STATE_SATURATION: {
|
|
232
|
+
pillar: PILLARS.PILLAR_3,
|
|
233
|
+
severity: 'HIGH',
|
|
234
|
+
directive: 'Extract co-located state and effects into a dedicated domain hook or reducer'
|
|
235
|
+
},
|
|
236
|
+
RENDER_TREE_DEPTH_EXCEEDED: {
|
|
237
|
+
pillar: PILLARS.PILLAR_2,
|
|
238
|
+
severity: 'HIGH',
|
|
239
|
+
directive: 'Extract nested JSX ternaries into computed descriptor objects or early returns'
|
|
240
|
+
},
|
|
241
|
+
PROP_SURFACE_BLOAT: {
|
|
242
|
+
pillar: PILLARS.PILLAR_4,
|
|
243
|
+
severity: 'MEDIUM',
|
|
244
|
+
directive: 'Encapsulate flat primitive props into a cohesive domain type or composable'
|
|
218
245
|
}
|
|
219
246
|
};
|
package/cli/audit/rules.js
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
} from './extended-visitors.js';
|
|
21
21
|
import { createPatternVisitors, recordTemplatePatterns } from './pattern-detector.js';
|
|
22
22
|
import { createHookShapeRegistry } from './hook-shape-validator.js';
|
|
23
|
+
import { isBabelParsable, getLanguageForFile } from '../languages.js';
|
|
24
|
+
import { analyzeCSharpCode } from './csharp-analyzer.js';
|
|
23
25
|
|
|
24
26
|
export { PILLARS, RULE_REGISTRY, createHookShapeRegistry };
|
|
25
27
|
|
|
@@ -34,7 +36,11 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
34
36
|
const isView = relativePath.includes('/views/') || relativePath.includes('/pages/') || /View\.[tj]sx?$/.test(baseName);
|
|
35
37
|
const isRootTypeFile = baseName === 'types.ts' || baseName === 'global.d.ts';
|
|
36
38
|
|
|
39
|
+
const config = options.config?.rules || options.config || {};
|
|
40
|
+
const enforceFileLength = config.enforceFileLength === true || config.profile === 'atomic-strict';
|
|
41
|
+
|
|
37
42
|
// Pillar 1: Sliding Scale Monolith Detection
|
|
43
|
+
const moleculeLimit = enforceFileLength ? 100 : (config.maxLineCountWarning || 250);
|
|
38
44
|
if (lineCount > 500) {
|
|
39
45
|
const tier = resolveMonolithTier(lineCount);
|
|
40
46
|
violations.push({
|
|
@@ -47,7 +53,7 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
47
53
|
pillar: PILLARS.PILLAR_1,
|
|
48
54
|
directive: tier.directive
|
|
49
55
|
});
|
|
50
|
-
} else if (isMolecule && lineCount >
|
|
56
|
+
} else if (isMolecule && lineCount > moleculeLimit) {
|
|
51
57
|
const tier = resolveMoleculeTier(lineCount);
|
|
52
58
|
violations.push({
|
|
53
59
|
filePath: relativePath,
|
|
@@ -103,7 +109,12 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
103
109
|
// Pillars 8-11: Extended Fast Checks (A11y, Security secrets, Fake tests, Co-located specs)
|
|
104
110
|
checkExtendedTextPatterns(content, lines, relativePath, filePath, violations);
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
const lang = getLanguageForFile(filePath);
|
|
113
|
+
if (lang?.id === 'csharp') {
|
|
114
|
+
analyzeCSharpCode(content, relativePath, violations);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.fast || !isBabelParsable(filePath)) {
|
|
107
118
|
return violations;
|
|
108
119
|
}
|
|
109
120
|
|
|
@@ -147,7 +158,7 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
147
158
|
|
|
148
159
|
const traverseFn = traverse.default || traverse;
|
|
149
160
|
const hookRegistry = options.hookRegistry || createHookShapeRegistry();
|
|
150
|
-
const visitors = createAstVisitors({ relativePath, violations, hookRegistry });
|
|
161
|
+
const visitors = createAstVisitors({ relativePath, violations, hookRegistry, config });
|
|
151
162
|
const slopVisitors = createAiSlopVisitors({ relativePath, violations });
|
|
152
163
|
const extendedVisitors = createExtendedVisitors({ relativePath, violations });
|
|
153
164
|
const patternVisitors = options.patternRegistry
|
package/cli/audit/rules.spec.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { auditCode } from "./rules.js";
|
|
4
|
+
import { formatScorecardSection } from './reporter-sections.js';
|
|
5
|
+
import { formatGradeASection } from './reporter-grades.js';
|
|
6
|
+
import { generateMarkdownReport } from './reporter-markdown.js';
|
|
4
7
|
|
|
5
8
|
test("Audit Rules: flags repetitive dispatch switch statement as CONTROL_FLOW_DISPATCH_SWITCH", () => {
|
|
6
9
|
const code = `
|
|
@@ -332,3 +335,27 @@ export const useSecond = () => {
|
|
|
332
335
|
assert.ok(inconsistentViolation.hazard.includes("isLoading"));
|
|
333
336
|
});
|
|
334
337
|
|
|
338
|
+
test("Audit Reporter: includes explicit architectural health notice distinguishing from production readiness", () => {
|
|
339
|
+
const mockReport = {
|
|
340
|
+
metrics: { scannedFiles: 5, totalLoc: 200, avgLoc: 40, largestFile: { filePath: 'src/a.ts', lineCount: 50 }, moleculeCount: 2, moleculeCompliantPct: 100, hookCount: 1 },
|
|
341
|
+
health: { score: 100, grade: 'A+', label: 'Pristine' },
|
|
342
|
+
pillars: { 'Pillar 1': { status: 'PASSED', violations: 0, critical: 0, high: 0, medium: 0, low: 0 } },
|
|
343
|
+
hotspots: [],
|
|
344
|
+
contextAnalysis: { riskLevel: 'LOW', estimatedTokens: 1000, estimatedExcessTokens: 0, potentialSavingsPct: 0 },
|
|
345
|
+
violations: [],
|
|
346
|
+
aiSlop: { score: 100, grade: 'A+', label: 'Pure Artisanal' }
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const scorecard = formatScorecardSection(mockReport);
|
|
350
|
+
assert.ok(scorecard.includes('Architectural grade measures AST rules only'));
|
|
351
|
+
assert.ok(scorecard.includes('chemx verify'));
|
|
352
|
+
|
|
353
|
+
const gradeA = formatGradeASection(mockReport);
|
|
354
|
+
assert.ok(gradeA.includes('Architectural grade measures AST rules only'));
|
|
355
|
+
assert.ok(gradeA.includes('chemx verify'));
|
|
356
|
+
|
|
357
|
+
const markdown = generateMarkdownReport(mockReport);
|
|
358
|
+
assert.ok(markdown.includes('Architectural grades measure AST rule compliance only'));
|
|
359
|
+
assert.ok(markdown.includes('chemx verify'));
|
|
360
|
+
});
|
|
361
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { RULE_REGISTRY } from './rules-registry.js';
|
|
3
|
+
import {
|
|
4
|
+
countBranchingDecisions,
|
|
5
|
+
countHookCalls,
|
|
6
|
+
findNestedTernary,
|
|
7
|
+
countDestructuredProps
|
|
8
|
+
} from './structural-weight.js';
|
|
9
|
+
|
|
10
|
+
const isComponentOrHook = (funcPath, relativePath = '') => {
|
|
11
|
+
const isComponentFile = /\.(jsx|tsx|vue|svelte)$/.test(relativePath) ||
|
|
12
|
+
relativePath.includes('/components/') ||
|
|
13
|
+
relativePath.includes('/molecules/') ||
|
|
14
|
+
relativePath.includes('/atoms/') ||
|
|
15
|
+
relativePath.includes('/organisms/') ||
|
|
16
|
+
relativePath.includes('/views/') ||
|
|
17
|
+
relativePath.includes('/hooks/') ||
|
|
18
|
+
relativePath.includes('/composables/');
|
|
19
|
+
|
|
20
|
+
if (isComponentFile) return true;
|
|
21
|
+
|
|
22
|
+
const id = funcPath.node.id;
|
|
23
|
+
if (id && t.isIdentifier(id)) {
|
|
24
|
+
if (/^[A-Z]/.test(id.name) || /^use[A-Z0-9]/.test(id.name)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let returnsJsx = false;
|
|
30
|
+
funcPath.traverse({
|
|
31
|
+
ReturnStatement(ret) {
|
|
32
|
+
if (ret.getFunctionParent() === funcPath) {
|
|
33
|
+
const arg = ret.node.argument;
|
|
34
|
+
if (t.isJSXElement(arg) || t.isJSXFragment(arg)) {
|
|
35
|
+
returnsJsx = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return returnsJsx;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const evaluateComponentStructuralWeight = ({
|
|
45
|
+
funcPath,
|
|
46
|
+
relativePath,
|
|
47
|
+
violations,
|
|
48
|
+
config = {}
|
|
49
|
+
}) => {
|
|
50
|
+
if (!isComponentOrHook(funcPath, relativePath)) return null;
|
|
51
|
+
|
|
52
|
+
const maxComplexity = config.maxCyclomaticComplexity || 12;
|
|
53
|
+
const maxHooks = config.maxHookDensity || 4;
|
|
54
|
+
const maxProps = config.maxPropCount || 7;
|
|
55
|
+
|
|
56
|
+
const complexity = countBranchingDecisions(funcPath);
|
|
57
|
+
const hooks = countHookCalls(funcPath);
|
|
58
|
+
const propCount = countDestructuredProps(funcPath);
|
|
59
|
+
const startLoc = funcPath.node.loc?.start;
|
|
60
|
+
const line = startLoc?.line || 1;
|
|
61
|
+
const column = startLoc?.column || 1;
|
|
62
|
+
|
|
63
|
+
if (complexity > maxComplexity) {
|
|
64
|
+
const meta = RULE_REGISTRY.COMPLEXITY_CYCLOMATIC_HIGH;
|
|
65
|
+
violations.push({
|
|
66
|
+
filePath: relativePath,
|
|
67
|
+
line,
|
|
68
|
+
column,
|
|
69
|
+
hazard: `High cyclomatic complexity (${complexity} > ${maxComplexity} threshold)`,
|
|
70
|
+
rule: 'COMPLEXITY_CYCLOMATIC_HIGH',
|
|
71
|
+
severity: meta.severity,
|
|
72
|
+
pillar: meta.pillar,
|
|
73
|
+
directive: meta.directive
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (hooks > maxHooks) {
|
|
78
|
+
const meta = RULE_REGISTRY.HOOK_STATE_SATURATION;
|
|
79
|
+
violations.push({
|
|
80
|
+
filePath: relativePath,
|
|
81
|
+
line,
|
|
82
|
+
column,
|
|
83
|
+
hazard: `Excessive hook & state density (${hooks} hooks > ${maxHooks} threshold)`,
|
|
84
|
+
rule: 'HOOK_STATE_SATURATION',
|
|
85
|
+
severity: meta.severity,
|
|
86
|
+
pillar: meta.pillar,
|
|
87
|
+
directive: meta.directive
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (propCount > maxProps) {
|
|
92
|
+
const meta = RULE_REGISTRY.PROP_SURFACE_BLOAT;
|
|
93
|
+
violations.push({
|
|
94
|
+
filePath: relativePath,
|
|
95
|
+
line,
|
|
96
|
+
column,
|
|
97
|
+
hazard: `Prop surface area bloat (${propCount} props > ${maxProps} threshold)`,
|
|
98
|
+
rule: 'PROP_SURFACE_BLOAT',
|
|
99
|
+
severity: meta.severity,
|
|
100
|
+
pillar: meta.pillar,
|
|
101
|
+
directive: meta.directive
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const nestedTernary = findNestedTernary(funcPath);
|
|
106
|
+
if (nestedTernary) {
|
|
107
|
+
const meta = RULE_REGISTRY.CONTROL_FLOW_NESTED_TERNARY;
|
|
108
|
+
violations.push({
|
|
109
|
+
filePath: relativePath,
|
|
110
|
+
line: nestedTernary.loc?.start.line || line,
|
|
111
|
+
column: nestedTernary.loc?.start.column || column,
|
|
112
|
+
hazard: 'Nested ternary expression detected in component logic',
|
|
113
|
+
rule: 'CONTROL_FLOW_NESTED_TERNARY',
|
|
114
|
+
severity: meta.severity,
|
|
115
|
+
pillar: meta.pillar,
|
|
116
|
+
directive: meta.directive
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { complexity, hooks, propCount };
|
|
121
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
|
|
3
|
+
export const countBranchingDecisions = (funcPath) => {
|
|
4
|
+
let complexity = 1;
|
|
5
|
+
funcPath.traverse({
|
|
6
|
+
IfStatement(p) {
|
|
7
|
+
if (p.getFunctionParent() === funcPath) complexity += 1;
|
|
8
|
+
},
|
|
9
|
+
ConditionalExpression(p) {
|
|
10
|
+
if (p.getFunctionParent() === funcPath) complexity += 1;
|
|
11
|
+
},
|
|
12
|
+
SwitchCase(p) {
|
|
13
|
+
if (p.getFunctionParent() === funcPath && p.node.test !== null) complexity += 1;
|
|
14
|
+
},
|
|
15
|
+
LogicalExpression(p) {
|
|
16
|
+
if (p.getFunctionParent() === funcPath) complexity += 1;
|
|
17
|
+
},
|
|
18
|
+
ForStatement(p) {
|
|
19
|
+
if (p.getFunctionParent() === funcPath) complexity += 1;
|
|
20
|
+
},
|
|
21
|
+
ForInStatement(p) {
|
|
22
|
+
if (p.getFunctionParent() === funcPath) complexity += 1;
|
|
23
|
+
},
|
|
24
|
+
ForOfStatement(p) {
|
|
25
|
+
if (p.getFunctionParent() === funcPath) complexity += 1;
|
|
26
|
+
},
|
|
27
|
+
WhileStatement(p) {
|
|
28
|
+
if (p.getFunctionParent() === funcPath) complexity += 1;
|
|
29
|
+
},
|
|
30
|
+
CatchClause(p) {
|
|
31
|
+
if (p.getFunctionParent() === funcPath) complexity += 1;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return complexity;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const countHookCalls = (funcPath) => {
|
|
38
|
+
let hooks = 0;
|
|
39
|
+
funcPath.traverse({
|
|
40
|
+
CallExpression(p) {
|
|
41
|
+
if (p.getFunctionParent() !== funcPath) return;
|
|
42
|
+
const callee = p.node.callee;
|
|
43
|
+
const isHookIdentifier = t.isIdentifier(callee) && /^use[A-Z0-9]/.test(callee.name);
|
|
44
|
+
if (isHookIdentifier) {
|
|
45
|
+
hooks += 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return hooks;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const findNestedTernary = (astPath) => {
|
|
53
|
+
let nested = null;
|
|
54
|
+
astPath.traverse({
|
|
55
|
+
ConditionalExpression(p) {
|
|
56
|
+
if (nested) return;
|
|
57
|
+
const parentCond = p.findParent((parent) => t.isConditionalExpression(parent.node));
|
|
58
|
+
if (parentCond) {
|
|
59
|
+
nested = p.node;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return nested;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const countDestructuredProps = (funcPath) => {
|
|
67
|
+
const firstParam = funcPath.node.params?.[0];
|
|
68
|
+
const isObjectPattern = t.isObjectPattern(firstParam);
|
|
69
|
+
if (isObjectPattern) {
|
|
70
|
+
return firstParam.properties.length;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const measureJsxDepth = (jsxElementPath) => {
|
|
76
|
+
let maxDepth = 1;
|
|
77
|
+
const walk = (node, depth) => {
|
|
78
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
79
|
+
const isJsxContainer = t.isJSXElement(node) || t.isJSXFragment(node);
|
|
80
|
+
if (isJsxContainer) {
|
|
81
|
+
const children = node.children || [];
|
|
82
|
+
for (const child of children) {
|
|
83
|
+
const isChildElement = t.isJSXElement(child) || t.isJSXFragment(child);
|
|
84
|
+
if (isChildElement) {
|
|
85
|
+
walk(child, depth + 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
walk(jsxElementPath.node, 1);
|
|
91
|
+
return maxDepth;
|
|
92
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { auditCode } from './rules.js';
|
|
4
|
+
|
|
5
|
+
describe('AST Structural Weight & Pragmatic Profile Audit', () => {
|
|
6
|
+
it('does NOT flag a 120-line cohesive molecule in default pragmatic mode', () => {
|
|
7
|
+
// Generate a cohesive 115-line component with low complexity
|
|
8
|
+
const staticRows = Array.from({ length: 100 }, (_, i) => ` { id: ${i}, label: 'Item ${i}' },`).join('\n');
|
|
9
|
+
const code = `
|
|
10
|
+
export const SimpleTable = () => {
|
|
11
|
+
const items = [
|
|
12
|
+
${staticRows}
|
|
13
|
+
];
|
|
14
|
+
return items.length;
|
|
15
|
+
};
|
|
16
|
+
`;
|
|
17
|
+
const violations = auditCode(code, 'src/components/molecules/m-table.js', 'src/components/molecules/m-table.js', {
|
|
18
|
+
config: { profile: 'pragmatic', enforceFileLength: false }
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const moleculeLineViolation = violations.find((v) => v.rule === 'LINE_BUDGET_MOLECULE');
|
|
22
|
+
assert.strictEqual(moleculeLineViolation, undefined, 'Pragmatic mode should not flag a cohesive molecule under 250 lines');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('strictly flags a 105-line molecule when profile is atomic-strict', () => {
|
|
26
|
+
const staticRows = Array.from({ length: 102 }, (_, i) => ` { id: ${i} },`).join('\n');
|
|
27
|
+
const code = `
|
|
28
|
+
export const StrictCard = () => {
|
|
29
|
+
const rows = [
|
|
30
|
+
${staticRows}
|
|
31
|
+
];
|
|
32
|
+
return rows.length;
|
|
33
|
+
};
|
|
34
|
+
`;
|
|
35
|
+
const violations = auditCode(code, 'src/components/molecules/m-card.js', 'src/components/molecules/m-card.js', {
|
|
36
|
+
config: { profile: 'atomic-strict', enforceFileLength: true }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const moleculeLineViolation = violations.find((v) => v.rule === 'LINE_BUDGET_MOLECULE');
|
|
40
|
+
assert.ok(moleculeLineViolation, 'Atomic-strict mode should enforce 100-line molecule cap');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('detects high cyclomatic complexity (CC > 12) via COMPLEXITY_CYCLOMATIC_HIGH', () => {
|
|
44
|
+
const code = `
|
|
45
|
+
export function ComplexComponent(a, b, c, d, e, f, g) {
|
|
46
|
+
if (a === 1) return 1;
|
|
47
|
+
if (b === 2) return 2;
|
|
48
|
+
if (c === 3) return 3;
|
|
49
|
+
if (d === 4) return 4;
|
|
50
|
+
if (e === 5) return 5;
|
|
51
|
+
if (f === 6) return 6;
|
|
52
|
+
if (g === 7) return 7;
|
|
53
|
+
if (a && b) return 8;
|
|
54
|
+
if (c && d) return 9;
|
|
55
|
+
if (e && f) return 10;
|
|
56
|
+
if (a || g) return 11;
|
|
57
|
+
if (b || c) return 12;
|
|
58
|
+
if (d || e) return 13;
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
const violations = auditCode(code, 'src/components/ComplexComponent.jsx', 'src/components/ComplexComponent.jsx', {
|
|
63
|
+
config: { maxCyclomaticComplexity: 12 }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const complexityViolation = violations.find((v) => v.rule === 'COMPLEXITY_CYCLOMATIC_HIGH');
|
|
67
|
+
assert.ok(complexityViolation, 'Should detect cyclomatic complexity exceeding threshold');
|
|
68
|
+
assert.strictEqual(complexityViolation.severity, 'HIGH');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('detects hook & state saturation (hooks > 4) via HOOK_STATE_SATURATION', () => {
|
|
72
|
+
const code = `
|
|
73
|
+
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
74
|
+
|
|
75
|
+
export function MonolithicComponent() {
|
|
76
|
+
const [s1, setS1] = useState(1);
|
|
77
|
+
const [s2, setS2] = useState(2);
|
|
78
|
+
const [s3, setS3] = useState(3);
|
|
79
|
+
useEffect(() => {}, []);
|
|
80
|
+
const m1 = useMemo(() => s1 + s2, [s1, s2]);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
const violations = auditCode(code, 'src/Component.jsx', 'src/Component.jsx', {
|
|
85
|
+
config: { maxHookDensity: 4 }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const hookViolation = violations.find((v) => v.rule === 'HOOK_STATE_SATURATION');
|
|
89
|
+
assert.ok(hookViolation, 'Should detect excessive hook density without domain encapsulation');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('detects nested ternaries via CONTROL_FLOW_NESTED_TERNARY', () => {
|
|
93
|
+
const code = `
|
|
94
|
+
export const BadTernary = (props) => {
|
|
95
|
+
return props.a ? (props.b ? 'X' : 'Y') : 'Z';
|
|
96
|
+
};
|
|
97
|
+
`;
|
|
98
|
+
const violations = auditCode(code, 'src/Ternary.jsx', 'src/Ternary.jsx');
|
|
99
|
+
const ternaryViolation = violations.find((v) => v.rule === 'CONTROL_FLOW_NESTED_TERNARY');
|
|
100
|
+
assert.ok(ternaryViolation, 'Should detect nested ternary expression');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('detects prop surface area bloat (props > 7) via PROP_SURFACE_BLOAT', () => {
|
|
104
|
+
const code = `
|
|
105
|
+
export const BloatedProps = ({ p1, p2, p3, p4, p5, p6, p7, p8, p9 }) => {
|
|
106
|
+
return p1;
|
|
107
|
+
};
|
|
108
|
+
`;
|
|
109
|
+
const violations = auditCode(code, 'src/Props.jsx', 'src/Props.jsx', {
|
|
110
|
+
config: { maxPropCount: 7 }
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const propViolation = violations.find((v) => v.rule === 'PROP_SURFACE_BLOAT');
|
|
114
|
+
assert.ok(propViolation, 'Should detect prop count exceeding threshold');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -3,7 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import { hasGum, gumChoose, gumInput, promptQuestion } from './terminal.js';
|
|
4
4
|
import { resolveGitAuditScope } from './audit-preflight-git.js';
|
|
5
5
|
|
|
6
|
-
const CANDIDATE_NAMES = [
|
|
6
|
+
const CANDIDATE_NAMES = [
|
|
7
|
+
'src', 'app', 'cli', 'components', 'lib', 'packages', 'modules',
|
|
8
|
+
'server', 'backend', 'api', 'Services', 'Controllers', 'Handlers', 'Domain', 'pkg', 'cmd', 'internal'
|
|
9
|
+
];
|
|
7
10
|
|
|
8
11
|
export const detectCandidateDirectories = (cwd = process.cwd()) => {
|
|
9
12
|
return CANDIDATE_NAMES.filter((name) => {
|