@chemx/starter-kit 26.9.24-393 → 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.
Files changed (94) hide show
  1. package/README.md +68 -8
  2. package/blueprints/atoms/a-button.svelte +30 -0
  3. package/blueprints/atoms/a-button.tsx +36 -0
  4. package/blueprints/molecule-capsule/m-chemx-badge/m-chemx-badge.svelte +35 -0
  5. package/blueprints/molecule-capsule/m-sample-card.svelte +58 -0
  6. package/blueprints/molecule-capsule/m-sample-card.tsx +3 -3
  7. package/blueprints/molecule-capsule/m-sample-card.vue +60 -0
  8. package/blueprints/view-template.svelte +18 -0
  9. package/blueprints/view-template.vue +18 -0
  10. package/blueprints/workflows/chemx-audit.yml +36 -0
  11. package/cli/audit/ast-visitors.js +25 -13
  12. package/cli/audit/csharp-analyzer.js +80 -0
  13. package/cli/audit/extended-text-patterns.js +11 -2
  14. package/cli/audit/pattern-detector.js +5 -3
  15. package/cli/audit/pattern-detector.spec.js +17 -0
  16. package/cli/audit/reporter-grades.js +1 -0
  17. package/cli/audit/reporter-markdown.js +2 -0
  18. package/cli/audit/reporter-sections.js +3 -0
  19. package/cli/audit/rules-registry.js +27 -0
  20. package/cli/audit/rules.js +14 -3
  21. package/cli/audit/rules.spec.js +27 -0
  22. package/cli/audit/structural-weight-evaluator.js +121 -0
  23. package/cli/audit/structural-weight.js +92 -0
  24. package/cli/audit/structural-weight.spec.js +116 -0
  25. package/cli/audit-preflight-scope.js +4 -1
  26. package/cli/audit.js +9 -10
  27. package/cli/commands/cmd-audit.js +1 -1
  28. package/cli/commands/cmd-router.js +2 -1
  29. package/cli/config/config.spec.js +51 -0
  30. package/cli/config/index.js +37 -0
  31. package/cli/config/loader.js +83 -0
  32. package/cli/config/profiles.js +56 -0
  33. package/cli/config.spec.js +1 -0
  34. package/cli/create.js +21 -8
  35. package/cli/create.spec.js +91 -0
  36. package/cli/generator-help.js +48 -0
  37. package/cli/generator-indexer.js +41 -0
  38. package/cli/generator.js +8 -43
  39. package/cli/generator.spec.js +29 -0
  40. package/cli/languages.js +122 -0
  41. package/cli/languages.spec.js +44 -0
  42. package/cli/mcp/tools-patch.js +7 -5
  43. package/cli/mcp/tools-team-tasks.js +56 -10
  44. package/cli/polyglot.spec.js +111 -0
  45. package/cli/scaffold-frameworks.js +133 -0
  46. package/cli/scaffold.js +67 -37
  47. package/cli/search-ast.js +41 -9
  48. package/cli/search-db.js +2 -8
  49. package/cli/search-queries-semantic.js +28 -9
  50. package/cli/search-tokenizer.js +52 -0
  51. package/cli/search.js +2 -5
  52. package/cli/search.spec.js +51 -3
  53. package/cli/team/team-commands-vds.js +70 -0
  54. package/cli/team/team-commands.js +72 -3
  55. package/cli/team/team-db-lock-promotion.js +29 -18
  56. package/cli/team/team-db-locks.js +54 -43
  57. package/cli/team/team-db-task-helpers.js +18 -0
  58. package/cli/team/team-db-tasks.js +45 -51
  59. package/cli/team/team-db-transaction.js +52 -0
  60. package/cli/team/team-format.js +60 -0
  61. package/cli/team/team-multiprocess-concurrency.spec.js +193 -0
  62. package/cli/team/team-release-train.js +65 -0
  63. package/cli/team/team-release-train.spec.js +91 -0
  64. package/cli/team/team-schema-vds.js +39 -0
  65. package/cli/team/team-schema.js +21 -23
  66. package/cli/team/team-tasks-asana.spec.js +60 -0
  67. package/cli/team/team-triage.js +12 -0
  68. package/cli/team/team-vds.js +88 -0
  69. package/cli/team/team-vds.spec.js +125 -0
  70. package/cli/team/team-verification-gate.spec.js +27 -0
  71. package/cli/ui-actions-vds.js +48 -0
  72. package/cli/ui-actions.js +1 -0
  73. package/cli/ui-client-api.js +8 -0
  74. package/cli/ui-client-script.js +33 -29
  75. package/cli/ui-dev-watcher.js +61 -0
  76. package/cli/ui-direct-routes.spec.js +55 -0
  77. package/cli/ui-handlers.js +7 -8
  78. package/cli/ui-index.html +416 -26
  79. package/cli/ui-kanban.spec.js +11 -0
  80. package/cli/ui-server-routes.js +14 -6
  81. package/cli/ui-server.js +17 -21
  82. package/cli/ui-sse.js +23 -10
  83. package/cli/ui-sse.spec.js +26 -0
  84. package/cli/ui-styles.js +20 -4
  85. package/cli/ui-template-kanban.js +23 -27
  86. package/cli/ui-template-task-sheet.js +87 -0
  87. package/cli/ui-template-vds-badges.js +34 -0
  88. package/cli/ui-template.js +13 -10
  89. package/cli/verify-helpers.js +173 -0
  90. package/cli/verify.js +132 -176
  91. package/cli/verify.spec.js +61 -0
  92. package/package.json +5 -1
  93. package/scripts/pre-commit.sh +4 -0
  94. package/docs/CHANGELOG.md +0 -332
@@ -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
  };
@@ -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 > 100) {
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
- if (options.fast) {
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
@@ -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 = ['src', 'app', 'cli', 'components', 'lib', 'packages', 'modules'];
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) => {
package/cli/audit.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { isSourceFile as isPolyglotSourceFile } from './languages.js';
4
+ import { loadProjectConfig } from './config/index.js';
3
5
  import { auditCode, PILLARS, RULE_REGISTRY, createHookShapeRegistry } from './audit/rules.js';
4
6
  import { createPatternRegistry } from './audit/pattern-detector.js';
5
7
  import {
@@ -82,14 +84,8 @@ const IGNORED_DIRS = new Set([
82
84
  'out'
83
85
  ]);
84
86
 
85
- const EXCLUDED_NAME_PATTERNS = ['.test.', '.spec.', '.min.'];
86
-
87
87
  const isSourceFile = (name) => {
88
- const isExtensionValid = /\.(tsx|ts|jsx|js|vue)$/.test(name);
89
- if (!isExtensionValid) return false;
90
- if (name.endsWith('.d.ts')) return false;
91
- const isExcluded = EXCLUDED_NAME_PATTERNS.some((pat) => name.includes(pat));
92
- return !isExcluded;
88
+ return isPolyglotSourceFile(name, { includeTests: false });
93
89
  };
94
90
 
95
91
  export const auditFile = (filePath, relativePath) => {
@@ -117,7 +113,8 @@ const auditFileEntry = (fullPath, relPath, scanOptions) => {
117
113
  const fileViolations = auditCode(content, fullPath, relPath, {
118
114
  patternRegistry: scanOptions.patternRegistry,
119
115
  hookRegistry: scanOptions.hookRegistry,
120
- fast: scanOptions.fast
116
+ fast: scanOptions.fast,
117
+ config: scanOptions.config
121
118
  });
122
119
 
123
120
  return { fileStat, hookCount, fileViolations };
@@ -179,11 +176,13 @@ export const runAudit = (targetDir = 'src', options = {}) => {
179
176
  const absoluteTarget = path.isAbsolute(targetDir) ? targetDir : path.resolve(cwd, targetDir);
180
177
  const patternRegistry = createPatternRegistry();
181
178
  const hookRegistry = createHookShapeRegistry();
179
+ const config = options.config || loadProjectConfig(cwd);
182
180
  const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, {
183
181
  patternRegistry,
184
182
  hookRegistry,
185
183
  fast: Boolean(options.fast),
186
- fileList: options.fileList || null
184
+ fileList: options.fileList || null,
185
+ config
187
186
  });
188
187
 
189
188
  const crossHookViolations = hookRegistry.validateCrossHookConsistency();
@@ -229,7 +228,7 @@ export const runAudit = (targetDir = 'src', options = {}) => {
229
228
  const contextAnalysis = calculateTokenBurnAnalytics(fileStats, options);
230
229
  const hotspots = calculateHotspots(violations, fileStats, 5);
231
230
  const aiSlop = calculateAiSlopScore(violations, scannedFiles);
232
- const patterns = patternRegistry.resolveHarmonizationCandidates(hotspots);
231
+ const patterns = patternRegistry.resolveHarmonizationCandidates(hotspots, { ruleOfThree: config?.rules?.ruleOfThreeAbstractions ?? config?.ruleOfThreeAbstractions ?? true });
233
232
  const roadmap = buildRemediationRoadmap({ hotspots, violations, patterns });
234
233
 
235
234
  const stage = options.stage || (options.relax ? 'draft' : 'strict');
@@ -101,7 +101,7 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
101
101
  fileList = preflight.fileList;
102
102
  }
103
103
 
104
- const auditOptions = { outputFile, model, costPerMillion, fast: isFast, fileList, stage };
104
+ const auditOptions = { outputFile, model, costPerMillion, fast: isFast, fileList, stage, config: projectConfig };
105
105
  const report = executeAstAudit(targetDir, auditOptions);
106
106
  saveAuditSnapshot(report);
107
107
  try {
@@ -202,7 +202,8 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
202
202
  const port = portArg ? parseInt(portArg.split('=')[1], 10) : 4173;
203
203
  const hostArg = rawArgs.find((a) => a.startsWith('--host='));
204
204
  const host = hostArg ? hostArg.split('=')[1] : '0.0.0.0';
205
- const { server } = await startUiServer({ port, host, isCli: true, cwd: process.cwd() });
205
+ const isDev = rawArgs.includes('--dev') || rawArgs.includes('-d') || process.env.CHEMX_UI_DEV === '1';
206
+ const { server } = await startUiServer({ port, host, dev: isDev, isCli: true, cwd: process.cwd() });
206
207
  await new Promise((resolve) => {
207
208
  const shutdown = () => {
208
209
  server.close(() => resolve());
@@ -0,0 +1,51 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { DEFAULT_PROFILE, getProfileDefaults, loadProjectConfig } from './index.js';
4
+
5
+ describe('Chemical X Configuration & Profiles', () => {
6
+ it('defines pragmatic as the default profile with greybeard staff thresholds', () => {
7
+ assert.strictEqual(DEFAULT_PROFILE, 'pragmatic');
8
+ const defaults = getProfileDefaults('pragmatic');
9
+ assert.strictEqual(defaults.profile, 'pragmatic');
10
+ assert.strictEqual(defaults.enforceFileLength, false);
11
+ assert.strictEqual(defaults.ruleOfThreeAbstractions, true);
12
+ assert.strictEqual(defaults.maxCyclomaticComplexity, 12);
13
+ assert.strictEqual(defaults.maxHookDensity, 4);
14
+ assert.strictEqual(defaults.preferDesignTokens, 'warning');
15
+ });
16
+
17
+ it('defines atomic-strict profile with strict 100-line capsule limit', () => {
18
+ const strict = getProfileDefaults('atomic-strict');
19
+ assert.strictEqual(strict.profile, 'atomic-strict');
20
+ assert.strictEqual(strict.enforceFileLength, true);
21
+ assert.strictEqual(strict.ruleOfThreeAbstractions, false);
22
+ assert.strictEqual(strict.maxLineCountWarning, 100);
23
+ assert.strictEqual(strict.preferDesignTokens, 'error');
24
+ });
25
+
26
+ it('defines loose profile with permissive thresholds', () => {
27
+ const loose = getProfileDefaults('loose');
28
+ assert.strictEqual(loose.profile, 'loose');
29
+ assert.strictEqual(loose.enforceFileLength, false);
30
+ assert.strictEqual(loose.maxCyclomaticComplexity, 25);
31
+ assert.strictEqual(loose.preferDesignTokens, 'off');
32
+ });
33
+
34
+ it('falls back to default profile for unknown profile names', () => {
35
+ const fallback = getProfileDefaults('unknown-profile');
36
+ assert.strictEqual(fallback.profile, 'pragmatic');
37
+ });
38
+
39
+ it('parses --profile CLI argument and overrides file config', () => {
40
+ const cfg = loadProjectConfig(process.cwd(), ['--profile=atomic-strict']);
41
+ assert.strictEqual(cfg.profile, 'atomic-strict');
42
+ assert.strictEqual(cfg.rules.enforceFileLength, true);
43
+ });
44
+
45
+ it('defaults to pragmatic profile when no flags or config files specify otherwise', () => {
46
+ const cfg = loadProjectConfig('/tmp/non-existent-dir-for-chemx-test');
47
+ assert.strictEqual(cfg.profile, 'pragmatic');
48
+ assert.strictEqual(cfg.rules.enforceFileLength, false);
49
+ assert.strictEqual(cfg.rules.maxCyclomaticComplexity, 12);
50
+ });
51
+ });
@@ -0,0 +1,37 @@
1
+ import { PROFILES, DEFAULT_PROFILE, getProfileDefaults } from './profiles.js';
2
+ import { findAndLoadConfigFile } from './loader.js';
3
+
4
+ const parseCliProfile = (rawArgs = []) => {
5
+ const profileArg = rawArgs.find((arg) => arg.startsWith('--profile='));
6
+ if (profileArg) {
7
+ const parts = profileArg.split('=');
8
+ return parts[1]?.trim()?.toLowerCase() || null;
9
+ }
10
+ return null;
11
+ };
12
+
13
+ export const loadProjectConfig = (cwd = process.cwd(), rawArgs = []) => {
14
+ const fileConfig = findAndLoadConfigFile(cwd);
15
+ const cliProfile = parseCliProfile(rawArgs);
16
+
17
+ const isCliProfileExplicit = Boolean(cliProfile);
18
+ const selectedProfile = cliProfile || fileConfig.profile || DEFAULT_PROFILE;
19
+ const profileDefaults = getProfileDefaults(selectedProfile);
20
+
21
+ const effectiveRules = {
22
+ ...profileDefaults,
23
+ ...(isCliProfileExplicit ? {} : fileConfig.rules)
24
+ };
25
+
26
+ return {
27
+ source: fileConfig.source,
28
+ profile: selectedProfile,
29
+ rules: effectiveRules,
30
+ overrides: fileConfig.overrides || [],
31
+ raw: fileConfig.raw || {},
32
+ stage: fileConfig.raw?.stage || 'strict'
33
+ };
34
+ };
35
+
36
+ export { PROFILES, DEFAULT_PROFILE, getProfileDefaults, findAndLoadConfigFile };
37
+ export default loadProjectConfig;