@polymorphism-tech/morph-spec 8.1.1 → 8.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1.1
6
+ **Version:** 8.1.2
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -394,4 +394,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
394
394
 
395
395
  ---
396
396
 
397
- *morph-spec v8.1.1 by [Polymorphism Tech](https://polymorphism.tech)*
397
+ *morph-spec v8.1.2 by [Polymorphism Tech](https://polymorphism.tech)*
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.1.1",
3
+ "version": "8.1.2",
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",
@@ -237,4 +237,4 @@ These files are never edited directly. Use CLI commands or `morph-spec update` i
237
237
 
238
238
  ---
239
239
 
240
- *morph-spec v8.1.1 by Polymorphism Tech*
240
+ *morph-spec v8.1.2 by Polymorphism Tech*
@@ -256,4 +256,4 @@ morph-spec doctor
256
256
 
257
257
  ---
258
258
 
259
- *morph-spec v8.1.1 by Polymorphism Tech*
259
+ *morph-spec v8.1.2 by Polymorphism Tech*
@@ -179,8 +179,9 @@ try {
179
179
  if (existsSync(configPath)) {
180
180
  const config = JSON.parse(readFileSync(configPath, 'utf-8'));
181
181
  const stack = config?.project?.stack;
182
+ const { frontendPath, backendPath } = config?.project ?? {};
182
183
  if (stack && currentPhase === 'implement') {
183
- const buildResult = runBuildCheck(cwd, stack);
184
+ const buildResult = runBuildCheck(cwd, stack, undefined, { frontendPath, backendPath });
184
185
  if (buildResult.status === 'fail') {
185
186
  warnings.push('⚠ Build/typecheck failed:');
186
187
  warnings.push(buildResult.output);
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Build/Typecheck verification utility.
3
3
  *
4
- * Stack-aware: determines the correct build command based on project stack.
5
- * Used by the Stop hook to verify code health before session ends.
4
+ * Stack-aware: determines the correct build command(s) based on project stack.
5
+ * Used by the Stop hook and `morph-spec verify` to verify code health.
6
6
  *
7
7
  * Pure functions — no I/O except the actual build execution.
8
8
  */
@@ -11,8 +11,31 @@ import { execSync } from 'child_process';
11
11
  import { existsSync } from 'fs';
12
12
  import { join } from 'path';
13
13
 
14
+ const DOTNET_BUILD_CMD = 'dotnet build --no-restore --nologo -v q';
15
+ const TSC_BUILD_CMD = 'npx tsc --noEmit';
16
+
17
+ function isDotnetStack(s) {
18
+ return s.includes('dotnet') || s.includes('aspnet');
19
+ }
20
+
21
+ function isFrontendStack(s) {
22
+ return s.includes('nextjs') || s.includes('node') || s.includes('react') || s.includes('vue');
23
+ }
24
+
14
25
  /**
15
- * Determine the build command for a given stack.
26
+ * Determine the build command for a SINGLE (non-monorepo) stack string.
27
+ *
28
+ * Kept for backward compatibility with single-stack projects and existing
29
+ * callers. A monorepo stack that mixes a frontend token with "dotnet" (e.g.
30
+ * the detector's own `${frontendStack}+dotnet` format, or a hyphenated
31
+ * `dotnet-nextjs`) should resolve through `getBuildSteps()` instead: this
32
+ * function alone can only ever return ONE command, and — because it checks
33
+ * `dotnet` before the frontend tokens — a compound stack always resolves to
34
+ * `dotnet build` even when the frontend is what actually needs checking (or
35
+ * when there's no root-level `.sln`/`.csproj`, e.g. in a monorepo where the
36
+ * backend lives in a subfolder). `getBuildSteps()` runs BOTH sides, each
37
+ * from its own working directory, instead of one command winning by
38
+ * substring match order.
16
39
  *
17
40
  * @param {string} stack - Project stack (e.g., 'dotnet-nextjs', 'nextjs')
18
41
  * @param {boolean} [hasTsConfig] - Whether tsconfig.json exists (for TS projects)
@@ -22,17 +45,53 @@ export function getBuildCommand(stack, hasTsConfig = false) {
22
45
  if (!stack) return null;
23
46
  const s = stack.toLowerCase();
24
47
 
25
- if (s.includes('dotnet')) {
26
- return 'dotnet build --no-restore --nologo -v q';
48
+ if (isDotnetStack(s)) {
49
+ return DOTNET_BUILD_CMD;
27
50
  }
28
51
 
29
- if ((s.includes('nextjs') || s.includes('node') || s.includes('react')) && hasTsConfig) {
30
- return 'npx tsc --noEmit';
52
+ if (isFrontendStack(s) && hasTsConfig) {
53
+ return TSC_BUILD_CMD;
31
54
  }
32
55
 
33
56
  return null;
34
57
  }
35
58
 
59
+ /**
60
+ * Resolve the build step(s) to run for a project, monorepo-aware.
61
+ *
62
+ * A monorepo stack mixes a frontend token and "dotnet" (see
63
+ * `detectProject()`'s own `${frontendStack}+dotnet` format) — in that case
64
+ * BOTH sides are checked, each from its own working directory
65
+ * (`frontendPath` / `backendPath`, as stored in
66
+ * `.morph/config/config.json → project`), instead of one command silently
67
+ * winning — or running from the wrong cwd — by substring match order.
68
+ * Single-stack projects (no `frontendPath`/`backendPath`, or a stack that
69
+ * only matches one side) resolve to exactly one step, from `cwd`, matching
70
+ * `getBuildCommand()`'s existing single-stack behavior.
71
+ *
72
+ * @param {string} stack
73
+ * @param {{ cwd: string, frontendPath?: string, backendPath?: string }} opts
74
+ * @returns {Array<{ command: string, cwd: string }>}
75
+ */
76
+ export function getBuildSteps(stack, { cwd, frontendPath, backendPath } = {}) {
77
+ if (!stack || !cwd) return [];
78
+ const s = stack.toLowerCase();
79
+ const steps = [];
80
+
81
+ if (isDotnetStack(s)) {
82
+ steps.push({ command: DOTNET_BUILD_CMD, cwd: backendPath ? join(cwd, backendPath) : cwd });
83
+ }
84
+
85
+ if (isFrontendStack(s)) {
86
+ const frontendDir = frontendPath ? join(cwd, frontendPath) : cwd;
87
+ if (existsSync(join(frontendDir, 'tsconfig.json'))) {
88
+ steps.push({ command: TSC_BUILD_CMD, cwd: frontendDir });
89
+ }
90
+ }
91
+
92
+ return steps;
93
+ }
94
+
36
95
  /**
37
96
  * Parse build command result into a structured object.
38
97
  *
@@ -52,28 +111,42 @@ export function parseBuildResult(exitCode, output) {
52
111
  }
53
112
 
54
113
  /**
55
- * Run build check for a project.
114
+ * Run build check for a project — monorepo-aware (see `getBuildSteps()`).
115
+ *
116
+ * For a compound stack (e.g. "nextjs+dotnet"), runs each resolved step in
117
+ * its own working directory and fails if any step fails; output from all
118
+ * failing steps is concatenated (prefixed by the command that produced it)
119
+ * so a frontend typecheck failure and a backend build failure are never
120
+ * silently masked by one another.
56
121
  *
57
122
  * @param {string} cwd - Project root directory
58
123
  * @param {string} stack - Project stack string
59
124
  * @param {number} [timeout=30000] - Timeout in ms
125
+ * @param {{ frontendPath?: string, backendPath?: string }} [paths] - Monorepo sub-paths, from config.json → project
60
126
  * @returns {{ status: 'pass'|'fail'|'skip', output: string }}
61
127
  */
62
- export function runBuildCheck(cwd, stack, timeout = 30000) {
63
- const hasTsConfig = existsSync(join(cwd, 'tsconfig.json'));
64
- const cmd = getBuildCommand(stack, hasTsConfig);
65
- if (!cmd) return { status: 'skip', output: '' };
66
-
67
- try {
68
- const output = execSync(cmd, {
69
- cwd,
70
- encoding: 'utf-8',
71
- timeout,
72
- stdio: ['pipe', 'pipe', 'pipe']
73
- });
74
- return parseBuildResult(0, output);
75
- } catch (err) {
76
- const output = (err.stdout || '') + (err.stderr || '');
77
- return parseBuildResult(err.status || 1, output);
128
+ export function runBuildCheck(cwd, stack, timeout = 30000, paths = {}) {
129
+ const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath });
130
+ if (steps.length === 0) return { status: 'skip', output: '' };
131
+
132
+ const failures = [];
133
+ for (const step of steps) {
134
+ try {
135
+ execSync(step.command, {
136
+ cwd: step.cwd,
137
+ encoding: 'utf-8',
138
+ timeout,
139
+ stdio: ['pipe', 'pipe', 'pipe'],
140
+ });
141
+ } catch (err) {
142
+ const output = (err.stdout || '') + (err.stderr || '');
143
+ const parsed = parseBuildResult(err.status || 1, output);
144
+ failures.push(`[${step.command}] ${parsed.output}`);
145
+ }
146
+ }
147
+
148
+ if (failures.length > 0) {
149
+ return { status: 'fail', output: failures.join('\n\n') };
78
150
  }
151
+ return { status: 'pass', output: '' };
79
152
  }
@@ -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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
8
8
  ---
9
9
 
10
10
  # morph-implement
@@ -4,7 +4,7 @@ description: Guia a produção dos três artefatos de planejamento de uma featur
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1"
7
+ cliVersion: "8.1.2"
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.1.1",
3
+ "version": "8.1.2",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -18,7 +18,7 @@
18
18
  import { existsSync, readFileSync } from 'fs';
19
19
  import { join } from 'path';
20
20
 
21
- import { runBuildCheck, getBuildCommand } from '../../../framework/hooks/shared/build-check.js';
21
+ import { runBuildCheck, getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
22
22
  import { runTestSuite, resolveTestCommand } from '../tasks/test-runner.js';
23
23
  import { runValidation } from './validation-runner.js';
24
24
  import { loadRubric, aggregateScores } from './score-aggregator.js';
@@ -58,12 +58,13 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
58
58
  } = options;
59
59
 
60
60
  const stack = readStack(projectPath);
61
+ const paths = readProjectPaths(projectPath);
61
62
 
62
63
  const nodes = {};
63
64
  const feedback = [];
64
65
 
65
66
  // 1. build
66
- nodes.build = await runBuildNode({ projectPath, stack, timeout, skipBuild, _runBuildCheck });
67
+ nodes.build = await runBuildNode({ projectPath, stack, paths, timeout, skipBuild, _runBuildCheck });
67
68
  appendBuildFeedback(feedback, nodes.build);
68
69
 
69
70
  // 2. tests — skipped entirely (no invocation) when build already failed.
@@ -94,20 +95,20 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
94
95
  // Build node
95
96
  // ============================================================================
96
97
 
97
- async function runBuildNode({ projectPath, stack, timeout, skipBuild, _runBuildCheck }) {
98
+ async function runBuildNode({ projectPath, stack, paths = {}, timeout, skipBuild, _runBuildCheck }) {
98
99
  if (skipBuild) {
99
100
  return { status: 'skip', command: null, durationMs: 0, reason: 'requested', output: '' };
100
101
  }
101
102
 
102
- const hasTsConfig = existsSync(join(projectPath, 'tsconfig.json'));
103
- const command = getBuildCommand(stack, hasTsConfig);
104
- if (!command) {
103
+ const steps = getBuildSteps(stack, { cwd: projectPath, frontendPath: paths.frontendPath, backendPath: paths.backendPath });
104
+ if (steps.length === 0) {
105
105
  const reason = stack ? `no build command for stack "${stack}"` : 'no stack configured';
106
106
  return { status: 'skip', command: null, durationMs: 0, reason, output: '' };
107
107
  }
108
+ const command = steps.map((s) => s.command).join(' && ');
108
109
 
109
110
  const start = Date.now();
110
- const result = await _runBuildCheck(projectPath, stack, timeout);
111
+ const result = await _runBuildCheck(projectPath, stack, timeout, paths);
111
112
  const durationMs = Date.now() - start;
112
113
 
113
114
  return {
@@ -311,6 +312,22 @@ function readStack(projectPath) {
311
312
  }
312
313
  }
313
314
 
315
+ /** Read frontend/backend sub-paths for a monorepo, so the build node can run
316
+ * each stack's build command from its own directory instead of assuming a
317
+ * single root (see getBuildSteps() in build-check.js). Absent in single-stack
318
+ * projects — every consumer treats missing paths as "use projectPath". */
319
+ function readProjectPaths(projectPath) {
320
+ try {
321
+ const configPath = join(projectPath, '.morph', 'config', 'config.json');
322
+ if (!existsSync(configPath)) return {};
323
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
324
+ const { frontendPath, backendPath } = config?.project ?? {};
325
+ return { frontendPath, backendPath };
326
+ } catch {
327
+ return {};
328
+ }
329
+ }
330
+
314
331
  function linesOf(text) {
315
332
  const trimmed = (text || '').trim();
316
333
  return trimmed ? trimmed.split('\n') : [];