@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.
Files changed (138) 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/history.js +22 -6
  6. package/cli/audit/hook-shape-validator.js +312 -0
  7. package/cli/audit/prompts.js +13 -10
  8. package/cli/audit/prompts.spec.js +52 -1
  9. package/cli/audit/reporter-clones.js +33 -0
  10. package/cli/audit/reporter-hotspot-graph.js +34 -0
  11. package/cli/audit/rules-registry.js +6 -1
  12. package/cli/audit/rules.js +10 -2
  13. package/cli/audit/rules.spec.js +129 -0
  14. package/cli/audit-stages.spec.js +30 -0
  15. package/cli/audit.js +12 -3
  16. package/cli/commands/cmd-audit.js +56 -12
  17. package/cli/commands/cmd-project.js +100 -0
  18. package/cli/commands/cmd-router.js +52 -3
  19. package/cli/commands-schema.js +28 -0
  20. package/cli/create.spec.js +2 -2
  21. package/cli/exploder.js +258 -0
  22. package/cli/exploder.spec.js +99 -0
  23. package/cli/generator-compact.spec.js +104 -0
  24. package/cli/generator-framework.spec.js +72 -0
  25. package/cli/generator-templates/archetypes/collection-archetypes.js +9 -9
  26. package/cli/generator-templates/archetypes/index.js +34 -4
  27. package/cli/generator-templates/archetypes/input-archetypes.js +2 -2
  28. package/cli/generator-templates/archetypes/layout-archetypes.js +2 -2
  29. package/cli/generator-templates/archetypes/minimal-archetypes.js +131 -0
  30. package/cli/generator-templates/archetypes/status-archetypes.js +4 -4
  31. package/cli/generator-templates/archetypes/vector-matcher.js +50 -0
  32. package/cli/generator-templates/archetypes/vector-matcher.spec.js +25 -0
  33. package/cli/generator-templates/compact.js +130 -0
  34. package/cli/generator-templates/controller-converters.js +93 -0
  35. package/cli/generator-templates/controller.js +2 -64
  36. package/cli/generator-templates/react.js +1 -1
  37. package/cli/generator-templates/svelte.js +1 -1
  38. package/cli/generator-templates/types.js +2 -2
  39. package/cli/generator-templates/vue.js +1 -1
  40. package/cli/generator-templates.js +1 -0
  41. package/cli/generator-templates.spec.js +6 -1
  42. package/cli/generator.js +94 -7
  43. package/cli/help.js +1 -1
  44. package/cli/index.js +23 -8
  45. package/cli/installer-templates.js +1 -1
  46. package/cli/mcp/antigravity.js +5 -2
  47. package/cli/mcp/installer.js +7 -7
  48. package/cli/mcp/installer.spec.js +3 -3
  49. package/cli/mcp/manifests.js +4 -2
  50. package/cli/mcp/prompts.js +1 -1
  51. package/cli/mcp/server.js +2 -0
  52. package/cli/mcp/tools-audit.js +97 -0
  53. package/cli/mcp/tools-generate.js +62 -0
  54. package/cli/mcp/tools-patch.js +72 -0
  55. package/cli/mcp/tools-patterns.js +71 -0
  56. package/cli/mcp/tools-project.js +75 -0
  57. package/cli/mcp/tools-project.spec.js +51 -0
  58. package/cli/mcp/tools-q-modes.js +69 -0
  59. package/cli/mcp/tools-q.js +58 -0
  60. package/cli/mcp/tools-read.js +69 -0
  61. package/cli/mcp/tools-search-util.js +35 -0
  62. package/cli/mcp/tools-search.js +15 -294
  63. package/cli/mcp/tools-team-feed.js +57 -0
  64. package/cli/mcp/tools-team-locks.js +63 -0
  65. package/cli/mcp/tools-team-mailbox.js +32 -0
  66. package/cli/mcp/tools-team-tasks.js +85 -0
  67. package/cli/mcp/tools-team.js +37 -183
  68. package/cli/mcp/tools-verify.js +47 -0
  69. package/cli/mcp/tools-write.js +33 -0
  70. package/cli/mcp/tools.js +79 -372
  71. package/cli/mutators.js +10 -23
  72. package/cli/pillars-schema.js +3 -3
  73. package/cli/project-detector.js +47 -0
  74. package/cli/reader-shorthand.spec.js +60 -0
  75. package/cli/reader.js +67 -24
  76. package/cli/search-bulletproof.spec.js +104 -0
  77. package/cli/search-db.js +33 -5
  78. package/cli/search-queries-hotspot-graph.js +65 -0
  79. package/cli/search-queries-hotspot-graph.spec.js +66 -0
  80. package/cli/search-queries-similar.js +84 -0
  81. package/cli/search-queries-similar.spec.js +43 -0
  82. package/cli/search-schema.js +79 -11
  83. package/cli/search.js +13 -2
  84. package/cli/team/index.js +9 -0
  85. package/cli/team/team-commands.js +106 -64
  86. package/cli/team/team-concurrency-stress.spec.js +95 -0
  87. package/cli/team/team-db-lock-promotion.js +15 -11
  88. package/cli/team/team-db-mailbox.js +86 -0
  89. package/cli/team/team-db-task-helpers.js +80 -28
  90. package/cli/team/team-db-tasks.js +13 -5
  91. package/cli/team/team-flags.js +72 -0
  92. package/cli/team/team-format.js +87 -46
  93. package/cli/team/team-mailbox-empirical.spec.js +191 -0
  94. package/cli/team/team-mailbox.spec.js +93 -0
  95. package/cli/team/team-memory.js +84 -0
  96. package/cli/team/team-memory.spec.js +74 -0
  97. package/cli/team/team-projects-coordinator.js +100 -0
  98. package/cli/team/team-projects-format.js +45 -0
  99. package/cli/team/team-projects-memory.js +70 -0
  100. package/cli/team/team-projects.js +94 -0
  101. package/cli/team/team-projects.spec.js +116 -0
  102. package/cli/team/team-schema-memory.js +25 -0
  103. package/cli/team/team-schema-projects.js +50 -0
  104. package/cli/team/team-schema.js +33 -14
  105. package/cli/team/team-tasks-hierarchy.spec.js +176 -0
  106. package/cli/team/team-tokens.js +95 -0
  107. package/cli/team/team-triage.js +274 -52
  108. package/cli/team/team-verification-gate.spec.js +253 -0
  109. package/cli/team/team.spec.js +1 -1
  110. package/cli/tesseract-hud.js +59 -0
  111. package/cli/tesseract-manifesto.js +61 -0
  112. package/cli/tesseract-state.js +58 -0
  113. package/cli/tesseract.js +81 -0
  114. package/cli/tesseract.spec.js +55 -0
  115. package/cli/trend.js +84 -0
  116. package/cli/trend.spec.js +51 -0
  117. package/cli/ui-actions-tasks.js +13 -5
  118. package/cli/ui-actions.js +10 -2
  119. package/cli/ui-client-api.js +2 -2
  120. package/cli/ui-client-script.js +12 -15
  121. package/cli/ui-e2e-verification.spec.js +13 -3
  122. package/cli/ui-filetree.spec.js +5 -5
  123. package/cli/ui-handlers.js +10 -2
  124. package/cli/ui-html.js +26 -34
  125. package/cli/ui-index.html +905 -0
  126. package/cli/ui-kanban.spec.js +12 -12
  127. package/cli/ui-server.js +19 -3
  128. package/cli/ui-sse.js +57 -0
  129. package/cli/ui-sse.spec.js +74 -0
  130. package/cli/ui-styles.js +195 -81
  131. package/cli/ui-template-kanban.js +15 -1
  132. package/cli/ui-vbulletin.spec.js +12 -13
  133. package/cli/ui.spec.js +12 -5
  134. package/cli/verify.js +19 -8
  135. package/package.json +3 -1
  136. package/scripts/check-framework-generation.mjs +195 -0
  137. package/scripts/pre-commit.sh +2 -7
  138. 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
+ });
@@ -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
- if (!fs.existsSync(dir)) {
17
- fs.mkdirSync(dir, { recursive: true });
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
- fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
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
- fs.writeFileSync(historyPath, JSON.stringify(trimmed, null, 2), 'utf-8');
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
- fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
210
- isNewBaseline = true;
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
+ };
@@ -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(' Locations:');
52
+ lines.push(` Action: chemx team task list --rule=${rg.rule}`);
53
53
 
54
- const tree = buildPathTree(rg.violations);
55
- const treeLines = renderTreeLines(tree, 0);
56
- lines.push(...treeLines);
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
- '- Query & Inspect: Run `pnpm q "<target>" --inspect` (or `npx chemx search "<target>" --inspect`) to inspect component props, symbols, and hooks before editing.',
66
- '- Tier Filter: Run `pnpm q "<query>" --tier=molecule` (or `atom`, `organism`, `hook`) to find related capsules.',
67
- '- Zero-Overhead JSON: Run `pnpm q "<query>" --json` for minified AST metadata without burning context tokens on whole files.',
68
- '- Re-Verify Score: Run `npx chemx audit` after refactoring to ensure Molecular Health Index (MHI) and letter grades improve.'
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 strictly limit returns to 3 to 5 properties (State + Status + Actions).');
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
+ });