@polymorphism-tech/morph-spec 8.32.0 → 8.34.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/CLAUDE.md +4 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +26 -2
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +4 -5
- package/framework/agents.json +1 -0
- package/framework/docs/CLI.md +71 -68
- package/framework/hooks/README.md +11 -1
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
- package/framework/hooks/claude-code/core/chain.js +27 -7
- package/framework/hooks/claude-code/core/worktree-context.js +83 -14
- package/framework/hooks/claude-code/core/worktree-create.js +206 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
- package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/command-tokens.js +23 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/git-paths.js +25 -0
- package/framework/hooks/shared/hook-response.js +17 -0
- package/framework/hooks/shared/morph-cli.js +134 -0
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rules/infrastructure-standards.md +3 -1
- 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 +3 -3
- 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 +3 -1
- package/framework/skills/core/morph-plan/SKILL.md +6 -3
- 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/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +30 -0
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +231 -231
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +88 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +1 -1
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/e2e/e2e.js +77 -39
- package/src/commands/index.js +13 -13
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +8 -6
- package/src/commands/project/env.js +95 -0
- package/src/commands/project/worktree.js +266 -52
- package/src/commands/state/delete.js +8 -0
- package/src/commands/state/finish.js +29 -34
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +430 -418
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +5 -1
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/e2e/compose-env.js +6 -11
- package/src/lib/e2e/stack-teardown.js +115 -0
- package/src/lib/generators/architecture-scanner.js +2 -1
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/graph/graphifyignore.js +2 -1
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +218 -39
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/verify-runner.js +37 -4
- package/src/lib/worktree/dirs.js +32 -0
- package/src/lib/worktree/env-allocator.js +45 -11
- package/src/lib/worktree/worktree-audit.js +40 -2
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +18 -13
- package/src/utils/hooks-installer.js +46 -3
- package/src/utils/skills-installer.js +187 -187
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
|
@@ -8,32 +8,46 @@
|
|
|
8
8
|
* .sln) → "dotnet test <proj> --nologo -v q" for each match
|
|
9
9
|
* 4. package.json present AND has scripts.test → "npm test"
|
|
10
10
|
* 5. null (no test suite detected)
|
|
11
|
+
*
|
|
12
|
+
* Step 3 deliberately does NOT see linked git worktrees. A worktree is another
|
|
13
|
+
* feature's checkout of the SAME repo: its `tests/X.Tests.csproj` is a copy of
|
|
14
|
+
* the one already matched at the root, so including it both doubles the wall
|
|
15
|
+
* clock and reports on code that is not the tree under verification. In the
|
|
16
|
+
* field this produced a red `tests` node — "fail" at exactly the timeout, with
|
|
17
|
+
* the first run's own summary reading "Com falha: 0" — while nothing had
|
|
18
|
+
* failed. Two independent filters carry that rule (see collectDotnetTestProjects):
|
|
19
|
+
* the CONVENTION path (worktrees/, .worktrees/) and, for worktrees created
|
|
20
|
+
* anywhere else, the registry git itself keeps.
|
|
11
21
|
*/
|
|
12
22
|
|
|
13
|
-
import { execSync } from 'child_process';
|
|
14
|
-
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
15
|
-
import { join } from 'path';
|
|
23
|
+
import { execSync, execFileSync } from 'child_process';
|
|
24
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from 'fs';
|
|
25
|
+
import { join, resolve, relative, isAbsolute } from 'path';
|
|
16
26
|
import { globSync } from 'glob';
|
|
17
27
|
|
|
28
|
+
import { WORKTREES_DIR, LEGACY_WORKTREES_DIR } from '../../commands/project/worktree.js';
|
|
29
|
+
|
|
18
30
|
/**
|
|
19
|
-
*
|
|
31
|
+
* Timeout budget granted to ONE test project. The chained `a && b` command is
|
|
32
|
+
* given this much per link: a fixed cap made every multi-project chain born
|
|
33
|
+
* condemned — it timed out for being long, not for being broken.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_TEST_TIMEOUT_PER_PROJECT = 120_000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the test command for a project, together with how many test projects
|
|
39
|
+
* it chains — the timeout budget is derived from that count.
|
|
20
40
|
*
|
|
21
41
|
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
22
|
-
* @returns {string | null
|
|
42
|
+
* @returns {{ command: string | null, projectCount: number, source: string }}
|
|
43
|
+
* `source` is one of 'config' | 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'.
|
|
23
44
|
*/
|
|
24
|
-
export function
|
|
45
|
+
export function resolveTestPlan(projectPath = '.') {
|
|
25
46
|
// 1. Read from .morph/config/config.json
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const cmd = config?.project?.testCommand;
|
|
31
|
-
if (cmd && typeof cmd === 'string') {
|
|
32
|
-
return cmd;
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
// Malformed config — fall through to detection
|
|
36
|
-
}
|
|
47
|
+
const config = readConfig(projectPath);
|
|
48
|
+
const configured = config?.project?.testCommand;
|
|
49
|
+
if (configured && typeof configured === 'string') {
|
|
50
|
+
return { command: configured, projectCount: 1, source: 'config' };
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
// 2. Detect .NET project (sln/csproj at root) → dotnet test
|
|
@@ -43,7 +57,7 @@ export function resolveTestCommand(projectPath = '.') {
|
|
|
43
57
|
(entry) => entry.endsWith('.sln') || entry.endsWith('.csproj')
|
|
44
58
|
);
|
|
45
59
|
if (hasDotnetProject) {
|
|
46
|
-
return 'dotnet test';
|
|
60
|
+
return { command: 'dotnet test', projectCount: 1, source: 'dotnet-root' };
|
|
47
61
|
}
|
|
48
62
|
} catch {
|
|
49
63
|
// Unreadable project directory — fall through to next detection
|
|
@@ -53,19 +67,13 @@ export function resolveTestCommand(projectPath = '.') {
|
|
|
53
67
|
// (e.g. tests/MyApp.Tests/MyApp.Tests.csproj), common in monorepos that
|
|
54
68
|
// have no root .sln. Run each matched project explicitly rather than a
|
|
55
69
|
// bare `dotnet test` (which finds nothing to build at the root).
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.sort()
|
|
64
|
-
.map((p) => `dotnet test "${p}" --nologo -v q`)
|
|
65
|
-
.join(' && ');
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
// Glob failure — fall through to next detection
|
|
70
|
+
const testProjects = collectDotnetTestProjects(projectPath);
|
|
71
|
+
if (testProjects.length > 0) {
|
|
72
|
+
return {
|
|
73
|
+
command: testProjects.map((p) => `dotnet test "${p}" --nologo -v q`).join(' && '),
|
|
74
|
+
projectCount: testProjects.length,
|
|
75
|
+
source: 'dotnet-scan',
|
|
76
|
+
};
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
// 4. package.json present AND has scripts.test → npm test
|
|
@@ -74,7 +82,7 @@ export function resolveTestCommand(projectPath = '.') {
|
|
|
74
82
|
try {
|
|
75
83
|
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
76
84
|
if (pkg?.scripts?.test) {
|
|
77
|
-
return 'npm test';
|
|
85
|
+
return { command: 'npm test', projectCount: 1, source: 'npm' };
|
|
78
86
|
}
|
|
79
87
|
} catch {
|
|
80
88
|
// Malformed package.json — fall through to null
|
|
@@ -82,43 +90,214 @@ export function resolveTestCommand(projectPath = '.') {
|
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
// 5. Nothing found
|
|
85
|
-
return null;
|
|
93
|
+
return { command: null, projectCount: 0, source: 'none' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the test command for a project.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
100
|
+
* @returns {string | null} The test command to run, or null if none found
|
|
101
|
+
*/
|
|
102
|
+
export function resolveTestCommand(projectPath = '.') {
|
|
103
|
+
return resolveTestPlan(projectPath).command;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find the *Tests.csproj files that belong to THIS tree — never to a linked
|
|
108
|
+
* worktree of the same repo.
|
|
109
|
+
*
|
|
110
|
+
* Two filters, because one is not enough:
|
|
111
|
+
* - the glob ignore covers worktrees created by convention (`worktrees/{f}`
|
|
112
|
+
* and the legacy `.worktrees/{f}`), which is where `morph-spec worktree
|
|
113
|
+
* setup` puts them;
|
|
114
|
+
* - `git worktree list --porcelain` covers every OTHER location, including a
|
|
115
|
+
* worktree someone created by hand outside the convention. Fail-open: no
|
|
116
|
+
* git, no answer, no filter — the convention filter still stands.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} projectPath
|
|
119
|
+
* @returns {string[]} Project-relative csproj paths, sorted
|
|
120
|
+
*/
|
|
121
|
+
function collectDotnetTestProjects(projectPath) {
|
|
122
|
+
let matches;
|
|
123
|
+
try {
|
|
124
|
+
matches = globSync('**/*Tests.csproj', {
|
|
125
|
+
cwd: projectPath,
|
|
126
|
+
ignore: [
|
|
127
|
+
'**/bin/**',
|
|
128
|
+
'**/obj/**',
|
|
129
|
+
'**/node_modules/**',
|
|
130
|
+
`${WORKTREES_DIR}/**`,
|
|
131
|
+
`${LEGACY_WORKTREES_DIR}/**`,
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
} catch {
|
|
135
|
+
// Glob failure — caller falls through to the next detection step
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const linked = listLinkedWorktreePaths(projectPath);
|
|
140
|
+
if (linked.length > 0) {
|
|
141
|
+
matches = matches.filter(
|
|
142
|
+
(p) => !linked.some((wt) => isInside(canonicalPath(resolve(projectPath, p)), wt))
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return matches.sort();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* One spelling per directory. Git prints the LONG form of a path; the tree
|
|
151
|
+
* under verification may be addressed by its 8.3 SHORT form (`MORPHT~1` —
|
|
152
|
+
* every GitHub Windows runner's temp dir, and any user path with a short
|
|
153
|
+
* name). `path.resolve` does not expand aliases, so `isInside(long, short)`
|
|
154
|
+
* says "not inside" for the same directory and a linked worktree leaks into
|
|
155
|
+
* the test command. Only `realpathSync.native` asks the OS. Fail-open to the
|
|
156
|
+
* plain resolve for a path that does not exist (yet).
|
|
157
|
+
*/
|
|
158
|
+
function canonicalPath(p) {
|
|
159
|
+
try {
|
|
160
|
+
return realpathSync.native(p);
|
|
161
|
+
} catch {
|
|
162
|
+
return resolve(p);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Absolute paths of the git worktrees linked to this repo, excluding the tree
|
|
168
|
+
* being verified. Fail-open — a repo without git (or a git that errors) yields
|
|
169
|
+
* an empty list, and discovery proceeds on the convention filter alone.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} projectPath
|
|
172
|
+
* @returns {string[]}
|
|
173
|
+
*/
|
|
174
|
+
function listLinkedWorktreePaths(projectPath) {
|
|
175
|
+
let out;
|
|
176
|
+
try {
|
|
177
|
+
out = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
178
|
+
cwd: projectPath,
|
|
179
|
+
stdio: 'pipe',
|
|
180
|
+
encoding: 'utf8',
|
|
181
|
+
windowsHide: true,
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const self = canonicalPath(projectPath);
|
|
188
|
+
return out
|
|
189
|
+
.split('\n')
|
|
190
|
+
.filter((line) => line.startsWith('worktree '))
|
|
191
|
+
.map((line) => canonicalPath(line.slice('worktree '.length).trim()))
|
|
192
|
+
.filter((p) => p && p !== self);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** True when `child` is `parent` or lives under it. */
|
|
196
|
+
function isInside(child, parent) {
|
|
197
|
+
const rel = relative(parent, child);
|
|
198
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readConfig(projectPath) {
|
|
202
|
+
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
203
|
+
if (!existsSync(configPath)) return null;
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(readFileSync(configPath, 'utf8'));
|
|
206
|
+
} catch {
|
|
207
|
+
// Malformed config — treated as absent by every caller
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resolve the timeout budget for the test node.
|
|
214
|
+
*
|
|
215
|
+
* Precedence: explicit override (`morph-spec verify --timeout`) >
|
|
216
|
+
* `config.project.testTimeoutMs` > default scaled by the number of chained
|
|
217
|
+
* test projects.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} projectPath
|
|
220
|
+
* @param {number} projectCount - How many test projects the command chains
|
|
221
|
+
* @param {number} [override] - Explicit override in ms, if the caller has one
|
|
222
|
+
* @returns {number} Timeout in ms
|
|
223
|
+
*/
|
|
224
|
+
export function resolveTestTimeout(projectPath, projectCount, override) {
|
|
225
|
+
if (Number.isFinite(override) && override > 0) return override;
|
|
226
|
+
|
|
227
|
+
const configured = readConfig(projectPath)?.project?.testTimeoutMs;
|
|
228
|
+
if (Number.isFinite(configured) && configured > 0) return configured;
|
|
229
|
+
|
|
230
|
+
return DEFAULT_TEST_TIMEOUT_PER_PROJECT * Math.max(1, projectCount);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Count the test runs that reported a summary before the command was cut off.
|
|
235
|
+
* Heuristic by construction (the summary line is localized), and used only to
|
|
236
|
+
* name WHICH link of the chain was still running when the clock ran out.
|
|
237
|
+
*/
|
|
238
|
+
function countCompletedRuns(output) {
|
|
239
|
+
const matches = (output || '').match(
|
|
240
|
+
/(?:^|\n)\s*(?:Aprovado|Passed|Failed|Reprovado|Com falha|Erro)!/gi
|
|
241
|
+
);
|
|
242
|
+
return matches ? matches.length : 0;
|
|
86
243
|
}
|
|
87
244
|
|
|
88
245
|
/**
|
|
89
246
|
* Run the project's test suite.
|
|
90
247
|
*
|
|
91
248
|
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
92
|
-
* @param {object} [opts]
|
|
249
|
+
* @param {object} [opts]
|
|
250
|
+
* @param {number} [opts.timeout] - Explicit timeout in ms (highest precedence)
|
|
93
251
|
* @param {Function} [opts._execSync] - Override execSync for unit testing
|
|
94
|
-
* @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string }}
|
|
252
|
+
* @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string, timedOut?: boolean, timeoutMs?: number, message?: string }}
|
|
95
253
|
*/
|
|
96
254
|
export function runTestSuite(projectPath = '.', opts = {}) {
|
|
97
|
-
const cmd =
|
|
255
|
+
const { command: cmd, projectCount } = resolveTestPlan(projectPath);
|
|
98
256
|
|
|
99
257
|
if (!cmd) {
|
|
100
258
|
return { skipped: true, reason: 'No testCommand configured and no known project file detected' };
|
|
101
259
|
}
|
|
102
260
|
|
|
103
261
|
const exec = opts._execSync ?? execSync;
|
|
262
|
+
const timeout = resolveTestTimeout(projectPath, projectCount, opts.timeout);
|
|
104
263
|
|
|
105
264
|
try {
|
|
106
265
|
const output = exec(cmd, {
|
|
107
266
|
cwd: projectPath,
|
|
108
|
-
timeout
|
|
267
|
+
timeout,
|
|
109
268
|
encoding: 'utf8',
|
|
110
269
|
stdio: 'pipe'
|
|
111
270
|
});
|
|
112
|
-
return { passed: true, output: output ?? '', exitCode: 0 };
|
|
271
|
+
return { passed: true, output: output ?? '', exitCode: 0, timeoutMs: timeout };
|
|
113
272
|
} catch (err) {
|
|
114
273
|
const output = (err.stdout || '') + (err.stderr || '');
|
|
274
|
+
|
|
275
|
+
// A timeout is NOT a red suite: the command was interrupted, so no verdict
|
|
276
|
+
// exists. Saying so by name is the whole point — the field diagnosis of
|
|
277
|
+
// this cost a reading of durationMs against a truncated log.
|
|
278
|
+
if (err.code === 'ETIMEDOUT' || err.killed === true) {
|
|
279
|
+
const total = Math.max(1, projectCount);
|
|
280
|
+
const at = Math.min(countCompletedRuns(output) + 1, total);
|
|
281
|
+
const where = total > 1 ? ` na execução ${at} de ${total}` : '';
|
|
282
|
+
return {
|
|
283
|
+
passed: false,
|
|
284
|
+
timedOut: true,
|
|
285
|
+
timeoutMs: timeout,
|
|
286
|
+
buildFailed: false,
|
|
287
|
+
message: `timeout após ${timeout} ms${where} — nenhum teste falhou, o comando foi interrompido`,
|
|
288
|
+
output,
|
|
289
|
+
exitCode: err.status ?? null,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
115
293
|
const BUILD_FAIL = /Build FAILED|error MSB\d+|error CS\d+|Could not find.*\.csproj/i;
|
|
116
294
|
const buildFailed = BUILD_FAIL.test(output);
|
|
117
295
|
return {
|
|
118
296
|
passed: false,
|
|
119
297
|
buildFailed,
|
|
120
298
|
output,
|
|
121
|
-
exitCode: err.status ?? 1
|
|
299
|
+
exitCode: err.status ?? 1,
|
|
300
|
+
timeoutMs: timeout,
|
|
122
301
|
};
|
|
123
302
|
}
|
|
124
303
|
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checkpoint Validation Persistence
|
|
3
|
-
*
|
|
4
|
-
* Persists checkpoint validation results to disk for traceability.
|
|
5
|
-
* Each checkpoint writes a JSON file to:
|
|
6
|
-
* .morph/features/{feature}/3-implement/validations/checkpoint-{N}.json
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
10
|
-
import { join } from 'path';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Persist a checkpoint validation result to disk.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} projectPath - Project root
|
|
16
|
-
* @param {string} featureName - Feature name
|
|
17
|
-
* @param {{ checkpointNum: number, timestamp: string, passed: boolean, validators: Object, summary: string, feature?: string, phase?: string }} result - Validation result
|
|
18
|
-
* @returns {string} Path to the saved JSON file
|
|
19
|
-
*/
|
|
20
|
-
export function persistCheckpointValidation(projectPath, featureName, result) {
|
|
21
|
-
const dir = join(projectPath, `.morph/features/${featureName}/3-implement/validations`);
|
|
22
|
-
mkdirSync(dir, { recursive: true });
|
|
23
|
-
|
|
24
|
-
const filename = `checkpoint-${result.checkpointNum}.json`;
|
|
25
|
-
const filePath = join(dir, filename);
|
|
26
|
-
|
|
27
|
-
const output = {
|
|
28
|
-
checkpointNum: result.checkpointNum,
|
|
29
|
-
timestamp: result.timestamp || new Date().toISOString(),
|
|
30
|
-
passed: result.passed,
|
|
31
|
-
validators: result.validators || {},
|
|
32
|
-
summary: result.summary || '',
|
|
33
|
-
feature: result.feature || featureName,
|
|
34
|
-
phase: result.phase || 'implement',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
|
|
38
|
-
return filePath;
|
|
39
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Validation Persistence
|
|
3
|
+
*
|
|
4
|
+
* Persists checkpoint validation results to disk for traceability.
|
|
5
|
+
* Each checkpoint writes a JSON file to:
|
|
6
|
+
* .morph/features/{feature}/3-implement/validations/checkpoint-{N}.json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Persist a checkpoint validation result to disk.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectPath - Project root
|
|
16
|
+
* @param {string} featureName - Feature name
|
|
17
|
+
* @param {{ checkpointNum: number, timestamp: string, passed: boolean, validators: Object, summary: string, feature?: string, phase?: string }} result - Validation result
|
|
18
|
+
* @returns {string} Path to the saved JSON file
|
|
19
|
+
*/
|
|
20
|
+
export function persistCheckpointValidation(projectPath, featureName, result) {
|
|
21
|
+
const dir = join(projectPath, `.morph/features/${featureName}/3-implement/validations`);
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
const filename = `checkpoint-${result.checkpointNum}.json`;
|
|
25
|
+
const filePath = join(dir, filename);
|
|
26
|
+
|
|
27
|
+
const output = {
|
|
28
|
+
checkpointNum: result.checkpointNum,
|
|
29
|
+
timestamp: result.timestamp || new Date().toISOString(),
|
|
30
|
+
passed: result.passed,
|
|
31
|
+
validators: result.validators || {},
|
|
32
|
+
summary: result.summary || '',
|
|
33
|
+
feature: result.feature || featureName,
|
|
34
|
+
phase: result.phase || 'implement',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf8');
|
|
38
|
+
return filePath;
|
|
39
|
+
}
|