@chemx/starter-kit 26.10.4-235 → 26.10.8-344

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 (128) hide show
  1. package/AGENTS.md +495 -0
  2. package/blueprints/molecule-capsule/m-sample-card.svelte +1 -1
  3. package/blueprints/molecule-capsule/m-sample-card.vue +1 -1
  4. package/cli/audit/ai-slop-detector.js +29 -1
  5. package/cli/audit/ast-visitors.js +116 -1
  6. package/cli/audit/audit-summary.js +30 -0
  7. package/cli/audit/gate-verdict.js +30 -0
  8. package/cli/audit/history.js +47 -10
  9. package/cli/audit/ratchet.js +64 -0
  10. package/cli/audit/rules-predicates.js +200 -0
  11. package/cli/audit/rules-registry.js +25 -0
  12. package/cli/audit-scope.js +44 -0
  13. package/cli/build/detector.js +0 -2
  14. package/cli/commands/cmd-audit.js +58 -11
  15. package/cli/commands/cmd-router.js +38 -1
  16. package/cli/commands/cmd-wrappers.js +322 -0
  17. package/cli/commands-schema.js +101 -9
  18. package/cli/errors/catcher.js +46 -3
  19. package/cli/help.js +41 -0
  20. package/cli/host-shims.js +77 -0
  21. package/cli/index.js +6 -0
  22. package/cli/installer.js +4 -44
  23. package/cli/mcp/antigravity.js +12 -5
  24. package/cli/mcp/call-scope.js +93 -0
  25. package/cli/mcp/index.js +6 -2
  26. package/cli/mcp/manifests.js +32 -26
  27. package/cli/mcp/server.js +26 -27
  28. package/cli/mcp/tools-audit.js +27 -11
  29. package/cli/mcp/tools-search-util.js +1 -10
  30. package/cli/mcp/tools-team-tasks.js +6 -4
  31. package/cli/mcp/tools.js +53 -8
  32. package/cli/navigator-actions.js +1 -1
  33. package/cli/navigator-banner-helpers.js +15 -5
  34. package/cli/navigator-banner.js +2 -9
  35. package/cli/navigator-guide.js +2 -2
  36. package/cli/path-scope.js +1 -1
  37. package/cli/pillars-schema.js +8 -82
  38. package/cli/pillars-wizard.js +56 -44
  39. package/cli/pillars-write-guard.js +52 -0
  40. package/cli/reader-markdown.js +26 -0
  41. package/cli/reader.js +89 -12
  42. package/cli/scaffold-frameworks.js +29 -0
  43. package/cli/scaffold.js +2 -10
  44. package/cli/search-commands.js +96 -0
  45. package/cli/search-db.js +11 -2
  46. package/cli/search-schema.js +22 -9
  47. package/cli/search.js +51 -50
  48. package/cli/team/task-list-view.js +43 -0
  49. package/cli/team/team-commands.js +11 -6
  50. package/cli/team/team-flags.js +1 -0
  51. package/cli/team/team-format.js +4 -2
  52. package/cli/verify.js +33 -15
  53. package/package.json +10 -5
  54. package/cli/audit/autofix.spec.js +0 -85
  55. package/cli/audit/clone-detector.spec.js +0 -46
  56. package/cli/audit/csharp-analyzer.spec.js +0 -216
  57. package/cli/audit/pattern-detector.spec.js +0 -113
  58. package/cli/audit/prompts.spec.js +0 -367
  59. package/cli/audit/rules.spec.js +0 -361
  60. package/cli/audit/social-git.spec.js +0 -71
  61. package/cli/audit/structural-weight.spec.js +0 -116
  62. package/cli/audit-preflight.spec.js +0 -115
  63. package/cli/audit-stages.spec.js +0 -30
  64. package/cli/blast-radius.spec.js +0 -113
  65. package/cli/build/detector.spec.js +0 -81
  66. package/cli/columnar.spec.js +0 -49
  67. package/cli/config/config.spec.js +0 -51
  68. package/cli/config.spec.js +0 -1
  69. package/cli/create.spec.js +0 -291
  70. package/cli/embeddings.spec.js +0 -92
  71. package/cli/errors/errors.spec.js +0 -163
  72. package/cli/exploder.spec.js +0 -99
  73. package/cli/friction-fixes.spec.js +0 -96
  74. package/cli/generator-compact.spec.js +0 -104
  75. package/cli/generator-framework.spec.js +0 -315
  76. package/cli/generator-jig.spec.js +0 -148
  77. package/cli/generator-templates/archetypes/vector-matcher.spec.js +0 -25
  78. package/cli/generator-templates.spec.js +0 -187
  79. package/cli/generator.spec.js +0 -288
  80. package/cli/help.spec.js +0 -109
  81. package/cli/installer.spec.js +0 -72
  82. package/cli/languages.spec.js +0 -65
  83. package/cli/mcp/installer.spec.js +0 -118
  84. package/cli/mcp/server.spec.js +0 -710
  85. package/cli/mcp/tools-project.spec.js +0 -51
  86. package/cli/mutators.spec.js +0 -176
  87. package/cli/navigator-banner.spec.js +0 -68
  88. package/cli/patcher.spec.js +0 -190
  89. package/cli/path-traversal.spec.js +0 -242
  90. package/cli/pillars.spec.js +0 -138
  91. package/cli/polyglot.spec.js +0 -222
  92. package/cli/project-detector.spec.js +0 -123
  93. package/cli/reader-logic.spec.js +0 -160
  94. package/cli/reader-shorthand.spec.js +0 -60
  95. package/cli/search-bulletproof.spec.js +0 -104
  96. package/cli/search-queries-hotspot-graph.spec.js +0 -66
  97. package/cli/search-queries-similar.spec.js +0 -43
  98. package/cli/search.spec.js +0 -294
  99. package/cli/team/team-adversarial-locks.spec.js +0 -176
  100. package/cli/team/team-adversarial-telemetry.spec.js +0 -93
  101. package/cli/team/team-concurrency-stress.spec.js +0 -95
  102. package/cli/team/team-lock-concurrency.spec.js +0 -182
  103. package/cli/team/team-mailbox-empirical.spec.js +0 -191
  104. package/cli/team/team-mailbox.spec.js +0 -93
  105. package/cli/team/team-memory.spec.js +0 -123
  106. package/cli/team/team-multi-model-telemetry.spec.js +0 -80
  107. package/cli/team/team-multiprocess-concurrency.spec.js +0 -193
  108. package/cli/team/team-projects.spec.js +0 -116
  109. package/cli/team/team-release-train.spec.js +0 -91
  110. package/cli/team/team-tasks-asana.spec.js +0 -60
  111. package/cli/team/team-tasks-hierarchy.spec.js +0 -176
  112. package/cli/team/team-vds.spec.js +0 -125
  113. package/cli/team/team-verification-gate.spec.js +0 -280
  114. package/cli/team/team.spec.js +0 -574
  115. package/cli/tesseract.spec.js +0 -55
  116. package/cli/trace.spec.js +0 -92
  117. package/cli/trend.spec.js +0 -51
  118. package/cli/ui-canonical-routes.spec.js +0 -130
  119. package/cli/ui-direct-routes.spec.js +0 -55
  120. package/cli/ui-e2e-verification.spec.js +0 -197
  121. package/cli/ui-filetree.spec.js +0 -175
  122. package/cli/ui-kanban.spec.js +0 -217
  123. package/cli/ui-sse.spec.js +0 -100
  124. package/cli/ui-vbulletin.spec.js +0 -188
  125. package/cli/ui-workbench-dbstudio.spec.js +0 -172
  126. package/cli/ui.spec.js +0 -312
  127. package/cli/verify-lint.spec.js +0 -44
  128. package/cli/verify.spec.js +0 -211
@@ -5,7 +5,12 @@ import {
5
5
  isCustomHookFunction,
6
6
  resolveStartLine,
7
7
  isZeroDelayTimeout,
8
- isUnguardedConsoleCall
8
+ isUnguardedConsoleCall,
9
+ isSilentGuardClause,
10
+ isSwallowedCatch,
11
+ countOptionalChainingDepth,
12
+ isCombinatorCall,
13
+ isRawBooleanArg
9
14
  } from './rules-predicates.js';
10
15
  import { validateHookReturnShape } from './hook-shape-validator.js';
11
16
  import { evaluateComponentStructuralWeight } from './structural-weight-evaluator.js';
@@ -117,6 +122,22 @@ export const createAstVisitors = ({ relativePath, violations, hookRegistry, conf
117
122
  });
118
123
  }
119
124
  }
125
+
126
+ const silentGuard = isSilentGuardClause(astPath, t);
127
+ if (silentGuard) {
128
+ const line = astPath.node.loc?.start.line || 1;
129
+ const meta = RULE_REGISTRY.CONTROL_FLOW_SILENT_GUARD;
130
+ violations.push({
131
+ filePath: relativePath,
132
+ line,
133
+ column: astPath.node.loc?.start.column || 1,
134
+ hazard: `Silent guard abort detected in ${silentGuard.funcName}: bare return; swallows error condition without logging or error state. Return [null, error] or emit diagnostic per Directive 3.G.`,
135
+ rule: 'CONTROL_FLOW_SILENT_GUARD',
136
+ severity: meta.severity,
137
+ pillar: meta.pillar,
138
+ directive: meta.directive
139
+ });
140
+ }
120
141
  },
121
142
 
122
143
  ConditionalExpression(astPath) {
@@ -299,6 +320,100 @@ export const createAstVisitors = ({ relativePath, violations, hookRegistry, conf
299
320
  directive: meta.directive
300
321
  });
301
322
  }
323
+
324
+ // Pillar 6: Orphaned Event Listener
325
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property, { name: 'addEventListener' })) {
326
+ const fnParent = astPath.getFunctionParent();
327
+ let hasCleanup = false;
328
+ if (fnParent) {
329
+ const fnName = fnParent.node.id?.name || '';
330
+ if (fnName === 'listen') {
331
+ hasCleanup = true;
332
+ } else {
333
+ fnParent.traverse({
334
+ CallExpression(innerCall) {
335
+ const innerCallee = innerCall.node.callee;
336
+ if (t.isMemberExpression(innerCallee) && t.isIdentifier(innerCallee.property, { name: 'removeEventListener' })) {
337
+ hasCleanup = true;
338
+ }
339
+ }
340
+ });
341
+ }
342
+ }
343
+ if (!hasCleanup) {
344
+ const line = astPath.node.loc?.start.line || 1;
345
+ const meta = RULE_REGISTRY.LIFECYCLE_ORPHANED_LISTENER;
346
+ violations.push({
347
+ filePath: relativePath,
348
+ line,
349
+ column: astPath.node.loc?.start.column || 1,
350
+ hazard: 'Raw addEventListener call lacking lifecycle disposer or removeEventListener teardown',
351
+ rule: 'LIFECYCLE_ORPHANED_LISTENER',
352
+ severity: meta.severity,
353
+ pillar: meta.pillar,
354
+ directive: meta.directive
355
+ });
356
+ }
357
+ }
358
+
359
+ // Pillar 2: Combinator Raw Boolean Arguments
360
+ if (isCombinatorCall(callee, t)) {
361
+ const hasRawArg = astPath.node.arguments.some((arg) => isRawBooleanArg(arg, t));
362
+ if (hasRawArg) {
363
+ const line = astPath.node.loc?.start.line || 1;
364
+ const meta = RULE_REGISTRY.COMBINATOR_RAW_BOOLEAN;
365
+ violations.push({
366
+ filePath: relativePath,
367
+ line,
368
+ column: astPath.node.loc?.start.column || 1,
369
+ hazard: `Raw boolean expression passed to ${callee.name}(). Decompose into named predicate or thunk.`,
370
+ rule: 'COMBINATOR_RAW_BOOLEAN',
371
+ severity: meta.severity,
372
+ pillar: meta.pillar,
373
+ directive: meta.directive
374
+ });
375
+ }
376
+ }
377
+ },
378
+
379
+ // Pillar 2: Swallowed Exceptions
380
+ CatchClause(astPath) {
381
+ if (isSwallowedCatch(astPath, t)) {
382
+ const line = astPath.node.loc?.start.line || 1;
383
+ const meta = RULE_REGISTRY.ERROR_SWALLOWED_EXCEPTION;
384
+ violations.push({
385
+ filePath: relativePath,
386
+ line,
387
+ column: astPath.node.loc?.start.column || 1,
388
+ hazard: 'Swallowed exception in catch block without active handling, logging, or ResultTuple',
389
+ rule: 'ERROR_SWALLOWED_EXCEPTION',
390
+ severity: meta.severity,
391
+ pillar: meta.pillar,
392
+ directive: meta.directive
393
+ });
394
+ }
395
+ },
396
+
397
+ // Pillar 4: Deep Optional Chaining Churn
398
+ OptionalMemberExpression(astPath) {
399
+ const isParentOptional = t.isOptionalMemberExpression(astPath.parent) || t.isOptionalCallExpression(astPath.parent);
400
+ if (!isParentOptional) {
401
+ const depth = countOptionalChainingDepth(astPath.node, t);
402
+ if (depth >= 3) {
403
+ const line = astPath.node.loc?.start.line || 1;
404
+ const meta = RULE_REGISTRY.DATA_FLOW_OPTIONAL_CHAINING_CHURN;
405
+ violations.push({
406
+ filePath: relativePath,
407
+ line,
408
+ column: astPath.node.loc?.start.column || 1,
409
+ hazard: `Deep optional chaining churn (${depth} chained operators >= 3 limit). Level data shapes line 1.`,
410
+ rule: 'DATA_FLOW_OPTIONAL_CHAINING_CHURN',
411
+ severity: meta.severity,
412
+ pillar: meta.pillar,
413
+ directive: meta.directive
414
+ });
415
+ }
416
+ }
302
417
  },
303
418
 
304
419
  // Pillar 4: Type Co-location
@@ -0,0 +1,30 @@
1
+ import { resolveProjectName, resolveCostFigures } from '../navigator-banner-helpers.js';
2
+
3
+ const countHazards = (violations = []) => {
4
+ const counts = { critical: 0, highMedium: 0, low: 0 };
5
+ for (const v of violations) {
6
+ const isCritical = v.severity === 'CRITICAL';
7
+ const isHighOrMedium = v.severity === 'HIGH' || v.severity === 'MEDIUM';
8
+ if (isCritical) counts.critical += 1;
9
+ else if (isHighOrMedium) counts.highMedium += 1;
10
+ else counts.low += 1;
11
+ }
12
+ return counts;
13
+ };
14
+
15
+ export const buildAuditSummary = (report, { projectRoot, scope }) => {
16
+ const gate = report.gate ?? {};
17
+ const summary = {
18
+ project: resolveProjectName(projectRoot),
19
+ scope,
20
+ files: report.metrics?.scannedFiles ?? 0,
21
+ loc: report.metrics?.totalLoc ?? 0,
22
+ tokens: { estimate: report.contextAnalysis?.estimatedTokens ?? 0, savingsPct: report.contextAnalysis?.potentialSavingsPct ?? 0 },
23
+ health: { score: report.health.score, grade: report.health.grade, label: report.health.label ?? null },
24
+ aiSlop: { score: report.aiSlop?.score ?? 100, grade: report.aiSlop?.grade ?? 'A+' },
25
+ hazards: countHazards(report.violations),
26
+ cost: resolveCostFigures(report.contextAnalysis),
27
+ gate: { passing: gate.isPassing ?? null, basis: gate.basis ?? null, regressions: (gate.regressions ?? []).slice(0, 5), note: gate.note ?? null }
28
+ };
29
+ return report.rebaseline ? { ...summary, rebaseline: report.rebaseline } : summary;
30
+ };
@@ -0,0 +1,30 @@
1
+ import { readRatchet, evaluateRatchet } from './ratchet.js';
2
+
3
+ const RATCHET_STATUSES = new Set(['pass', 'fail', 'invalid']);
4
+
5
+ const isSevereViolation = (v) => v.severity === 'CRITICAL' || v.severity === 'HIGH';
6
+
7
+ export const evaluateGateVerdict = ({ violations, ratchetEval }) => {
8
+ const isRatchetBasis = RATCHET_STATUSES.has(ratchetEval.status);
9
+ if (isRatchetBasis) {
10
+ return {
11
+ isPassing: ratchetEval.status === 'pass',
12
+ basis: 'ratchet',
13
+ regressions: ratchetEval.regressions,
14
+ note: ratchetEval.message ?? null
15
+ };
16
+ }
17
+ const hasSevereViolation = violations.some(isSevereViolation);
18
+ return { isPassing: !hasSevereViolation, basis: 'severity', regressions: [], note: ratchetEval.message ?? null };
19
+ };
20
+
21
+ const PARTIAL_SCAN_EVAL = {
22
+ status: 'partial',
23
+ regressions: [],
24
+ message: 'Partial scan (--git, --changed, or --fast): the ratchet applies to full scans only; using severity gate.'
25
+ };
26
+
27
+ export const computeGateVerdict = ({ projectRoot, scope, violations, isPartialScan = false }) => {
28
+ const ratchetEval = isPartialScan ? PARTIAL_SCAN_EVAL : evaluateRatchet(readRatchet(projectRoot), { scope, violations });
29
+ return evaluateGateVerdict({ violations, ratchetEval });
30
+ };
@@ -11,10 +11,43 @@ const GREEN = '\x1b[32m';
11
11
  const YELLOW = '\x1b[33m';
12
12
  const RED = '\x1b[31m';
13
13
 
14
+ export const findChemxDir = (startDir = process.cwd()) => {
15
+ const hasCustomRoot = Boolean(process.env.CHEMX_PROJECT_ROOT);
16
+ if (hasCustomRoot) {
17
+ return path.resolve(process.env.CHEMX_PROJECT_ROOT, '.chemx');
18
+ }
19
+
20
+ let current = path.resolve(startDir);
21
+ let gitRoot = null;
22
+ while (true) {
23
+ const candidate = path.join(current, '.chemx');
24
+ const hasExistingChemx = fs.existsSync(candidate);
25
+ if (hasExistingChemx) {
26
+ return candidate;
27
+ }
28
+ const hasGit = !gitRoot && fs.existsSync(path.join(current, '.git'));
29
+ if (hasGit) {
30
+ gitRoot = current;
31
+ }
32
+ const parent = path.dirname(current);
33
+ const isRootReached = parent === current;
34
+ if (isRootReached) break;
35
+ current = parent;
36
+ }
37
+
38
+ const hasDiscoveredGitRoot = Boolean(gitRoot);
39
+ if (hasDiscoveredGitRoot) {
40
+ return path.join(gitRoot, '.chemx');
41
+ }
42
+
43
+ return path.resolve(startDir, '.chemx');
44
+ };
45
+
14
46
  export const ensureChemxDir = (cwd = process.cwd()) => {
15
- const dir = path.resolve(cwd, '.chemx');
47
+ const dir = findChemxDir(cwd);
16
48
  try {
17
- if (!fs.existsSync(dir)) {
49
+ const isDirMissing = !fs.existsSync(dir);
50
+ if (isDirMissing) {
18
51
  fs.mkdirSync(dir, { recursive: true });
19
52
  }
20
53
  } catch {
@@ -22,17 +55,21 @@ export const ensureChemxDir = (cwd = process.cwd()) => {
22
55
  }
23
56
 
24
57
  // Ensure .chemx is gitignored if git repo exists
25
- const gitDir = path.resolve(cwd, '.git');
26
- if (fs.existsSync(gitDir)) {
27
- const gitignorePath = path.resolve(cwd, '.gitignore');
58
+ const targetParent = path.dirname(dir);
59
+ const gitDir = path.resolve(targetParent, '.git');
60
+ const hasGit = fs.existsSync(gitDir);
61
+ if (hasGit) {
62
+ const gitignorePath = path.resolve(targetParent, '.gitignore');
28
63
  try {
29
- let content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
30
- if (!content.includes('.chemx')) {
31
- const trailingNewline = content.endsWith('\n') || content.length === 0 ? '' : '\n';
64
+ const hasGitignore = fs.existsSync(gitignorePath);
65
+ let content = hasGitignore ? fs.readFileSync(gitignorePath, 'utf-8') : '';
66
+ const hasChemxEntry = content.includes('.chemx');
67
+ if (!hasChemxEntry) {
68
+ const hasTrailingNewline = content.endsWith('\n') || content.length === 0;
69
+ const trailingNewline = hasTrailingNewline ? '' : '\n';
32
70
  fs.appendFileSync(gitignorePath, `${trailingNewline}# Chemical X local telemetry & audit history\n.chemx/\n`, 'utf-8');
33
71
  }
34
- } catch (err) {
35
- const error = err instanceof Error ? err : new Error(String(err));
72
+ } catch {
36
73
  return dir;
37
74
  }
38
75
  }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export const RATCHET_FILE = 'chemx-ratchet.json';
5
+ const RATCHET_VERSION = 1;
6
+
7
+ export const countViolationsByRule = (violations = []) => {
8
+ const counts = {};
9
+ for (const v of violations) counts[v.rule] = (counts[v.rule] ?? 0) + 1;
10
+ return counts;
11
+ };
12
+
13
+ const sortKeys = (record) => Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
14
+
15
+ const isRatchetShape = (value) => {
16
+ const isObject = value !== null && typeof value === 'object';
17
+ return isObject && typeof value.scope === 'string' && typeof value.rules === 'object' && value.rules !== null;
18
+ };
19
+
20
+ export const readRatchet = (projectRoot) => {
21
+ const file = path.join(projectRoot, RATCHET_FILE);
22
+ const isMissing = !fs.existsSync(file);
23
+ if (isMissing) return { status: 'absent' };
24
+ try {
25
+ const ratchet = JSON.parse(fs.readFileSync(file, 'utf-8'));
26
+ const isValid = isRatchetShape(ratchet);
27
+ if (isValid) return { status: 'ok', ratchet };
28
+ return { status: 'invalid', message: `${RATCHET_FILE} is missing "scope" or "rules".` };
29
+ } catch (err) {
30
+ return { status: 'invalid', message: `${RATCHET_FILE} is not valid JSON: ${err.message}` };
31
+ }
32
+ };
33
+
34
+ export const writeRatchet = (projectRoot, { scope, violations }) => {
35
+ const ratchet = { version: RATCHET_VERSION, scope, rules: sortKeys(countViolationsByRule(violations)) };
36
+ fs.writeFileSync(path.join(projectRoot, RATCHET_FILE), JSON.stringify(ratchet, null, 2) + '\n', 'utf-8');
37
+ return ratchet;
38
+ };
39
+
40
+ const findRegressions = (rules, violations) => {
41
+ const current = countViolationsByRule(violations);
42
+ return Object.entries(current)
43
+ .map(([rule, count]) => ({ rule, baseline: rules[rule] ?? 0, current: count }))
44
+ .filter((r) => r.current > r.baseline)
45
+ .sort((a, b) => a.rule.localeCompare(b.rule));
46
+ };
47
+
48
+ export const evaluateRatchet = (readResult, { scope, violations }) => {
49
+ const isUnusable = readResult.status !== 'ok';
50
+ if (isUnusable) return { status: readResult.status, regressions: [], message: readResult.message ?? null };
51
+
52
+ const recordedScope = readResult.ratchet.scope;
53
+ const isScopeMismatch = recordedScope !== scope;
54
+ if (isScopeMismatch) {
55
+ return {
56
+ status: 'scope-mismatch',
57
+ regressions: [],
58
+ message: `${RATCHET_FILE} was recorded for scope "${recordedScope}", not "${scope}"; using severity gate.`
59
+ };
60
+ }
61
+ const regressions = findRegressions(readResult.ratchet.rules, violations);
62
+ const hasRegressions = regressions.length > 0;
63
+ return { status: hasRegressions ? 'fail' : 'pass', regressions, message: null };
64
+ };
@@ -138,3 +138,203 @@ export const isNonInteractiveSession = (rawArgs, env = process.env) => {
138
138
  const isNotTTY = Boolean(process.stdout && process.stdout.isTTY === false);
139
139
  return [hasCliFlag, hasOutputFlag, hasFormatFlag, hasCiEnv, isNotTTY].some(Boolean);
140
140
  };
141
+
142
+ const EXEMPT_TEST_PATTERNS = [
143
+ /abort/i,
144
+ /signal/i,
145
+ /mounted/i,
146
+ /unmount/i,
147
+ /destroy/i,
148
+ /cleanup/i,
149
+ /timer/i,
150
+ /timeout/i,
151
+ /interval/i,
152
+ /debounce/i,
153
+ /throttle/i,
154
+ /\bon[A-Z]/,
155
+ /\bcall(?:back|backFn)?\b/i,
156
+ /\bcb\b/i,
157
+ /\bhandler\b/i
158
+ ];
159
+
160
+ const MUTATING_FUNCTION_PATTERNS = [
161
+ /^handle[A-Z]/,
162
+ /^on[A-Z]/,
163
+ /^(?:save|update|delete|remove|create|add|insert|post|send|process|submit|dispatch|execute|mutate|checkout|charge|sync|write)/i
164
+ ];
165
+
166
+ const PURE_PREDICATE_PATTERNS = [
167
+ /^(?:is|has|can|should|check)[A-Z]/
168
+ ];
169
+
170
+ const isDiagnosticOrErrorStatement = (stmt, t) => {
171
+ if (t.isThrowStatement(stmt)) return true;
172
+ if (t.isExpressionStatement(stmt)) {
173
+ const expr = stmt.expression;
174
+ if (t.isCallExpression(expr)) {
175
+ const callee = expr.callee;
176
+ if (t.isMemberExpression(callee)) {
177
+ const objName = t.isIdentifier(callee.object) ? callee.object.name : '';
178
+ const propName = t.isIdentifier(callee.property) ? callee.property.name : '';
179
+ if (['console', 'logger', 'log'].includes(objName)) return true;
180
+ if (/^(?:error|warn|info|trace|notify|alert)/i.test(propName)) return true;
181
+ }
182
+ if (t.isIdentifier(callee) && /^(?:report|notify|alert|set.*Error|handleError)/i.test(callee.name)) return true;
183
+ }
184
+ if (t.isAssignmentExpression(expr)) {
185
+ const left = expr.left;
186
+ if (t.isMemberExpression(left) && t.isIdentifier(left.property) && /error/i.test(left.property.name)) return true;
187
+ if (t.isIdentifier(left) && /error/i.test(left.name)) return true;
188
+ }
189
+ }
190
+ return false;
191
+ };
192
+
193
+ export const isSilentGuardClause = (ifPath, t) => {
194
+ const node = ifPath.node;
195
+ if (!node || node.alternate) return false;
196
+
197
+ let hasBareReturn = false;
198
+ let hasDiagnosticOrHandling = false;
199
+
200
+ const checkStatement = (stmt) => {
201
+ if (t.isReturnStatement(stmt)) {
202
+ const isBare = !stmt.argument || (t.isIdentifier(stmt.argument) && stmt.argument.name === 'undefined');
203
+ if (isBare) hasBareReturn = true;
204
+ } else if (isDiagnosticOrErrorStatement(stmt, t)) {
205
+ hasDiagnosticOrHandling = true;
206
+ }
207
+ };
208
+
209
+ if (t.isReturnStatement(node.consequent)) {
210
+ checkStatement(node.consequent);
211
+ } else if (t.isBlockStatement(node.consequent)) {
212
+ for (const stmt of node.consequent.body) {
213
+ checkStatement(stmt);
214
+ }
215
+ }
216
+
217
+ if (!hasBareReturn || hasDiagnosticOrHandling) return false;
218
+
219
+ const collectTestIdentifiers = (n) => {
220
+ const names = [];
221
+ const walk = (item) => {
222
+ if (!item) return;
223
+ if (t.isIdentifier(item)) {
224
+ names.push(item.name);
225
+ } else if (t.isMemberExpression(item)) {
226
+ walk(item.object);
227
+ walk(item.property);
228
+ } else if (t.isUnaryExpression(item)) {
229
+ walk(item.argument);
230
+ } else if (t.isLogicalExpression(item) || t.isBinaryExpression(item)) {
231
+ walk(item.left);
232
+ walk(item.right);
233
+ } else if (t.isCallExpression(item)) {
234
+ walk(item.callee);
235
+ for (const arg of item.arguments) walk(arg);
236
+ }
237
+ };
238
+ walk(n);
239
+ return names;
240
+ };
241
+
242
+ const testIdentifiers = collectTestIdentifiers(node.test);
243
+ const isExemptTest = testIdentifiers.some((name) => EXEMPT_TEST_PATTERNS.some((pat) => pat.test(name)));
244
+ if (isExemptTest) return false;
245
+
246
+ const funcParent = ifPath.getFunctionParent();
247
+ if (!funcParent) return false;
248
+
249
+ let funcName = '';
250
+ if (funcParent.node.id?.name) {
251
+ funcName = funcParent.node.id.name;
252
+ } else if (funcParent.parentPath?.isVariableDeclarator() && t.isIdentifier(funcParent.parentPath.node.id)) {
253
+ funcName = funcParent.parentPath.node.id.name;
254
+ } else if (t.isIdentifier(funcParent.node.key)) {
255
+ funcName = funcParent.node.key.name;
256
+ }
257
+
258
+ const isAsync = Boolean(funcParent.node.async);
259
+ const isMutatingName = MUTATING_FUNCTION_PATTERNS.some((pat) => pat.test(funcName));
260
+ const isPurePredicate = PURE_PREDICATE_PATTERNS.some((pat) => pat.test(funcName));
261
+
262
+ if (isPurePredicate && !isAsync && !isMutatingName) return false;
263
+
264
+ const isTargetFunction = isAsync || isMutatingName;
265
+ if (!isTargetFunction) return false;
266
+
267
+ return { funcName: funcName || (isAsync ? 'async operation' : 'command handler') };
268
+ };
269
+
270
+ export const isSwallowedCatch = (astPath, t) => {
271
+ const body = astPath.node.body?.body || [];
272
+ if (body.length === 0) return true;
273
+
274
+ const nonNoopStatements = body.filter((stmt) => !t.isEmptyStatement(stmt));
275
+ if (nonNoopStatements.length === 0) return true;
276
+
277
+ let hasActiveHandling = false;
278
+ astPath.traverse({
279
+ ThrowStatement() {
280
+ hasActiveHandling = true;
281
+ },
282
+ ReturnStatement(retPath) {
283
+ if (retPath.node.argument !== null) {
284
+ hasActiveHandling = true;
285
+ }
286
+ },
287
+ CallExpression(callPath) {
288
+ const callee = callPath.node.callee;
289
+ if (t.isMemberExpression(callee)) {
290
+ const objName = t.isIdentifier(callee.object) ? callee.object.name : '';
291
+ const propName = t.isIdentifier(callee.property) ? callee.property.name : '';
292
+ const isLogger = ['console', 'logger', 'log', 'telemetry', 'reportError'].includes(objName);
293
+ const isHandlingMethod = ['error', 'warn', 'info', 'captureException', 'track'].includes(propName);
294
+ const isProcessStderr = t.isMemberExpression(callee.object) &&
295
+ t.isIdentifier(callee.object.object, { name: 'process' }) &&
296
+ t.isIdentifier(callee.object.property, { name: 'stderr' });
297
+ if (isLogger || isHandlingMethod || isProcessStderr) {
298
+ hasActiveHandling = true;
299
+ }
300
+ } else if (t.isIdentifier(callee)) {
301
+ const isHandler = ['handleError', 'reportError', 'captureException', 'toResult'].includes(callee.name);
302
+ if (isHandler) {
303
+ hasActiveHandling = true;
304
+ }
305
+ }
306
+ }
307
+ });
308
+
309
+ return !hasActiveHandling;
310
+ };
311
+
312
+ export const countOptionalChainingDepth = (node, t) => {
313
+ let count = 0;
314
+ let curr = node;
315
+ while (curr && (t.isOptionalMemberExpression(curr) || t.isOptionalCallExpression(curr) || t.isMemberExpression(curr))) {
316
+ if (curr.optional) {
317
+ count++;
318
+ }
319
+ curr = curr.object || curr.callee;
320
+ }
321
+ return count;
322
+ };
323
+
324
+ const COMBINATOR_NAMES = new Set(['all', 'any', 'none', 'allPass', 'anyPass', 'nonePass']);
325
+
326
+ export const isCombinatorCall = (callee, t) => {
327
+ if (t.isIdentifier(callee)) {
328
+ return COMBINATOR_NAMES.has(callee.name);
329
+ }
330
+ return false;
331
+ };
332
+
333
+ export const isRawBooleanArg = (arg, t) => {
334
+ if (t.isBinaryExpression(arg)) return true;
335
+ if (t.isLogicalExpression(arg)) return true;
336
+ if (t.isBooleanLiteral(arg)) return true;
337
+ if (t.isUnaryExpression(arg, { operator: '!' })) return true;
338
+ return false;
339
+ };
340
+
@@ -111,6 +111,21 @@ export const RULE_REGISTRY = {
111
111
  severity: 'MEDIUM',
112
112
  directive: 'Replace repetitive dispatch switch with an O(1) keyed dictionary or method map (Directive 3.E)'
113
113
  },
114
+ CONTROL_FLOW_SILENT_GUARD: {
115
+ pillar: PILLARS.PILLAR_2,
116
+ severity: 'MEDIUM',
117
+ directive: 'Replace silent bare return in side-effecting logic with explicit ResultTuple, logger diagnostic, or error state (Directive 3.G)'
118
+ },
119
+ ERROR_SWALLOWED_EXCEPTION: {
120
+ pillar: PILLARS.PILLAR_2,
121
+ severity: 'CRITICAL',
122
+ directive: 'Do not swallow caught exceptions silently; log with error context or return explicit ResultTuple error (Directive 3.B)'
123
+ },
124
+ COMBINATOR_RAW_BOOLEAN: {
125
+ pillar: PILLARS.PILLAR_2,
126
+ severity: 'MEDIUM',
127
+ directive: 'Do not pass inline boolean expressions or raw booleans into logical combinators; use named predicates or thunks (Directive 3.A)'
128
+ },
114
129
  HOOK_SATURATION: {
115
130
  pillar: PILLARS.PILLAR_3,
116
131
  severity: 'HIGH',
@@ -141,6 +156,11 @@ export const RULE_REGISTRY = {
141
156
  severity: 'CRITICAL',
142
157
  directive: 'Replace render-hack setTimeout(0) with await nextTick() or RAF'
143
158
  },
159
+ LIFECYCLE_ORPHANED_LISTENER: {
160
+ pillar: PILLARS.PILLAR_6,
161
+ severity: 'HIGH',
162
+ directive: 'Wrap event listeners in self-cleaning disposers or register pairing removeEventListener (Directive 3.F)'
163
+ },
144
164
  TYPE_COLOCATION: {
145
165
  pillar: PILLARS.PILLAR_4,
146
166
  severity: 'MEDIUM',
@@ -156,6 +176,11 @@ export const RULE_REGISTRY = {
156
176
  severity: 'MEDIUM',
157
177
  directive: 'Use live API data or explicit empty states instead of mock placeholders'
158
178
  },
179
+ DATA_FLOW_OPTIONAL_CHAINING_CHURN: {
180
+ pillar: PILLARS.PILLAR_4,
181
+ severity: 'LOW',
182
+ directive: 'Level incoming data shapes line 1 using sentinels or normalizeArray instead of deep optional chaining churn (Directive 4.D)'
183
+ },
159
184
  RAW_INLINE_STYLE: {
160
185
  pillar: PILLARS.PILLAR_5,
161
186
  severity: 'MEDIUM',
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { findAndLoadConfigFile } from './config/loader.js';
4
+
5
+ export const SOURCE_ROOT_CANDIDATES = ['src', 'lib', 'app', 'cli', 'packages', 'blueprints'];
6
+
7
+ const isDirectory = (dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory();
8
+
9
+ export const toRelDir = (projectRoot, dir) => path.relative(projectRoot, dir).split(path.sep).join('/') || '.';
10
+
11
+ const scopeFromPath = (projectRoot, requested, source) => {
12
+ const dir = path.resolve(projectRoot, requested);
13
+ const isMissing = !isDirectory(dir);
14
+ if (isMissing) {
15
+ const origin = source === 'config' ? 'Configured scope' : 'Audit directory';
16
+ return { ok: false, reason: 'missing', message: `${origin} "${requested}" does not exist (resolved to ${dir}).`, candidates: [] };
17
+ }
18
+ return { ok: true, dir, relDir: toRelDir(projectRoot, dir), source };
19
+ };
20
+
21
+ const readConfiguredScope = (projectRoot) => {
22
+ const scope = findAndLoadConfigFile(projectRoot).raw?.scope;
23
+ const isUsable = typeof scope === 'string' && scope.length > 0;
24
+ return isUsable ? scope : null;
25
+ };
26
+
27
+ export const resolveAuditScope = ({ projectRoot, explicitDir = null }) => {
28
+ if (explicitDir) return scopeFromPath(projectRoot, explicitDir, 'explicit');
29
+
30
+ const configuredScope = readConfiguredScope(projectRoot);
31
+ if (configuredScope) return scopeFromPath(projectRoot, configuredScope, 'config');
32
+
33
+ const candidates = SOURCE_ROOT_CANDIDATES.filter((name) => isDirectory(path.join(projectRoot, name))).sort();
34
+ const isAmbiguous = candidates.length >= 2;
35
+ if (isAmbiguous) {
36
+ return {
37
+ ok: false,
38
+ reason: 'ambiguous',
39
+ message: `Ambiguous audit scope: found ${candidates.join(', ')}. Pass --dir=<path> or set "scope" in .chemxrc.`,
40
+ candidates
41
+ };
42
+ }
43
+ return { ok: true, dir: projectRoot, relDir: '.', source: 'root' };
44
+ };
@@ -18,8 +18,6 @@ export const findProjectRoot = (startDir = process.cwd()) => {
18
18
  if (pkgFile) return path.dirname(pkgFile);
19
19
  const chemxDir = findFileUpward('.chemx', startDir);
20
20
  if (chemxDir) return path.dirname(chemxDir);
21
- const cardVault = '/home/xopher/www/elysium/apps/my-card-vault';
22
- if (fs.existsSync(cardVault)) return cardVault;
23
21
  return startDir;
24
22
  };
25
23