@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.
Files changed (130) hide show
  1. package/cli/audit/ast-visitors.js +10 -17
  2. package/cli/audit/autofix.js +17 -2
  3. package/cli/audit/clone-detector.js +64 -0
  4. package/cli/audit/clone-detector.spec.js +46 -0
  5. package/cli/audit/hook-shape-validator.js +312 -0
  6. package/cli/audit/prompts.js +1 -1
  7. package/cli/audit/reporter-clones.js +33 -0
  8. package/cli/audit/reporter-hotspot-graph.js +34 -0
  9. package/cli/audit/rules-registry.js +6 -1
  10. package/cli/audit/rules.js +10 -2
  11. package/cli/audit/rules.spec.js +129 -0
  12. package/cli/audit-stages.spec.js +30 -0
  13. package/cli/audit.js +12 -3
  14. package/cli/commands/cmd-audit.js +43 -3
  15. package/cli/commands/cmd-project.js +100 -0
  16. package/cli/commands/cmd-router.js +34 -2
  17. package/cli/commands-schema.js +14 -0
  18. package/cli/create.spec.js +2 -2
  19. package/cli/exploder.js +258 -0
  20. package/cli/exploder.spec.js +99 -0
  21. package/cli/generator-compact.spec.js +104 -0
  22. package/cli/generator-framework.spec.js +72 -0
  23. package/cli/generator-templates/archetypes/collection-archetypes.js +9 -9
  24. package/cli/generator-templates/archetypes/index.js +34 -4
  25. package/cli/generator-templates/archetypes/input-archetypes.js +2 -2
  26. package/cli/generator-templates/archetypes/layout-archetypes.js +2 -2
  27. package/cli/generator-templates/archetypes/minimal-archetypes.js +131 -0
  28. package/cli/generator-templates/archetypes/status-archetypes.js +4 -4
  29. package/cli/generator-templates/archetypes/vector-matcher.js +50 -0
  30. package/cli/generator-templates/archetypes/vector-matcher.spec.js +25 -0
  31. package/cli/generator-templates/compact.js +130 -0
  32. package/cli/generator-templates/controller-converters.js +93 -0
  33. package/cli/generator-templates/controller.js +2 -64
  34. package/cli/generator-templates/react.js +1 -1
  35. package/cli/generator-templates/svelte.js +1 -1
  36. package/cli/generator-templates/types.js +2 -2
  37. package/cli/generator-templates/vue.js +1 -1
  38. package/cli/generator-templates.js +1 -0
  39. package/cli/generator-templates.spec.js +6 -1
  40. package/cli/generator.js +94 -7
  41. package/cli/help.js +1 -1
  42. package/cli/index.js +21 -8
  43. package/cli/installer-templates.js +1 -1
  44. package/cli/mcp/antigravity.js +3 -2
  45. package/cli/mcp/installer.js +7 -7
  46. package/cli/mcp/installer.spec.js +3 -3
  47. package/cli/mcp/manifests.js +3 -2
  48. package/cli/mcp/prompts.js +1 -1
  49. package/cli/mcp/server.js +2 -0
  50. package/cli/mcp/tools-audit.js +95 -0
  51. package/cli/mcp/tools-generate.js +62 -0
  52. package/cli/mcp/tools-patch.js +72 -0
  53. package/cli/mcp/tools-patterns.js +71 -0
  54. package/cli/mcp/tools-project.js +75 -0
  55. package/cli/mcp/tools-project.spec.js +51 -0
  56. package/cli/mcp/tools-q-modes.js +69 -0
  57. package/cli/mcp/tools-q.js +58 -0
  58. package/cli/mcp/tools-read.js +69 -0
  59. package/cli/mcp/tools-search-util.js +35 -0
  60. package/cli/mcp/tools-search.js +15 -294
  61. package/cli/mcp/tools-team-feed.js +57 -0
  62. package/cli/mcp/tools-team-locks.js +63 -0
  63. package/cli/mcp/tools-team-mailbox.js +32 -0
  64. package/cli/mcp/tools-team-tasks.js +84 -0
  65. package/cli/mcp/tools-team.js +37 -183
  66. package/cli/mcp/tools-verify.js +47 -0
  67. package/cli/mcp/tools-write.js +33 -0
  68. package/cli/mcp/tools.js +72 -376
  69. package/cli/mutators.js +10 -23
  70. package/cli/pillars-schema.js +3 -3
  71. package/cli/project-detector.js +47 -0
  72. package/cli/reader-shorthand.spec.js +60 -0
  73. package/cli/reader.js +67 -24
  74. package/cli/search-bulletproof.spec.js +104 -0
  75. package/cli/search-db.js +33 -5
  76. package/cli/search-queries-hotspot-graph.js +65 -0
  77. package/cli/search-queries-hotspot-graph.spec.js +66 -0
  78. package/cli/search-queries-similar.js +84 -0
  79. package/cli/search-queries-similar.spec.js +43 -0
  80. package/cli/search-schema.js +30 -10
  81. package/cli/search.js +9 -2
  82. package/cli/team/index.js +9 -0
  83. package/cli/team/team-commands.js +104 -64
  84. package/cli/team/team-concurrency-stress.spec.js +95 -0
  85. package/cli/team/team-db-mailbox.js +86 -0
  86. package/cli/team/team-db-task-helpers.js +30 -28
  87. package/cli/team/team-db-tasks.js +13 -5
  88. package/cli/team/team-flags.js +69 -0
  89. package/cli/team/team-format.js +87 -46
  90. package/cli/team/team-mailbox-empirical.spec.js +191 -0
  91. package/cli/team/team-mailbox.spec.js +93 -0
  92. package/cli/team/team-memory.js +84 -0
  93. package/cli/team/team-memory.spec.js +74 -0
  94. package/cli/team/team-projects-coordinator.js +100 -0
  95. package/cli/team/team-projects-format.js +45 -0
  96. package/cli/team/team-projects-memory.js +70 -0
  97. package/cli/team/team-projects.js +94 -0
  98. package/cli/team/team-projects.spec.js +116 -0
  99. package/cli/team/team-schema-memory.js +25 -0
  100. package/cli/team/team-schema-projects.js +50 -0
  101. package/cli/team/team-schema.js +33 -14
  102. package/cli/team/team-tasks-hierarchy.spec.js +80 -0
  103. package/cli/team/team-tokens.js +95 -0
  104. package/cli/team/team-triage.js +159 -12
  105. package/cli/team/team-verification-gate.spec.js +253 -0
  106. package/cli/team/team.spec.js +1 -1
  107. package/cli/trend.js +84 -0
  108. package/cli/trend.spec.js +51 -0
  109. package/cli/ui-actions-tasks.js +13 -5
  110. package/cli/ui-actions.js +10 -2
  111. package/cli/ui-client-api.js +2 -2
  112. package/cli/ui-client-script.js +12 -15
  113. package/cli/ui-e2e-verification.spec.js +13 -3
  114. package/cli/ui-filetree.spec.js +5 -5
  115. package/cli/ui-handlers.js +10 -2
  116. package/cli/ui-html.js +26 -34
  117. package/cli/ui-index.html +905 -0
  118. package/cli/ui-kanban.spec.js +12 -12
  119. package/cli/ui-server.js +7 -0
  120. package/cli/ui-sse.js +57 -0
  121. package/cli/ui-sse.spec.js +74 -0
  122. package/cli/ui-styles.js +195 -81
  123. package/cli/ui-template-kanban.js +15 -1
  124. package/cli/ui-vbulletin.spec.js +12 -13
  125. package/cli/ui.spec.js +12 -5
  126. package/cli/verify.js +19 -8
  127. package/package.json +3 -1
  128. package/scripts/check-framework-generation.mjs +195 -0
  129. package/scripts/pre-commit.sh +2 -7
  130. 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 && t.isObjectExpression(retPath.node.argument)) {
49
- const propCount = retPath.node.argument.properties.length;
50
- if (propCount > 5) {
51
- const line = retPath.node.loc?.start.line || 1;
52
- const meta = RULE_REGISTRY.HOOK_RETURN_OVERLOAD;
53
- violations.push({
54
- filePath: relativePath,
55
- line,
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
  });
@@ -63,7 +63,9 @@ export const autofixContent = (content, options = {}) => {
63
63
  }
64
64
 
65
65
  // 3. AI lazy placeholder comment removal
66
- if (!shouldDropLine && shouldFix('AI_SLOP_LAZY_PLACEHOLDER') && TRUNCATION_REGEX.test(line)) {
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
- if (!shouldDropLine && shouldFix('TYPOGRAPHY_EM_DASH') && line.includes('\u2014')) {
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
+ };
@@ -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 strictly limit returns to 3 to 5 properties (State + Status + Actions).');
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: 'Limit hook return values to 3 to 5 properties (State + Status + Actions)'
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,
@@ -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 visitors = createAstVisitors({ relativePath, violations });
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
  };