@polymorphism-tech/morph-spec 4.8.19 → 4.9.0

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 (137) hide show
  1. package/CLAUDE.md +21 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +15 -56
  4. package/bin/task-manager.js +115 -14
  5. package/bin/validate.js +67 -33
  6. package/claude-plugin.json +1 -1
  7. package/docs/CHEATSHEET.md +201 -203
  8. package/docs/QUICKSTART.md +2 -2
  9. package/framework/CLAUDE.md +21 -0
  10. package/framework/agents.json +698 -176
  11. package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
  12. package/framework/hooks/claude-code/post-tool-use/dispatch.js +2 -2
  13. package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +155 -0
  14. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +1 -1
  15. package/framework/hooks/claude-code/session-start/inject-morph-context.js +71 -2
  16. package/framework/hooks/claude-code/statusline.py +76 -30
  17. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +14 -6
  18. package/framework/hooks/shared/activity-logger.js +0 -24
  19. package/framework/hooks/shared/phase-utils.js +3 -0
  20. package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
  21. package/framework/hooks/shared/stale-task-reset.js +57 -0
  22. package/framework/hooks/shared/state-reader.js +2 -2
  23. package/framework/hooks/shared/worktree-helpers.js +53 -0
  24. package/framework/phases.json +40 -8
  25. package/framework/skills/level-0-meta/brainstorming/SKILL.md +1 -1
  26. package/framework/skills/level-0-meta/code-review/SKILL.md +1 -1
  27. package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +163 -163
  28. package/framework/skills/level-0-meta/frontend-review/SKILL.md +5 -5
  29. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
  30. package/framework/skills/level-0-meta/morph-init/SKILL.md +5 -5
  31. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +4 -4
  32. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
  33. package/framework/skills/level-0-meta/post-implementation/SKILL.md +59 -12
  34. package/framework/skills/level-0-meta/simulation-checklist/SKILL.md +1 -1
  35. package/framework/skills/level-0-meta/terminal-title/SKILL.md +1 -1
  36. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +1 -1
  37. package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +6 -5
  38. package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +1 -1
  39. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +215 -189
  40. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +251 -251
  41. package/framework/skills/level-1-workflows/phase-design/SKILL.md +382 -365
  42. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +492 -450
  43. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +194 -190
  44. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +270 -270
  45. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +285 -285
  46. package/framework/standards/STANDARDS.json +640 -88
  47. package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
  48. package/framework/templates/REGISTRY.json +1825 -1909
  49. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  50. package/framework/templates/docs/onboarding.md +1 -5
  51. package/package.json +2 -6
  52. package/src/commands/agents/dispatch-agents.js +55 -4
  53. package/src/commands/project/doctor.js +16 -47
  54. package/src/commands/project/init.js +1 -1
  55. package/src/commands/project/status.js +2 -2
  56. package/src/commands/project/update.js +381 -365
  57. package/src/commands/project/worktree.js +154 -0
  58. package/src/commands/state/advance-phase.js +120 -30
  59. package/src/commands/state/approve.js +2 -2
  60. package/src/commands/state/index.js +7 -8
  61. package/src/commands/state/phase-runner.js +1 -1
  62. package/src/commands/state/state.js +61 -6
  63. package/src/commands/tasks/task.js +78 -99
  64. package/src/commands/templates/template-render.js +93 -173
  65. package/src/commands/trust/trust.js +26 -21
  66. package/src/core/paths/output-schema.js +15 -0
  67. package/src/core/state/state-manager.js +28 -54
  68. package/src/core/workflows/workflow-detector.js +9 -87
  69. package/src/lib/phase-chain/phase-validator.js +330 -0
  70. package/src/lib/stack/stack-profile.js +88 -0
  71. package/src/lib/tasks/task-classifier.js +16 -0
  72. package/src/lib/tasks/test-runner.js +77 -0
  73. package/src/lib/trust/trust-manager.js +32 -144
  74. package/src/lib/validators/spec-validator.js +58 -4
  75. package/src/lib/validators/validation-runner.js +23 -11
  76. package/src/scripts/setup-infra.js +240 -224
  77. package/src/utils/agents-installer.js +2 -2
  78. package/src/utils/banner.js +1 -1
  79. package/src/utils/claude-settings-manager.js +1 -1
  80. package/src/utils/file-copier.js +1 -0
  81. package/src/utils/hooks-installer.js +258 -8
  82. package/framework/hooks/dev/check-sync-health.js +0 -117
  83. package/framework/hooks/dev/guard-version-numbers.js +0 -57
  84. package/framework/hooks/dev/sync-standards-registry.js +0 -60
  85. package/framework/hooks/dev/sync-template-registry.js +0 -60
  86. package/framework/hooks/dev/validate-skill-format.js +0 -70
  87. package/framework/hooks/dev/validate-standard-format.js +0 -73
  88. package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
  89. package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
  90. package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
  91. package/framework/workflows/configs/design-impl.json +0 -49
  92. package/framework/workflows/configs/express.json +0 -45
  93. package/framework/workflows/configs/fast-track.json +0 -42
  94. package/framework/workflows/configs/full-morph.json +0 -79
  95. package/framework/workflows/configs/fusion.json +0 -39
  96. package/framework/workflows/configs/long-running.json +0 -33
  97. package/framework/workflows/configs/spec-only.json +0 -43
  98. package/framework/workflows/configs/ui-refresh.json +0 -49
  99. package/framework/workflows/configs/zero-touch.json +0 -82
  100. package/src/commands/project/monitor.js +0 -295
  101. package/src/commands/project/tutorial.js +0 -115
  102. package/src/commands/state/validate-phase.js +0 -238
  103. package/src/commands/templates/generate-contracts.js +0 -445
  104. package/src/core/orchestrator.js +0 -171
  105. package/src/core/registry/command-registry.js +0 -28
  106. package/src/core/registry/index.js +0 -8
  107. package/src/core/registry/validator-registry.js +0 -204
  108. package/src/core/templates/template-validator.js +0 -296
  109. package/src/generator/config-generator.js +0 -206
  110. package/src/generator/templates/config.json.template +0 -40
  111. package/src/generator/templates/project.md.template +0 -67
  112. package/src/lib/agents/micro-agent-factory.js +0 -161
  113. package/src/lib/analysis/complexity-analyzer.js +0 -441
  114. package/src/lib/analysis/index.js +0 -7
  115. package/src/lib/analytics/analytics-engine.js +0 -345
  116. package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
  117. package/src/lib/checkpoints/index.js +0 -7
  118. package/src/lib/context/context-bundler.js +0 -241
  119. package/src/lib/context/context-optimizer.js +0 -212
  120. package/src/lib/context/context-tracker.js +0 -273
  121. package/src/lib/context/core-four-tracker.js +0 -201
  122. package/src/lib/context/mcp-optimizer.js +0 -200
  123. package/src/lib/execution/fusion-executor.js +0 -304
  124. package/src/lib/execution/parallel-executor.js +0 -270
  125. package/src/lib/hooks/stop-hook-executor.js +0 -286
  126. package/src/lib/hops/hop-composer.js +0 -221
  127. package/src/lib/phase-chain/eligibility-checker.js +0 -243
  128. package/src/lib/threads/thread-coordinator.js +0 -238
  129. package/src/lib/threads/thread-manager.js +0 -317
  130. package/src/lib/tracking/artifact-trail.js +0 -202
  131. package/src/scanner/project-scanner.js +0 -242
  132. package/src/ui/diff-display.js +0 -91
  133. package/src/ui/interactive-wizard.js +0 -96
  134. package/src/ui/user-review.js +0 -211
  135. package/src/ui/wizard-questions.js +0 -188
  136. package/src/utils/color-utils.js +0 -70
  137. package/src/utils/process-handler.js +0 -97
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Task classifier — identifies test/validation tasks by title.
3
+ */
4
+
5
+ const TEST_TASK_PATTERN = /\b(tests?|testing|specs?|coverage|e2e|unit|integration|integra[cç][aã]o|valida[cç][aã]o|cobertura|testes?)\b/i;
6
+
7
+ /**
8
+ * Returns true if the task title indicates a testing or validation task.
9
+ *
10
+ * @param {string} taskTitle
11
+ * @returns {boolean}
12
+ */
13
+ export function isTestTask(taskTitle) {
14
+ if (!taskTitle) return false;
15
+ return TEST_TASK_PATTERN.test(taskTitle);
16
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Test Runner — Resolves and executes the project's test suite.
3
+ *
4
+ * Resolution order for test command:
5
+ * 1. .morph/config/config.json → project.testCommand
6
+ * 2. package.json present → "npm test"
7
+ * 3. null (no test suite detected)
8
+ */
9
+
10
+ import { execSync } from 'child_process';
11
+ import { existsSync, readFileSync } from 'fs';
12
+ import { join } from 'path';
13
+
14
+ /**
15
+ * Resolve the test command for a project.
16
+ *
17
+ * @param {string} [projectPath] - Project root directory (defaults to '.')
18
+ * @returns {string | null} The test command to run, or null if none found
19
+ */
20
+ export function resolveTestCommand(projectPath = '.') {
21
+ // 1. Read from .morph/config/config.json
22
+ const configPath = join(projectPath, '.morph', 'config', 'config.json');
23
+ if (existsSync(configPath)) {
24
+ try {
25
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
26
+ const cmd = config?.project?.testCommand;
27
+ if (cmd && typeof cmd === 'string') {
28
+ return cmd;
29
+ }
30
+ } catch {
31
+ // Malformed config — fall through to detection
32
+ }
33
+ }
34
+
35
+ // 2. Detect package.json → npm test
36
+ const packageJsonPath = join(projectPath, 'package.json');
37
+ if (existsSync(packageJsonPath)) {
38
+ return 'npm test';
39
+ }
40
+
41
+ // 3. Nothing found
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Run the project's test suite.
47
+ *
48
+ * @param {string} [projectPath] - Project root directory (defaults to '.')
49
+ * @param {object} [opts] - Optional overrides for testing
50
+ * @param {Function} [opts._execSync] - Override execSync for unit testing
51
+ * @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string }}
52
+ */
53
+ export function runTestSuite(projectPath = '.', opts = {}) {
54
+ const cmd = resolveTestCommand(projectPath);
55
+
56
+ if (!cmd) {
57
+ return { skipped: true, reason: 'No testCommand configured and no known project file detected' };
58
+ }
59
+
60
+ const exec = opts._execSync ?? execSync;
61
+
62
+ try {
63
+ const output = exec(cmd, {
64
+ cwd: projectPath,
65
+ timeout: 120_000,
66
+ encoding: 'utf8',
67
+ stdio: 'pipe'
68
+ });
69
+ return { passed: true, output: output ?? '', exitCode: 0 };
70
+ } catch (err) {
71
+ return {
72
+ passed: false,
73
+ output: (err.stdout || '') + (err.stderr || ''),
74
+ exitCode: err.status ?? 1
75
+ };
76
+ }
77
+ }
@@ -1,32 +1,25 @@
1
1
  /**
2
- * Trust Manager — Track-record and auto-approval logic
3
- *
4
- * Computes trust level from checkpoint pass rate history.
5
- * Enables auto-approval gates for features with proven track records.
2
+ * Trust Manager — Simplified 3-level trust model
6
3
  *
7
4
  * Trust levels:
8
- * low < 80% pass rate
9
- * medium 80–90%
10
- * high 90–95%
11
- * maximum > 95%
5
+ * manual — nothing auto-approved (default)
6
+ * high — auto-approve low-risk gates (design, tasks)
7
+ * auto — auto-approve all gates
8
+ *
9
+ * Set with: morph-spec trust set <feature> <level>
12
10
  */
13
11
 
14
- import { readFileSync, writeFileSync, existsSync } from 'fs';
12
+ import { readFileSync, existsSync } from 'fs';
15
13
  import { join } from 'path';
14
+ import { saveState } from '../../core/state/state-manager.js';
16
15
 
17
- // Default trust config (matches llm-interaction.json thresholds)
18
- const TRUST_THRESHOLDS = {
19
- low: 0,
20
- medium: 0.80,
21
- high: 0.90,
22
- maximum: 0.95
23
- };
16
+ const VALID_LEVELS = ['manual', 'high', 'auto'];
24
17
 
25
- // Gates that can be auto-approved by trust level
26
- const AUTO_APPROVE_GATES = {
27
- medium: ['design'],
18
+ // Gates that can be auto-approved at each trust level
19
+ export const AUTO_APPROVE_GATES = {
20
+ manual: [],
28
21
  high: ['design', 'tasks'],
29
- maximum: ['design', 'tasks', 'proposal']
22
+ auto: ['design', 'tasks', 'proposal', 'uiux']
30
23
  };
31
24
 
32
25
  /**
@@ -43,86 +36,41 @@ function loadState() {
43
36
  }
44
37
  }
45
38
 
46
- /**
47
- * Save state.json
48
- * @param {Object} state
49
- */
50
- function saveState(state) {
51
- const statePath = join(process.cwd(), '.morph/state.json');
52
- writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
53
- }
54
-
55
- /**
56
- * Calculate trust level from checkpoint history
57
- * @param {Array} checkpoints - Array of { passed: boolean, ... }
58
- * @returns {{ level: string, passRate: number, total: number, passed: number }}
59
- */
60
- export function calculateTrust(checkpoints = []) {
61
- if (checkpoints.length === 0) {
62
- return { level: 'low', passRate: 0, total: 0, passed: 0 };
63
- }
64
-
65
- const total = checkpoints.length;
66
- const passed = checkpoints.filter(c => c.passed).length;
67
- const passRate = passed / total;
68
-
69
- let level = 'low';
70
- if (passRate >= TRUST_THRESHOLDS.maximum) level = 'maximum';
71
- else if (passRate >= TRUST_THRESHOLDS.high) level = 'high';
72
- else if (passRate >= TRUST_THRESHOLDS.medium) level = 'medium';
73
-
74
- return { level, passRate, total, passed };
75
- }
76
-
77
39
  /**
78
40
  * Get current trust config for a feature
79
41
  * @param {string} featureName
80
- * @returns {{ level: string, passRate: number, autoApprove: string[], override?: string }}
42
+ * @returns {{ level: string, autoApprove: string[], source: string, overrideReason?: string }}
81
43
  */
82
44
  export function getTrust(featureName) {
83
45
  const state = loadState();
84
46
  const feature = state.features?.[featureName];
85
47
 
86
48
  if (!feature) {
87
- return { level: 'low', passRate: 0, autoApprove: [], source: 'default' };
88
- }
89
-
90
- // Check for manual override first
91
- if (feature.trustConfig?.override) {
92
- return {
93
- level: feature.trustConfig.override.level,
94
- passRate: feature.trustConfig.passRate || 0,
95
- autoApprove: AUTO_APPROVE_GATES[feature.trustConfig.override.level] || [],
96
- source: 'manual',
97
- overrideReason: feature.trustConfig.override.reason,
98
- overrideAt: feature.trustConfig.override.setAt
99
- };
49
+ return { level: 'manual', autoApprove: [], source: 'default' };
100
50
  }
101
51
 
102
- // Calculate from checkpoint history
103
- const checkpoints = feature.checkpoints || [];
104
- const { level, passRate, total, passed } = calculateTrust(checkpoints);
52
+ const override = feature.trustConfig?.override;
53
+ const level = VALID_LEVELS.includes(override?.level) ? override.level : 'manual';
54
+ const source = override ? 'manual' : 'default';
105
55
 
106
56
  return {
107
57
  level,
108
- passRate,
109
- total,
110
- passed,
111
58
  autoApprove: AUTO_APPROVE_GATES[level] || [],
112
- source: 'calculated'
59
+ source,
60
+ overrideReason: override?.reason,
61
+ overrideAt: override?.setAt
113
62
  };
114
63
  }
115
64
 
116
65
  /**
117
66
  * Manually set trust level with reason
118
67
  * @param {string} featureName
119
- * @param {string} level - 'low' | 'medium' | 'high' | 'maximum'
120
- * @param {string} reason
68
+ * @param {string} level - 'manual' | 'high' | 'auto'
69
+ * @param {string} [reason]
121
70
  */
122
71
  export function setTrust(featureName, level, reason) {
123
- const validLevels = ['low', 'medium', 'high', 'maximum'];
124
- if (!validLevels.includes(level)) {
125
- throw new Error(`Invalid trust level: ${level}. Valid: ${validLevels.join(', ')}`);
72
+ if (!VALID_LEVELS.includes(level)) {
73
+ throw new Error(`Invalid trust level: ${level}. Valid: ${VALID_LEVELS.join(', ')}`);
126
74
  }
127
75
 
128
76
  const state = loadState();
@@ -136,7 +84,7 @@ export function setTrust(featureName, level, reason) {
136
84
 
137
85
  state.features[featureName].trustConfig.override = {
138
86
  level,
139
- reason,
87
+ reason: reason || `Manual override to ${level}`,
140
88
  setAt: new Date().toISOString(),
141
89
  setBy: 'manual'
142
90
  };
@@ -147,12 +95,12 @@ export function setTrust(featureName, level, reason) {
147
95
  feature: featureName,
148
96
  level,
149
97
  autoApprove: AUTO_APPROVE_GATES[level] || [],
150
- reason
98
+ reason: state.features[featureName].trustConfig.override.reason
151
99
  };
152
100
  }
153
101
 
154
102
  /**
155
- * Clear manual trust override (revert to calculated)
103
+ * Clear manual trust override (reverts to default: manual)
156
104
  * @param {string} featureName
157
105
  */
158
106
  export function clearTrustOverride(featureName) {
@@ -166,7 +114,7 @@ export function clearTrustOverride(featureName) {
166
114
  /**
167
115
  * Check if a gate should be auto-approved based on trust level
168
116
  * @param {string} featureName
169
- * @param {string} gate - 'design' | 'tasks' | 'proposal'
117
+ * @param {string} gate - 'design' | 'tasks' | 'proposal' | 'uiux'
170
118
  * @returns {{ autoApprove: boolean, level: string, reason: string }}
171
119
  */
172
120
  export function shouldAutoApprove(featureName, gate) {
@@ -176,33 +124,19 @@ export function shouldAutoApprove(featureName, gate) {
176
124
  return {
177
125
  autoApprove: false,
178
126
  level: trust.level,
179
- reason: `Trust level "${trust.level}" does not auto-approve "${gate}" gate (need: ${getMinLevelForGate(gate)})`
127
+ reason: `Trust level "${trust.level}" does not auto-approve "${gate}" gate`
180
128
  };
181
129
  }
182
130
 
183
131
  return {
184
132
  autoApprove: true,
185
133
  level: trust.level,
186
- reason: `Auto-approved: ${trust.level} trust (${Math.round(trust.passRate * 100)}% pass rate, ${trust.total} checkpoints)`,
187
- passRate: trust.passRate,
188
- checkpointsTotal: trust.total
134
+ reason: `Auto-approved: trust level "${trust.level}" includes gate "${gate}"`
189
135
  };
190
136
  }
191
137
 
192
138
  /**
193
- * Get minimum trust level required for a gate
194
- * @param {string} gate
195
- * @returns {string}
196
- */
197
- function getMinLevelForGate(gate) {
198
- for (const [level, gates] of Object.entries(AUTO_APPROVE_GATES)) {
199
- if (gates.includes(gate)) return level;
200
- }
201
- return 'maximum';
202
- }
203
-
204
- /**
205
- * Get trust history for all features
139
+ * Get trust info for all features
206
140
  * @returns {Array} Feature trust summaries
207
141
  */
208
142
  export function getTrustHistory() {
@@ -214,9 +148,6 @@ export function getTrustHistory() {
214
148
  return {
215
149
  feature: name,
216
150
  level: trust.level,
217
- passRate: trust.passRate,
218
- checkpointsTotal: trust.total || 0,
219
- checkpointsPassed: trust.passed || 0,
220
151
  autoApprove: trust.autoApprove,
221
152
  source: trust.source,
222
153
  phase: feature.phase,
@@ -224,46 +155,3 @@ export function getTrustHistory() {
224
155
  };
225
156
  });
226
157
  }
227
-
228
- /**
229
- * Auto-calculate and update trust in state
230
- * @param {string} featureName
231
- * @returns {Object} Updated trust config
232
- */
233
- export function autoCalculateTrust(featureName) {
234
- const state = loadState();
235
- const feature = state.features?.[featureName];
236
-
237
- if (!feature) {
238
- throw new Error(`Feature not found: ${featureName}`);
239
- }
240
-
241
- const checkpoints = feature.checkpoints || [];
242
- const { level, passRate, total, passed } = calculateTrust(checkpoints);
243
-
244
- if (!state.features[featureName].trustConfig) {
245
- state.features[featureName].trustConfig = {};
246
- }
247
-
248
- state.features[featureName].trustConfig = {
249
- ...state.features[featureName].trustConfig,
250
- level,
251
- passRate,
252
- checkpointsTotal: total,
253
- checkpointsPassed: passed,
254
- lastCalculated: new Date().toISOString()
255
- };
256
-
257
- saveState(state);
258
-
259
- return {
260
- feature: featureName,
261
- level,
262
- passRate,
263
- total,
264
- passed,
265
- autoApprove: AUTO_APPROVE_GATES[level] || []
266
- };
267
- }
268
-
269
- export { TRUST_THRESHOLDS, AUTO_APPROVE_GATES };
@@ -9,7 +9,8 @@
9
9
 
10
10
  import { readFileSync, existsSync } from 'fs';
11
11
  import { join } from 'path';
12
- import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
12
+ import { getAbsoluteOutputPath, resolveContractsOutputType } from '../../core/paths/output-schema.js';
13
+ import { getStackProfile } from '../stack/stack-profile.js';
13
14
 
14
15
  /**
15
16
  * Validate spec.md and contracts.cs at design time
@@ -20,8 +21,11 @@ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
20
21
  * @returns {Object} { status, errors, warnings, issues }
21
22
  */
22
23
  export async function validateSpec(projectPath, featureName, options = {}) {
24
+ const { stack, isNextjs } = getStackProfile(projectPath);
25
+ const contractsType = resolveContractsOutputType(stack);
26
+
23
27
  const specPath = getAbsoluteOutputPath(projectPath, featureName, 'spec');
24
- const contractsPath = getAbsoluteOutputPath(projectPath, featureName, 'contracts');
28
+ const contractsPath = getAbsoluteOutputPath(projectPath, featureName, contractsType);
25
29
 
26
30
  const issues = [];
27
31
 
@@ -32,10 +36,12 @@ export async function validateSpec(projectPath, featureName, options = {}) {
32
36
  issues.push(...specIssues);
33
37
  }
34
38
 
35
- // Validate contracts.cs
39
+ // Validate contracts file (stack-aware)
36
40
  if (existsSync(contractsPath)) {
37
41
  const contractsContent = readFileSync(contractsPath, 'utf8');
38
- const contractsIssues = validateContractsCs(contractsContent);
42
+ const contractsIssues = isNextjs
43
+ ? validateContractsTs(contractsContent)
44
+ : validateContractsCs(contractsContent);
39
45
  issues.push(...contractsIssues);
40
46
  }
41
47
 
@@ -240,6 +246,54 @@ function validateContractsCs(content) {
240
246
  return issues;
241
247
  }
242
248
 
249
+ /**
250
+ * Validate contracts.ts structure for Next.js projects.
251
+ * Checks: has exports, has Zod schemas, no C# syntax.
252
+ */
253
+ function validateContractsTs(content) {
254
+ const issues = [];
255
+
256
+ // Must have at least one export
257
+ if (!/^export\s+(interface|type|const|function)/m.test(content)) {
258
+ issues.push({
259
+ level: 'error',
260
+ type: 'contracts',
261
+ message: 'contracts.ts has no exported types, interfaces, or schemas',
262
+ solution: 'Add at least one export interface or export type'
263
+ });
264
+ }
265
+
266
+ // Should have at least one Zod schema (warning only)
267
+ if (!content.includes('z.object(')) {
268
+ issues.push({
269
+ level: 'warning',
270
+ type: 'contracts',
271
+ message: 'contracts.ts has no Zod schemas (z.object)',
272
+ solution: "Add Zod schemas for runtime validation. Import from 'zod'."
273
+ });
274
+ }
275
+
276
+ // Must NOT contain C# syntax
277
+ const csharpPatterns = [
278
+ { re: /^namespace\s+/m, name: 'namespace declaration' },
279
+ { re: /^using\s+System/m, name: 'C# using directive' },
280
+ { re: /\bpublic\s+record\b/m, name: 'C# record type' },
281
+ { re: /\bpublic\s+class\b/m, name: 'C# class declaration' },
282
+ ];
283
+ for (const { re, name } of csharpPatterns) {
284
+ if (re.test(content)) {
285
+ issues.push({
286
+ level: 'error',
287
+ type: 'contracts',
288
+ message: `contracts.ts contains C# syntax (${name})`,
289
+ solution: 'This file was generated for .NET — regenerate contracts using the phase-design skill for a Next.js project'
290
+ });
291
+ }
292
+ }
293
+
294
+ return issues;
295
+ }
296
+
243
297
  /**
244
298
  * Check if a string is PascalCase
245
299
  */
@@ -11,7 +11,14 @@ import { existsSync, readFileSync } from 'fs';
11
11
  import { join } from 'path';
12
12
  import chalk from 'chalk';
13
13
  import { loadState } from '../../core/state/state-manager.js';
14
- import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
14
+ import { getAbsoluteOutputPath, resolveContractsOutputType } from '../../core/paths/output-schema.js';
15
+ import { getStackProfile } from '../stack/stack-profile.js';
16
+
17
+ /** Validators that only make sense for .NET/Blazor projects. */
18
+ const DOTNET_ONLY_VALIDATORS = new Set([
19
+ 'architecture', 'packages', 'blazor', 'blazor-concurrency',
20
+ 'blazor-state', 'contract-compliance'
21
+ ]);
15
22
 
16
23
  /**
17
24
  * Load agent → validators map from agents.json (data-driven)
@@ -92,9 +99,11 @@ export async function runValidation(projectPath, featureName, options = {}) {
92
99
  console.log(chalk.gray(` (+ ${constraints.enabledValidators.length} from decisions)`));
93
100
  }
94
101
 
95
- // Always run contract compliance if contracts.cs exists
96
- const contractsPath = getAbsoluteOutputPath(projectPath, featureName, 'contracts');
97
- if (existsSync(contractsPath)) {
102
+ // Always run contract compliance if the contracts file exists (stack-aware)
103
+ const _profileForContracts = getStackProfile(projectPath);
104
+ const contractsType = resolveContractsOutputType(_profileForContracts.stack);
105
+ const contractsPath = getAbsoluteOutputPath(projectPath, featureName, contractsType);
106
+ if (existsSync(contractsPath) && !_profileForContracts.isNextjs) {
98
107
  validatorIds.push('contract-compliance');
99
108
  }
100
109
 
@@ -152,26 +161,29 @@ export async function runValidation(projectPath, featureName, options = {}) {
152
161
  * @returns {string[]} Array of validator IDs to run
153
162
  */
154
163
  function detectValidators(featureName, projectPath = '.') {
164
+ const profile = getStackProfile(projectPath);
155
165
  const state = loadState(false);
166
+
156
167
  if (!state || !state.features[featureName]) {
157
- return ['architecture', 'packages']; // Default validators
168
+ return [...profile.defaultValidators]; // stack-aware defaults
158
169
  }
159
170
 
160
171
  const feature = state.features[featureName];
161
172
  const agents = feature.activeAgents || [];
162
- const validatorSet = new Set();
173
+ const validatorSet = new Set(profile.defaultValidators); // stack defaults first
163
174
 
164
- // Load agent → validators map from agents.json (data-driven)
165
175
  const agentValidatorMap = loadAgentValidatorMap(projectPath);
166
-
167
176
  for (const agentId of agents) {
168
177
  const validators = agentValidatorMap[agentId] || [];
169
178
  validators.forEach(v => validatorSet.add(v));
170
179
  }
171
180
 
172
- // Always include architecture and packages as baseline
173
- validatorSet.add('architecture');
174
- validatorSet.add('packages');
181
+ // Filter out .NET-only validators for non-.NET projects
182
+ if (!profile.isDotnet) {
183
+ for (const v of DOTNET_ONLY_VALIDATORS) {
184
+ validatorSet.delete(v);
185
+ }
186
+ }
175
187
 
176
188
  return Array.from(validatorSet);
177
189
  }