@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 +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/hooks/claude-code/core/validate-completion.js +2 -1
- package/framework/hooks/shared/build-check.js +97 -24
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +1 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/lib/validators/verify-runner.js +24 -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.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.
|
|
397
|
+
*morph-spec v8.1.2 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
26
|
-
return
|
|
48
|
+
if (isDotnetStack(s)) {
|
|
49
|
+
return DOTNET_BUILD_CMD;
|
|
27
50
|
}
|
|
28
51
|
|
|
29
|
-
if ((s
|
|
30
|
-
return
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
7
|
+
cliVersion: "8.1.2"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
7
|
+
cliVersion: "8.1.2"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { existsSync, readFileSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
20
|
|
|
21
|
-
import { runBuildCheck,
|
|
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
|
|
103
|
-
|
|
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') : [];
|