@polymorphism-tech/morph-spec 9.7.0 → 10.0.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.
- package/CLAUDE.md +11 -11
- package/README.md +4 -4
- package/bin/morph-spec.js +41 -44
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +24 -30
- package/framework/MORPH.md +47 -123
- package/framework/agents.json +6 -374
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-archive.md +4 -3
- package/framework/commands/morph-hotfix.md +8 -30
- package/framework/commands/morph-infra.md +2 -2
- package/framework/commands/morph-media.md +10 -9
- package/framework/commands/morph-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +100 -102
- package/framework/evals/review-checklist.md +46 -54
- package/framework/hooks/README.md +4 -4
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/README.md +1 -1
- package/framework/hooks/claude-code/core/denylist.json +4 -4
- package/framework/hooks/claude-code/core/gate-guard.js +9 -0
- package/framework/hooks/claude-code/core/masked-exit.js +49 -0
- package/framework/hooks/claude-code/core/state-sync.js +4 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
- package/framework/hooks/claude-code/core/test-budget.js +10 -70
- package/framework/hooks/claude-code/core/validate-completion.js +11 -24
- package/framework/hooks/claude-code/core/worktree-context.js +5 -22
- package/framework/hooks/claude-code/core/worktree-create.js +6 -45
- package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/gate-event.js +2 -2
- package/framework/hooks/shared/graph-refresh.js +20 -3
- package/framework/hooks/shared/heavy-command.js +32 -1
- package/framework/hooks/shared/heavy-config.js +1 -1
- package/framework/hooks/shared/heavy-slots.js +41 -4
- package/framework/hooks/shared/next-step.js +63 -24
- package/framework/hooks/shared/phase-utils.js +6 -9
- package/framework/hooks/shared/report-progress.js +86 -0
- package/framework/hooks/shared/state-reader.js +3 -2
- package/framework/hooks/statusline.js +332 -0
- package/framework/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +9 -1
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- package/framework/rules/nextjs-standards.md +7 -0
- package/framework/rules/plan-grammar.md +53 -0
- package/framework/rules/ui-design.md +27 -0
- package/framework/schemas/config.schema.json +12 -19
- package/framework/schemas/feature.schema.json +7 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +38 -166
- package/framework/skills/core/morph-apply/references/debugging.md +8 -0
- package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
- package/framework/skills/core/morph-apply/references/machine.md +29 -0
- package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
- package/framework/skills/core/morph-apply/references/review.md +65 -0
- package/framework/skills/core/morph-plan/SKILL.md +42 -108
- package/framework/skills/core/morph-plan/references/critique.md +40 -0
- package/framework/skills/core/morph-plan/references/interview.md +25 -0
- package/framework/skills/core/morph-uiux/SKILL.md +47 -64
- package/framework/skills/core/morph-uiux/surfaces.json +32 -0
- package/framework/standards/STANDARDS.json +32 -5
- package/framework/standards/STANDARDS.meta.json +5 -1
- package/framework/standards/ai-agents/agent-session.md +2 -3
- package/framework/standards/ai-agents/context-providers.md +2 -2
- package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +2 -2
- package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +2 -2
- package/framework/standards/architecture/stack-baseline.md +47 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
- package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
- package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
- package/framework/standards/frontend/design-system/art-direction.md +2 -2
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
- package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- 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/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +12 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- 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/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/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +88 -23
- package/src/commands/graph/diff.js +74 -0
- package/src/commands/graph/status.js +1 -1
- package/src/commands/observability/cost.js +2 -4
- package/src/commands/observability/telemetry.js +1 -1
- package/src/commands/project/doctor.js +23 -39
- package/src/commands/project/fleet.js +5 -6
- package/src/commands/project/init.js +63 -36
- package/src/commands/project/stack-scan.js +3 -42
- package/src/commands/project/stack-set.js +26 -0
- package/src/commands/project/status.js +12 -13
- package/src/commands/project/update.js +39 -3
- package/src/commands/project/worktree.js +27 -16
- package/src/commands/state/advance.js +17 -17
- package/src/commands/state/approve.js +0 -6
- package/src/commands/state/archive.js +27 -1
- package/src/commands/state/auto-approval.js +1 -1
- package/src/commands/state/create.js +0 -5
- package/src/commands/state/finish.js +43 -1
- package/src/commands/state/gate-check.js +37 -30
- package/src/commands/state/gate-decision.js +3 -2
- package/src/commands/state/next.js +1 -0
- package/src/commands/state/recap.js +67 -0
- package/src/commands/state/report-check.js +39 -146
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +6 -6
- package/src/core/paths/output-schema.js +3 -3
- package/src/core/state/state-manager.js +2 -4
- package/src/dashboard/app/components/CommandPalette.jsx +2 -5
- package/src/dashboard/app/components/GatePauses.jsx +116 -0
- package/src/dashboard/app/components/SessionMap.jsx +77 -0
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/Activity.jsx +4 -0
- package/src/dashboard/app/pages/AgentMap.jsx +1 -2
- package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Portfolio.jsx +40 -5
- package/src/dashboard/scanner/activity-scanner.js +29 -0
- package/src/dashboard/scanner/agents-scanner.js +11 -14
- package/src/dashboard/scanner/features-scanner.js +45 -24
- package/src/dashboard/scanner/project-api.js +11 -2
- package/src/dashboard/scanner/skills-scanner.js +11 -1
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/e2e/applicability.js +3 -1
- package/src/lib/git/tree-state.js +1 -16
- package/src/lib/graph/diff-tree.js +127 -0
- package/src/lib/graph/graphify-runtime.js +12 -5
- package/src/lib/heavy/gate.js +22 -1
- package/src/lib/installers/memory-installer.js +2 -2
- package/src/lib/installers/rules-installer.js +57 -0
- package/src/lib/installers/stack-assets.js +17 -0
- package/src/lib/installers/stack-sync.js +219 -0
- package/src/lib/plan/checks.js +55 -110
- package/src/lib/plan/critique.js +28 -0
- package/src/lib/plan/media.js +42 -0
- package/src/lib/plan/plan-html.js +125 -0
- package/src/lib/plan/plan-parser.js +34 -45
- package/src/lib/recap/piper.js +36 -0
- package/src/lib/recap/recap.js +232 -0
- package/src/lib/review-reports.js +33 -7
- package/src/lib/risk-detector.js +90 -11
- package/src/lib/specs/screen-checklist.js +33 -48
- package/src/lib/stack/dotnet-build-ignores.js +8 -9
- package/src/lib/tasks/test-nodes.js +24 -6
- package/src/lib/tasks/test-runner.js +40 -1
- package/src/lib/telemetry/metrics.js +27 -29
- package/src/lib/validators/reuse/reuse-validator.js +1 -9
- package/src/lib/validators/verify-runner.js +50 -19
- package/src/lib/worktree/fleet-scanner.js +7 -20
- package/src/lib/worktree/worktree-audit.js +0 -37
- package/src/scripts/setup-infra.js +15 -50
- package/src/utils/agents-installer.js +27 -201
- package/src/utils/hooks-installer.js +11 -95
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
- package/framework/hooks/claude-code/statusline.py +0 -699
- package/framework/hooks/claude-code/statusline.sh +0 -11
- package/framework/hooks/shared/agent-reap.js +0 -74
- package/framework/hooks/shared/slice-progress.js +0 -110
- package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
- package/framework/skills/core/morph-machine/SKILL.md +0 -46
- package/framework/skills/core/morph-review/SKILL.md +0 -129
- package/framework/skills/core/morph-standards/SKILL.md +0 -52
- package/framework/standards/PROVENANCE.json +0 -119
- package/src/commands/project/worktree-reap.js +0 -34
- package/src/commands/state/handoff.js +0 -65
- package/src/commands/validation/mutate.js +0 -263
- package/src/commands/validation/scout-check.js +0 -28
- package/src/lib/generators/handoff-generator.js +0 -172
- package/src/lib/mutation/adapters.js +0 -79
- package/src/lib/mutation/commands.js +0 -61
- package/src/lib/mutation/dotnet-introspection.js +0 -281
- package/src/lib/mutation/manifest.js +0 -144
- package/src/lib/mutation/plan.js +0 -44
- package/src/lib/mutation/report.js +0 -68
- package/src/lib/mutation/runner.js +0 -301
- package/src/lib/mutation/schemata.js +0 -60
- package/src/lib/mutation/snapshot.js +0 -65
- package/src/lib/mutation/subset.js +0 -109
- package/src/lib/mutation/trx.js +0 -50
- package/src/lib/scout/scout-check.js +0 -293
- package/src/lib/validators/traceability-check.js +0 -103
- package/src/lib/worktree/agent-reaper.js +0 -247
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "1.0.0",
|
|
3
|
-
"_note": "Standards distilled from an external source. The source is not a dependency at runtime: it lives in another repository and is not needed to use the framework. The same mechanism covers content distilled into a SKILL — an entry declares `standards`, `skills`, or both, and every declared name must resolve. This file exists so drift is detectable instead of silent. Pin `sourceTree` when distilling, and re-pin deliberately after reviewing what changed. See test/framework/standards-provenance.test.js.",
|
|
4
|
-
"sources": [
|
|
5
|
-
{
|
|
6
|
-
"id": "morph-lp-cinematic",
|
|
7
|
-
"repo": "morph-HUB",
|
|
8
|
-
"path": "skills/morph-lp-cinematic",
|
|
9
|
-
"envVar": "MORPH_HUB",
|
|
10
|
-
"defaultCheckout": "R:/Polymorphism Tech/repos/morph-HUB",
|
|
11
|
-
"sourceTree": "2987b44241cae479b1aedc63f689cdcd6ef6fb94",
|
|
12
|
-
"sourceCommit": "c4c9164",
|
|
13
|
-
"distilledOn": "2026-08-21",
|
|
14
|
-
"distilledFrom": [
|
|
15
|
-
"references/build-stack.md",
|
|
16
|
-
"references/component-recipes.md",
|
|
17
|
-
"references/premium-finish.md",
|
|
18
|
-
"references/seo-and-qa.md",
|
|
19
|
-
"references/image-prompt.md",
|
|
20
|
-
"references/seedance-scrub-prompt.md",
|
|
21
|
-
"references/seedance-loop-prompt.md"
|
|
22
|
-
],
|
|
23
|
-
"notDistilled": [
|
|
24
|
-
"references/last-mile.md",
|
|
25
|
-
"references/copy-and-messaging.md",
|
|
26
|
-
"references/section-blueprint.md"
|
|
27
|
-
],
|
|
28
|
-
"notDistilledReason": "Deliberate split: a standard carries knowledge, a skill carries process. Deploy and copy strategy stay with the skill. `references/gates.md` LEFT this list on 2026-09-09: the reason that kept it out was \"a skill carries process\", and the destination is now a skill — see the \"cinematic-lp\" source below, which distils it into morph-media. Review of 2026-09-29 (source c4c9164): only tool-independent facts were ported (the scrub_frames manifest contract for frame-scrub, chained clips for ai-video-generation); the rest was already in the standards or stays with the skill (ffmpeg-to-media_tool swaps, model prices, SKILL.md, gates.md, assets/).",
|
|
29
|
-
"standards": [
|
|
30
|
-
"frontend-scroll-driven-frame-scrub",
|
|
31
|
-
"frontend-scroll-driven-smooth-scroll",
|
|
32
|
-
"frontend-scroll-driven-scroll-components",
|
|
33
|
-
"frontend-design-system-premium-finish",
|
|
34
|
-
"frontend-design-system-ai-image-generation",
|
|
35
|
-
"frontend-design-system-ai-video-generation",
|
|
36
|
-
"frontend-seo-meta-schema-audit"
|
|
37
|
-
],
|
|
38
|
-
"skills": []
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"id": "morph-image",
|
|
42
|
-
"repo": "morph-HUB",
|
|
43
|
-
"path": "skills/morph-image",
|
|
44
|
-
"envVar": "MORPH_HUB",
|
|
45
|
-
"defaultCheckout": "R:/Polymorphism Tech/repos/morph-HUB",
|
|
46
|
-
"sourceTree": "e55dcf307278583120601c3c5d55118302335f7b",
|
|
47
|
-
"sourceCommit": "c4c9164",
|
|
48
|
-
"distilledOn": "2026-09-09",
|
|
49
|
-
"distilledFrom": [
|
|
50
|
-
"SKILL.md"
|
|
51
|
-
],
|
|
52
|
-
"notDistilled": [
|
|
53
|
-
"skill.json",
|
|
54
|
-
"evals/"
|
|
55
|
-
],
|
|
56
|
-
"notDistilledReason": "Manifest and eval fixtures of the source skill: packaging of the origin, not knowledge. The MCP tool names of the source were also NOT carried over — morph-media is about the authoring decisions, and a tool table pinned to one MCP server would rot on its own.",
|
|
57
|
-
"standards": [],
|
|
58
|
-
"skills": [
|
|
59
|
-
"morph-media"
|
|
60
|
-
]
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"id": "cinematic-lp",
|
|
64
|
-
"repo": "morph-HUB",
|
|
65
|
-
"path": "skills/cinematic-lp/references",
|
|
66
|
-
"envVar": "MORPH_HUB",
|
|
67
|
-
"defaultCheckout": "R:/Polymorphism Tech/repos/morph-HUB",
|
|
68
|
-
"sourceTree": "3c557425d649ed477bf6645b9297b6d1d72ae4a3",
|
|
69
|
-
"sourceCommit": "c4c9164",
|
|
70
|
-
"distilledOn": "2026-09-09",
|
|
71
|
-
"distilledFrom": [
|
|
72
|
-
"gates.md"
|
|
73
|
-
],
|
|
74
|
-
"notDistilled": [
|
|
75
|
-
"build-stack.md",
|
|
76
|
-
"component-recipes.md",
|
|
77
|
-
"copy-and-messaging.md",
|
|
78
|
-
"image-prompt.md",
|
|
79
|
-
"last-mile.md",
|
|
80
|
-
"mcp-setup.md",
|
|
81
|
-
"premium-finish.md",
|
|
82
|
-
"section-blueprint.md",
|
|
83
|
-
"seo-and-qa.md",
|
|
84
|
-
"video-loop-examples.md",
|
|
85
|
-
"video-loop-prompt.md",
|
|
86
|
-
"video-scrub-prompt.md"
|
|
87
|
-
],
|
|
88
|
-
"notDistilledReason": "Only the HITL gate mechanics were distilled: the N-option picker, \"rejection goes back to the same step\", and \"show MOVEMENT, never a still\". The rest is either already a standard (the frame-scrub / smooth-scroll / premium-finish family, from the morph-lp-cinematic source above) or build/deploy strategy that belongs to the calling workflow. Review of 2026-09-29 (source c4c9164): gates.md did not change; the changes were in image-prompt.md and mcp-setup.md, both not distilled. Re-pinned only.",
|
|
89
|
-
"standards": [],
|
|
90
|
-
"skills": [
|
|
91
|
-
"morph-media"
|
|
92
|
-
]
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
"id": "brand-design",
|
|
96
|
-
"repo": "morph-HUB",
|
|
97
|
-
"path": "skills/brand-design/references",
|
|
98
|
-
"envVar": "MORPH_HUB",
|
|
99
|
-
"defaultCheckout": "R:/Polymorphism Tech/repos/morph-HUB",
|
|
100
|
-
"sourceTree": "7b637a08e931d8b7ba5be7fc75b851d89cd6e6f4",
|
|
101
|
-
"sourceCommit": "c4c9164",
|
|
102
|
-
"distilledOn": "2026-09-09",
|
|
103
|
-
"distilledFrom": [
|
|
104
|
-
"image-generation.md"
|
|
105
|
-
],
|
|
106
|
-
"notDistilled": [
|
|
107
|
-
"brand-book.md",
|
|
108
|
-
"checklist.md",
|
|
109
|
-
"example-identidade-visual.md",
|
|
110
|
-
"phases.md"
|
|
111
|
-
],
|
|
112
|
-
"notDistilledReason": "Only the image-per-phase map and the \"concept, never the final vector\" rule were distilled. The narrative prompt recipes were NOT copied verbatim: they are bound to one brand pipeline, and morph-media carries the prompt STRUCTURE instead, which survives the model changing. Brand book, checklist and phases are the process of another skill. Review of 2026-09-29 (source c4c9164): the image-per-phase table and the \"concept, never the final vector\" rule are unchanged (the text now says \"image model\" instead of Gemini); what was added is morph-assets call mechanics (project_set, @brand, outputPath, models, variants, dryRun), tool-bound and kept out on purpose. Re-pinned only.",
|
|
113
|
-
"standards": [],
|
|
114
|
-
"skills": [
|
|
115
|
-
"morph-media"
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
]
|
|
119
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { reapAgentWorktrees } from '../../lib/worktree/agent-reaper.js';
|
|
3
|
-
|
|
4
|
-
const label = (item) => item.path ?? item.branch ?? item.hex;
|
|
5
|
-
|
|
6
|
-
function printReport(out) {
|
|
7
|
-
const mode = out.dryRun ? ' (simulação)' : '';
|
|
8
|
-
console.log(chalk.bold(`\nWorktrees de sub-agent${mode} — graça de ${out.graceMinutes} min\n`));
|
|
9
|
-
if (out.busy) console.log(chalk.yellow(' ⏳ outro reaper está em execução; nada foi feito\n'));
|
|
10
|
-
|
|
11
|
-
for (const item of out.wouldReap ?? []) console.log(chalk.cyan(` → removeria: ${label(item)}`));
|
|
12
|
-
for (const item of out.reaped) console.log(chalk.green(` ✓ removido: ${label(item)}${item.backup ? chalk.gray(` (backup: ${item.backup})`) : ''}`));
|
|
13
|
-
for (const item of out.preserved) {
|
|
14
|
-
console.log(chalk.yellow(` ⚠ preservado: ${label(item)} — ${item.reason}${item.detail ? `: ${item.detail}` : ''}`));
|
|
15
|
-
}
|
|
16
|
-
if (out.active) console.log(chalk.gray(` ${out.active} agente(s) ainda ativo(s), intocado(s)`));
|
|
17
|
-
const nothing = !out.reaped.length && !out.preserved.length && !out.active && !(out.wouldReap ?? []).length;
|
|
18
|
-
if (nothing && !out.busy) console.log(chalk.green(' ✓ nenhuma worktree de sub-agent sobrando'));
|
|
19
|
-
console.log();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function worktreeReapCommand(options = {}) {
|
|
23
|
-
const grace = options.grace === undefined ? undefined : Number(options.grace);
|
|
24
|
-
if (grace !== undefined && !(Number.isFinite(grace) && grace >= 0)) {
|
|
25
|
-
console.error(chalk.red(`--grace precisa ser um número de minutos >= 0 (recebi "${options.grace}")`));
|
|
26
|
-
process.exit(2);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const out = await reapAgentWorktrees(options.cwd || process.cwd(), { dryRun: options.dryRun === true, graceMinutes: grace });
|
|
30
|
-
if (options.json) console.log(JSON.stringify(out));
|
|
31
|
-
else if (out.ok) printReport(out);
|
|
32
|
-
else console.log(chalk.gray(` fora de um repositório git — nada a fazer (${out.error})`));
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { buildHandoff, writeHandoff } from '../../lib/generators/handoff-generator.js';
|
|
3
|
-
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
4
|
-
|
|
5
|
-
// With a live worktree the root copy is a stale birth skeleton: build and write in the
|
|
6
|
-
// owning tree (`resolveFeatureTree`) unless `--here` forces the cwd.
|
|
7
|
-
export function handoffCommand(featureName, options = {}) {
|
|
8
|
-
const cwd = process.cwd();
|
|
9
|
-
|
|
10
|
-
const owner = options.here ? { source: 'cwd', tree: cwd, branch: null, worktree: null } : resolveFeatureTree(featureName, cwd);
|
|
11
|
-
|
|
12
|
-
if (owner.source === 'none') {
|
|
13
|
-
if (options.json) {
|
|
14
|
-
console.log(JSON.stringify({ error: 'feature-not-found', feature: featureName }));
|
|
15
|
-
} else {
|
|
16
|
-
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
17
|
-
}
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (owner.source === 'branch-only') {
|
|
22
|
-
// The handoff points at files on disk; a truth materialized in no tree is refused.
|
|
23
|
-
const hint = `morph-spec worktree setup ${featureName} (ou: git checkout ${owner.branch})`;
|
|
24
|
-
if (options.json) {
|
|
25
|
-
console.log(JSON.stringify({ error: 'feature-not-in-tree', feature: featureName, branch: owner.branch, hint }));
|
|
26
|
-
} else {
|
|
27
|
-
console.error(chalk.red(`\n❌ "${featureName}" está commitada em \`${owner.branch}\`, mas não está materializada em árvore nenhuma.`));
|
|
28
|
-
console.error(chalk.gray(` ${hint}\n`));
|
|
29
|
-
}
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const tree = owner.tree;
|
|
34
|
-
const result = buildHandoff(featureName, tree, { worktree: owner.worktree });
|
|
35
|
-
if (!result) {
|
|
36
|
-
if (options.json) {
|
|
37
|
-
console.log(JSON.stringify({ error: 'feature-not-found', feature: featureName, tree }));
|
|
38
|
-
} else {
|
|
39
|
-
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
40
|
-
}
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const path = writeHandoff(featureName, tree, { worktree: owner.worktree });
|
|
45
|
-
if (!path) {
|
|
46
|
-
if (options.json) {
|
|
47
|
-
console.log(JSON.stringify({ error: 'write-failed', feature: featureName, tree }));
|
|
48
|
-
} else {
|
|
49
|
-
console.error(chalk.red(`\n❌ Could not write handoff.md for ${featureName}\n`));
|
|
50
|
-
}
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (options.json) {
|
|
55
|
-
console.log(JSON.stringify({ path, feature: featureName, tree, source: owner.source, ...result.meta }));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
console.log(chalk.green(`\n✅ Handoff gerado: ${path}`));
|
|
60
|
-
if (tree !== cwd) {
|
|
61
|
-
console.log(chalk.yellow(` resolvido para ${tree} — a cópia da raiz é esqueleto de nascimento, não a verdade.`));
|
|
62
|
-
}
|
|
63
|
-
console.log(chalk.gray(` fase ${result.meta.phase} · fatias ${result.meta.slicesDone}/${result.meta.slicesTotal} · ${result.meta.generatedAt}`));
|
|
64
|
-
console.log(chalk.gray(' A próxima sessão lê este documento antes de continuar (ponteiros, não cópias).\n'));
|
|
65
|
-
}
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { join, resolve } from 'node:path';
|
|
5
|
-
import { discoverDotnetBuildTargets } from '../../lib/tasks/test-runner.js';
|
|
6
|
-
import { dotnetTestMode } from '../../lib/mutation/dotnet-introspection.js';
|
|
7
|
-
import { dotnetBuildCommands } from '../../lib/mutation/commands.js';
|
|
8
|
-
import { resolveTestNodes } from '../../lib/tasks/test-nodes.js';
|
|
9
|
-
import { changedPathsAgainst, detectDefaultBranch, getHeadSha } from '../../lib/git/default-branch.js';
|
|
10
|
-
import { createHeavyGate, HeavyBusyError } from '../../lib/heavy/gate.js';
|
|
11
|
-
import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
12
|
-
import { resolveGitCommonDir } from '../../../framework/hooks/shared/git-paths.js';
|
|
13
|
-
import { BACKSLASH_TOKEN, ManifestError } from '../../lib/mutation/manifest.js';
|
|
14
|
-
import { groupByNode, loadPlan } from '../../lib/mutation/plan.js';
|
|
15
|
-
import { dotnetSubset, jsSubset, mutatedTypesOf, testFilesCiting, testProjectDirsOf } from '../../lib/mutation/subset.js';
|
|
16
|
-
import { isFinished } from '../../dashboard/scanner/feature-status.js';
|
|
17
|
-
import { dotnetAdapter, genericAdapter, spawnExec } from '../../lib/mutation/adapters.js';
|
|
18
|
-
import { noSubsetReason, runMutation } from '../../lib/mutation/runner.js';
|
|
19
|
-
import { renderSection, summarize } from '../../lib/mutation/report.js';
|
|
20
|
-
import { readJournal, restoreSnapshot } from '../../lib/mutation/snapshot.js';
|
|
21
|
-
|
|
22
|
-
const EXIT = { ok: 0, failed: 1, usage: 2, busy: 3, unsafe: 4 };
|
|
23
|
-
const KEEP_RUNS = 2;
|
|
24
|
-
const DEFAULT_JOBS = 2;
|
|
25
|
-
|
|
26
|
-
// One heavy slot per worker: the first waits, the others are taken only if free now,
|
|
27
|
-
// so a busy machine degrades to sequential instead of queueing.
|
|
28
|
-
export function workerGate({ root, feature, jobs }) {
|
|
29
|
-
const config = readHeavyConfig(root);
|
|
30
|
-
const gates = [createHeavyGate({ projectPath: root, label: 'mutate', feature, config })];
|
|
31
|
-
const quiet = { ...config, waitTimeoutSec: 0, warnings: [] };
|
|
32
|
-
for (let i = 1; i < jobs; i++) {
|
|
33
|
-
const { MORPH_HEAVY_LEASE: _inherited, ...env } = process.env;
|
|
34
|
-
gates.push(createHeavyGate({ projectPath: root, label: 'mutate', feature, config: quiet, env }));
|
|
35
|
-
}
|
|
36
|
-
let acquired = 0;
|
|
37
|
-
return {
|
|
38
|
-
get jobs() { return acquired; },
|
|
39
|
-
enter(node) {
|
|
40
|
-
gates[0].enter(node);
|
|
41
|
-
acquired = 1;
|
|
42
|
-
for (const gate of gates.slice(1)) {
|
|
43
|
-
try { gate.enter(node); acquired += 1; } catch (err) {
|
|
44
|
-
if (!(err instanceof HeavyBusyError)) throw err;
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
release() {
|
|
50
|
-
for (const gate of [...gates].reverse()) gate.release();
|
|
51
|
-
acquired = 0;
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const gitOut = (cwd, args) => execFileSync('git', args, { cwd, stdio: 'pipe', encoding: 'utf8', windowsHide: true });
|
|
57
|
-
|
|
58
|
-
function workRoot(root) {
|
|
59
|
-
const common = resolveGitCommonDir(root);
|
|
60
|
-
return common ? join(common, 'morph-spec', 'mutation') : join(root, '.morph', 'tmp', 'mutation');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function pruneOldRuns(base, keep) {
|
|
64
|
-
if (!existsSync(base)) return;
|
|
65
|
-
const runs = readdirSync(base).sort();
|
|
66
|
-
for (const name of runs.slice(0, Math.max(0, runs.length - keep))) {
|
|
67
|
-
const done = ['g0', 'g1', 'g2', 'g3'].every((g) => {
|
|
68
|
-
const journal = readJournal(join(base, name, g, 'snapshot'));
|
|
69
|
-
return journal === null || journal.state === 'restored';
|
|
70
|
-
});
|
|
71
|
-
if (done) rmSync(join(base, name), { recursive: true, force: true });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function fail(message, code) {
|
|
76
|
-
console.error(chalk.red(message));
|
|
77
|
-
process.exitCode = code;
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function restoreLatest(root, out = console) {
|
|
82
|
-
const base = workRoot(root);
|
|
83
|
-
if (!existsSync(base)) { out.log('nada a restaurar: nenhuma rodada de mutação registrada'); return []; }
|
|
84
|
-
const restored = [];
|
|
85
|
-
for (const run of readdirSync(base).sort()) {
|
|
86
|
-
for (const group of readdirSync(join(base, run))) {
|
|
87
|
-
const journal = readJournal(join(base, run, group, 'snapshot'));
|
|
88
|
-
if (!journal || journal.state === 'restored') continue;
|
|
89
|
-
const report = restoreSnapshot(journal);
|
|
90
|
-
restored.push({ run, group, files: report });
|
|
91
|
-
out.log(`${run}/${group}: ${report.every((r) => r.ok) ? 'restaurado' : 'FALHOU'} — ${report.map((r) => `${r.file} ${r.ok ? 'ok' : 'md5 diverge'}`).join(', ')}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (restored.length === 0) out.log('nada a restaurar: toda rodada registrada já foi restaurada');
|
|
95
|
-
return restored;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function testDirsOf(node, root, { projectsOnly = false } = {}) {
|
|
99
|
-
const cwd = resolve(root, node.cwd || '.');
|
|
100
|
-
const targets = discoverDotnetBuildTargets(node, cwd);
|
|
101
|
-
return testProjectDirsOf(root, projectsOnly ? targets.filter((t) => !/\.slnx?$/i.test(t)) : targets, cwd);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function subsetFor({ node, group, root }) {
|
|
105
|
-
const mutatedFiles = group.regions.map((r) => r.file);
|
|
106
|
-
if (node.runner === 'dotnet') {
|
|
107
|
-
const base = detectDefaultBranch(root);
|
|
108
|
-
const changed = base ? changedPathsAgainst(base, root) : [];
|
|
109
|
-
return dotnetSubset({ root, testProjectDirs: testDirsOf(node, root), mutatedFiles, changed });
|
|
110
|
-
}
|
|
111
|
-
return jsSubset({ runner: node.runner, command: node.command, root, mutatedFiles });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function e2eManualProtocol(feature, node) {
|
|
115
|
-
const f = feature ?? '<feature>';
|
|
116
|
-
const round = `eval "$(morph-spec env ${f} --with-secrets)" && morph-spec e2e up ${f} && morph-spec heavy run -- ${node.command}`;
|
|
117
|
-
return 'mutate só recompila localmente, e a suíte que alcança esse código testa o container do compose: o mutante sairia sobrevivente em falso. ' +
|
|
118
|
-
`Prove à mão pelo nó "${node.id}", cada passo numa linha só e encadeado por \`&&\` (nunca \`;\`: um passo que falha não pode deixar a suíte rodar sobre o código original; ` +
|
|
119
|
-
'e o env da feature se perde entre chamadas de Bash): ' +
|
|
120
|
-
`(1) base verde antes do 1º mutante: \`${round}\`; ` +
|
|
121
|
-
`(2) por mutante: edite a linha && \`${round}\`; ` +
|
|
122
|
-
`(3) restaure, confirme com \`git diff --quiet -- <arquivo>\` e suba a stack de novo (\`eval "$(morph-spec env ${f} --with-secrets)" && morph-spec e2e up ${f}\`). ` +
|
|
123
|
-
'Registre cada mutante em 4-review (expect, resultado, teste que o matou).';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// A .cs file belongs to every dotnet suite, by runner, not by project reference: a unit suite
|
|
127
|
-
// "owns" code only an e2e suite executes. With an e2e node declared, a unit suite with no test
|
|
128
|
-
// naming a type of the mutated file is not evidence, so the run is refused.
|
|
129
|
-
function uncitedUnderE2e({ root, groups, nodes }) {
|
|
130
|
-
const e2e = nodes.filter((n) => n.e2e);
|
|
131
|
-
if (e2e.length === 0) return [];
|
|
132
|
-
const e2eDirs = new Set(e2e.filter((n) => n.runner === 'dotnet').flatMap((n) => testDirsOf(n, root)));
|
|
133
|
-
const uncited = [];
|
|
134
|
-
for (const { node, regions } of groups.filter((g) => g.node.runner === 'dotnet')) {
|
|
135
|
-
const direct = new Set(testDirsOf(node, root, { projectsOnly: true }));
|
|
136
|
-
const dirs = testDirsOf(node, root).filter((d) => direct.has(d) || !e2eDirs.has(d));
|
|
137
|
-
for (const region of regions) {
|
|
138
|
-
const types = mutatedTypesOf(root, [region.file]);
|
|
139
|
-
if (testFilesCiting({ root, testProjectDirs: dirs, types, exclude: [region.file] }).length === 0) uncited.push({ region, node, types, e2e });
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return uncited;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function soleActiveFeature(root) {
|
|
146
|
-
const dir = join(root, '.morph', 'features');
|
|
147
|
-
let names;
|
|
148
|
-
try { names = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return null; }
|
|
149
|
-
const active = names.filter((name) => {
|
|
150
|
-
try { return !isFinished(JSON.parse(readFileSync(join(dir, name, 'feature.json'), 'utf8'))); } catch { return false; }
|
|
151
|
-
});
|
|
152
|
-
return active.length === 1 ? active[0] : null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export async function mutateCommand(featureArg, options = {}) {
|
|
156
|
-
const root = resolve(options.cwd ?? process.cwd());
|
|
157
|
-
if (options.restore) { restoreLatest(root); return null; }
|
|
158
|
-
|
|
159
|
-
const feature = featureArg ?? soleActiveFeature(root);
|
|
160
|
-
if (!feature && !options.plan) return fail('sem feature: passe <feature> (nenhuma, ou mais de uma, em andamento) ou o manifesto por --plan.', EXIT.usage);
|
|
161
|
-
const planPath = resolve(root, options.plan ?? join('.morph', 'features', feature, '4-review', 'mutants.json'));
|
|
162
|
-
if (!existsSync(planPath)) return fail(`manifesto não encontrado: ${planPath}\nEscreva-o com a ferramenta Write, se você a tem; só com Bash, heredoc de delimitador entre aspas simples, com a barra invertida do código como ${BACKSLASH_TOKEN} — formato em .morph/framework/CLI.md, seção "### mutate".`, EXIT.usage);
|
|
163
|
-
|
|
164
|
-
let regions;
|
|
165
|
-
try {
|
|
166
|
-
regions = loadPlan({ root, planPath });
|
|
167
|
-
} catch (err) {
|
|
168
|
-
if (err instanceof ManifestError) return fail(err.message, EXIT.usage);
|
|
169
|
-
if (err instanceof SyntaxError) return fail(`${err.message}\nSe o manifesto tem barra invertida, o transporte do Bash pode tê-la alterado: escreva cada uma como ${BACKSLASH_TOKEN}, que a ferramenta expande.`, EXIT.usage);
|
|
170
|
-
throw err;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const { nodes } = resolveTestNodes(root);
|
|
174
|
-
const { groups, orphans, stackOnly } = groupByNode(regions, nodes, root);
|
|
175
|
-
if (orphans.length > 0) {
|
|
176
|
-
return fail(`nenhuma suíte declarada roda: ${orphans.map((o) => `${o.id} (${o.file})`).join(', ')}\nDeclare o nó em project.tests[] (.morph/config/config.json).`, EXIT.usage);
|
|
177
|
-
}
|
|
178
|
-
if (stackOnly.length > 0) {
|
|
179
|
-
const named = stackOnly.map(({ region, node }) => `${region.id} (${region.file}) → nó "${node.id}"`).join(', ');
|
|
180
|
-
return fail(`só uma suíte e2e: true cobre: ${named}\nProve a guarda por um teste de unidade, ou: ${e2eManualProtocol(feature, stackOnly[0].node)}`, EXIT.usage);
|
|
181
|
-
}
|
|
182
|
-
const mtp = groups.find((g) => g.node.runner === 'dotnet' && dotnetTestMode(resolve(root, g.node.cwd || '.')) === 'mtp');
|
|
183
|
-
if (mtp) return fail(`a suíte "${mtp.node.id}" roda no Microsoft.Testing.Platform (global.json): o TRX do VSTest, de onde o veredito e o motivo saem, não existe nela. Suporte a MTP ainda não há — rode a mutação com a suíte inteira à mão.`, EXIT.usage);
|
|
184
|
-
const uncompilable = groups.find((g) => g.node.runner === 'dotnet' && dotnetBuildCommands(g.node, root).length === 0);
|
|
185
|
-
if (uncompilable) return fail(`a suíte "${uncompilable.node.id}" não deixa descobrir o que compilar: o comando não nomeia .sln/.slnx/.csproj e o diretório alvo não tem uma única solution (ou um único projeto). Sem isso, todo mutante rodaria sobre o binário antigo e sairia "sobrevivente". Nomeie a solution no comando ou declare o campo build no nó (config.json, project.tests[]).`, EXIT.usage);
|
|
186
|
-
const preDecided = groups.find((g) => g.node.status);
|
|
187
|
-
if (preDecided) return fail(`a suíte "${preDecided.node.id}" já vem decidida como ${preDecided.node.status}: ${preDecided.node.reason ?? ''}`, EXIT.usage);
|
|
188
|
-
const uncited = uncitedUnderE2e({ root, groups, nodes });
|
|
189
|
-
if (uncited.length > 0) {
|
|
190
|
-
const named = uncited.map(({ region, node, types }) => `${region.id} (${region.file}; tipos: ${types.join(', ') || 'nenhum legível'}) → nenhum teste da suíte "${node.id}" os cita`).join('\n');
|
|
191
|
-
const e2eIds = uncited[0].e2e.map((n) => `"${n.id}"`).join(', ');
|
|
192
|
-
return fail(`${named}\nNó e2e: true declarado: ${e2eIds}. ${e2eManualProtocol(feature, uncited[0].e2e[0])}`, EXIT.usage);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const subsets = groups.map((group) => subsetFor({ node: group.node, group, root }));
|
|
196
|
-
const noSubset = subsets.findIndex((s) => !s.usable);
|
|
197
|
-
if (noSubset >= 0) {
|
|
198
|
-
return fail(`suíte "${groups[noSubset].node.id}": ${noSubsetReason(subsets[noSubset].reason)}.`, EXIT.usage);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!options.allowDirty) {
|
|
202
|
-
const dirty = gitOut(root, ['status', '--porcelain', '--', ...new Set(regions.map((r) => r.file))]).trim();
|
|
203
|
-
if (dirty) return fail(`há alteração não commitada nos arquivos a mutar:\n${dirty}\nCommite o fix ANTES de mutar (ou use --allow-dirty sabendo que o snapshot é o único caminho de volta).`, EXIT.usage);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const runId = `mut-${Date.now().toString(36)}`;
|
|
207
|
-
const base = workRoot(root);
|
|
208
|
-
pruneOldRuns(base, KEEP_RUNS);
|
|
209
|
-
const git = { porcelain: () => gitOut(root, ['status', '--porcelain', '--', '.', ':(exclude).morph']), head: () => getHeadSha(root) };
|
|
210
|
-
const jobs = Math.max(1, Number.isInteger(options.jobs) ? options.jobs : DEFAULT_JOBS);
|
|
211
|
-
const gate = workerGate({ root, feature: feature ?? null, jobs });
|
|
212
|
-
const exec = options.exec ?? spawnExec;
|
|
213
|
-
|
|
214
|
-
const merged = { wallMs: 0, mutants: [], timings: [], restore: { files: [], ok: true, treeClean: true }, binary: { proof: 'unavailable', projects: [] }, baseline: {}, method: { schemata: 0, rebuild: 0, subset: {} } };
|
|
215
|
-
try {
|
|
216
|
-
for (const [index, group] of groups.entries()) {
|
|
217
|
-
const workDir = join(base, runId, `g${index}`);
|
|
218
|
-
mkdirSync(workDir, { recursive: true });
|
|
219
|
-
const subset = subsets[index];
|
|
220
|
-
const timeoutMs = options.timeoutMs ?? group.node.timeoutMs ?? 900_000;
|
|
221
|
-
const make = group.node.runner === 'dotnet' ? dotnetAdapter : genericAdapter;
|
|
222
|
-
const adapter = make({ node: group.node, root, workDir, exec, subset, timeoutMs });
|
|
223
|
-
const result = await runMutation({ root, node: group.node, regions: group.regions, workDir, runId, exec, adapter, gate, git, options: { timeoutMs, jobs }, log: (m) => options.json || console.error(chalk.gray(m)) });
|
|
224
|
-
merged.mutants.push(...result.mutants);
|
|
225
|
-
merged.timings.push(...result.timings);
|
|
226
|
-
merged.wallMs += result.wallMs;
|
|
227
|
-
merged.restore.files.push(...result.restore.files);
|
|
228
|
-
merged.restore.ok &&= result.restore.ok;
|
|
229
|
-
merged.restore.treeClean &&= result.restore.treeClean;
|
|
230
|
-
merged.restore.porcelainAfter = result.restore.porcelainAfter;
|
|
231
|
-
if (result.binary.proof !== 'unavailable') merged.binary = ['mismatch', 'unlocated'].includes(merged.binary.proof) ? merged.binary : result.binary;
|
|
232
|
-
merged.baseline = { ...merged.baseline, ...Object.fromEntries(Object.entries(result.baseline).filter(([, v]) => v)) };
|
|
233
|
-
merged.method.schemata += result.method.schemata;
|
|
234
|
-
merged.method.rebuild += result.method.rebuild;
|
|
235
|
-
merged.method.subset = result.method.subset;
|
|
236
|
-
merged.method.jobs = Math.max(merged.method.jobs ?? 1, result.method.jobs);
|
|
237
|
-
}
|
|
238
|
-
} catch (err) {
|
|
239
|
-
if (err instanceof HeavyBusyError) return fail(`suíte não rodou: ${err.message}. A fila já esperou o prazo dela: não repita em laço — registre a mutação como não executada (morph-spec heavy status).`, EXIT.busy);
|
|
240
|
-
if (err.code === 'baseline-build-failed') return fail(`${err.message}\n${String(err.output ?? '').split('\n').slice(-12).join('\n')}`, EXIT.failed);
|
|
241
|
-
throw err;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const order = new Map(regions.map((r, i) => [r.id, i]));
|
|
245
|
-
merged.mutants.sort((a, b) => order.get(a.id) - order.get(b.id));
|
|
246
|
-
const section = renderSection(merged, { head: git.head(), date: new Date().toISOString().slice(0, 10), subset: subsets[0] ?? null });
|
|
247
|
-
const outRel = options.out ?? (feature ? join('.morph', 'features', feature, '4-review', 'mutation-section.md') : null);
|
|
248
|
-
const outPath = outRel ? resolve(root, outRel) : null;
|
|
249
|
-
if (outPath) {
|
|
250
|
-
mkdirSync(join(outPath, '..'), { recursive: true });
|
|
251
|
-
writeFileSync(outPath, section);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const tally = summarize(merged);
|
|
255
|
-
if (options.json) console.log(JSON.stringify({ ...merged, tally, section: outPath }, null, 2));
|
|
256
|
-
else {
|
|
257
|
-
console.log(section);
|
|
258
|
-
console.log(chalk.gray(outPath ? `seção gravada em ${outPath}` : 'seção não gravada: sem feature nem --out'));
|
|
259
|
-
console.log(`${chalk.green(`${tally.killed} mortos`)} · ${tally.survived ? chalk.yellow(`${tally.survived} sobreviveram`) : '0 sobreviveram'} · ${tally['build-error']} não compilaram · ${tally.inconclusive} inconclusivos`);
|
|
260
|
-
}
|
|
261
|
-
if (!merged.restore.ok || ['mismatch', 'unlocated'].includes(merged.binary.proof) || !merged.restore.treeClean) process.exitCode = EXIT.unsafe;
|
|
262
|
-
return { ...merged, tally, section: outPath };
|
|
263
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { resolve } from 'path';
|
|
4
|
-
import { checkScoutMap, resolveScoutRoot } from '../../lib/scout/scout-check.js';
|
|
5
|
-
|
|
6
|
-
export function scoutCheckCommand(mapPath, options = {}) {
|
|
7
|
-
const file = resolve(mapPath);
|
|
8
|
-
if (!existsSync(file)) {
|
|
9
|
-
if (options.json) console.log(JSON.stringify({ ok: false, error: 'map-not-found', map: file }));
|
|
10
|
-
else console.error(chalk.red(`scout-check: mapa não encontrado: ${file}`));
|
|
11
|
-
process.exit(2);
|
|
12
|
-
}
|
|
13
|
-
const result = { map: file, ...checkScoutMap(readFileSync(file, 'utf8'), { root: resolveScoutRoot(options.root) }) };
|
|
14
|
-
|
|
15
|
-
if (options.json) {
|
|
16
|
-
console.log(JSON.stringify(result, null, 2));
|
|
17
|
-
} else if (result.ok) {
|
|
18
|
-
console.log(chalk.green(`✓ scout-check: ${result.citations.length} citações conferidas contra ${result.root}`));
|
|
19
|
-
} else {
|
|
20
|
-
console.log(chalk.red(`✗ scout-check: ${result.failures.length} falha(s) em ${file} (raiz ${result.root})`));
|
|
21
|
-
for (const f of result.failures) {
|
|
22
|
-
const where = f.line ? `L${f.line}` : '—';
|
|
23
|
-
console.log(` ${where} ${chalk.yellow(f.kind)} ${f.citation ?? ''} ${f.detail}`);
|
|
24
|
-
}
|
|
25
|
-
console.log(chalk.gray(' Mapa reprovado: descarte e re-despache o scout UMA vez com estas falhas no prompt (MORPH.md §6).'));
|
|
26
|
-
}
|
|
27
|
-
process.exit(result.ok ? 0 : 1);
|
|
28
|
-
}
|