@chemx/create-chemx 26.9.21-8 → 26.9.23-1269
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/hook-shape-validator.js +312 -0
- package/cli/audit/prompts.js +1 -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 +43 -3
- package/cli/commands/cmd-project.js +100 -0
- package/cli/commands/cmd-router.js +34 -2
- package/cli/commands-schema.js +14 -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 +21 -8
- package/cli/installer-templates.js +1 -1
- package/cli/mcp/antigravity.js +3 -2
- package/cli/mcp/installer.js +7 -7
- package/cli/mcp/installer.spec.js +3 -3
- package/cli/mcp/manifests.js +3 -2
- package/cli/mcp/prompts.js +1 -1
- package/cli/mcp/server.js +2 -0
- package/cli/mcp/tools-audit.js +95 -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 +84 -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 +72 -376
- 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 +30 -10
- package/cli/search.js +9 -2
- package/cli/team/index.js +9 -0
- package/cli/team/team-commands.js +104 -64
- package/cli/team/team-concurrency-stress.spec.js +95 -0
- package/cli/team/team-db-mailbox.js +86 -0
- package/cli/team/team-db-task-helpers.js +30 -28
- package/cli/team/team-db-tasks.js +13 -5
- package/cli/team/team-flags.js +69 -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 +80 -0
- package/cli/team/team-tokens.js +95 -0
- package/cli/team/team-triage.js +159 -12
- package/cli/team/team-verification-gate.spec.js +253 -0
- package/cli/team/team.spec.js +1 -1
- 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 +7 -0
- 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
|
+
});
|
|
@@ -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|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)([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
|
@@ -296,7 +296,7 @@ export const buildHotspotsPrompt = (report, options = {}) => {
|
|
|
296
296
|
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
297
|
lines.push('2. Molecular Limits: Molecule capsules must strictly remain under 100 lines per file.');
|
|
298
298
|
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
|
|
299
|
+
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
300
|
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
301
|
lines.push('6. Zero breaking changes to external component APIs, route exports, or existing props.');
|
|
302
302
|
if (!isSubSection) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ANSI } from '../theme.js';
|
|
2
|
+
|
|
3
|
+
export const formatCloneReport = (cloneResult) => {
|
|
4
|
+
const { count, threshold, exactClones = [], nearClones = [], pairs = [] } = cloneResult;
|
|
5
|
+
const hasPairs = Array.isArray(pairs) && pairs.length > 0;
|
|
6
|
+
if (!hasPairs) {
|
|
7
|
+
return ` ${ANSI.LIME}✔ Zero duplicate clone hazards detected (threshold: ${threshold})${ANSI.RESET}\n`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const lines = [
|
|
11
|
+
` ${ANSI.BOLD}${ANSI.PINK}Semantic Clone Detection (${count} candidate pairs >= ${threshold})${ANSI.RESET}`
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const hasExact = exactClones.length > 0;
|
|
15
|
+
if (hasExact) {
|
|
16
|
+
lines.push(` ${ANSI.RED}${ANSI.BOLD}Hazard: ${exactClones.length} high-similarity duplicate(s) (>= 0.95)${ANSI.RESET}`);
|
|
17
|
+
for (const p of exactClones) {
|
|
18
|
+
const simPct = Math.round(p.similarity * 100);
|
|
19
|
+
lines.push(` ${ANSI.RED}[${simPct}%]${ANSI.RESET} ${p.fileA} <-> ${p.fileB}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const hasNear = nearClones.length > 0;
|
|
24
|
+
if (hasNear) {
|
|
25
|
+
lines.push(` ${ANSI.GOLD}Harmonization Candidates: ${nearClones.length} structural clone(s)${ANSI.RESET}`);
|
|
26
|
+
for (const p of nearClones) {
|
|
27
|
+
const simPct = Math.round(p.similarity * 100);
|
|
28
|
+
lines.push(` ${ANSI.GOLD}[${simPct}%]${ANSI.RESET} ${p.fileA} <-> ${p.fileB}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return lines.join('\n') + '\n';
|
|
33
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ANSI } from '../theme.js';
|
|
2
|
+
|
|
3
|
+
export const formatHotspotGraphReport = (graphResult) => {
|
|
4
|
+
const { count, hotspots = [] } = graphResult;
|
|
5
|
+
const hasHotspots = Array.isArray(hotspots) && hotspots.length > 0;
|
|
6
|
+
if (!hasHotspots) {
|
|
7
|
+
return ` ${ANSI.LIME}✔ Zero cascading violation hotspots detected.${ANSI.RESET}\n`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const lines = [
|
|
11
|
+
` ${ANSI.BOLD}${ANSI.PINK}Cascading Violation Hotspot Graph (${count} files with propagating debt)${ANSI.RESET}`
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
for (let idx = 0; idx < hotspots.length; idx++) {
|
|
15
|
+
const h = hotspots[idx];
|
|
16
|
+
const rank = idx + 1;
|
|
17
|
+
const blast = h.blastRadius;
|
|
18
|
+
const critStr = h.criticalCount > 0 ? `${ANSI.RED}${h.criticalCount} crit${ANSI.RESET}, ` : '';
|
|
19
|
+
const highStr = h.highCount > 0 ? `${ANSI.GOLD}${h.highCount} high${ANSI.RESET}, ` : '';
|
|
20
|
+
const medStr = `${h.medCount} med`;
|
|
21
|
+
|
|
22
|
+
lines.push(
|
|
23
|
+
` ${ANSI.BOLD}#${rank} ${ANSI.CYAN}${h.filePath}${ANSI.RESET} [${h.tier}, ${h.lines} LOC]`
|
|
24
|
+
);
|
|
25
|
+
lines.push(
|
|
26
|
+
` ${ANSI.RED}Cascading Risk: ${h.cascadingRisk} pts${ANSI.RESET} (Violations: ${critStr}${highStr}${medStr})`
|
|
27
|
+
);
|
|
28
|
+
lines.push(
|
|
29
|
+
` ${ANSI.DIM}Blast Radius: ${blast.totalImpact} consumers (${blast.directConsumers} direct, ${blast.impactedTests} tests across ${blast.depth} hops)${ANSI.RESET}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return lines.join('\n') + '\n';
|
|
34
|
+
};
|
|
@@ -117,9 +117,14 @@ export const RULE_REGISTRY = {
|
|
|
117
117
|
directive: 'Extract related state and effects into dedicated domain hooks'
|
|
118
118
|
},
|
|
119
119
|
HOOK_RETURN_OVERLOAD: {
|
|
120
|
+
pillar: PILLARS.PILLAR_3,
|
|
121
|
+
severity: 'MEDIUM',
|
|
122
|
+
directive: 'Limit hook return values to 3 to 5 properties (Deprecated: use HOOK_SHAPE_CONTRACT)'
|
|
123
|
+
},
|
|
124
|
+
HOOK_SHAPE_CONTRACT: {
|
|
120
125
|
pillar: PILLARS.PILLAR_3,
|
|
121
126
|
severity: 'HIGH',
|
|
122
|
-
directive: '
|
|
127
|
+
directive: 'Structure hook returns into flat State, Status, and verb-prefixed Actions buckets'
|
|
123
128
|
},
|
|
124
129
|
CONTROLLER_VIEW_MISMATCH: {
|
|
125
130
|
pillar: PILLARS.PILLAR_3,
|
package/cli/audit/rules.js
CHANGED
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
createExtendedVisitors
|
|
20
20
|
} from './extended-visitors.js';
|
|
21
21
|
import { createPatternVisitors, recordTemplatePatterns } from './pattern-detector.js';
|
|
22
|
+
import { createHookShapeRegistry } from './hook-shape-validator.js';
|
|
22
23
|
|
|
23
|
-
export { PILLARS, RULE_REGISTRY };
|
|
24
|
+
export { PILLARS, RULE_REGISTRY, createHookShapeRegistry };
|
|
24
25
|
|
|
25
26
|
export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
26
27
|
const violations = [];
|
|
@@ -145,7 +146,8 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
const traverseFn = traverse.default || traverse;
|
|
148
|
-
const
|
|
149
|
+
const hookRegistry = options.hookRegistry || createHookShapeRegistry();
|
|
150
|
+
const visitors = createAstVisitors({ relativePath, violations, hookRegistry });
|
|
149
151
|
const slopVisitors = createAiSlopVisitors({ relativePath, violations });
|
|
150
152
|
const extendedVisitors = createExtendedVisitors({ relativePath, violations });
|
|
151
153
|
const patternVisitors = options.patternRegistry
|
|
@@ -169,5 +171,11 @@ export const auditCode = (content, filePath, relativePath, options = {}) => {
|
|
|
169
171
|
|
|
170
172
|
traverseFn(ast, mergedVisitors);
|
|
171
173
|
|
|
174
|
+
const shouldRunLocalConsistency = !options.hookRegistry;
|
|
175
|
+
if (shouldRunLocalConsistency) {
|
|
176
|
+
const consistencyViolations = hookRegistry.validateCrossHookConsistency();
|
|
177
|
+
violations.push(...consistencyViolations);
|
|
178
|
+
}
|
|
179
|
+
|
|
172
180
|
return violations;
|
|
173
181
|
};
|