@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.
- package/AGENTS.md +495 -0
- package/blueprints/molecule-capsule/m-sample-card.svelte +1 -1
- package/blueprints/molecule-capsule/m-sample-card.vue +1 -1
- package/cli/audit/ai-slop-detector.js +29 -1
- package/cli/audit/ast-visitors.js +116 -1
- package/cli/audit/audit-summary.js +30 -0
- package/cli/audit/gate-verdict.js +30 -0
- package/cli/audit/history.js +47 -10
- package/cli/audit/ratchet.js +64 -0
- package/cli/audit/rules-predicates.js +200 -0
- package/cli/audit/rules-registry.js +25 -0
- package/cli/audit-scope.js +44 -0
- package/cli/build/detector.js +0 -2
- package/cli/commands/cmd-audit.js +58 -11
- package/cli/commands/cmd-router.js +38 -1
- package/cli/commands/cmd-wrappers.js +322 -0
- package/cli/commands-schema.js +101 -9
- package/cli/errors/catcher.js +46 -3
- package/cli/help.js +41 -0
- package/cli/host-shims.js +77 -0
- package/cli/index.js +6 -0
- package/cli/installer.js +4 -44
- package/cli/mcp/antigravity.js +12 -5
- package/cli/mcp/call-scope.js +93 -0
- package/cli/mcp/index.js +6 -2
- package/cli/mcp/manifests.js +32 -26
- package/cli/mcp/server.js +26 -27
- package/cli/mcp/tools-audit.js +27 -11
- package/cli/mcp/tools-search-util.js +1 -10
- package/cli/mcp/tools-team-tasks.js +6 -4
- package/cli/mcp/tools.js +53 -8
- package/cli/navigator-actions.js +1 -1
- package/cli/navigator-banner-helpers.js +15 -5
- package/cli/navigator-banner.js +2 -9
- package/cli/navigator-guide.js +2 -2
- package/cli/path-scope.js +1 -1
- package/cli/pillars-schema.js +8 -82
- package/cli/pillars-wizard.js +56 -44
- package/cli/pillars-write-guard.js +52 -0
- package/cli/reader-markdown.js +26 -0
- package/cli/reader.js +89 -12
- package/cli/scaffold-frameworks.js +29 -0
- package/cli/scaffold.js +2 -10
- package/cli/search-commands.js +96 -0
- package/cli/search-db.js +11 -2
- package/cli/search-schema.js +22 -9
- package/cli/search.js +51 -50
- package/cli/team/task-list-view.js +43 -0
- package/cli/team/team-commands.js +11 -6
- package/cli/team/team-flags.js +1 -0
- package/cli/team/team-format.js +4 -2
- package/cli/verify.js +33 -15
- package/package.json +10 -5
- package/cli/audit/autofix.spec.js +0 -85
- package/cli/audit/clone-detector.spec.js +0 -46
- package/cli/audit/csharp-analyzer.spec.js +0 -216
- package/cli/audit/pattern-detector.spec.js +0 -113
- package/cli/audit/prompts.spec.js +0 -367
- package/cli/audit/rules.spec.js +0 -361
- package/cli/audit/social-git.spec.js +0 -71
- package/cli/audit/structural-weight.spec.js +0 -116
- package/cli/audit-preflight.spec.js +0 -115
- package/cli/audit-stages.spec.js +0 -30
- package/cli/blast-radius.spec.js +0 -113
- package/cli/build/detector.spec.js +0 -81
- package/cli/columnar.spec.js +0 -49
- package/cli/config/config.spec.js +0 -51
- package/cli/config.spec.js +0 -1
- package/cli/create.spec.js +0 -291
- package/cli/embeddings.spec.js +0 -92
- package/cli/errors/errors.spec.js +0 -163
- package/cli/exploder.spec.js +0 -99
- package/cli/friction-fixes.spec.js +0 -96
- package/cli/generator-compact.spec.js +0 -104
- package/cli/generator-framework.spec.js +0 -315
- package/cli/generator-jig.spec.js +0 -148
- package/cli/generator-templates/archetypes/vector-matcher.spec.js +0 -25
- package/cli/generator-templates.spec.js +0 -187
- package/cli/generator.spec.js +0 -288
- package/cli/help.spec.js +0 -109
- package/cli/installer.spec.js +0 -72
- package/cli/languages.spec.js +0 -65
- package/cli/mcp/installer.spec.js +0 -118
- package/cli/mcp/server.spec.js +0 -710
- package/cli/mcp/tools-project.spec.js +0 -51
- package/cli/mutators.spec.js +0 -176
- package/cli/navigator-banner.spec.js +0 -68
- package/cli/patcher.spec.js +0 -190
- package/cli/path-traversal.spec.js +0 -242
- package/cli/pillars.spec.js +0 -138
- package/cli/polyglot.spec.js +0 -222
- package/cli/project-detector.spec.js +0 -123
- package/cli/reader-logic.spec.js +0 -160
- package/cli/reader-shorthand.spec.js +0 -60
- package/cli/search-bulletproof.spec.js +0 -104
- package/cli/search-queries-hotspot-graph.spec.js +0 -66
- package/cli/search-queries-similar.spec.js +0 -43
- package/cli/search.spec.js +0 -294
- package/cli/team/team-adversarial-locks.spec.js +0 -176
- package/cli/team/team-adversarial-telemetry.spec.js +0 -93
- package/cli/team/team-concurrency-stress.spec.js +0 -95
- package/cli/team/team-lock-concurrency.spec.js +0 -182
- package/cli/team/team-mailbox-empirical.spec.js +0 -191
- package/cli/team/team-mailbox.spec.js +0 -93
- package/cli/team/team-memory.spec.js +0 -123
- package/cli/team/team-multi-model-telemetry.spec.js +0 -80
- package/cli/team/team-multiprocess-concurrency.spec.js +0 -193
- package/cli/team/team-projects.spec.js +0 -116
- package/cli/team/team-release-train.spec.js +0 -91
- package/cli/team/team-tasks-asana.spec.js +0 -60
- package/cli/team/team-tasks-hierarchy.spec.js +0 -176
- package/cli/team/team-vds.spec.js +0 -125
- package/cli/team/team-verification-gate.spec.js +0 -280
- package/cli/team/team.spec.js +0 -574
- package/cli/tesseract.spec.js +0 -55
- package/cli/trace.spec.js +0 -92
- package/cli/trend.spec.js +0 -51
- package/cli/ui-canonical-routes.spec.js +0 -130
- package/cli/ui-direct-routes.spec.js +0 -55
- package/cli/ui-e2e-verification.spec.js +0 -197
- package/cli/ui-filetree.spec.js +0 -175
- package/cli/ui-kanban.spec.js +0 -217
- package/cli/ui-sse.spec.js +0 -100
- package/cli/ui-vbulletin.spec.js +0 -188
- package/cli/ui-workbench-dbstudio.spec.js +0 -172
- package/cli/ui.spec.js +0 -312
- package/cli/verify-lint.spec.js +0 -44
- 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
|
+
};
|
package/cli/audit/history.js
CHANGED
|
@@ -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 =
|
|
47
|
+
const dir = findChemxDir(cwd);
|
|
16
48
|
try {
|
|
17
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
+
};
|
package/cli/build/detector.js
CHANGED
|
@@ -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
|
|