@chemx/starter-kit 26.9.26-253 → 26.9.28-1234
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/cli/audit/ast-visitors.js +7 -1
- package/cli/audit/csharp-analyzer.js +360 -7
- package/cli/audit/csharp-analyzer.spec.js +216 -0
- package/cli/audit/extended-text-patterns.js +6 -3
- package/cli/audit/metrics.js +5 -3
- package/cli/audit/reporter-markdown.js +1 -1
- package/cli/audit/reporter-sections.js +1 -1
- package/cli/audit/rules-helpers.js +7 -1
- package/cli/audit/rules-registry.js +10 -0
- package/cli/audit/rules.js +2 -2
- package/cli/audit.js +16 -8
- package/cli/commands/cmd-audit.js +6 -4
- package/cli/config/loader.js +1 -0
- package/cli/config/profiles.js +3 -0
- package/cli/friction-fixes.spec.js +96 -0
- package/cli/installer-templates.js +37 -6
- package/cli/mcp/manifests.js +7 -2
- package/cli/mcp/tools-patch.js +4 -3
- package/cli/mcp/tools-read.js +3 -1
- package/cli/mcp/tools-team-locks.js +10 -3
- package/cli/mcp/tools-team-tasks.js +5 -2
- package/cli/mcp/tools-write.js +2 -1
- package/cli/mcp/tools.js +44 -1
- package/cli/patcher.js +3 -2
- package/cli/path-scope.js +113 -0
- package/cli/path-traversal.spec.js +242 -0
- package/cli/reader.js +3 -1
- package/cli/search.d.ts +2 -1
- package/cli/search.js +1 -21
- package/cli/search.spec.js +12 -0
- package/cli/team/team-commands-lock.js +17 -5
- package/cli/team/team-commands.js +2 -2
- package/cli/team/team-db-locks.js +50 -15
- package/package.json +1 -1
- package/scripts/pre-commit.sh +28 -6
package/cli/audit/metrics.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { PILLARS } from './rules.js';
|
|
2
2
|
|
|
3
|
+
export const isSlopViolation = (v) => Boolean(v.isAiSlop || (v.rule && v.rule.startsWith('AI_SLOP_')));
|
|
4
|
+
|
|
3
5
|
export const calculateMolecularHealthScore = (violations, totalFiles) => {
|
|
4
6
|
if (totalFiles === 0) {
|
|
5
7
|
return { score: 100, grade: 'A+', label: 'Crystalline Molecular' };
|
|
@@ -7,7 +9,7 @@ export const calculateMolecularHealthScore = (violations, totalFiles) => {
|
|
|
7
9
|
|
|
8
10
|
let penalty = 0;
|
|
9
11
|
for (const v of violations) {
|
|
10
|
-
if (v
|
|
12
|
+
if (isSlopViolation(v)) continue;
|
|
11
13
|
if (v.severity === 'CRITICAL') penalty += 8;
|
|
12
14
|
else if (v.severity === 'HIGH') penalty += 4;
|
|
13
15
|
else if (v.severity === 'MEDIUM') penalty += 2;
|
|
@@ -57,7 +59,7 @@ export const calculatePillarBreakdown = (violations) => {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
for (const v of violations) {
|
|
60
|
-
if (v
|
|
62
|
+
if (isSlopViolation(v)) continue;
|
|
61
63
|
const pillarName = v.pillar || PILLARS.PILLAR_1;
|
|
62
64
|
if (!breakdown[pillarName]) {
|
|
63
65
|
breakdown[pillarName] = {
|
|
@@ -181,7 +183,7 @@ export const calculateHotspots = (violations, fileStats, limit = 5) => {
|
|
|
181
183
|
export const calculateQuantumHealthScore = calculateMolecularHealthScore;
|
|
182
184
|
|
|
183
185
|
export const calculateAiSlopScore = (violations, totalFiles) => {
|
|
184
|
-
const slopViolations = violations.filter(
|
|
186
|
+
const slopViolations = violations.filter(isSlopViolation);
|
|
185
187
|
if (totalFiles === 0) {
|
|
186
188
|
return {
|
|
187
189
|
score: 100,
|
|
@@ -128,7 +128,7 @@ export const generateMarkdownReport = (report) => {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
const slopViolations = violations.filter((v) => v.isAiSlop);
|
|
131
|
+
const slopViolations = violations.filter((v) => v.isAiSlop || (v.rule && v.rule.startsWith('AI_SLOP_')));
|
|
132
132
|
lines.push('---');
|
|
133
133
|
lines.push('');
|
|
134
134
|
lines.push('## 5. AI Slop & Code Authenticity Audit');
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from './prompts.js';
|
|
28
28
|
import { resolveFirstDefined } from './rules-predicates.js';
|
|
29
29
|
|
|
30
|
-
const isSlopViolation = (v) => Boolean(v.isAiSlop);
|
|
30
|
+
const isSlopViolation = (v) => Boolean(v.isAiSlop || (v.rule && v.rule.startsWith('AI_SLOP_')));
|
|
31
31
|
|
|
32
32
|
const formatHotspotItem = (h, idx) => {
|
|
33
33
|
const monolithBadge = resolveHotspotBadge(h.lineCount);
|
|
@@ -17,7 +17,13 @@ export const countLogicalOperators = (node) => {
|
|
|
17
17
|
export const extractParseableCode = (content, ext) => {
|
|
18
18
|
if (ext === '.vue') {
|
|
19
19
|
const scriptMatch = content.match(/<script\b[^>]*>([\s\S]*?)<\/script>/i);
|
|
20
|
-
|
|
20
|
+
if (!scriptMatch) return '';
|
|
21
|
+
const scriptStartIndex = scriptMatch.index || 0;
|
|
22
|
+
const preScript = content.slice(0, scriptStartIndex);
|
|
23
|
+
const openTag = scriptMatch[0].match(/<script\b[^>]*>/i)?.[0] || '';
|
|
24
|
+
const preContent = preScript + openTag;
|
|
25
|
+
const leadingNewlines = preContent.split('\n').length - 1;
|
|
26
|
+
return '\n'.repeat(leadingNewlines) + scriptMatch[1];
|
|
21
27
|
}
|
|
22
28
|
return content;
|
|
23
29
|
};
|
|
@@ -242,5 +242,15 @@ export const RULE_REGISTRY = {
|
|
|
242
242
|
pillar: PILLARS.PILLAR_4,
|
|
243
243
|
severity: 'MEDIUM',
|
|
244
244
|
directive: 'Encapsulate flat primitive props into a cohesive domain type or composable'
|
|
245
|
+
},
|
|
246
|
+
LAYER_VIOLATION_CONTROLLER: {
|
|
247
|
+
pillar: PILLARS.PILLAR_4,
|
|
248
|
+
severity: 'CRITICAL',
|
|
249
|
+
directive: 'Do not inject DbContext or raw persistence into presentation controllers; route through MediatR/CQRS handlers'
|
|
250
|
+
},
|
|
251
|
+
COUPLING_EXCESSIVE_INJECTION: {
|
|
252
|
+
pillar: PILLARS.PILLAR_1,
|
|
253
|
+
severity: 'HIGH',
|
|
254
|
+
directive: 'Decompose high-coupling controllers; reduce injected services or route via MediatR'
|
|
245
255
|
}
|
|
246
256
|
};
|
package/cli/audit/rules.js
CHANGED
|
@@ -107,11 +107,11 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
107
107
|
checkSlopTextPatterns(content, lines, relativePath, violations);
|
|
108
108
|
|
|
109
109
|
// Pillars 8-11: Extended Fast Checks (A11y, Security secrets, Fake tests, Co-located specs)
|
|
110
|
-
checkExtendedTextPatterns(content, lines, relativePath, filePath, violations);
|
|
110
|
+
checkExtendedTextPatterns(content, lines, relativePath, filePath, violations, config);
|
|
111
111
|
|
|
112
112
|
const lang = getLanguageForFile(filePath);
|
|
113
113
|
if (lang?.id === 'csharp') {
|
|
114
|
-
analyzeCSharpCode(content, relativePath, violations);
|
|
114
|
+
analyzeCSharpCode(content, relativePath, violations, config);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (options.fast || !isBabelParsable(filePath)) {
|
package/cli/audit.js
CHANGED
|
@@ -84,12 +84,12 @@ const IGNORED_DIRS = new Set([
|
|
|
84
84
|
'out'
|
|
85
85
|
]);
|
|
86
86
|
|
|
87
|
-
const isSourceFile = (name) => {
|
|
88
|
-
return isPolyglotSourceFile(name, { includeTests: false });
|
|
87
|
+
const isSourceFile = (name, options = {}) => {
|
|
88
|
+
return isPolyglotSourceFile(name, { includeTests: false, ...options });
|
|
89
89
|
};
|
|
90
90
|
|
|
91
91
|
export const auditFile = (filePath, relativePath) => {
|
|
92
|
-
if (!isSourceFile(path.basename(filePath))) return [];
|
|
92
|
+
if (!isSourceFile(path.basename(filePath), { includeTests: true })) return [];
|
|
93
93
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
94
94
|
return auditCode(content, filePath, relativePath);
|
|
95
95
|
};
|
|
@@ -125,12 +125,18 @@ export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
|
|
|
125
125
|
let fileStats = [];
|
|
126
126
|
let totalHooks = 0;
|
|
127
127
|
|
|
128
|
+
const targetRel = path.relative(baseDir, targetDir);
|
|
129
|
+
const isTargetingTests = /(?:^|[\\/])(?:tests?|specs?)(?:[\\/]|$)/i.test(targetRel) ||
|
|
130
|
+
/(?:^|[\\/])(?:tests?|specs?)(?:[\\/]|$)/i.test(targetDir);
|
|
131
|
+
const includeTests = Boolean(scanOptions.includeTests || isTargetingTests);
|
|
132
|
+
const effectiveScanOptions = { ...scanOptions, includeTests };
|
|
133
|
+
|
|
128
134
|
if (scanOptions.fileList && scanOptions.fileList.length > 0) {
|
|
129
135
|
for (const item of scanOptions.fileList) {
|
|
130
136
|
const fullPath = path.isAbsolute(item) ? item : path.resolve(baseDir, item);
|
|
131
137
|
const relPath = path.relative(baseDir, fullPath);
|
|
132
|
-
if (fs.existsSync(fullPath) && isSourceFile(path.basename(fullPath))) {
|
|
133
|
-
const result = auditFileEntry(fullPath, relPath,
|
|
138
|
+
if (fs.existsSync(fullPath) && isSourceFile(path.basename(fullPath), { includeTests: true })) {
|
|
139
|
+
const result = auditFileEntry(fullPath, relPath, effectiveScanOptions);
|
|
134
140
|
violations = violations.concat(result.fileViolations);
|
|
135
141
|
fileStats.push(result.fileStat);
|
|
136
142
|
totalHooks += result.hookCount;
|
|
@@ -150,13 +156,13 @@ export const scanTree = (targetDir, baseDir, scanOptions = {}) => {
|
|
|
150
156
|
|
|
151
157
|
if (entry.isDirectory()) {
|
|
152
158
|
if (!IGNORED_DIRS.has(entry.name)) {
|
|
153
|
-
const sub = scanTree(fullPath, baseDir,
|
|
159
|
+
const sub = scanTree(fullPath, baseDir, effectiveScanOptions);
|
|
154
160
|
violations = violations.concat(sub.violations);
|
|
155
161
|
fileStats = fileStats.concat(sub.fileStats);
|
|
156
162
|
totalHooks += sub.totalHooks;
|
|
157
163
|
}
|
|
158
|
-
} else if (isSourceFile(entry.name)) {
|
|
159
|
-
const result = auditFileEntry(fullPath, relPath,
|
|
164
|
+
} else if (isSourceFile(entry.name, { includeTests })) {
|
|
165
|
+
const result = auditFileEntry(fullPath, relPath, effectiveScanOptions);
|
|
160
166
|
violations = violations.concat(result.fileViolations);
|
|
161
167
|
fileStats.push(result.fileStat);
|
|
162
168
|
totalHooks += result.hookCount;
|
|
@@ -177,11 +183,13 @@ export const runAudit = (targetDir = 'src', options = {}) => {
|
|
|
177
183
|
const patternRegistry = createPatternRegistry();
|
|
178
184
|
const hookRegistry = createHookShapeRegistry();
|
|
179
185
|
const config = options.config || loadProjectConfig(cwd);
|
|
186
|
+
const includeTests = Boolean(options.includeTests);
|
|
180
187
|
const { violations, fileStats, totalHooks } = scanTree(absoluteTarget, cwd, {
|
|
181
188
|
patternRegistry,
|
|
182
189
|
hookRegistry,
|
|
183
190
|
fast: Boolean(options.fast),
|
|
184
191
|
fileList: options.fileList || null,
|
|
192
|
+
includeTests,
|
|
185
193
|
config
|
|
186
194
|
});
|
|
187
195
|
|
|
@@ -101,7 +101,8 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
|
|
|
101
101
|
fileList = preflight.fileList;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const
|
|
104
|
+
const includeTests = rawArgs.includes('--include-tests') || rawArgs.includes('--tests');
|
|
105
|
+
const auditOptions = { outputFile, model, costPerMillion, fast: isFast, fileList, stage, config: projectConfig, includeTests };
|
|
105
106
|
const report = executeAstAudit(targetDir, auditOptions);
|
|
106
107
|
saveAuditSnapshot(report);
|
|
107
108
|
try {
|
|
@@ -112,7 +113,8 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
|
|
|
112
113
|
const shouldTriage = !rawArgs.includes('--no-triage');
|
|
113
114
|
if (shouldTriage) {
|
|
114
115
|
const createdTasks = autoGenerateTasksFromAudit(syncRes.db, { cwd: process.cwd(), targetDir });
|
|
115
|
-
|
|
116
|
+
const shouldLogTriage = isCli && !isJson && createdTasks.length > 0;
|
|
117
|
+
if (shouldLogTriage) {
|
|
116
118
|
process.stdout.write(`\x1b[32m✔\x1b[0m Auto-triage synchronized ${createdTasks.length} team task(s) in SQLite backlog.\n`);
|
|
117
119
|
}
|
|
118
120
|
}
|
|
@@ -141,8 +143,8 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
|
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
|
-
} catch {
|
|
145
|
-
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (process.env.DEBUG) process.stderr.write(`[debug] Indexing bypassed: ${err?.message}\n`);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
// Stage 1: Atomic failure predicates
|
package/cli/config/loader.js
CHANGED
|
@@ -25,6 +25,7 @@ const normalizeRuleKeys = (rules = {}) => {
|
|
|
25
25
|
'max-prop-count': 'maxPropCount',
|
|
26
26
|
'prefer-design-tokens': 'preferDesignTokens',
|
|
27
27
|
'enforce-file-length': 'enforceFileLength',
|
|
28
|
+
'enforce-colocated-tests': 'enforceColocatedTests',
|
|
28
29
|
'rule-of-three-abstractions': 'ruleOfThreeAbstractions',
|
|
29
30
|
'ai-slop-detection': 'aiSlopDetection',
|
|
30
31
|
'max-line-count-warning': 'maxLineCountWarning'
|
package/cli/config/profiles.js
CHANGED
|
@@ -14,6 +14,7 @@ export const PROFILES = {
|
|
|
14
14
|
maxPropCount: 7,
|
|
15
15
|
preferDesignTokens: 'warning',
|
|
16
16
|
enforceFileLength: false,
|
|
17
|
+
enforceColocatedTests: false,
|
|
17
18
|
ruleOfThreeAbstractions: true,
|
|
18
19
|
aiSlopDetection: 'critical',
|
|
19
20
|
maxLineCountWarning: 250
|
|
@@ -28,6 +29,7 @@ export const PROFILES = {
|
|
|
28
29
|
maxPropCount: 5,
|
|
29
30
|
preferDesignTokens: 'error',
|
|
30
31
|
enforceFileLength: true,
|
|
32
|
+
enforceColocatedTests: true,
|
|
31
33
|
ruleOfThreeAbstractions: false,
|
|
32
34
|
aiSlopDetection: 'critical',
|
|
33
35
|
maxLineCountWarning: 100
|
|
@@ -42,6 +44,7 @@ export const PROFILES = {
|
|
|
42
44
|
maxPropCount: 12,
|
|
43
45
|
preferDesignTokens: 'off',
|
|
44
46
|
enforceFileLength: false,
|
|
47
|
+
enforceColocatedTests: false,
|
|
45
48
|
ruleOfThreeAbstractions: true,
|
|
46
49
|
aiSlopDetection: 'medium',
|
|
47
50
|
maxLineCountWarning: 500
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { handleChemx } from './mcp/tools.js';
|
|
6
|
+
import { auditCode } from './audit/rules.js';
|
|
7
|
+
|
|
8
|
+
test('Friction 1: team_task add and team task add persist task rows', async () => {
|
|
9
|
+
const resCmd = await handleChemx({ command: 'team task add CLI Task Test' });
|
|
10
|
+
assert.ok(resCmd && resCmd.id, 'Expected task object with id from team task add');
|
|
11
|
+
assert.strictEqual(resCmd.title, 'CLI Task Test');
|
|
12
|
+
|
|
13
|
+
const resCmd2 = await handleChemx({ command: 'team_task add Direct SubCmd Task' });
|
|
14
|
+
assert.ok(resCmd2 && resCmd2.id, 'Expected task object with id from team_task add');
|
|
15
|
+
assert.strictEqual(resCmd2.title, 'Direct SubCmd Task');
|
|
16
|
+
|
|
17
|
+
const resInfer = await handleChemx({ action: 'team_task', params: { title: 'Inferred Add Task' } });
|
|
18
|
+
assert.ok(resInfer && resInfer.id, 'Expected task object with id when title is given');
|
|
19
|
+
assert.strictEqual(resInfer.title, 'Inferred Add Task');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('Friction 2: patch handles all parameter aliases and command strings', async () => {
|
|
23
|
+
const testFile = path.resolve('test-patch-spec.txt');
|
|
24
|
+
fs.writeFileSync(testFile, 'line 1\nOLD_STRING\nline 3\n');
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await handleChemx({ action: 'patch', params: { path: 'test-patch-spec.txt', target: 'OLD_STRING', replace: 'NEW_STRING_1' } });
|
|
28
|
+
assert.strictEqual(fs.readFileSync(testFile, 'utf8'), 'line 1\nNEW_STRING_1\nline 3\n');
|
|
29
|
+
|
|
30
|
+
await handleChemx({ action: 'patch', params: { path: 'test-patch-spec.txt', search: 'NEW_STRING_1', replacement: 'NEW_STRING_2' } });
|
|
31
|
+
assert.strictEqual(fs.readFileSync(testFile, 'utf8'), 'line 1\nNEW_STRING_2\nline 3\n');
|
|
32
|
+
|
|
33
|
+
await handleChemx({ action: 'patch', params: { path: 'test-patch-spec.txt', targetContent: 'NEW_STRING_2', replacementContent: 'FINAL_STRING' } });
|
|
34
|
+
assert.strictEqual(fs.readFileSync(testFile, 'utf8'), 'line 1\nFINAL_STRING\nline 3\n');
|
|
35
|
+
} finally {
|
|
36
|
+
if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Friction 3: Vue file AST line numbers match file line numbers exactly', () => {
|
|
41
|
+
const vueContent = [
|
|
42
|
+
'<template>',
|
|
43
|
+
' <div>',
|
|
44
|
+
' <p>Line 3</p>',
|
|
45
|
+
' <p>Line 4</p>',
|
|
46
|
+
' <p>Line 5</p>',
|
|
47
|
+
' <p>Line 6</p>',
|
|
48
|
+
' <p>Line 7</p>',
|
|
49
|
+
' <p>Line 8</p>',
|
|
50
|
+
' <p>Line 9</p>',
|
|
51
|
+
' <p>Line 10</p>',
|
|
52
|
+
' </div>',
|
|
53
|
+
'</template>',
|
|
54
|
+
'',
|
|
55
|
+
'<script setup lang="ts">',
|
|
56
|
+
'const cond = a && b && c;',
|
|
57
|
+
'</script>'
|
|
58
|
+
].join('\n');
|
|
59
|
+
|
|
60
|
+
const recorded = [];
|
|
61
|
+
const fakeReg = { record: (type, sig, loc) => recorded.push({ type, sig, loc }) };
|
|
62
|
+
auditCode(vueContent, 'SampleVue.vue', 'SampleVue.vue', { patternRegistry: fakeReg });
|
|
63
|
+
|
|
64
|
+
const condRec = recorded.find((r) => r.type === 'PREDICATE_LOGIC');
|
|
65
|
+
assert.ok(condRec, 'Expected PREDICATE_LOGIC recorded');
|
|
66
|
+
assert.strictEqual(condRec.loc.line, 15, 'Expected condition on line 15');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('Friction 4: HOOK_SATURATION excludes canonical controller return wrappers', () => {
|
|
70
|
+
const hookCode = [
|
|
71
|
+
'export function useMyComposable() {',
|
|
72
|
+
' const a = useA();',
|
|
73
|
+
' const b = useB();',
|
|
74
|
+
' const c = useC();',
|
|
75
|
+
' const d = useD();',
|
|
76
|
+
' const e = useE();',
|
|
77
|
+
' return useSharedStatusController({ a, b, c, d, e });',
|
|
78
|
+
'}'
|
|
79
|
+
].join('\n');
|
|
80
|
+
|
|
81
|
+
const hookViolations = auditCode(hookCode, 'useMyComposable.ts', 'src/useMyComposable.ts');
|
|
82
|
+
const hookSat = hookViolations.filter((v) => v.rule === 'HOOK_SATURATION');
|
|
83
|
+
assert.strictEqual(hookSat.length, 0, 'useSharedStatusController should not trigger HOOK_SATURATION');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('Friction 5: TEST_MISSING_COLOCATED is gated by profile', () => {
|
|
87
|
+
const compCode = '<template><button>Click</button></template>';
|
|
88
|
+
|
|
89
|
+
const defaultViolations = auditCode(compCode, 'src/components/molecules/m-button.vue', 'src/components/molecules/m-button.vue', { config: { profile: 'pragmatic' } });
|
|
90
|
+
const testMissingDefault = defaultViolations.filter((v) => v.rule === 'TEST_MISSING_COLOCATED');
|
|
91
|
+
assert.strictEqual(testMissingDefault.length, 0, 'Pragmatic profile should not flag TEST_MISSING_COLOCATED');
|
|
92
|
+
|
|
93
|
+
const strictViolations = auditCode(compCode, 'src/components/molecules/m-button.vue', 'src/components/molecules/m-button.vue', { config: { profile: 'atomic-strict' } });
|
|
94
|
+
const testMissingStrict = strictViolations.filter((v) => v.rule === 'TEST_MISSING_COLOCATED');
|
|
95
|
+
assert.strictEqual(testMissingStrict.length, 1, 'Atomic-strict profile should flag TEST_MISSING_COLOCATED');
|
|
96
|
+
});
|
|
@@ -62,27 +62,47 @@ MIN_SCORE="\${CHEMX_MIN_SCORE:-\${CONF_MIN_SCORE:-${minScore}}}"
|
|
|
62
62
|
MAX_LINES="\${CHEMX_MAX_LINES:-\${CONF_MAX_LINES:-500}}"
|
|
63
63
|
MAX_MOLECULE_LINES="\${CHEMX_MAX_MOLECULE_LINES:-\${CONF_MAX_MOL:-100}}"
|
|
64
64
|
|
|
65
|
-
STAGED_FILES=\$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(jsx?|tsx?|vue|svelte)\$' | grep -vE '(\\.(d\\.ts|min\\.|test\\.|spec\\.))')
|
|
65
|
+
STAGED_FILES=\$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(jsx?|tsx?|vue|svelte|cs|py|go)\$' | grep -vE '(\\.(d\\.ts|min\\.|test\\.|spec\\.))')
|
|
66
66
|
[ -z "\$STAGED_FILES" ] && exit 0
|
|
67
67
|
|
|
68
68
|
FAILED=0
|
|
69
69
|
ERRORS=""
|
|
70
|
+
EXCEEDED_FILES=""
|
|
70
71
|
for F in \$STAGED_FILES; do
|
|
71
72
|
[ ! -f "\$F" ] && continue
|
|
72
73
|
L=\$(wc -l < "\$F" | tr -d ' ')
|
|
73
74
|
case "\$F" in
|
|
74
75
|
*molecules*|*/m-*|m-*)
|
|
75
|
-
[ "\$L" -gt "\$MAX_MOLECULE_LINES" ]
|
|
76
|
+
if [ "\$L" -gt "\$MAX_MOLECULE_LINES" ]; then
|
|
77
|
+
FAILED=1
|
|
78
|
+
ERRORS="\${ERRORS}\\n \${C_RED}✕\${C_RESET} \$F (\$L LOC > \$MAX_MOLECULE_LINES molecule limit)"
|
|
79
|
+
EXCEEDED_FILES="\${EXCEEDED_FILES}\\n- \$F (\$L LOC > \$MAX_MOLECULE_LINES limit)"
|
|
80
|
+
fi
|
|
81
|
+
;;
|
|
76
82
|
*)
|
|
77
|
-
[ "\$L" -gt "\$MAX_LINES" ]
|
|
83
|
+
if [ "\$L" -gt "\$MAX_LINES" ]; then
|
|
84
|
+
FAILED=1
|
|
85
|
+
ERRORS="\${ERRORS}\\n \${C_RED}✕\${C_RESET} \$F (\$L LOC > \$MAX_LINES file budget)"
|
|
86
|
+
EXCEEDED_FILES="\${EXCEEDED_FILES}\\n- \$F (\$L LOC > \$MAX_LINES limit)"
|
|
87
|
+
fi
|
|
88
|
+
;;
|
|
78
89
|
esac
|
|
79
90
|
done
|
|
80
91
|
|
|
81
92
|
if [ "\$FAILED" -eq 1 ]; then
|
|
82
93
|
printf "\\n%s%s[Chemical X] Commit Blocked: Staged files exceed architectural line budgets%s\\n" "\$C_BOLD" "\$C_RED" "\$C_RESET"
|
|
83
94
|
printf "%b\\n\\n" "\$ERRORS"
|
|
84
|
-
printf "%
|
|
85
|
-
printf "
|
|
95
|
+
printf "%s╭──────────────────────────────────────────────────────────────────────────╮%s\\n" "\$C_CYAN" "\$C_RESET"
|
|
96
|
+
printf "%s│ 🤖 AI REFACTOR PROMPT (Copy & paste into your AI assistant): │%s\\n" "\$C_CYAN" "\$C_RESET"
|
|
97
|
+
printf "%s╰──────────────────────────────────────────────────────────────────────────╯%s\\n" "\$C_CYAN" "\$C_RESET"
|
|
98
|
+
printf "Please refactor the following files that exceed Chemical X line budgets:%b\\n\\n" "\$EXCEEDED_FILES"
|
|
99
|
+
printf "Refactor Directives:\\n"
|
|
100
|
+
printf "1. Decompose monolithic logic into crystalline single-purpose modules (< %s lines for files, < %s lines for molecules).\\n" "\$MAX_LINES" "\$MAX_MOLECULE_LINES"
|
|
101
|
+
printf "2. Extract presentation into Table-of-Contents views and business state into composables/services.\\n"
|
|
102
|
+
printf "3. Preserve all existing symbols, exports, and public API contracts.\\n"
|
|
103
|
+
printf "4. Decompose complex inline booleans and flatten nested control flow.\\n"
|
|
104
|
+
printf "%s────────────────────────────────────────────────────────────────────────────%s\\n\\n" "\$C_CYAN" "\$C_RESET"
|
|
105
|
+
printf "%s💡 Tip: To bypass line budgets temporarily: CHEMX_SKIP_PRECOMMIT=1 git commit%s\\n\\n" "\$C_YELLOW" "\$C_RESET"
|
|
86
106
|
exit 1
|
|
87
107
|
fi
|
|
88
108
|
|
|
@@ -104,7 +124,18 @@ if [ -n "\$AUDIT_BIN" ]; then
|
|
|
104
124
|
if ! AUDIT_OUT=\$(eval "\$AUDIT_BIN audit --git --min-grade=\$MIN_GRADE --min-score=\$MIN_SCORE --non-interactive" < /dev/null 2>&1); then
|
|
105
125
|
printf "\\n%s%s[Chemical X] Commit Blocked: Architectural health verification failed%s\\n" "\$C_BOLD" "\$C_RED" "\$C_RESET"
|
|
106
126
|
printf "%s\\n\\n" "\$AUDIT_OUT"
|
|
107
|
-
printf "%s
|
|
127
|
+
printf "%s╭──────────────────────────────────────────────────────────────────────────╮%s\\n" "\$C_CYAN" "\$C_RESET"
|
|
128
|
+
printf "%s│ 🤖 AI REFACTOR PROMPT (Copy & paste into your AI assistant): │%s\\n" "\$C_CYAN" "\$C_RESET"
|
|
129
|
+
printf "%s╰──────────────────────────────────────────────────────────────────────────╯%s\\n" "\$C_CYAN" "\$C_RESET"
|
|
130
|
+
printf "Please fix the Chemical X architectural hazards reported above in staged files.\\n\\n"
|
|
131
|
+
printf "Refactor Directives:\\n"
|
|
132
|
+
printf "1. Surgically resolve each flagged Critical and High severity hazard.\\n"
|
|
133
|
+
printf "2. Decompose monoliths into single-purpose crystalline capsules.\\n"
|
|
134
|
+
printf "3. Preserve all existing symbols, exports, and test contracts.\\n"
|
|
135
|
+
printf "4. Verify with 'chemx audit' after making changes.\\n"
|
|
136
|
+
printf "%s────────────────────────────────────────────────────────────────────────────%s\\n\\n" "\$C_CYAN" "\$C_RESET"
|
|
137
|
+
printf "%s💡 Tip: Run 'chemx audit' locally to inspect details or run autofixes.%s\\n" "\$C_CYAN" "\$C_RESET"
|
|
138
|
+
printf " To bypass this check temporarily: CHEMX_SKIP_PRECOMMIT=1 git commit\\n\\n"
|
|
108
139
|
exit 1
|
|
109
140
|
fi
|
|
110
141
|
fi
|
package/cli/mcp/manifests.js
CHANGED
|
@@ -303,10 +303,15 @@ export const SUB_TOOLS = [
|
|
|
303
303
|
properties: {
|
|
304
304
|
path: { type: 'string', description: 'Target file path' },
|
|
305
305
|
targetContent: { type: 'string', description: 'Exact string chunk to replace' },
|
|
306
|
+
target: { type: 'string', description: 'Alias for target text chunk to replace' },
|
|
307
|
+
search: { type: 'string', description: 'Alias for target text chunk to replace' },
|
|
306
308
|
replacementContent: { type: 'string', description: 'New replacement content' },
|
|
307
|
-
|
|
309
|
+
replacement: { type: 'string', description: 'Alias for replacement content' },
|
|
310
|
+
replace: { type: 'string', description: 'Alias for replacement content' },
|
|
311
|
+
allowMultiple: { type: 'boolean', description: 'Allow multiple replacements' },
|
|
312
|
+
multiple: { type: 'boolean', description: 'Alias for allowMultiple' }
|
|
308
313
|
},
|
|
309
|
-
required: ['path'
|
|
314
|
+
required: ['path']
|
|
310
315
|
}
|
|
311
316
|
},
|
|
312
317
|
{
|
package/cli/mcp/tools-patch.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { syncSingleFileIndex } from '../search.js';
|
|
3
3
|
import { patchFile } from '../patcher.js';
|
|
4
4
|
import { handleCheckCommand } from '../search-commands.js';
|
|
5
|
+
import { resolveSafePath } from '../path-scope.js';
|
|
5
6
|
|
|
6
7
|
export const isSevereViolation = (v) => {
|
|
7
8
|
const isCritical = v.severity === 'CRITICAL';
|
|
@@ -36,10 +37,10 @@ export const handleChemxPatch = (args = {}, cwd = process.cwd()) => {
|
|
|
36
37
|
const hasRequiredArgs = hasPath && hasTargetContent && hasReplacementContent;
|
|
37
38
|
|
|
38
39
|
if (!hasRequiredArgs) {
|
|
39
|
-
throw new Error('chemx_patch requires "path", "targetContent", and "replacementContent"
|
|
40
|
+
throw new Error('chemx_patch requires "path", target content ("targetContent", "target", or "search"), and replacement content ("replacementContent", "replacement", or "replace").');
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const targetPath =
|
|
43
|
+
const targetPath = resolveSafePath(args.path, cwd);
|
|
43
44
|
const result = patchFile(targetPath, {
|
|
44
45
|
targetContent,
|
|
45
46
|
replacementContent,
|
|
@@ -69,6 +70,6 @@ export const handleChemxCheck = (args = {}, cwd = process.cwd()) => {
|
|
|
69
70
|
if (!args.path) {
|
|
70
71
|
throw new Error('chemx_check requires "path" argument.');
|
|
71
72
|
}
|
|
72
|
-
const targetPath =
|
|
73
|
+
const targetPath = resolveSafePath(args.path, cwd);
|
|
73
74
|
return handleCheckCommand(targetPath, { isJson: true, isCli: false });
|
|
74
75
|
};
|
package/cli/mcp/tools-read.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from '../search-queries.js';
|
|
7
7
|
import { findSimilarSymbols } from '../search-queries-similar.js';
|
|
8
8
|
import { readTokenOptimized } from '../reader.js';
|
|
9
|
+
import { resolveSafePath } from '../path-scope.js';
|
|
9
10
|
import { EXT_LANG_MAP, resolveTargetCwd } from './tools-search-util.js';
|
|
10
11
|
|
|
11
12
|
const buildConnectionCard = (db, symbol, targetPath) => {
|
|
@@ -72,8 +73,9 @@ export const handleChemxRead = (args = {}, cwd = process.cwd()) => {
|
|
|
72
73
|
if (endLine === undefined && colonMatch[3]) endLine = parseInt(colonMatch[3], 10);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
const targetPath =
|
|
76
|
+
const targetPath = resolveSafePath(rawPath, targetCwd);
|
|
76
77
|
const res = readTokenOptimized(targetPath, {
|
|
78
|
+
cwd: targetCwd,
|
|
77
79
|
outline: args.outline,
|
|
78
80
|
logic: args.logic,
|
|
79
81
|
template: args.template,
|
|
@@ -6,23 +6,30 @@ import {
|
|
|
6
6
|
getFileLockStatus
|
|
7
7
|
} from '../team/team-db.js';
|
|
8
8
|
import { handleError } from '../errors/index.js';
|
|
9
|
+
import { isPathTraversal } from '../path-scope.js';
|
|
9
10
|
|
|
10
11
|
export const handleChemxTeamLock = async (args = {}, cwd = process.cwd()) => {
|
|
11
12
|
const db = openIndexDb(cwd);
|
|
12
13
|
if (!db) return { error: 'sqlite_unavailable' };
|
|
13
14
|
const action = args.action || 'status';
|
|
14
15
|
|
|
16
|
+
if (args.filePath && isPathTraversal(args.filePath, cwd)) {
|
|
17
|
+
return { error: 'path_traversal' };
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
if (action === 'acquire') {
|
|
16
21
|
return requestFileLock(db, args.filePath, args.agentId, {
|
|
17
22
|
purpose: args.purpose,
|
|
18
|
-
ttlMs: args.ttlMs
|
|
23
|
+
ttlMs: args.ttlMs,
|
|
24
|
+
cwd
|
|
19
25
|
});
|
|
20
26
|
}
|
|
21
27
|
if (action === 'release') {
|
|
22
|
-
return releaseFileLock(db, args.filePath, args.agentId);
|
|
28
|
+
return releaseFileLock(db, args.filePath, args.agentId, { cwd });
|
|
23
29
|
}
|
|
24
30
|
if (action === 'status') {
|
|
25
|
-
const status = getFileLockStatus(db, args.filePath);
|
|
31
|
+
const status = getFileLockStatus(db, args.filePath, { cwd });
|
|
32
|
+
if (!status) return null;
|
|
26
33
|
return {
|
|
27
34
|
lease: status.lease,
|
|
28
35
|
waiters: toColumnar(status.waiters, ['id', 'agent_id', 'priority', 'status', 'requested_at'])
|
|
@@ -12,7 +12,10 @@ import { freezeReleaseTrain } from '../team/team-release-train.js';
|
|
|
12
12
|
export const handleChemxTeamTask = async (args = {}, cwd = process.cwd()) => {
|
|
13
13
|
const db = openIndexDb(cwd);
|
|
14
14
|
if (!db) return { error: 'sqlite_unavailable' };
|
|
15
|
-
|
|
15
|
+
let action = args.action || args.subAction;
|
|
16
|
+
if (!action) {
|
|
17
|
+
action = Boolean(args.title) ? 'add' : 'list';
|
|
18
|
+
}
|
|
16
19
|
|
|
17
20
|
if (action === 'list') {
|
|
18
21
|
const parentId = args.parentId !== undefined ? args.parentId : args.parent;
|
|
@@ -53,7 +56,7 @@ export const handleChemxTeamTask = async (args = {}, cwd = process.cwd()) => {
|
|
|
53
56
|
const parentId = args.parentId !== undefined ? args.parentId : args.parent;
|
|
54
57
|
const task = createTask(db, {
|
|
55
58
|
title: args.title,
|
|
56
|
-
target_path: args.targetPath,
|
|
59
|
+
target_path: args.targetPath || args.target,
|
|
57
60
|
tier: args.tier || 'molecule',
|
|
58
61
|
priority: args.priority || 2,
|
|
59
62
|
assigned_agent_id: args.assignedAgentId || null,
|
package/cli/mcp/tools-write.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { syncSingleFileIndex } from '../search.js';
|
|
3
3
|
import { writeFile } from '../patcher.js';
|
|
4
4
|
import { formatPatchWarnings } from './tools-patch.js';
|
|
5
|
+
import { resolveSafePath } from '../path-scope.js';
|
|
5
6
|
|
|
6
7
|
export const handleChemxWrite = (args = {}, cwd = process.cwd()) => {
|
|
7
8
|
const hasPath = Boolean(args.path);
|
|
@@ -12,7 +13,7 @@ export const handleChemxWrite = (args = {}, cwd = process.cwd()) => {
|
|
|
12
13
|
throw new Error('chemx_write requires "path" and "content" arguments.');
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
const targetPath =
|
|
16
|
+
const targetPath = resolveSafePath(args.path, cwd);
|
|
16
17
|
const result = writeFile(targetPath, {
|
|
17
18
|
content: args.content,
|
|
18
19
|
cwd
|
package/cli/mcp/tools.js
CHANGED
|
@@ -76,10 +76,53 @@ const parseCommand = (command, params) => {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
+
if (subCmd === 'patch') {
|
|
80
|
+
return {
|
|
81
|
+
action: 'patch',
|
|
82
|
+
params: {
|
|
83
|
+
path: parts[1] || params?.path,
|
|
84
|
+
target: params?.target || params?.targetContent || params?.search,
|
|
85
|
+
replacement: params?.replacement || params?.replacementContent || params?.replace,
|
|
86
|
+
...params
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
79
90
|
if (subCmd === 'q' || subCmd === 'search') return { action: 'q', params: { query: parts.slice(1).join(' '), ...params } };
|
|
80
91
|
if (subCmd === 'team') {
|
|
81
92
|
const TEAM_ACTIONS = { status: 'team_status', feed: 'team_feed', task: 'team_task', lock: 'team_lock', inbox: 'team_inbox', dm: 'team_dm' };
|
|
82
|
-
|
|
93
|
+
const sub = parts[1] || 'status';
|
|
94
|
+
const action = TEAM_ACTIONS[sub] || 'team_task';
|
|
95
|
+
const parsedParams = { ...params };
|
|
96
|
+
const isTaskSub = sub === 'task' || !TEAM_ACTIONS[parts[1]];
|
|
97
|
+
if (isTaskSub) {
|
|
98
|
+
const taskSub = TEAM_ACTIONS[parts[1]] ? parts[2] : parts[1];
|
|
99
|
+
const isCreate = ['add', 'create', 'new'].includes(taskSub);
|
|
100
|
+
if (isCreate) {
|
|
101
|
+
parsedParams.subAction = 'add';
|
|
102
|
+
const titleTokens = parts.slice(TEAM_ACTIONS[parts[1]] ? 3 : 2);
|
|
103
|
+
if (titleTokens.length > 0 && !parsedParams.title) {
|
|
104
|
+
parsedParams.title = titleTokens.join(' ').replace(/^["']|["']$/g, '');
|
|
105
|
+
}
|
|
106
|
+
} else if (taskSub) {
|
|
107
|
+
parsedParams.subAction = taskSub;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { action, params: parsedParams };
|
|
111
|
+
}
|
|
112
|
+
if (subCmd === 'team_task') {
|
|
113
|
+
const parsedParams = { ...params };
|
|
114
|
+
const taskSub = parts[1];
|
|
115
|
+
const isCreate = ['add', 'create', 'new'].includes(taskSub);
|
|
116
|
+
if (isCreate) {
|
|
117
|
+
parsedParams.subAction = 'add';
|
|
118
|
+
const titleTokens = parts.slice(2);
|
|
119
|
+
if (titleTokens.length > 0 && !parsedParams.title) {
|
|
120
|
+
parsedParams.title = titleTokens.join(' ').replace(/^["']|["']$/g, '');
|
|
121
|
+
}
|
|
122
|
+
} else if (taskSub) {
|
|
123
|
+
parsedParams.subAction = taskSub;
|
|
124
|
+
}
|
|
125
|
+
return { action: 'team_task', params: parsedParams };
|
|
83
126
|
}
|
|
84
127
|
if (subCmd === 'project' || subCmd === 'coordinator') {
|
|
85
128
|
const subAction = parts[1] || 'status';
|
package/cli/patcher.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { ANSI } from './theme.js';
|
|
4
4
|
import { syncSingleFileIndex } from './search.js';
|
|
5
5
|
import { auditFile } from './audit.js';
|
|
6
|
+
import { resolveSafePath } from './path-scope.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Surgically applies a search-and-replace block to a file.
|
|
@@ -37,7 +38,7 @@ export const patchFile = (targetPath, params = {}) => {
|
|
|
37
38
|
throw new Error('replacementContent is required for patching');
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
const resolvedPath =
|
|
41
|
+
const resolvedPath = resolveSafePath(targetPath, cwd);
|
|
41
42
|
if (!fs.existsSync(resolvedPath)) {
|
|
42
43
|
throw new Error(`File not found: ${targetPath}`);
|
|
43
44
|
}
|
|
@@ -253,7 +254,7 @@ export const writeFile = (targetPath, params = {}) => {
|
|
|
253
254
|
throw new Error('content is required for writeFile');
|
|
254
255
|
}
|
|
255
256
|
|
|
256
|
-
const resolvedPath =
|
|
257
|
+
const resolvedPath = resolveSafePath(targetPath, cwd);
|
|
257
258
|
const dir = path.dirname(resolvedPath);
|
|
258
259
|
if (!fs.existsSync(dir)) {
|
|
259
260
|
fs.mkdirSync(dir, { recursive: true });
|