@polymorphism-tech/morph-spec 8.9.0 → 8.11.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 (33) hide show
  1. package/README.md +2 -2
  2. package/claude-plugin.json +1 -1
  3. package/docs/CHEATSHEET.md +1 -1
  4. package/docs/QUICKSTART.md +1 -1
  5. package/framework/mcp/registry.json +6 -5
  6. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  7. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  8. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  9. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  10. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  11. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  12. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  13. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  14. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  15. package/framework/skills/core/morph-review/SKILL.md +1 -1
  16. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  17. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  18. package/package.json +1 -1
  19. package/src/commands/mcp/mcp-setup.js +6 -1
  20. package/src/commands/project/doctor.js +10 -1
  21. package/src/commands/project/init.js +20 -9
  22. package/src/core/templates/template-registry.js +0 -59
  23. package/src/dashboard/scanner/hooks-scanner.js +64 -6
  24. package/src/dashboard/scanner/standards-scanner.js +15 -3
  25. package/src/lib/detectors/claude-config-detector.js +37 -5
  26. package/src/lib/generators/architecture-scanner.js +19 -6
  27. package/src/lib/stack/stack-profile.js +0 -5
  28. package/src/lib/validators/architecture/architecture-validator.js +12 -1
  29. package/src/lib/validators/contracts/contract-compliance-validator.js +7 -2
  30. package/src/lib/validators/packages/package-validator.js +14 -1
  31. package/src/lib/validators/validation-runner.js +133 -28
  32. package/src/lib/validators/content/content-validator.js +0 -89
  33. package/src/lib/validators/content/index.js +0 -7
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
4
4
 
5
5
  **Package:** `@polymorphism-tech/morph-spec`
6
- **Version:** 8.9.0
6
+ **Version:** 8.11.0
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -368,4 +368,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
368
368
 
369
369
  ---
370
370
 
371
- *morph-spec v8.9.0 by [Polymorphism Tech](https://polymorphism.tech)*
371
+ *morph-spec v8.11.0 by [Polymorphism Tech](https://polymorphism.tech)*
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.9.0",
3
+ "version": "8.11.0",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -185,4 +185,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
185
185
 
186
186
  ---
187
187
 
188
- *morph-spec v8.9.0 by Polymorphism Tech*
188
+ *morph-spec v8.11.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.9.0 by Polymorphism Tech*
252
+ *morph-spec v8.11.0 by Polymorphism Tech*
@@ -3,7 +3,7 @@
3
3
  "description": "Centralized MCP registry with phase relevance and install configuration for morph-spec workflows",
4
4
  "mcps": {
5
5
  "context7": {
6
- "phases": ["proposal", "design", "tasks", "implement"],
6
+ "phases": ["proposal", "plan", "implement"],
7
7
  "tools": ["resolve-library-id", "query-docs"],
8
8
  "detection": "enabledPlugins includes context7 or mcpServers includes context7",
9
9
  "fallback": "WebSearch + WebFetch for library documentation",
@@ -86,7 +86,7 @@
86
86
  }
87
87
  },
88
88
  "asaas": {
89
- "phases": ["design", "implement"],
89
+ "phases": ["plan", "implement"],
90
90
  "tools": ["enumerate_endpoints", "get_endpoint_schema", "generate_code_example", "execute_api_call", "search_docs"],
91
91
  "detection": "mcpServers includes asaas",
92
92
  "fallback": "WebFetch https://docs.asaas.com for API documentation",
@@ -201,11 +201,12 @@
201
201
  }
202
202
  }
203
203
  },
204
+ "_phaseMatrixNote": "Keys must be the v5 phases from framework/phases.json. The v4 keys 'design' and 'tasks' both collapsed into 'plan'; 'review' was absent entirely, so a project in review resolved to undefined. Stack-scoped MCPs (asaas) stay out of this matrix on purpose — only universal ones are listed.",
204
205
  "phaseMatrix": {
205
206
  "proposal": ["context7"],
206
207
  "uiux": ["playwright", "context7", "stitch", "gemini-image"],
207
- "design": ["context7"],
208
- "tasks": ["context7"],
209
- "implement": ["context7", "playwright", "vercel", "gemini-image"]
208
+ "plan": ["context7"],
209
+ "implement": ["context7", "playwright", "vercel", "gemini-image"],
210
+ "review": ["playwright", "context7"]
210
211
  }
211
212
  }
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
4
4
  version: core
5
5
  when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # create-umbraco-lp
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # migrate-lp-to-umbraco
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
4
4
  version: core
5
5
  when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
4
4
  version: core
5
5
  when-to-invoke: ao receber um novo request de feature, antes de morph-plan
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-brainstorming
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
4
4
  version: core
5
5
  when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
6
6
  user-invokable: false
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-eval
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
4
4
  version: core
5
5
  when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-hotfix
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
4
4
  version: core
5
5
  when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
6
6
  user-invokable: false
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-implement
@@ -4,7 +4,7 @@ description: Guia a produção dos artefatos de planejamento de uma feature (use
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-proposal
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
4
4
  version: core
5
5
  when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-stack-scan
@@ -4,7 +4,7 @@ description: Localiza e carrega um standard específico do STANDARDS.json sob de
4
4
  version: core
5
5
  when-to-invoke: quando precisar consultar um standard específico durante a implementação
6
6
  user-invokable: true
7
- cliVersion: "8.9.0"
7
+ cliVersion: "8.11.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.9.0",
3
+ "version": "8.11.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -34,8 +34,13 @@ import { detectProject } from '../../lib/detectors/index.js';
34
34
  /**
35
35
  * Detect the current project's stack.
36
36
  * Prefers .morph/config/config.json (set during morph-spec init) over filesystem re-detection.
37
+ *
38
+ * Exported for testing: the stack decides which MCPs are relevant, and it is
39
+ * the one branch here worth pinning — `.morph/config/config.json` is the only
40
+ * place a compound stack (`dotnet-nextjs`) survives, since filesystem
41
+ * re-detection collapses it to whichever half it notices first.
37
42
  */
38
- async function detectStack(targetPath) {
43
+ export async function detectStack(targetPath) {
39
44
  // 1. Prefer the authoritative config (handles compound stacks like nextjs+dotnet)
40
45
  try {
41
46
  const configPath = join(targetPath, '.morph', 'config', 'config.json');
@@ -647,7 +647,16 @@ export async function doctorCommand(options = {}) {
647
647
 
648
648
  // Worktree reconciliation mode (C2)
649
649
  if (options.worktrees) {
650
- return doctorWorktreesCommand(process.cwd(), options);
650
+ const clean = await doctorWorktreesCommand(process.cwd(), options);
651
+ // The audit result has to reach the exit code, not just the terminal.
652
+ // Commander discards an action's return value, so this reported success to
653
+ // every script while printing "⚠ 5 inconsistência(s)" to the human — a
654
+ // `doctor --worktrees || echo falhou` in CI or a pre-commit hook saw a
655
+ // clean run with orphaned worktrees piling up. The flag-misuse branch
656
+ // above already treats the exit code as this command's signal channel;
657
+ // only the audit itself was left out of it.
658
+ if (!clean) process.exitCode = 1;
659
+ return clean;
651
660
  }
652
661
 
653
662
  const targetPath = process.cwd();
@@ -15,7 +15,7 @@ import { getInstalledCLIVersion, checkProjectOutdated } from '../../utils/versio
15
15
  import { installClaudeHooks } from '../../utils/claude-settings-manager.js';
16
16
  import inquirer from 'inquirer';
17
17
  import { detectProject } from '../../lib/detectors/index.js';
18
- import { detectClaudeConfig, mapMcpsToPhases, formatConfigSummary } from '../../lib/detectors/claude-config-detector.js';
18
+ import { detectClaudeConfig, mcpAvailabilityByPhase, formatConfigSummary } from '../../lib/detectors/claude-config-detector.js';
19
19
  import { generateSettings, saveIntegrations } from '../../lib/generators/settings-generator.js';
20
20
  import {
21
21
  orchestrateMcpSetup,
@@ -25,7 +25,8 @@ import {
25
25
  generateSetupInstructions,
26
26
  formatMcpStatusTable,
27
27
  isRemoteMcp,
28
- setupStitch
28
+ setupStitch,
29
+ loadMcpRegistry
29
30
  } from '../../lib/installers/mcp-installer.js';
30
31
  import { setupInfra, installRulesForStack } from '../../scripts/setup-infra.js';
31
32
  import { installSkills } from '../../utils/skills-installer.js';
@@ -147,14 +148,24 @@ export async function initCommand(options) {
147
148
  logger.info(summary);
148
149
  }
149
150
 
150
- // Show MCP-to-phase mapping
151
+ // Show MCP-to-phase mapping.
152
+ //
153
+ // This called mapMcpsToPhases with one argument where it takes three,
154
+ // so `phaseMatrix[currentPhase]` threw a TypeError on every project
155
+ // that had any MCP configured. The outer catch reported it as
156
+ // "Claude config detection skipped" — which reads like "nothing to
157
+ // detect" — and everything below it never ran: integrations.json was
158
+ // never written and the optimized settings were never generated.
151
159
  if (claudeConfigDetected.mcpServers.length > 0) {
152
- const phaseMap = mapMcpsToPhases(claudeConfigDetected.mcpServers);
153
- logger.blank();
154
- logger.dim('MCP usage by phase:');
155
- for (const [phase, mcps] of Object.entries(phaseMap)) {
156
- if (mcps.length > 0) {
157
- logger.dim(` ${phase}: ${mcps.map(m => m.name).join(', ')}`);
160
+ const byPhase = mcpAvailabilityByPhase(
161
+ claudeConfigDetected.mcpServers,
162
+ loadMcpRegistry().phaseMatrix || {}
163
+ );
164
+ if (byPhase.length > 0) {
165
+ logger.blank();
166
+ logger.dim('MCP usage by phase:');
167
+ for (const { phase, available } of byPhase) {
168
+ logger.dim(` ${phase}: ${available.join(', ')}`);
158
169
  }
159
170
  }
160
171
  }
@@ -151,65 +151,6 @@ export function getRequiredTemplates(stackId = null, projectPath = null) {
151
151
  return templates;
152
152
  }
153
153
 
154
- /**
155
- * Get infrastructure templates for a specific stack.
156
- *
157
- * @param {string} stackId - Stack identifier
158
- * @param {string} [projectPath] - Optional project path
159
- * @returns {Array} Infrastructure template array
160
- */
161
- export function getInfrastructureTemplates(stackId, projectPath = null) {
162
- const registry = loadTemplateRegistry(projectPath);
163
- return registry.infrastructureTemplates?.[stackId] || [];
164
- }
165
-
166
- /**
167
- * Get contract templates for a specific stack.
168
- *
169
- * @param {string} stackId - Stack identifier
170
- * @param {string} [projectPath] - Optional project path
171
- * @returns {Array} Contract template array
172
- */
173
- export function getContractTemplates(stackId, projectPath = null) {
174
- const registry = loadTemplateRegistry(projectPath);
175
- return registry.contractTemplates?.[stackId] || [];
176
- }
177
-
178
- /**
179
- * Get integration templates for a specific stack.
180
- *
181
- * @param {string} stackId - Stack identifier
182
- * @param {string} [projectPath] - Optional project path
183
- * @returns {Array} Integration template array
184
- */
185
- export function getIntegrationTemplates(stackId, projectPath = null) {
186
- const registry = loadTemplateRegistry(projectPath);
187
- return registry.integrationTemplates?.[stackId] || [];
188
- }
189
-
190
- /**
191
- * Get SaaS templates for a specific stack.
192
- *
193
- * @param {string} stackId - Stack identifier
194
- * @param {string} [projectPath] - Optional project path
195
- * @returns {Array} SaaS template array
196
- */
197
- export function getSaasTemplates(stackId, projectPath = null) {
198
- const registry = loadTemplateRegistry(projectPath);
199
- return registry.saasTemplates?.[stackId] || [];
200
- }
201
-
202
- /**
203
- * Get example templates.
204
- *
205
- * @param {string} [projectPath] - Optional project path
206
- * @returns {Array} Example template array
207
- */
208
- export function getExampleTemplates(projectPath = null) {
209
- const registry = loadTemplateRegistry(projectPath);
210
- return registry.exampleTemplates || [];
211
- }
212
-
213
154
  /**
214
155
  * Search templates by keyword (searches id, name, description).
215
156
  *
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, readdirSync, existsSync } from 'fs';
2
- import { join, basename, dirname } from 'path';
2
+ import { join, basename } from 'path';
3
+ import { getHookDefinitions } from '../../utils/hooks-installer.js';
3
4
 
4
5
  function findJsFiles(dir) {
5
6
  const results = [];
@@ -25,17 +26,74 @@ function extractImports(source) {
25
26
  return imports;
26
27
  }
27
28
 
29
+ /**
30
+ * Claude Code event name → the kebab-case key the Hook Inspector groups by.
31
+ * `pre-compact` and `teammate-idle` have no MORPH hooks — those nodes render
32
+ * empty on purpose.
33
+ */
34
+ const EVENT_KEYS = {
35
+ SessionStart: 'session-start',
36
+ UserPromptSubmit: 'user-prompt',
37
+ PreToolUse: 'pre-tool-use',
38
+ PostToolUse: 'post-tool-use',
39
+ Stop: 'stop',
40
+ SubagentStop: 'stop',
41
+ PreCompact: 'pre-compact',
42
+ };
43
+
44
+ /**
45
+ * Build filename → [{ event, matcher }] from the installer's MORPH_HOOKS.
46
+ *
47
+ * The event is NOT derivable from the path. Before the v4→v5 migration the
48
+ * hooks sat in per-event folders, so `basename(dirname(file))` produced the
49
+ * event key; the migration flattened everything into `core/`, and from then on
50
+ * every hook reported `event: 'core'`. Nothing crashed — the Inspector groups
51
+ * by event, found no key it knew, and rendered every node empty. Twelve hooks,
52
+ * zero shown.
53
+ *
54
+ * `getHookDefinitions()` is the single source of truth (the same array the
55
+ * installer writes into settings.local.json), so the registration and the
56
+ * drawing can no longer disagree.
57
+ */
58
+ export function hookRegistrations(definitions = getHookDefinitions()) {
59
+ const byFile = new Map();
60
+ for (const def of definitions) {
61
+ const key = EVENT_KEYS[def.event];
62
+ if (!key) continue;
63
+ for (const h of def.hooks || []) {
64
+ const file = basename(String(h.command || '').trim());
65
+ if (!file.endsWith('.js')) continue;
66
+ if (!byFile.has(file)) byFile.set(file, []);
67
+ byFile.get(file).push({ event: key, rawEvent: def.event, matcher: def.matcher || null });
68
+ }
69
+ }
70
+ return byFile;
71
+ }
72
+
28
73
  export function scanHooks(frameworkDir) {
29
74
  const hooksDir = join(frameworkDir, 'hooks', 'claude-code');
30
75
  if (!existsSync(hooksDir)) return [];
31
- return findJsFiles(hooksDir).map(filePath => {
76
+
77
+ const registrations = hookRegistrations();
78
+
79
+ return findJsFiles(hooksDir).flatMap(filePath => {
32
80
  const source = readFileSync(filePath, 'utf8');
33
- return {
34
- event: basename(dirname(filePath)),
81
+ const filename = basename(filePath);
82
+ const base = {
35
83
  name: basename(filePath, '.js'),
36
- filename: basename(filePath),
37
- source, imports: extractImports(source),
84
+ filename,
85
+ source,
86
+ imports: extractImports(source),
38
87
  description: extractJsDocDescription(source),
39
88
  };
89
+
90
+ // One entry per (file × event): telemetry-log is registered on both
91
+ // PostToolUse and UserPromptSubmit, and must show under both.
92
+ const regs = registrations.get(filename);
93
+ if (!regs || regs.length === 0) {
94
+ // On disk but never registered — real dead weight, and worth seeing.
95
+ return [{ ...base, event: 'unregistered', matcher: null }];
96
+ }
97
+ return regs.map(r => ({ ...base, event: r.event, rawEvent: r.rawEvent, matcher: r.matcher }));
40
98
  });
41
99
  }
@@ -7,9 +7,21 @@ export function scanStandards(frameworkDir, agents) {
7
7
  const sJson = JSON.parse(readFileSync(standardsJsonPath, 'utf8'));
8
8
  const refMap = {};
9
9
  for (const agent of agents) {
10
- for (const std of agent.standards) {
11
- if (!refMap[std.id]) refMap[std.id] = [];
12
- refMap[std.id].push({ agentId: agent.id, scope: std.scope, priority: std.priority });
10
+ for (const std of agent.standards || []) {
11
+ // agents.json stores standards as plain id STRINGS. Reading `std.id` off
12
+ // a string yields undefined, so every reference piled into refMap
13
+ // [undefined] and the lookup below missed all 60 — the Standards graph
14
+ // drew standards with no agent edge at all. The object form is the v4
15
+ // shape (`{ id, scope, priority }`); still accepted so a mixed or
16
+ // restored agents.json does not silently lose edges again.
17
+ const id = typeof std === 'string' ? std : std?.id;
18
+ if (!id) continue;
19
+ if (!refMap[id]) refMap[id] = [];
20
+ refMap[id].push({
21
+ agentId: agent.id,
22
+ scope: typeof std === 'string' ? null : (std.scope ?? null),
23
+ priority: typeof std === 'string' ? null : (std.priority ?? null),
24
+ });
13
25
  }
14
26
  }
15
27
  const list = sJson.standards.map(std => {
@@ -115,14 +115,24 @@ export function detectClaudeConfig(targetPath) {
115
115
  /**
116
116
  * Cross-reference configured MCPs with the MCP registry's phase matrix.
117
117
  *
118
+ * `phaseMatrix` and `currentPhase` default rather than being required: the
119
+ * `init` caller passed only `mcpServers`, so `phaseMatrix[currentPhase]` threw
120
+ * a TypeError that `init`'s outer catch swallowed as
121
+ * "Claude config detection skipped" — taking `saveIntegrations` and
122
+ * `generateSettings` down with it, since both run after this line.
123
+ *
118
124
  * @param {Array<{name: string}>} mcpServers - Detected MCP server list
119
- * @param {Object} phaseMatrix - From mcp-registry.json → phaseMatrix
120
- * @param {string} currentPhase - Current morph-spec phase
125
+ * @param {Object} [phaseMatrix] - From framework/mcp/registry.json → phaseMatrix
126
+ * @param {string} [currentPhase] - Phase to look up; omit for every phase
121
127
  * @returns {{ available: string[], recommended: string[], missing: string[] }}
122
128
  */
123
- export function mapMcpsToPhases(mcpServers, phaseMatrix, currentPhase) {
124
- const configuredNames = new Set(mcpServers.map(m => m.name.toLowerCase()));
125
- const recommended = (phaseMatrix[currentPhase] || []);
129
+ export function mapMcpsToPhases(mcpServers, phaseMatrix = {}, currentPhase = null) {
130
+ const configuredNames = new Set((mcpServers || []).map(m => m.name.toLowerCase()));
131
+ // No phase asked for → the union across every phase, deduped. That is what a
132
+ // caller showing "which of my MCPs this framework uses" wants.
133
+ const recommended = currentPhase
134
+ ? (phaseMatrix[currentPhase] || [])
135
+ : [...new Set(Object.values(phaseMatrix).flat())];
126
136
 
127
137
  return {
128
138
  available: recommended.filter(name => configuredNames.has(name)),
@@ -131,6 +141,28 @@ export function mapMcpsToPhases(mcpServers, phaseMatrix, currentPhase) {
131
141
  };
132
142
  }
133
143
 
144
+ /**
145
+ * The MCP-by-phase table `init` prints: every phase that recommends at least
146
+ * one of the MCPs this machine actually has configured.
147
+ *
148
+ * Separate from `mapMcpsToPhases` because the caller wants a per-phase
149
+ * breakdown, not one phase's slice — reaching for the wrong one is what
150
+ * produced `Object.entries()` over `{available, recommended, missing}` and a
151
+ * `.map(m => m.name)` over plain strings.
152
+ *
153
+ * @param {Array<{name: string}>} mcpServers
154
+ * @param {Object} phaseMatrix
155
+ * @returns {Array<{phase: string, available: string[]}>} phases with ≥1 match
156
+ */
157
+ export function mcpAvailabilityByPhase(mcpServers, phaseMatrix = {}) {
158
+ return Object.keys(phaseMatrix)
159
+ .map(phase => ({
160
+ phase,
161
+ available: mapMcpsToPhases(mcpServers, phaseMatrix, phase).available,
162
+ }))
163
+ .filter(({ available }) => available.length > 0);
164
+ }
165
+
134
166
  /**
135
167
  * Format a human-readable one-liner summary of detected config.
136
168
  *
@@ -16,6 +16,7 @@
16
16
 
17
17
  import fs from 'fs/promises';
18
18
  import path from 'path';
19
+ import { fileURLToPath } from 'url';
19
20
  import { existsSync, readFileSync } from 'fs';
20
21
  import { renderTemplate } from '../../core/templates/template-renderer.js';
21
22
 
@@ -469,14 +470,29 @@ function resolveTemplatePath(projectPath, templateRelPath) {
469
470
 
470
471
  // 2. CLI package framework/ (for running from source or during init)
471
472
  const cliFramework = path.join(
472
- path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')),
473
- '..', '..', '..', 'framework', 'templates', templateRelPath
473
+ cliRootDir(), 'framework', 'templates', templateRelPath
474
474
  );
475
475
  if (existsSync(cliFramework)) return cliFramework;
476
476
 
477
477
  return null;
478
478
  }
479
479
 
480
+ /**
481
+ * The CLI package root, as a real filesystem path.
482
+ *
483
+ * fileURLToPath, not `new URL(...).pathname`: the latter leaves the path
484
+ * percent-encoded, so an install under a directory with a space resolved to
485
+ * ".../Program%20Files/..." — a path that never exists. `resolveTemplatePath`
486
+ * then returned null and the caller fell through to its plain-text fallback
487
+ * WITHOUT any warning. The first lines of that fallback are identical to the
488
+ * template's, which is why nobody noticed the tables were gone. On Windows a
489
+ * global npm install lives under a spaced path by default, so this was the
490
+ * common case, not the edge one.
491
+ */
492
+ function cliRootDir() {
493
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
494
+ }
495
+
480
496
  /**
481
497
  * Render architecture context to .morph/context/README.md
482
498
  *
@@ -509,10 +525,7 @@ export async function renderArchitectureContext(projectPath, scanResult, options
509
525
  // Get morph-spec version
510
526
  let morphVersion = 'unknown';
511
527
  try {
512
- const pkgPath = path.join(
513
- path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')),
514
- '..', '..', '..', 'package.json'
515
- );
528
+ const pkgPath = path.join(cliRootDir(), 'package.json');
516
529
  if (existsSync(pkgPath)) {
517
530
  morphVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
518
531
  }
@@ -9,7 +9,6 @@ import { join } from 'path';
9
9
  * @property {boolean} isDotnet
10
10
  * @property {boolean} isNextjs
11
11
  * @property {string[]} defaultValidators
12
- * @property {string[]} checkpointValidators
13
12
  * @property {string|null} buildCommand
14
13
  */
15
14
 
@@ -21,19 +20,16 @@ const PROFILES = {
21
20
  dotnet: {
22
21
  isDotnet: true, isNextjs: false,
23
22
  defaultValidators: ['architecture', 'packages'],
24
- checkpointValidators: ['architecture', 'packages', 'design-system', 'security'],
25
23
  buildCommand: 'dotnet build --no-restore',
26
24
  },
27
25
  nextjs: {
28
26
  isDotnet: false, isNextjs: true,
29
27
  defaultValidators: ['nextjs-component'],
30
- checkpointValidators: ['nextjs-component', 'css'],
31
28
  buildCommand: 'npx tsc --noEmit',
32
29
  },
33
30
  empty: {
34
31
  isDotnet: false, isNextjs: false,
35
32
  defaultValidators: [],
36
- checkpointValidators: [],
37
33
  buildCommand: null,
38
34
  },
39
35
  };
@@ -57,7 +53,6 @@ function mergeProfiles(profiles) {
57
53
  isDotnet: profiles.some((p) => p.isDotnet),
58
54
  isNextjs: profiles.some((p) => p.isNextjs),
59
55
  defaultValidators: [...new Set(profiles.flatMap((p) => p.defaultValidators))],
60
- checkpointValidators: [...new Set(profiles.flatMap((p) => p.checkpointValidators))],
61
56
  buildCommand: profiles.map((p) => p.buildCommand).find(Boolean) ?? null,
62
57
  };
63
58
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { readFileSync } from 'fs';
11
+ import { join } from 'path';
11
12
  import chalk from 'chalk';
12
13
 
13
14
  /**
@@ -76,6 +77,11 @@ export class ArchitectureValidator {
76
77
  totalIssues: allIssues.length,
77
78
  errors: allIssues.filter(i => i.level === 'error').length,
78
79
  warnings: allIssues.filter(i => i.level === 'warning').length,
80
+ // Top-level too, not only nested under `results`: the runner reports
81
+ // "❌ 2 error(s)" from the counts but prints the issue list, and reading
82
+ // only the top level meant a failure arrived with no file, no line and
83
+ // no fix — impossible to act on.
84
+ issues: allIssues,
79
85
  results
80
86
  };
81
87
  }
@@ -84,7 +90,12 @@ export class ArchitectureValidator {
84
90
  * Validate Program.cs
85
91
  */
86
92
  async validateProgramCs() {
87
- const programCsPath = 'Program.cs';
93
+ // Resolved against `projectPath`, not the cwd. The constructor stored
94
+ // `projectPath` and nothing ever read it, so this validator inspected
95
+ // whatever directory the process happened to be in — in a worktree or a
96
+ // fleet run that is a different project, and reporting "no Program.cs,
97
+ // nothing to check" reads exactly like a pass.
98
+ const programCsPath = join(this.projectPath, 'Program.cs');
88
99
 
89
100
  try {
90
101
  const content = readFileSync(programCsPath, 'utf-8');
@@ -11,7 +11,7 @@ import { readFileSync, existsSync } from 'fs';
11
11
  import { join } from 'path';
12
12
  import { glob } from 'glob';
13
13
  import chalk from 'chalk';
14
- import { getAbsoluteOutputPath } from '../../../core/paths/output-schema.js';
14
+ import { getAbsoluteOutputPath, resolveContractsOutputType } from '../../../core/paths/output-schema.js';
15
15
 
16
16
  /**
17
17
  * Validate that contracts.cs interfaces are implemented in project code
@@ -22,7 +22,12 @@ import { getAbsoluteOutputPath } from '../../../core/paths/output-schema.js';
22
22
  * @returns {Object} { status, errors, warnings, issues, coverage }
23
23
  */
24
24
  export async function validateContracts(projectPath, featureName, options = {}) {
25
- const contractsPath = getAbsoluteOutputPath(projectPath, featureName, 'contracts');
25
+ // `'contracts'` is not an output type — `getAbsoluteOutputPath` threw
26
+ // "Unknown output type" on every call. The runner already resolves this the
27
+ // right way before deciding to schedule this validator
28
+ // (`resolveContractsOutputType()`); using the same resolver here is what
29
+ // keeps the two from disagreeing again.
30
+ const contractsPath = getAbsoluteOutputPath(projectPath, featureName, resolveContractsOutputType());
26
31
 
27
32
  if (!existsSync(contractsPath)) {
28
33
  return {
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { readFileSync } from 'fs';
11
+ import { join } from 'path';
11
12
  import { glob } from 'glob';
12
13
  import chalk from 'chalk';
13
14
 
@@ -82,11 +83,17 @@ export class PackageValidator {
82
83
  }
83
84
 
84
85
  const results = [];
86
+ const allIssues = [];
85
87
 
86
88
  for (const file of csprojFiles) {
87
- const result = await this.validateFile(file);
89
+ // glob returns paths relative to `cwd: this.projectPath`, so they only
90
+ // resolve against projectPath — reading them relative to process.cwd()
91
+ // raised ENOENT whenever the two differed (worktree, fleet, a verify
92
+ // launched from the primary root), and the throw read as a clean pass.
93
+ const result = await this.validateFile(join(this.projectPath, file));
88
94
  if (result.issues.length > 0) {
89
95
  results.push({ file, ...result });
96
+ allIssues.push(...result.issues.map(i => ({ ...i, file })));
90
97
  }
91
98
  }
92
99
 
@@ -94,6 +101,12 @@ export class PackageValidator {
94
101
  status: results.length === 0 ? 'ok' : 'error',
95
102
  totalFiles: csprojFiles.length,
96
103
  filesWithIssues: results.length,
104
+ // The runner's contract. Without these three the findings below were
105
+ // read as `errors: 0` and the run reported "✅ All validations passed"
106
+ // while holding a list of NuGet packages that do not build on .NET 10.
107
+ errors: allIssues.filter(i => i.level === 'error').length,
108
+ warnings: allIssues.filter(i => i.level === 'warning').length,
109
+ issues: allIssues,
97
110
  results
98
111
  };
99
112
  }
@@ -16,7 +16,11 @@ import { aggregateScores, loadRubric } from './score-aggregator.js';
16
16
 
17
17
  /**
18
18
  * Heavy validators skipped in 'light' mode (checkpoint-only).
19
- * Light mode runs only spec, content, and stack-specific lightweight validators.
19
+ *
20
+ * What light mode actually leaves behind is whatever the stack contributes that
21
+ * is NOT in this set — in practice `contrast` and `nextjs-component`. There are
22
+ * no `spec` or `content` validators; the switch in runSingleValidator has no
23
+ * case for either, and the old comment here promised both.
20
24
  */
21
25
  const HEAVY_VALIDATORS = new Set([
22
26
  'architecture', 'packages', 'contract-compliance',
@@ -49,27 +53,38 @@ export async function runValidation(projectPath, featureName, options = {}) {
49
53
  validators: {} // No validator-specific constraints
50
54
  };
51
55
 
52
- // Determine which validators to run
53
- const validatorIds = options.validators || detectValidators(projectPath);
56
+ // Determine which validators to run.
57
+ // COPY: the mode filter below used to splice in place, and when the caller
58
+ // passed `options.validators` that array WAS this array — so a light run
59
+ // emptied the caller's own list, and running the same list twice validated
60
+ // less the second time.
61
+ const detected = options.validators || detectValidators(projectPath);
62
+ let validatorIds = [...detected];
54
63
 
55
64
  // Filter by mode: 'light' skips heavy validators, 'full' (default) runs all
56
65
  const mode = options.mode || 'full';
66
+ const droppedByMode = [];
57
67
  if (mode === 'light') {
58
- const beforeCount = validatorIds.length;
59
- for (let i = validatorIds.length - 1; i >= 0; i--) {
60
- if (HEAVY_VALIDATORS.has(validatorIds[i])) {
61
- validatorIds.splice(i, 1);
62
- }
63
- }
68
+ validatorIds = validatorIds.filter(id => {
69
+ if (HEAVY_VALIDATORS.has(id)) { droppedByMode.push(id); return false; }
70
+ return true;
71
+ });
64
72
  }
65
73
 
66
74
  // Merge constraint-enabled validators
67
75
  validatorIds.push(...constraints.enabledValidators);
68
76
 
69
77
  if (validatorIds.length === 0) {
70
- result.summary = 'No validators ran — no validators configured for this stack';
71
- console.log(chalk.gray('\n ℹ️ No validators configured for this stack'));
78
+ // Say which of the two it is. On a pure `dotnet` project every default
79
+ // validator is heavy, so a light run legitimately runs none — but the old
80
+ // message blamed the stack ("no validators configured"), which reads as
81
+ // "your stack has nothing to check" when the full run has two.
82
+ result.summary = droppedByMode.length > 0
83
+ ? `No validators ran — light mode skipped ${droppedByMode.length} heavy validator(s): ${droppedByMode.join(', ')}`
84
+ : 'No validators ran — no validators configured for this stack';
85
+ console.log(chalk.gray(`\n ℹ️ ${result.summary.replace('No validators ran — ', '')}`));
72
86
  result._skipReason = result.summary;
87
+ result._droppedByMode = droppedByMode;
73
88
  return result;
74
89
  }
75
90
 
@@ -87,7 +102,12 @@ export async function runValidation(projectPath, featureName, options = {}) {
87
102
  // Positive check on isDotnet (contract-compliance is a VSA/.NET concern) —
88
103
  // NOT `!isNextjs`, which used to wrongly suppress it for a compound
89
104
  // "dotnet-nextjs" stack that has a .NET backend but is ALSO isNextjs.
90
- if (existsSync(contractsPath) && _profileForContracts.isDotnet) {
105
+ // `mode: 'light'` has to hold here too. This push sits AFTER the mode
106
+ // filter, so contract-compliance — which is in HEAVY_VALIDATORS — used to
107
+ // slip into every light run that had a contracts file, making light mode
108
+ // heavier than declared on exactly the compound stacks it matters for.
109
+ const contractsIsHeavyAndSkipped = mode === 'light' && HEAVY_VALIDATORS.has('contract-compliance');
110
+ if (existsSync(contractsPath) && _profileForContracts.isDotnet && !contractsIsHeavyAndSkipped) {
91
111
  validatorIds.push('contract-compliance');
92
112
  }
93
113
  }
@@ -107,27 +127,34 @@ export async function runValidation(projectPath, featureName, options = {}) {
107
127
  const validatorResult = await runSingleValidator(validatorId, projectPath, featureName, validatorOptions);
108
128
 
109
129
  if (validatorResult) {
110
- result.errors += validatorResult.errors || 0;
111
- result.warnings += validatorResult.warnings || 0;
112
-
113
- if (validatorResult.issues) {
114
- result.issues.push(...validatorResult.issues);
115
- }
130
+ const { errors, warnings, issues } = normalizeValidatorResult(validatorId, validatorResult);
131
+ result.errors += errors;
132
+ result.warnings += warnings;
133
+ result.issues.push(...issues);
116
134
 
117
135
  // Store per-validator results for hooks
118
- result.results[validatorId] = {
119
- passed: (validatorResult.errors || 0) === 0,
120
- issues: validatorResult.issues || []
121
- };
136
+ result.results[validatorId] = { passed: errors === 0, issues };
122
137
  }
123
138
  } catch (err) {
139
+ // A validator that throws is a FAILED validation, not a silent skip.
140
+ // This used to record `passed: false` in `results` and stop there —
141
+ // `result.errors` never moved, so `result.passed = errors === 0` stayed
142
+ // true and the run reported "✅ All validations passed". The only trace
143
+ // was a console line, and `verify` silences stdout. That is how a broken
144
+ // import (`css`) and a path bug (`packages` under a different cwd) went
145
+ // unnoticed: both threw, both read as clean.
124
146
  if (options.verbose) {
125
147
  console.log(chalk.yellow(` ⚠ Validator '${validatorId}' failed: ${err.message}`));
126
148
  }
127
- result.results[validatorId] = {
128
- passed: false,
129
- error: err.message
149
+ result.errors += 1;
150
+ const issue = {
151
+ level: 'error',
152
+ validator: validatorId,
153
+ message: `Validator '${validatorId}' crashed: ${err.message}`,
154
+ fix: 'This is a bug in the validator itself — the code under test was never checked.'
130
155
  };
156
+ result.issues.push(issue);
157
+ result.results[validatorId] = { passed: false, error: err.message, issues: [issue] };
131
158
  }
132
159
  }
133
160
 
@@ -160,6 +187,68 @@ function detectValidators(projectPath = '.') {
160
187
  return [...profile.defaultValidators];
161
188
  }
162
189
 
190
+ /**
191
+ * Reduce any validator's return value to the runner's contract:
192
+ * `{ errors, warnings, issues }`.
193
+ *
194
+ * Validators were written at different times and return different shapes. The
195
+ * runner used to read `errors`/`warnings`/`issues` off the raw object, so a
196
+ * validator that reports through any other shape contributed ZERO — and since
197
+ * `passed = errors === 0`, its findings were recorded as a pass. Measured: of
198
+ * seven validators only two returned all three keys. `packages` — one of the
199
+ * two defaults for the `dotnet` stack — returned
200
+ * `{status, totalFiles, filesWithIssues, results}` and had every NuGet
201
+ * incompatibility it found thrown away, while `verify` reported 10/10.
202
+ *
203
+ * Reading the nested `results` shape here keeps that contract in ONE place
204
+ * instead of asking every validator to remember it.
205
+ *
206
+ * @param {string} validatorId
207
+ * @param {Object} raw - whatever the validator returned
208
+ * @returns {{errors: number, warnings: number, issues: Array}}
209
+ */
210
+ export function normalizeValidatorResult(validatorId, raw) {
211
+ if (!raw || typeof raw !== 'object') return { errors: 0, warnings: 0, issues: [] };
212
+
213
+ // Issues can be top-level, or nested one level under `results` — either as
214
+ // an array of `{issues}` (packages: one entry per .csproj) or an object of
215
+ // them (architecture: keyed by check name). The nested read is a FALLBACK,
216
+ // never additive: validators that expose both would otherwise report every
217
+ // finding twice.
218
+ const topLevel = Array.isArray(raw.issues) ? raw.issues : [];
219
+ const nested = raw.results && typeof raw.results === 'object'
220
+ ? (Array.isArray(raw.results) ? raw.results : Object.values(raw.results))
221
+ .flatMap(r => (r && Array.isArray(r.issues)) ? r.issues : [])
222
+ : [];
223
+ const issues = (topLevel.length ? topLevel : nested)
224
+ .map(i => ({ validator: validatorId, ...i }));
225
+
226
+ // Prefer the validator's own counts when it reports them — it may weigh
227
+ // issues differently than one-each. Otherwise derive from the issue levels.
228
+ const counted = {
229
+ errors: issues.filter(i => i.level === 'error').length,
230
+ warnings: issues.filter(i => i.level === 'warning').length,
231
+ };
232
+ const errors = Number.isFinite(raw.errors) ? raw.errors : counted.errors;
233
+ const warnings = Number.isFinite(raw.warnings) ? raw.warnings : counted.warnings;
234
+
235
+ // Last resort: a validator that says `status: 'error'` but exposes no
236
+ // countable detail still must not read as a pass.
237
+ if (errors === 0 && raw.status === 'error') {
238
+ return {
239
+ errors: 1,
240
+ warnings,
241
+ issues: issues.length ? issues : [{
242
+ level: 'error',
243
+ validator: validatorId,
244
+ message: raw.message || `${validatorId} reported status "error" without detail`,
245
+ }],
246
+ };
247
+ }
248
+
249
+ return { errors, warnings, issues };
250
+ }
251
+
163
252
  /**
164
253
  * Run a single validator by ID
165
254
  */
@@ -181,8 +270,10 @@ async function runSingleValidator(validatorId, projectPath, featureName, options
181
270
  }
182
271
 
183
272
  case 'css': {
184
- const { validateCss } = await import('./css/css-validator.js');
185
- return await validateCss(projectPath, options);
273
+ // The module exports `validateCssClasses`; this asked for `validateCss`,
274
+ // which is undefined — the call threw on every invocation.
275
+ const { validateCssClasses } = await import('./css/css-validator.js');
276
+ return await validateCssClasses(projectPath, options);
186
277
  }
187
278
 
188
279
  case 'contract-compliance': {
@@ -201,10 +292,24 @@ async function runSingleValidator(validatorId, projectPath, featureName, options
201
292
  }
202
293
 
203
294
  default:
295
+ // A warning, not silence: an id with no case runs nothing, and returning
296
+ // a bare null made that indistinguishable from a validator that ran and
297
+ // found nothing. A typo in a validator list would have read as a clean
298
+ // pass. Not an error — an unknown id is a configuration mistake, not a
299
+ // defect in the code under validation.
204
300
  if (options.verbose) {
205
301
  console.log(chalk.gray(` Unknown validator: ${validatorId}`));
206
302
  }
207
- return null;
303
+ return {
304
+ status: 'ok',
305
+ errors: 0,
306
+ warnings: 1,
307
+ issues: [{
308
+ level: 'warning',
309
+ message: `Unknown validator '${validatorId}' — nothing was run for it`,
310
+ fix: 'Check the validator id against the cases in runSingleValidator().',
311
+ }],
312
+ };
208
313
  }
209
314
  }
210
315
 
@@ -1,89 +0,0 @@
1
- import { readFileSync, existsSync } from 'fs';
2
-
3
- /**
4
- * Content Validator - Validates structure and content of output files
5
- *
6
- * Ensures spec.md, tasks.json, and other outputs have required sections
7
- * and proper structure before allowing phase transitions.
8
- */
9
-
10
- /**
11
- * Validate spec.md structure
12
- * @param {string} specPath - Path to spec.md file
13
- * @returns {Object} Validation result
14
- */
15
- export function validateSpecContent(specPath) {
16
- if (!existsSync(specPath)) {
17
- return {
18
- valid: false,
19
- missing: ['File does not exist'],
20
- errors: ['Spec file not found at: ' + specPath]
21
- };
22
- }
23
-
24
- const content = readFileSync(specPath, 'utf8');
25
-
26
- // Required sections (flexible regex handles numbered variants like '## 1. Overview')
27
- const requiredSections = [
28
- { label: '## Overview', pattern: /^##\s+(?:\d+\.?\s+)?Overview/i },
29
- { label: '## Functional Requirements', pattern: /^##\s+(?:\d+\.?\s+)?Functional Requirements/i },
30
- { label: '## Technical', pattern: /^##\s+(?:\d+\.?\s+)?Technical/i },
31
- { label: '## Data Model', pattern: /^##\s+(?:\d+\.?\s+)?Data Model/i },
32
- ];
33
-
34
- const lines = content.split('\n');
35
- const missing = requiredSections.filter(s =>
36
- !lines.some(line => s.pattern.test(line))
37
- );
38
-
39
- // Additional quality checks
40
- const errors = [];
41
- const warnings = [];
42
-
43
- // Missing sections are warnings, not errors — not every spec needs all sections
44
- // (e.g., frontend-only features may not have Data Model, CLI tools may not have Technical Architecture)
45
- if (missing.length > 0) {
46
- missing.forEach(s => {
47
- warnings.push(`Missing recommended section: ${s.label}`);
48
- });
49
- }
50
-
51
- // Check minimum content length
52
- if (content.length < 500) {
53
- warnings.push('Spec seems very short (< 500 characters). Consider adding more detail.');
54
- }
55
-
56
- // Check for placeholder text that should be replaced
57
- const placeholders = ['TODO', 'TBD', 'PLACEHOLDER', '{{'];
58
- placeholders.forEach(placeholder => {
59
- if (content.includes(placeholder)) {
60
- warnings.push(`Spec contains "${placeholder}" - ensure all placeholders are replaced`);
61
- }
62
- });
63
-
64
- // Check for anti-patterns mentioned in the plan
65
- if (content.toLowerCase().includes('manual azure portal')) {
66
- errors.push(
67
- 'Spec mentions manual Azure portal creation. ' +
68
- 'All infrastructure must be defined in Bicep (Infrastructure as Code).'
69
- );
70
- }
71
-
72
- if (content.toLowerCase().includes('create resource manually')) {
73
- errors.push(
74
- 'Spec mentions manual resource creation. ' +
75
- 'Use IaC (Bicep) for all infrastructure resources.'
76
- );
77
- }
78
-
79
- return {
80
- valid: errors.length === 0,
81
- missing,
82
- errors,
83
- warnings,
84
- sections: {
85
- required: requiredSections.length,
86
- found: requiredSections.length - missing.length
87
- }
88
- };
89
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Content Validators
3
- *
4
- * Content and spec validation.
5
- */
6
-
7
- export * from './content-validator.js';