@chemx/create-chemx 26.9.21-8 → 26.9.24-393
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 +10 -17
- package/cli/audit/autofix.js +17 -2
- package/cli/audit/clone-detector.js +64 -0
- package/cli/audit/clone-detector.spec.js +46 -0
- package/cli/audit/history.js +22 -6
- package/cli/audit/hook-shape-validator.js +312 -0
- package/cli/audit/prompts.js +13 -10
- package/cli/audit/prompts.spec.js +52 -1
- package/cli/audit/reporter-clones.js +33 -0
- package/cli/audit/reporter-hotspot-graph.js +34 -0
- package/cli/audit/rules-registry.js +6 -1
- package/cli/audit/rules.js +10 -2
- package/cli/audit/rules.spec.js +129 -0
- package/cli/audit-stages.spec.js +30 -0
- package/cli/audit.js +12 -3
- package/cli/commands/cmd-audit.js +56 -12
- package/cli/commands/cmd-project.js +100 -0
- package/cli/commands/cmd-router.js +52 -3
- package/cli/commands-schema.js +28 -0
- package/cli/create.spec.js +2 -2
- package/cli/exploder.js +258 -0
- package/cli/exploder.spec.js +99 -0
- package/cli/generator-compact.spec.js +104 -0
- package/cli/generator-framework.spec.js +72 -0
- package/cli/generator-templates/archetypes/collection-archetypes.js +9 -9
- package/cli/generator-templates/archetypes/index.js +34 -4
- package/cli/generator-templates/archetypes/input-archetypes.js +2 -2
- package/cli/generator-templates/archetypes/layout-archetypes.js +2 -2
- package/cli/generator-templates/archetypes/minimal-archetypes.js +131 -0
- package/cli/generator-templates/archetypes/status-archetypes.js +4 -4
- package/cli/generator-templates/archetypes/vector-matcher.js +50 -0
- package/cli/generator-templates/archetypes/vector-matcher.spec.js +25 -0
- package/cli/generator-templates/compact.js +130 -0
- package/cli/generator-templates/controller-converters.js +93 -0
- package/cli/generator-templates/controller.js +2 -64
- package/cli/generator-templates/react.js +1 -1
- package/cli/generator-templates/svelte.js +1 -1
- package/cli/generator-templates/types.js +2 -2
- package/cli/generator-templates/vue.js +1 -1
- package/cli/generator-templates.js +1 -0
- package/cli/generator-templates.spec.js +6 -1
- package/cli/generator.js +94 -7
- package/cli/help.js +1 -1
- package/cli/index.js +23 -8
- package/cli/installer-templates.js +1 -1
- package/cli/mcp/antigravity.js +5 -2
- package/cli/mcp/installer.js +7 -7
- package/cli/mcp/installer.spec.js +3 -3
- package/cli/mcp/manifests.js +4 -2
- package/cli/mcp/prompts.js +1 -1
- package/cli/mcp/server.js +2 -0
- package/cli/mcp/tools-audit.js +97 -0
- package/cli/mcp/tools-generate.js +62 -0
- package/cli/mcp/tools-patch.js +72 -0
- package/cli/mcp/tools-patterns.js +71 -0
- package/cli/mcp/tools-project.js +75 -0
- package/cli/mcp/tools-project.spec.js +51 -0
- package/cli/mcp/tools-q-modes.js +69 -0
- package/cli/mcp/tools-q.js +58 -0
- package/cli/mcp/tools-read.js +69 -0
- package/cli/mcp/tools-search-util.js +35 -0
- package/cli/mcp/tools-search.js +15 -294
- package/cli/mcp/tools-team-feed.js +57 -0
- package/cli/mcp/tools-team-locks.js +63 -0
- package/cli/mcp/tools-team-mailbox.js +32 -0
- package/cli/mcp/tools-team-tasks.js +85 -0
- package/cli/mcp/tools-team.js +37 -183
- package/cli/mcp/tools-verify.js +47 -0
- package/cli/mcp/tools-write.js +33 -0
- package/cli/mcp/tools.js +79 -372
- package/cli/mutators.js +10 -23
- package/cli/pillars-schema.js +3 -3
- package/cli/project-detector.js +47 -0
- package/cli/reader-shorthand.spec.js +60 -0
- package/cli/reader.js +67 -24
- package/cli/search-bulletproof.spec.js +104 -0
- package/cli/search-db.js +33 -5
- package/cli/search-queries-hotspot-graph.js +65 -0
- package/cli/search-queries-hotspot-graph.spec.js +66 -0
- package/cli/search-queries-similar.js +84 -0
- package/cli/search-queries-similar.spec.js +43 -0
- package/cli/search-schema.js +79 -11
- package/cli/search.js +13 -2
- package/cli/team/index.js +9 -0
- package/cli/team/team-commands.js +106 -64
- package/cli/team/team-concurrency-stress.spec.js +95 -0
- package/cli/team/team-db-lock-promotion.js +15 -11
- package/cli/team/team-db-mailbox.js +86 -0
- package/cli/team/team-db-task-helpers.js +80 -28
- package/cli/team/team-db-tasks.js +13 -5
- package/cli/team/team-flags.js +72 -0
- package/cli/team/team-format.js +87 -46
- package/cli/team/team-mailbox-empirical.spec.js +191 -0
- package/cli/team/team-mailbox.spec.js +93 -0
- package/cli/team/team-memory.js +84 -0
- package/cli/team/team-memory.spec.js +74 -0
- package/cli/team/team-projects-coordinator.js +100 -0
- package/cli/team/team-projects-format.js +45 -0
- package/cli/team/team-projects-memory.js +70 -0
- package/cli/team/team-projects.js +94 -0
- package/cli/team/team-projects.spec.js +116 -0
- package/cli/team/team-schema-memory.js +25 -0
- package/cli/team/team-schema-projects.js +50 -0
- package/cli/team/team-schema.js +33 -14
- package/cli/team/team-tasks-hierarchy.spec.js +176 -0
- package/cli/team/team-tokens.js +95 -0
- package/cli/team/team-triage.js +274 -52
- package/cli/team/team-verification-gate.spec.js +253 -0
- package/cli/team/team.spec.js +1 -1
- package/cli/tesseract-hud.js +59 -0
- package/cli/tesseract-manifesto.js +61 -0
- package/cli/tesseract-state.js +58 -0
- package/cli/tesseract.js +81 -0
- package/cli/tesseract.spec.js +55 -0
- package/cli/trend.js +84 -0
- package/cli/trend.spec.js +51 -0
- package/cli/ui-actions-tasks.js +13 -5
- package/cli/ui-actions.js +10 -2
- package/cli/ui-client-api.js +2 -2
- package/cli/ui-client-script.js +12 -15
- package/cli/ui-e2e-verification.spec.js +13 -3
- package/cli/ui-filetree.spec.js +5 -5
- package/cli/ui-handlers.js +10 -2
- package/cli/ui-html.js +26 -34
- package/cli/ui-index.html +905 -0
- package/cli/ui-kanban.spec.js +12 -12
- package/cli/ui-server.js +19 -3
- package/cli/ui-sse.js +57 -0
- package/cli/ui-sse.spec.js +74 -0
- package/cli/ui-styles.js +195 -81
- package/cli/ui-template-kanban.js +15 -1
- package/cli/ui-vbulletin.spec.js +12 -13
- package/cli/ui.spec.js +12 -5
- package/cli/verify.js +19 -8
- package/package.json +3 -1
- package/scripts/check-framework-generation.mjs +195 -0
- package/scripts/pre-commit.sh +2 -7
- package/scripts/publish-both.mjs +4 -0
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
isZeroDelayTimeout,
|
|
8
8
|
isUnguardedConsoleCall
|
|
9
9
|
} from './rules-predicates.js';
|
|
10
|
+
import { validateHookReturnShape } from './hook-shape-validator.js';
|
|
10
11
|
|
|
11
|
-
export const createAstVisitors = ({ relativePath, violations }) => {
|
|
12
|
+
export const createAstVisitors = ({ relativePath, violations, hookRegistry }) => {
|
|
12
13
|
return {
|
|
13
14
|
Function(astPath) {
|
|
14
15
|
const isCustomHook = isCustomHookFunction(astPath);
|
|
@@ -45,22 +46,14 @@ export const createAstVisitors = ({ relativePath, violations }) => {
|
|
|
45
46
|
if (isCustomHook) {
|
|
46
47
|
astPath.traverse({
|
|
47
48
|
ReturnStatement(retPath) {
|
|
48
|
-
if (retPath.getFunctionParent() === astPath
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
violations
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
column: retPath.node.loc?.start.column || 1,
|
|
57
|
-
hazard: `Hook return saturation (${propCount} properties > 5 limit)`,
|
|
58
|
-
rule: 'HOOK_RETURN_OVERLOAD',
|
|
59
|
-
severity: meta.severity,
|
|
60
|
-
pillar: meta.pillar,
|
|
61
|
-
directive: meta.directive
|
|
62
|
-
});
|
|
63
|
-
}
|
|
49
|
+
if (retPath.getFunctionParent() === astPath) {
|
|
50
|
+
validateHookReturnShape({
|
|
51
|
+
retPath,
|
|
52
|
+
astPath,
|
|
53
|
+
relativePath,
|
|
54
|
+
violations,
|
|
55
|
+
hookRegistry
|
|
56
|
+
});
|
|
64
57
|
}
|
|
65
58
|
}
|
|
66
59
|
});
|
package/cli/audit/autofix.js
CHANGED
|
@@ -63,7 +63,9 @@ export const autofixContent = (content, options = {}) => {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// 3. AI lazy placeholder comment removal
|
|
66
|
-
|
|
66
|
+
const hasLazyTruncation = TRUNCATION_REGEX.test(line);
|
|
67
|
+
const isLazyTruncationEligible = !shouldDropLine && shouldFix('AI_SLOP_LAZY_PLACEHOLDER');
|
|
68
|
+
if (isLazyTruncationEligible && hasLazyTruncation) {
|
|
67
69
|
fixes.push({
|
|
68
70
|
line: lineNumber,
|
|
69
71
|
rule: 'AI_SLOP_LAZY_PLACEHOLDER',
|
|
@@ -72,7 +74,9 @@ export const autofixContent = (content, options = {}) => {
|
|
|
72
74
|
shouldDropLine = true;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
const hasEmDash = line.includes('\u2014');
|
|
78
|
+
const isEmDashEligible = !shouldDropLine && shouldFix('TYPOGRAPHY_EM_DASH');
|
|
79
|
+
if (isEmDashEligible && hasEmDash) {
|
|
76
80
|
line = line.replace(/\u2014/g, '-');
|
|
77
81
|
fixes.push({
|
|
78
82
|
line: lineNumber,
|
|
@@ -81,6 +85,17 @@ export const autofixContent = (content, options = {}) => {
|
|
|
81
85
|
});
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
const hasTimeoutMacro = /setTimeout\([^,]+,\s*0\)/.test(line);
|
|
89
|
+
const isTimeoutEligible = !shouldDropLine && shouldFix('MACRO_TASK_OVER_MICRO_TASK');
|
|
90
|
+
if (isTimeoutEligible && hasTimeoutMacro) {
|
|
91
|
+
line = line.replace(/setTimeout\(([^,]+),\s*0\)/g, 'queueMicrotask($1)');
|
|
92
|
+
fixes.push({
|
|
93
|
+
line: lineNumber,
|
|
94
|
+
rule: 'MACRO_TASK_OVER_MICRO_TASK',
|
|
95
|
+
action: 'Replaced setTimeout(fn, 0) with queueMicrotask(fn)'
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
if (!shouldDropLine) {
|
|
85
100
|
resultLines.push(line);
|
|
86
101
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { deserializeVector, cosineSimilarity } from '../embeddings/vectorizer.js';
|
|
3
|
+
|
|
4
|
+
const isSpecOrTest = (filePath) => {
|
|
5
|
+
if (!filePath || typeof filePath !== 'string') return false;
|
|
6
|
+
return filePath.includes('.spec.') || filePath.includes('.test.') || filePath.includes('__tests__');
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const detectSemanticClones = (db, options = {}) => {
|
|
10
|
+
const isValidDb = Boolean(db);
|
|
11
|
+
if (!isValidDb) return { count: 0, threshold: 0.85, pairs: [] };
|
|
12
|
+
|
|
13
|
+
const threshold = typeof options.threshold === 'number' ? options.threshold : 0.85;
|
|
14
|
+
const limit = options.limit || 50;
|
|
15
|
+
|
|
16
|
+
const rows = db.prepare(
|
|
17
|
+
"SELECT file_path, target_name, vector FROM embeddings WHERE target_type = 'file'"
|
|
18
|
+
).all();
|
|
19
|
+
|
|
20
|
+
const entries = rows
|
|
21
|
+
.filter((r) => !isSpecOrTest(r.file_path))
|
|
22
|
+
.map((r) => ({
|
|
23
|
+
filePath: r.file_path,
|
|
24
|
+
name: r.target_name,
|
|
25
|
+
vec: deserializeVector(r.vector)
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const pairs = [];
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < entries.length; i++) {
|
|
31
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
32
|
+
const a = entries[i];
|
|
33
|
+
const b = entries[j];
|
|
34
|
+
|
|
35
|
+
const isSameFile = a.filePath === b.filePath;
|
|
36
|
+
if (isSameFile) continue;
|
|
37
|
+
|
|
38
|
+
const sim = cosineSimilarity(a.vec, b.vec);
|
|
39
|
+
const isClone = sim >= threshold;
|
|
40
|
+
|
|
41
|
+
if (isClone) {
|
|
42
|
+
pairs.push({
|
|
43
|
+
fileA: a.filePath,
|
|
44
|
+
fileB: b.filePath,
|
|
45
|
+
nameA: a.name,
|
|
46
|
+
nameB: b.name,
|
|
47
|
+
similarity: Number(sim.toFixed(4)),
|
|
48
|
+
isExact: sim >= 0.95
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pairs.sort((p1, p2) => p2.similarity - p1.similarity);
|
|
55
|
+
const cappedPairs = pairs.slice(0, limit);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
count: pairs.length,
|
|
59
|
+
threshold,
|
|
60
|
+
exactClones: cappedPairs.filter((p) => p.isExact),
|
|
61
|
+
nearClones: cappedPairs.filter((p) => !p.isExact),
|
|
62
|
+
pairs: cappedPairs
|
|
63
|
+
};
|
|
64
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { generateEmbedding, serializeVector } from '../embeddings/vectorizer.js';
|
|
5
|
+
import { detectSemanticClones } from './clone-detector.js';
|
|
6
|
+
import { formatCloneReport } from './reporter-clones.js';
|
|
7
|
+
|
|
8
|
+
test('clone-detector: detects high-similarity clones while ignoring spec companion files', () => {
|
|
9
|
+
const db = new DatabaseSync(':memory:');
|
|
10
|
+
db.exec(`
|
|
11
|
+
CREATE TABLE embeddings (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
+
file_path TEXT NOT NULL,
|
|
14
|
+
target_type TEXT NOT NULL,
|
|
15
|
+
target_name TEXT NOT NULL,
|
|
16
|
+
vector BLOB NOT NULL
|
|
17
|
+
);
|
|
18
|
+
`);
|
|
19
|
+
|
|
20
|
+
const insert = db.prepare('INSERT INTO embeddings (file_path, target_type, target_name, vector) VALUES (?, ?, ?, ?)');
|
|
21
|
+
const vecA = serializeVector(generateEmbedding('modal window popup backdrop blur close cancel confirm'));
|
|
22
|
+
const vecB = serializeVector(generateEmbedding('modal dialog popup backdrop blur cancel confirm close'));
|
|
23
|
+
const vecTest = serializeVector(generateEmbedding('modal window popup backdrop blur close cancel confirm spec test'));
|
|
24
|
+
const vecDiff = serializeVector(generateEmbedding('database sqlite migration column index'));
|
|
25
|
+
|
|
26
|
+
insert.run('src/ui/m-modal-a/m-modal-a.vue', 'file', 'm-modal-a.vue', vecA);
|
|
27
|
+
insert.run('src/ui/m-modal-b/m-modal-b.vue', 'file', 'm-modal-b.vue', vecB);
|
|
28
|
+
insert.run('src/ui/m-modal-a/m-modal-a.spec.ts', 'file', 'm-modal-a.spec.ts', vecTest);
|
|
29
|
+
insert.run('src/db/storage.ts', 'file', 'storage.ts', vecDiff);
|
|
30
|
+
|
|
31
|
+
const res = detectSemanticClones(db, { threshold: 0.8 });
|
|
32
|
+
assert.equal(res.count, 1);
|
|
33
|
+
assert.equal(res.pairs[0].fileA, 'src/ui/m-modal-a/m-modal-a.vue');
|
|
34
|
+
assert.equal(res.pairs[0].fileB, 'src/ui/m-modal-b/m-modal-b.vue');
|
|
35
|
+
assert.ok(res.pairs[0].similarity > 0.85);
|
|
36
|
+
|
|
37
|
+
const report = formatCloneReport(res);
|
|
38
|
+
assert.match(report, /Semantic Clone Detection/);
|
|
39
|
+
assert.match(report, /m-modal-a\.vue <-> src\/ui\/m-modal-b\/m-modal-b\.vue/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('clone-detector: handles empty or invalid db gracefully', () => {
|
|
43
|
+
assert.equal(detectSemanticClones(null).count, 0);
|
|
44
|
+
const emptyReport = formatCloneReport({ count: 0, threshold: 0.85, pairs: [] });
|
|
45
|
+
assert.match(emptyReport, /Zero duplicate clone hazards detected/);
|
|
46
|
+
});
|
package/cli/audit/history.js
CHANGED
|
@@ -13,8 +13,12 @@ const RED = '\x1b[31m';
|
|
|
13
13
|
|
|
14
14
|
export const ensureChemxDir = (cwd = process.cwd()) => {
|
|
15
15
|
const dir = path.resolve(cwd, '.chemx');
|
|
16
|
-
|
|
17
|
-
fs.
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.existsSync(dir)) {
|
|
18
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
return dir;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
// Ensure .chemx is gitignored if git repo exists
|
|
@@ -165,7 +169,11 @@ export const getAuditBaseline = (cwd = process.cwd()) => {
|
|
|
165
169
|
export const setAuditBaseline = (snapshot, cwd = process.cwd()) => {
|
|
166
170
|
ensureChemxDir(cwd);
|
|
167
171
|
const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
|
|
168
|
-
|
|
172
|
+
try {
|
|
173
|
+
fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
174
|
+
} catch {
|
|
175
|
+
// Read-only filesystem in sandbox
|
|
176
|
+
}
|
|
169
177
|
return snapshot;
|
|
170
178
|
};
|
|
171
179
|
|
|
@@ -196,7 +204,11 @@ export const saveAuditSnapshot = (report, cwd = process.cwd()) => {
|
|
|
196
204
|
// Keep last 50 snapshots
|
|
197
205
|
const trimmed = history.slice(-50);
|
|
198
206
|
const historyPath = path.resolve(cwd, '.chemx', 'history.json');
|
|
199
|
-
|
|
207
|
+
try {
|
|
208
|
+
fs.writeFileSync(historyPath, JSON.stringify(trimmed, null, 2), 'utf-8');
|
|
209
|
+
} catch {
|
|
210
|
+
// Read-only filesystem in sandbox
|
|
211
|
+
}
|
|
200
212
|
|
|
201
213
|
// Auto-establish first run or lower score as baseline floor
|
|
202
214
|
const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
|
|
@@ -206,8 +218,12 @@ export const saveAuditSnapshot = (report, cwd = process.cwd()) => {
|
|
|
206
218
|
const baselineScore = currentBaseline?.health?.score ?? 101;
|
|
207
219
|
|
|
208
220
|
if (!currentBaseline || currentScore < baselineScore) {
|
|
209
|
-
|
|
210
|
-
|
|
221
|
+
try {
|
|
222
|
+
fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
|
|
223
|
+
isNewBaseline = true;
|
|
224
|
+
} catch {
|
|
225
|
+
// Read-only filesystem in sandbox
|
|
226
|
+
}
|
|
211
227
|
}
|
|
212
228
|
|
|
213
229
|
const baseline = getAuditBaseline(cwd);
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { RULE_REGISTRY } from './rules-registry.js';
|
|
3
|
+
|
|
4
|
+
const VERB_PREFIX_REGEX = /^(add|remove|toggle|set|get|fetch|load|save|delete|clear|reset|refresh|update|create|edit|send|sync|init|setup|dispose|trigger|post|put|patch|apply|dismiss|select|deselect|filter|sort|search|open|close|show|hide|start|stop|cancel|execute|run|retry|submit|validate|mutate|next|prev|step|increment|decrement|insert|upsert|drop|enable|disable|register|unregister|login|logout|checkout|publish|archive|discard|confirm|undo|redo|claim|complete|acquire|release|download|upload|export|import|handle|play|pause|resume|seek|unlock|restore|advance|focus|notify|broadcast|copy|format)([A-Z0-9_].*)?$/;
|
|
5
|
+
|
|
6
|
+
const STATUS_HEURISTIC_REGEX = /^(is[A-Z]|has[A-Z]|can[A-Z]|should[A-Z])|.*(Error|Status)$/;
|
|
7
|
+
const STATUS_EXACT_NAMES = new Set(['status', 'error', 'loading', 'pending', 'ready']);
|
|
8
|
+
const ACTION_WRAPPER_KEYS = new Set(['actions', 'handlers', 'methods', 'operations']);
|
|
9
|
+
|
|
10
|
+
const RAW_DOM_REF_REGEX = /^(dom|element|container|input|button|target|root|node)?Ref$|.*(Element|Node|Dom)Ref$/i;
|
|
11
|
+
const INTERMEDIATE_VAL_REGEX = /^(temp|draft|raw|partial|interim|scratch|memoized)([A-Z_].*)?$/i;
|
|
12
|
+
const UNGROUPED_CALLBACK_REGEX = /^(on[A-Z]|callback$|.*Callback$)/;
|
|
13
|
+
|
|
14
|
+
const MUTUALLY_EXCLUSIVE_STATUSES = new Set([
|
|
15
|
+
'isloading', 'issuccess', 'iserror', 'isidle', 'isloaded', 'isfetching', 'ispending',
|
|
16
|
+
'loading', 'success', 'idle'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export const SEMANTIC_STATUS_CLUSTERS = {
|
|
20
|
+
LOADING: {
|
|
21
|
+
canonical: 'isLoading',
|
|
22
|
+
variants: new Set(['isloading', 'loading', 'ispending', 'pending', 'isfetching', 'fetching'])
|
|
23
|
+
},
|
|
24
|
+
ERROR: {
|
|
25
|
+
canonical: 'error',
|
|
26
|
+
variants: new Set(['error', 'iserror', 'haserror', 'err', 'errormessage'])
|
|
27
|
+
},
|
|
28
|
+
READY: {
|
|
29
|
+
canonical: 'isReady',
|
|
30
|
+
variants: new Set(['isready', 'ready', 'isinitialized', 'initialized'])
|
|
31
|
+
},
|
|
32
|
+
STATUS: {
|
|
33
|
+
canonical: 'status',
|
|
34
|
+
variants: new Set(['status', 'state', 'phase', 'stage'])
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const resolvePropKey = (prop) => {
|
|
39
|
+
if (t.isIdentifier(prop.key)) return prop.key.name;
|
|
40
|
+
if (t.isStringLiteral(prop.key)) return prop.key.value;
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isFunctionNode = (node, scope) => {
|
|
45
|
+
const isDirectFunction = t.isArrowFunctionExpression(node) || t.isFunctionExpression(node);
|
|
46
|
+
if (isDirectFunction) return true;
|
|
47
|
+
|
|
48
|
+
const isIdentifierNode = t.isIdentifier(node);
|
|
49
|
+
const canInspectScope = isIdentifierNode && Boolean(scope);
|
|
50
|
+
if (!canInspectScope) return false;
|
|
51
|
+
|
|
52
|
+
const binding = scope.getBinding(node.name);
|
|
53
|
+
if (!binding) return false;
|
|
54
|
+
|
|
55
|
+
if (binding.path.isFunctionDeclaration()) return true;
|
|
56
|
+
|
|
57
|
+
if (binding.path.isVariableDeclarator()) {
|
|
58
|
+
const init = binding.path.node.init;
|
|
59
|
+
const isInitFunction = t.isArrowFunctionExpression(init) || t.isFunctionExpression(init);
|
|
60
|
+
if (isInitFunction) return true;
|
|
61
|
+
const isCallbackHook = t.isCallExpression(init) && t.isIdentifier(init.callee, { name: 'useCallback' });
|
|
62
|
+
if (isCallbackHook) return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const hasNestedActions = (objectExpr, scope) => {
|
|
69
|
+
if (!t.isObjectExpression(objectExpr)) return false;
|
|
70
|
+
return objectExpr.properties.some((p) => {
|
|
71
|
+
if (t.isObjectMethod(p)) return true;
|
|
72
|
+
if (t.isObjectProperty(p)) {
|
|
73
|
+
const name = resolvePropKey(p);
|
|
74
|
+
const isVerbNamed = Boolean(name && VERB_PREFIX_REGEX.test(name));
|
|
75
|
+
if (isVerbNamed) return true;
|
|
76
|
+
if (isFunctionNode(p.value, scope)) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const createHookShapeRegistry = () => {
|
|
83
|
+
const entries = [];
|
|
84
|
+
|
|
85
|
+
const recordHook = (hookInfo) => {
|
|
86
|
+
entries.push(hookInfo);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const validateCrossHookConsistency = () => {
|
|
90
|
+
const consistencyViolations = [];
|
|
91
|
+
if (entries.length < 2) return consistencyViolations;
|
|
92
|
+
|
|
93
|
+
for (const [clusterKey, cluster] of Object.entries(SEMANTIC_STATUS_CLUSTERS)) {
|
|
94
|
+
const occurrences = [];
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
for (const prop of entry.statusProps) {
|
|
97
|
+
const lower = prop.name.toLowerCase();
|
|
98
|
+
if (cluster.variants.has(lower)) {
|
|
99
|
+
occurrences.push({ ...prop, hookName: entry.hookName, filePath: entry.filePath });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const uniqueNames = Array.from(new Set(occurrences.map((o) => o.name)));
|
|
105
|
+
if (uniqueNames.length > 1) {
|
|
106
|
+
for (const occ of occurrences) {
|
|
107
|
+
if (occ.name !== cluster.canonical) {
|
|
108
|
+
const meta = RULE_REGISTRY.HOOK_SHAPE_CONTRACT;
|
|
109
|
+
consistencyViolations.push({
|
|
110
|
+
filePath: occ.filePath,
|
|
111
|
+
line: occ.line,
|
|
112
|
+
column: occ.column,
|
|
113
|
+
hazard: `Inconsistent ${clusterKey.toLowerCase()} status property "${occ.name}". Rename to "${cluster.canonical}" to match hook shape conventions.`,
|
|
114
|
+
rule: 'HOOK_SHAPE_CONTRACT',
|
|
115
|
+
severity: meta.severity,
|
|
116
|
+
pillar: meta.pillar,
|
|
117
|
+
directive: meta.directive
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return consistencyViolations;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return { recordHook, validateCrossHookConsistency, entries };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const validateHookReturnShape = ({ retPath, astPath, relativePath, violations, hookRegistry }) => {
|
|
131
|
+
if (!t.isObjectExpression(retPath.node.argument)) return;
|
|
132
|
+
|
|
133
|
+
const properties = retPath.node.argument.properties;
|
|
134
|
+
const propCount = properties.length;
|
|
135
|
+
const hookName = astPath.node.id?.name || astPath.parentPath?.node?.id?.name || 'anonymousHook';
|
|
136
|
+
const meta = RULE_REGISTRY.HOOK_SHAPE_CONTRACT;
|
|
137
|
+
const overloadMeta = RULE_REGISTRY.HOOK_RETURN_OVERLOAD;
|
|
138
|
+
|
|
139
|
+
if (propCount > 5) {
|
|
140
|
+
violations.push({
|
|
141
|
+
filePath: relativePath,
|
|
142
|
+
line: retPath.node.loc?.start.line || 1,
|
|
143
|
+
column: retPath.node.loc?.start.column || 1,
|
|
144
|
+
hazard: `Hook return saturation (${propCount} properties > 5 limit; deprecated rule: migrate to HOOK_SHAPE_CONTRACT)`,
|
|
145
|
+
rule: 'HOOK_RETURN_OVERLOAD',
|
|
146
|
+
severity: overloadMeta.severity,
|
|
147
|
+
pillar: overloadMeta.pillar,
|
|
148
|
+
directive: overloadMeta.directive
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const statusProps = [];
|
|
153
|
+
const mutualStatuses = [];
|
|
154
|
+
|
|
155
|
+
for (const prop of properties) {
|
|
156
|
+
if (t.isSpreadElement(prop)) continue;
|
|
157
|
+
|
|
158
|
+
const keyName = resolvePropKey(prop);
|
|
159
|
+
if (!keyName) continue;
|
|
160
|
+
|
|
161
|
+
const line = prop.loc?.start.line || retPath.node.loc?.start.line || 1;
|
|
162
|
+
const column = prop.loc?.start.column || retPath.node.loc?.start.column || 1;
|
|
163
|
+
|
|
164
|
+
const isActionWrapperKey = ACTION_WRAPPER_KEYS.has(keyName);
|
|
165
|
+
if (isActionWrapperKey) {
|
|
166
|
+
violations.push({
|
|
167
|
+
filePath: relativePath,
|
|
168
|
+
line,
|
|
169
|
+
column,
|
|
170
|
+
hazard: `Nested action wrapper detected in hook return ("${keyName}"). Actions must be flat at top level per HOOK_SHAPE_CONTRACT.`,
|
|
171
|
+
rule: 'HOOK_SHAPE_CONTRACT',
|
|
172
|
+
severity: meta.severity,
|
|
173
|
+
pillar: meta.pillar,
|
|
174
|
+
directive: meta.directive
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const isPropObject = t.isObjectProperty(prop) && hasNestedActions(prop.value, astPath.scope);
|
|
180
|
+
if (isPropObject) {
|
|
181
|
+
violations.push({
|
|
182
|
+
filePath: relativePath,
|
|
183
|
+
line,
|
|
184
|
+
column,
|
|
185
|
+
hazard: `Action wrapper detected in property "${keyName}". Actions must be flat at top level per HOOK_SHAPE_CONTRACT.`,
|
|
186
|
+
rule: 'HOOK_SHAPE_CONTRACT',
|
|
187
|
+
severity: meta.severity,
|
|
188
|
+
pillar: meta.pillar,
|
|
189
|
+
directive: meta.directive
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const isMethod = t.isObjectMethod(prop);
|
|
195
|
+
const isPropFunc = t.isObjectProperty(prop) && isFunctionNode(prop.value, astPath.scope);
|
|
196
|
+
const isFunction = isMethod || isPropFunc;
|
|
197
|
+
|
|
198
|
+
if (isFunction) {
|
|
199
|
+
const isStatusPredicate = STATUS_HEURISTIC_REGEX.test(keyName);
|
|
200
|
+
if (isStatusPredicate) {
|
|
201
|
+
statusProps.push({ name: keyName, line, column });
|
|
202
|
+
if (MUTUALLY_EXCLUSIVE_STATUSES.has(keyName.toLowerCase())) {
|
|
203
|
+
mutualStatuses.push(keyName);
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const isUngrouped = UNGROUPED_CALLBACK_REGEX.test(keyName);
|
|
209
|
+
if (isUngrouped) {
|
|
210
|
+
violations.push({
|
|
211
|
+
filePath: relativePath,
|
|
212
|
+
line,
|
|
213
|
+
column,
|
|
214
|
+
hazard: `Property "${keyName}" is an invalid hook action. Actions must be verb-prefixed (e.g. addItem, toggleItem) rather than event handlers or callbacks.`,
|
|
215
|
+
rule: 'HOOK_SHAPE_CONTRACT',
|
|
216
|
+
severity: meta.severity,
|
|
217
|
+
pillar: meta.pillar,
|
|
218
|
+
directive: meta.directive
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
const isVerbPrefixed = VERB_PREFIX_REGEX.test(keyName);
|
|
222
|
+
if (!isVerbPrefixed) {
|
|
223
|
+
violations.push({
|
|
224
|
+
filePath: relativePath,
|
|
225
|
+
line,
|
|
226
|
+
column,
|
|
227
|
+
hazard: `Function "${keyName}" in hook return is missing an action verb prefix (e.g. addItem, toggleItem).`,
|
|
228
|
+
rule: 'HOOK_SHAPE_CONTRACT',
|
|
229
|
+
severity: meta.severity,
|
|
230
|
+
pillar: meta.pillar,
|
|
231
|
+
directive: meta.directive
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const isDomRef = RAW_DOM_REF_REGEX.test(keyName) || keyName === 'ref';
|
|
239
|
+
if (isDomRef) {
|
|
240
|
+
violations.push({
|
|
241
|
+
filePath: relativePath,
|
|
242
|
+
line,
|
|
243
|
+
column,
|
|
244
|
+
hazard: `Property "${keyName}" is a raw DOM ref. Hooks must not return raw DOM element refs.`,
|
|
245
|
+
rule: 'HOOK_SHAPE_CONTRACT',
|
|
246
|
+
severity: meta.severity,
|
|
247
|
+
pillar: meta.pillar,
|
|
248
|
+
directive: meta.directive
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const isIntermediate = INTERMEDIATE_VAL_REGEX.test(keyName);
|
|
254
|
+
if (isIntermediate) {
|
|
255
|
+
violations.push({
|
|
256
|
+
filePath: relativePath,
|
|
257
|
+
line,
|
|
258
|
+
column,
|
|
259
|
+
hazard: `Property "${keyName}" is an intermediate or temporary value. Hooks must return finished domain state.`,
|
|
260
|
+
rule: 'HOOK_SHAPE_CONTRACT',
|
|
261
|
+
severity: meta.severity,
|
|
262
|
+
pillar: meta.pillar,
|
|
263
|
+
directive: meta.directive
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const isCallback = UNGROUPED_CALLBACK_REGEX.test(keyName);
|
|
269
|
+
if (isCallback) {
|
|
270
|
+
violations.push({
|
|
271
|
+
filePath: relativePath,
|
|
272
|
+
line,
|
|
273
|
+
column,
|
|
274
|
+
hazard: `Property "${keyName}" is an ungrouped callback or handler. Actions must be verb-prefixed (e.g. addItem).`,
|
|
275
|
+
rule: 'HOOK_SHAPE_CONTRACT',
|
|
276
|
+
severity: meta.severity,
|
|
277
|
+
pillar: meta.pillar,
|
|
278
|
+
directive: meta.directive
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const isStatus = STATUS_HEURISTIC_REGEX.test(keyName) || STATUS_EXACT_NAMES.has(keyName.toLowerCase());
|
|
284
|
+
if (isStatus) {
|
|
285
|
+
statusProps.push({ name: keyName, line, column });
|
|
286
|
+
if (MUTUALLY_EXCLUSIVE_STATUSES.has(keyName.toLowerCase())) {
|
|
287
|
+
mutualStatuses.push(keyName);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (mutualStatuses.length >= 3) {
|
|
293
|
+
violations.push({
|
|
294
|
+
filePath: relativePath,
|
|
295
|
+
line: retPath.node.loc?.start.line || 1,
|
|
296
|
+
column: retPath.node.loc?.start.column || 1,
|
|
297
|
+
hazard: `Mutually exclusive lifecycle states returned as multiple independent booleans (${mutualStatuses.join(', ')}). Prefer a single discriminated status field per Directive 2.B.`,
|
|
298
|
+
rule: 'HOOK_SHAPE_CONTRACT',
|
|
299
|
+
severity: meta.severity,
|
|
300
|
+
pillar: meta.pillar,
|
|
301
|
+
directive: meta.directive
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (hookRegistry) {
|
|
306
|
+
hookRegistry.recordHook({
|
|
307
|
+
hookName,
|
|
308
|
+
filePath: relativePath,
|
|
309
|
+
statusProps
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
};
|
package/cli/audit/prompts.js
CHANGED
|
@@ -36,7 +36,7 @@ const renderTreeLines = (node, depth = 0) => {
|
|
|
36
36
|
return lines;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
export const formatGroupedPromptViolations = (violations = []) => {
|
|
39
|
+
export const formatGroupedPromptViolations = (violations = [], options = {}) => {
|
|
40
40
|
const hasNoViolations = violations.length === 0;
|
|
41
41
|
if (hasNoViolations) return [];
|
|
42
42
|
|
|
@@ -49,11 +49,14 @@ export const formatGroupedPromptViolations = (violations = []) => {
|
|
|
49
49
|
lines.push(`${idx + 1}. ${sevColor}[${rg.rule}]${RESET} ${BOLD}(${countLabel})${RESET}`);
|
|
50
50
|
lines.push(` Hazard: ${rg.hazard}`);
|
|
51
51
|
lines.push(` Directive: ${CYAN}${rg.directive}${RESET}`);
|
|
52
|
-
lines.push(
|
|
52
|
+
lines.push(` Action: chemx team task list --rule=${rg.rule}`);
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
if (options.detailedLocations) {
|
|
55
|
+
lines.push(' Locations:');
|
|
56
|
+
const tree = buildPathTree(rg.violations);
|
|
57
|
+
const treeLines = renderTreeLines(tree, 0);
|
|
58
|
+
lines.push(...treeLines);
|
|
59
|
+
}
|
|
57
60
|
lines.push('');
|
|
58
61
|
});
|
|
59
62
|
|
|
@@ -62,10 +65,10 @@ export const formatGroupedPromptViolations = (violations = []) => {
|
|
|
62
65
|
|
|
63
66
|
export const buildAgentCommandsSection = () => [
|
|
64
67
|
'### AI AGENT DISCOVERY & REFACTORING COMMANDS:',
|
|
65
|
-
'-
|
|
66
|
-
'-
|
|
67
|
-
'-
|
|
68
|
-
'-
|
|
68
|
+
'- Claim Work: Run `chemx team task list --status=queued` and claim via `chemx team task claim <id> --as=@coder`.',
|
|
69
|
+
'- Inspect Target: Run `chemx read <target_path> --outline` to inspect AST signatures without token bloat.',
|
|
70
|
+
'- Verify & Auto-Resolve: Run `chemx verify`. Clean files auto-reconcile tasks to done in SQLite.',
|
|
71
|
+
'- MCP Invocations: If invoking via MCP, pass command string directly via `chemx({ command: "..." })`.'
|
|
69
72
|
].join('\n');
|
|
70
73
|
|
|
71
74
|
export const buildGradeFPrompt = (report, options = {}) => {
|
|
@@ -296,7 +299,7 @@ export const buildHotspotsPrompt = (report, options = {}) => {
|
|
|
296
299
|
lines.push('1. Pre-Split Pattern Discovery: Audit recurring UI layouts, state machines, and predicates across monoliths first; extract canonical shared capsules before slicing to prevent bespoke pattern duplication.');
|
|
297
300
|
lines.push('2. Molecular Limits: Molecule capsules must strictly remain under 100 lines per file.');
|
|
298
301
|
lines.push('3. Table-of-Contents Views: Top-level page views must be 10 to 20 line declarative Table of Contents assembling molecules via named slots.');
|
|
299
|
-
lines.push('4. Composable Return Contracts: Custom hooks/composables must
|
|
302
|
+
lines.push('4. Composable Return Contracts: Custom hooks/composables must classify returns into flat State, Status, and verb-prefixed Actions buckets (Directive 4.A).');
|
|
300
303
|
lines.push('5. Type Co-location: Co-locate granular types/*.d.ts files inside each feature capsule (< 100 lines per type file) instead of creating type monoliths.');
|
|
301
304
|
lines.push('6. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
302
305
|
if (!isSubSection) {
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
buildAiSlopPrompt,
|
|
11
11
|
buildHotspotsPrompt,
|
|
12
12
|
buildPillarPrompt,
|
|
13
|
-
buildMasterPrompt
|
|
13
|
+
buildMasterPrompt,
|
|
14
|
+
formatGroupedPromptViolations
|
|
14
15
|
} from './prompts.js';
|
|
15
16
|
|
|
16
17
|
test('deduplicateRolePreambles: handles invalid and empty inputs', () => {
|
|
@@ -314,3 +315,53 @@ test('deduplicateRefactoringCommands: collapses multiple occurrences to single b
|
|
|
314
315
|
assert.ok(result.includes('Section 2 content'));
|
|
315
316
|
assert.ok(result.includes('- Command B'));
|
|
316
317
|
});
|
|
318
|
+
|
|
319
|
+
test('formatGroupedPromptViolations: outputs token-compact task action by default', () => {
|
|
320
|
+
const violations = [
|
|
321
|
+
{
|
|
322
|
+
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
323
|
+
severity: 'CRITICAL',
|
|
324
|
+
hazard: 'Unsanitized HTML injection',
|
|
325
|
+
directive: 'Sanitize dynamic HTML via DOMPurify',
|
|
326
|
+
filePath: 'src/view-a.vue',
|
|
327
|
+
line: 10
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
331
|
+
severity: 'CRITICAL',
|
|
332
|
+
hazard: 'Unsanitized HTML injection',
|
|
333
|
+
directive: 'Sanitize dynamic HTML via DOMPurify',
|
|
334
|
+
filePath: 'src/view-b.vue',
|
|
335
|
+
line: 25
|
|
336
|
+
}
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
const lines = formatGroupedPromptViolations(violations);
|
|
340
|
+
const text = lines.join('\n');
|
|
341
|
+
|
|
342
|
+
assert.ok(text.includes('[SECURITY_RAW_HTML_INJECTION]'));
|
|
343
|
+
assert.ok(text.includes('(2 items)'));
|
|
344
|
+
assert.ok(text.includes('Action: chemx team task list --rule=SECURITY_RAW_HTML_INJECTION'));
|
|
345
|
+
// Does not dump file locations tree by default
|
|
346
|
+
assert.ok(!text.includes('Locations:'));
|
|
347
|
+
assert.ok(!text.includes('view-a.vue:10'));
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('formatGroupedPromptViolations: renders file tree when detailedLocations is enabled', () => {
|
|
351
|
+
const violations = [
|
|
352
|
+
{
|
|
353
|
+
rule: 'SECURITY_RAW_HTML_INJECTION',
|
|
354
|
+
severity: 'CRITICAL',
|
|
355
|
+
hazard: 'Unsanitized HTML injection',
|
|
356
|
+
directive: 'Sanitize dynamic HTML via DOMPurify',
|
|
357
|
+
filePath: 'src/view-a.vue',
|
|
358
|
+
line: 10
|
|
359
|
+
}
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
const lines = formatGroupedPromptViolations(violations, { detailedLocations: true });
|
|
363
|
+
const text = lines.join('\n');
|
|
364
|
+
|
|
365
|
+
assert.ok(text.includes('Locations:'));
|
|
366
|
+
assert.ok(text.includes('view-a.vue:10'));
|
|
367
|
+
});
|