@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +86 -21
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +14 -72
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- 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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +3 -3
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +7 -7
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +18 -105
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +17 -111
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +2 -1
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +17 -17
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +31 -126
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/dotnet-test-introspection.js +0 -632
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
4
|
-
import { runVerify,
|
|
4
|
+
import { runVerify, VerifyUsageError } from '../../lib/validators/verify-runner.js';
|
|
5
5
|
import { resolvePrimaryRoot } from '../project/worktree.js';
|
|
6
6
|
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
7
7
|
import { canonicalPath, readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
@@ -12,43 +12,19 @@ import { treeFingerprint } from '../../lib/git/tree-fingerprint.js';
|
|
|
12
12
|
import { HeavyBusyError } from '../../lib/heavy/gate.js';
|
|
13
13
|
import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
|
|
14
14
|
import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
15
|
-
import { runCiVerify, CiUsageError } from '../../lib/ci/ci-verify.js';
|
|
15
|
+
import { runCiVerify, CiUsageError, reusableLocalStamp } from '../../lib/ci/ci-verify.js';
|
|
16
16
|
|
|
17
|
-
/** ✓ pass · ✗ fail · – skip */
|
|
18
17
|
function statusIcon(status) {
|
|
19
18
|
if (status === 'pass') return chalk.green('✓');
|
|
20
19
|
if (status === 'fail') return chalk.red('✗');
|
|
21
20
|
return chalk.gray('–');
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
* One line per test run naming what the task-scope recorte did to it (#87):
|
|
26
|
-
* narrowed (how many selectors, from how many test files), whole, out of scope
|
|
27
|
-
* or full, each with its reason. A green `tests` line alone cannot say that the
|
|
28
|
-
* task ran 12 tests instead of 3.082. Feature-scope runs carry no selection and
|
|
29
|
-
* print nothing extra; `feedback[]` is untouched either way.
|
|
30
|
-
*
|
|
31
|
-
* @param {Array<{id: string, selection: object|null}>} [runs]
|
|
32
|
-
* @returns {string[]}
|
|
33
|
-
*/
|
|
34
|
-
function selectionLines(runs = []) {
|
|
35
|
-
return runs
|
|
36
|
-
.filter((run) => run.selection)
|
|
37
|
-
.map((run) => {
|
|
38
|
-
const { mode, reason, selectors = [], files = [] } = run.selection;
|
|
39
|
-
const detail = mode === 'narrowed'
|
|
40
|
-
? `${selectors.length} selector(s) from ${files.length} test file(s)`
|
|
41
|
-
: (reason ?? '');
|
|
42
|
-
return chalk.gray(` ↳ ${run.id.padEnd(10)} ${mode.padEnd(12)} ${detail}`);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
23
|
function formatVerifyResult(result) {
|
|
47
24
|
const lines = [];
|
|
48
|
-
const scopeLabel = result.scope === 'task' ? `${result.feature}/${result.task}` : result.feature;
|
|
49
25
|
|
|
50
26
|
lines.push('');
|
|
51
|
-
lines.push(chalk.bold(`Verify — ${
|
|
27
|
+
lines.push(chalk.bold(`Verify — ${result.feature}`));
|
|
52
28
|
if (result.stack) lines.push(chalk.gray(`stack: ${result.stack}`));
|
|
53
29
|
lines.push('');
|
|
54
30
|
lines.push(result.status === 'pass' ? chalk.green.bold('✓ PASS') : chalk.red.bold('✗ FAIL'));
|
|
@@ -72,7 +48,6 @@ function formatVerifyResult(result) {
|
|
|
72
48
|
detail = `${node.durationMs}ms`;
|
|
73
49
|
}
|
|
74
50
|
lines.push(` ${icon} ${name.padEnd(11)} ${node.status.padEnd(5)} ${chalk.gray(detail)}`);
|
|
75
|
-
if (name === 'tests') lines.push(...selectionLines(node.runs));
|
|
76
51
|
}
|
|
77
52
|
|
|
78
53
|
if (result.feedback.length > 0) {
|
|
@@ -93,27 +68,9 @@ function formatVerifyResult(result) {
|
|
|
93
68
|
|
|
94
69
|
const samePath = (a, b) => canonicalPath(a).replace(/\\/g, '/') === canonicalPath(b).replace(/\\/g, '/');
|
|
95
70
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
* the PRIMARY ROOT so every tree gets the same answer. Any other tree (a
|
|
100
|
-
* sub-agent's `morph-task/*` tree branched from `morph/{f}`, a scratch
|
|
101
|
-
* checkout) must never write feature.json: its copy is not the one the gates
|
|
102
|
-
* read, and the write would merge-conflict with the owner's.
|
|
103
|
-
*
|
|
104
|
-
* The old rule read only the root's `worktree.path` pointer, which a feature
|
|
105
|
-
* created INSIDE its worktree never has: a `worktrees/homolog-v2` on
|
|
106
|
-
* `morph/chat-demo-simulacao` measured `unregistered-worktree` on 11/11 runs,
|
|
107
|
-
* and `repeatOf` (computed only when persisting) never fired. Git's worktree
|
|
108
|
-
* registry names that tree as the owner.
|
|
109
|
-
*
|
|
110
|
-
* The root is not special: with a live worktree owning the feature, the
|
|
111
|
-
* root's copy is the birth-instant skeleton and a stamp there is drift —
|
|
112
|
-
* `owner-tree-elsewhere`, the verify side of `assertOwnerTree`.
|
|
113
|
-
*
|
|
114
|
-
* @param {{ cwd: string, featureName: string, noPersist: boolean }} params
|
|
115
|
-
* @returns {{ persist: boolean, reason: string|null }}
|
|
116
|
-
*/
|
|
71
|
+
// Persists only in the tree that OWNS the feature (`resolveFeatureTree`, asked from the PRIMARY
|
|
72
|
+
// ROOT so every tree agrees): another tree's copy is not what the gates read, and writing it
|
|
73
|
+
// would merge-conflict with the owner's.
|
|
117
74
|
function decidePersistence({ cwd, featureName, noPersist }) {
|
|
118
75
|
if (noPersist) return { persist: false, reason: 'no-persist-flag' };
|
|
119
76
|
|
|
@@ -130,17 +87,8 @@ function baselineStampBlock(baseline) {
|
|
|
130
87
|
return `record-baseline: a contagem caiu em ${fell.map((u) => `${u.id} (${u.from} → ${u.to})`).join(', ')}`;
|
|
131
88
|
}
|
|
132
89
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
* writers of it — the per-scope history entry (`verifications.*`) and the
|
|
136
|
-
* singular gate stamp (`verification.nodes`). One function because they were
|
|
137
|
-
* two literal copies twenty-five lines apart: a fifth node meant remembering
|
|
138
|
-
* both, and a stamp missing a node the history has is precisely the kind of
|
|
139
|
-
* half-truth the gate reads as fact.
|
|
140
|
-
*
|
|
141
|
-
* @param {{ nodes: Record<string, { status: string }> }} result - runVerify() output
|
|
142
|
-
* @returns {{ build: string, tests: string, validators: string, e2e: string }}
|
|
143
|
-
*/
|
|
90
|
+
// The single writer of both node maps the schema fixes (`verification.nodes` and the history
|
|
91
|
+
// entry): a stamp missing a node the history has is a half-truth the gate reads as fact.
|
|
144
92
|
function nodeStatuses(result) {
|
|
145
93
|
return {
|
|
146
94
|
build: result.nodes.build.status,
|
|
@@ -151,25 +99,8 @@ function nodeStatuses(result) {
|
|
|
151
99
|
};
|
|
152
100
|
}
|
|
153
101
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
*
|
|
157
|
-
* The rollup cannot answer the question Gate 3 asks. A red eval and a red unit
|
|
158
|
-
* test both arrive as `tests: 'fail'`, and — worse — an eval node that was
|
|
159
|
-
* SKIPPED (task scope) leaves the rollup green, so a gate reading `tests` alone
|
|
160
|
-
* cannot tell "the eval ran and passed" from "the eval never ran".
|
|
161
|
-
*
|
|
162
|
-
* Four values, and the distinction between the last two is the whole point:
|
|
163
|
-
* - `'pass'` / `'fail'`: every declared eval node agreed, or at least one did
|
|
164
|
-
* not. Same pessimistic rollup rule the tests node already uses;
|
|
165
|
-
* - `'skip'`: eval nodes ARE declared, but this run did not execute them;
|
|
166
|
-
* - `null`: this project declares NO eval node. Not a skip — there is nothing
|
|
167
|
-
* to skip. `risk-detector.js` pauses on the two for different reasons,
|
|
168
|
-
* because they call for different actions.
|
|
169
|
-
*
|
|
170
|
-
* @param {{nodes: {tests: {runs?: Array}}}} result - runVerify() output
|
|
171
|
-
* @returns {'pass'|'fail'|'skip'|null}
|
|
172
|
-
*/
|
|
102
|
+
// `'skip'` = eval nodes declared but not run; `null` = no eval node declared. A skipped eval
|
|
103
|
+
// leaves the tests rollup green, so the gate needs this split to tell them apart.
|
|
173
104
|
function evalsStatusOf(result) {
|
|
174
105
|
const runs = (result.nodes.tests?.runs || []).filter((run) => run.runner === EVAL_RUNNER);
|
|
175
106
|
if (runs.length === 0) return null;
|
|
@@ -178,10 +109,7 @@ function evalsStatusOf(result) {
|
|
|
178
109
|
return 'skip';
|
|
179
110
|
}
|
|
180
111
|
|
|
181
|
-
|
|
182
|
-
* The options that make a FEATURE-scope run partial, each with the `reason` it
|
|
183
|
-
* reports when it keeps the stamp from being written — first match wins.
|
|
184
|
-
*/
|
|
112
|
+
// First match wins; each keeps the stamp from being written, with its `reason`.
|
|
185
113
|
const PARTIAL_RUN_FLAGS = [['filter', 'filtered'], ['skipTests', 'skip-tests'], ['skipBuild', 'skip-build']];
|
|
186
114
|
|
|
187
115
|
const CI_EXCLUSIVE_FLAGS = [
|
|
@@ -190,9 +118,9 @@ const CI_EXCLUSIVE_FLAGS = [
|
|
|
190
118
|
['noPersist', '--no-persist'],
|
|
191
119
|
];
|
|
192
120
|
|
|
193
|
-
async function runLocalVerify(featureName,
|
|
121
|
+
async function runLocalVerify(featureName, options) {
|
|
194
122
|
try {
|
|
195
|
-
return await runVerify(process.cwd(), featureName,
|
|
123
|
+
return await runVerify(process.cwd(), featureName, {
|
|
196
124
|
timeout: options.timeout,
|
|
197
125
|
skipTests: options.skipTests,
|
|
198
126
|
skipBuild: options.skipBuild,
|
|
@@ -203,14 +131,9 @@ async function runLocalVerify(featureName, taskId, options) {
|
|
|
203
131
|
refreshCache: options.refreshCache,
|
|
204
132
|
linux: options.linux,
|
|
205
133
|
linuxImage: options.linuxImage,
|
|
206
|
-
// Task scope: switch the recorte off (#87). At feature scope nothing is
|
|
207
|
-
// ever narrowed, so the flag is a documented no-op there, not an error.
|
|
208
|
-
full: options.full,
|
|
209
134
|
});
|
|
210
135
|
} catch (err) {
|
|
211
|
-
//
|
|
212
|
-
// verdict. `--filter` that selects nothing must not print green (or red):
|
|
213
|
-
// there is no verification to report, only a command to fix.
|
|
136
|
+
// Exit 2, never a verdict: a `--filter` that selects nothing must not print green or red.
|
|
214
137
|
if (err instanceof VerifyUsageError) {
|
|
215
138
|
console.error(chalk.red(`\n❌ ${err.message}\n`));
|
|
216
139
|
process.exit(2);
|
|
@@ -231,28 +154,9 @@ async function runLocalVerify(featureName, taskId, options) {
|
|
|
231
154
|
}
|
|
232
155
|
}
|
|
233
156
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
* for the node cadence; this command wraps it with CLI ergonomics
|
|
238
|
-
* (human/JSON output, exit codes) and context-aware persistence.
|
|
239
|
-
*
|
|
240
|
-
* Exit codes: 0 = pass (including all-skip) · 1 = fail (>=1 red node, or a
|
|
241
|
-
* `--record-baseline` the run refused to write) · 2 = usage error.
|
|
242
|
-
*
|
|
243
|
-
* Exit 2 is "the command cannot mean what you typed", and there are four
|
|
244
|
-
* paths to it — never a verdict, because a verdict about nothing is worse
|
|
245
|
-
* than a refusal:
|
|
246
|
-
* - unknown feature, or unknown task id (the task's `group` and `outputs`
|
|
247
|
-
* decide the scope, so it is a hard usage error here);
|
|
248
|
-
* - `--filter` that matches no declared node (a filtered-to-empty run is
|
|
249
|
-
* green and proves nothing);
|
|
250
|
-
* - `--record-baseline` with nowhere to write (no `config.json`), or with a
|
|
251
|
-
* task id (a recorte de task is never a fleet baseline, #87);
|
|
252
|
-
* - `--linux` without Docker.
|
|
253
|
-
* The last three arrive as `VerifyUsageError` from the runner.
|
|
254
|
-
*/
|
|
255
|
-
export async function verifyCommand(featureName, taskId, options = {}) {
|
|
157
|
+
// Exit 2 is "the command cannot mean what you typed" (unknown feature, `--filter` matching
|
|
158
|
+
// nothing, `--record-baseline` without config.json, `--linux` without Docker): never a verdict.
|
|
159
|
+
export async function verifyCommand(featureName, options = {}) {
|
|
256
160
|
let feature;
|
|
257
161
|
try {
|
|
258
162
|
feature = getFeature(featureName);
|
|
@@ -265,22 +169,13 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
265
169
|
}
|
|
266
170
|
|
|
267
171
|
if (options.ci) {
|
|
268
|
-
const conflict =
|
|
172
|
+
const conflict = CI_EXCLUSIVE_FLAGS.find(([flag]) => options[flag])?.[1];
|
|
269
173
|
if (conflict) {
|
|
270
174
|
console.error(chalk.red(`\n❌ --ci roda a suíte inteira da feature no CI; não combina com ${conflict}\n`));
|
|
271
175
|
process.exit(2);
|
|
272
176
|
}
|
|
273
177
|
}
|
|
274
178
|
|
|
275
|
-
// The runner's own reader (one reader of tasks.json for verify, #87): null
|
|
276
|
-
// means tasks.json or the task is missing, and both are this usage error.
|
|
277
|
-
if (taskId) {
|
|
278
|
-
if (!readTask(process.cwd(), featureName, taskId)) {
|
|
279
|
-
console.error(chalk.red(`\n❌ Task "${taskId}" not found in tasks.json for feature "${featureName}"\n`));
|
|
280
|
-
process.exit(2);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
179
|
let ci = null;
|
|
285
180
|
if (options.ci) {
|
|
286
181
|
try {
|
|
@@ -294,6 +189,17 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
294
189
|
console.error(chalk.yellow(`\n⏳ ${outcome.reason}. Não é vermelho e não há stamp: não repita em laço — siga e deixe o gate-check decidir.\n`));
|
|
295
190
|
process.exit(3);
|
|
296
191
|
} else {
|
|
192
|
+
const reused = reusableLocalStamp(process.cwd(), feature.verification);
|
|
193
|
+
if (reused) {
|
|
194
|
+
console.error(chalk.green(`\n↪ CI indisponível (${outcome.reason}) — stamp fresco reaproveitado (sha ${reused.sha.slice(0, 8)}, ${reused.ts}): nada mudou fora de .morph/ desde aquela rodada verde.\n`));
|
|
195
|
+
if (options.json) {
|
|
196
|
+
console.log(JSON.stringify({
|
|
197
|
+
feature: featureName, status: reused.status, nodes: reused.nodes, e2eReason: reused.e2eReason ?? null,
|
|
198
|
+
reused: { sha: reused.sha, ts: reused.ts, source: reused.source ?? 'local' }, persisted: false, reason: 'stamp-reused',
|
|
199
|
+
}, null, 2));
|
|
200
|
+
}
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
297
203
|
console.error(chalk.yellow(`\n↪ CI indisponível (${outcome.reason}) — verify local.\n`));
|
|
298
204
|
}
|
|
299
205
|
} catch (err) {
|
|
@@ -308,31 +214,27 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
308
214
|
// Taken before the run: it is the tree the run is about to read.
|
|
309
215
|
const fingerprint = treeFingerprint(process.cwd());
|
|
310
216
|
|
|
311
|
-
const result = ci ? ci.result : await runLocalVerify(featureName,
|
|
217
|
+
const result = ci ? ci.result : await runLocalVerify(featureName, options);
|
|
312
218
|
|
|
313
219
|
const cleanup = ci ? { status: 'skipped', reason: 'ci' } : afterHeavyRun({
|
|
314
220
|
projectPath: process.cwd(),
|
|
315
221
|
config: readHeavyConfig(process.cwd()),
|
|
316
|
-
scope: taskId ? 'task' : 'feature',
|
|
317
222
|
});
|
|
318
223
|
|
|
319
|
-
const
|
|
224
|
+
const historyKey = 'verifications.feature';
|
|
320
225
|
// A run that measured only part of the suite must not stand in for the proof a gate asks for:
|
|
321
|
-
// --filter and --skip
|
|
322
|
-
|
|
323
|
-
const partialFeatureRun = (!options.noPersist && !taskId)
|
|
324
|
-
? PARTIAL_RUN_FLAGS.find(([flag]) => options[flag])?.[1] ?? null
|
|
325
|
-
: null;
|
|
226
|
+
// --filter and --skip-*, or a --record-baseline that recorded nothing; --no-persist keeps its own reason.
|
|
227
|
+
const partialRun = options.noPersist ? null : PARTIAL_RUN_FLAGS.find(([flag]) => options[flag])?.[1] ?? null;
|
|
326
228
|
const baselineBlock = options.recordBaseline ? baselineStampBlock(result.baseline) : null;
|
|
327
229
|
const { persist, reason } = baselineBlock
|
|
328
230
|
? { persist: false, reason: baselineBlock }
|
|
329
|
-
:
|
|
330
|
-
? { persist: false, reason:
|
|
231
|
+
: partialRun
|
|
232
|
+
? { persist: false, reason: partialRun }
|
|
331
233
|
: decidePersistence({ cwd: process.cwd(), featureName, noPersist: options.noPersist });
|
|
332
234
|
|
|
333
235
|
let compact = null;
|
|
334
236
|
if (persist) {
|
|
335
|
-
const prevEntry =
|
|
237
|
+
const prevEntry = feature.verifications?.feature;
|
|
336
238
|
const now = new Date().toISOString();
|
|
337
239
|
compact = {
|
|
338
240
|
status: result.status,
|
|
@@ -340,42 +242,28 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
340
242
|
attempts: (prevEntry?.attempts ?? 0) + 1,
|
|
341
243
|
updatedAt: now,
|
|
342
244
|
};
|
|
343
|
-
await updateFeature(featureName,
|
|
245
|
+
await updateFeature(featureName, historyKey, compact);
|
|
344
246
|
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// EXACT commit it covers, the same way review-report.md's
|
|
349
|
-
// `**Reviewed-SHA:**` token lets approve.js detect a stale review. null
|
|
350
|
-
// in a non-git checkout (or before the first commit) — fail-open, never
|
|
351
|
-
// blocks persistence.
|
|
352
|
-
//
|
|
353
|
-
// `nodes` + `e2eReason` são acréscimo de campo, não mudança de forma: o
|
|
354
|
-
// rollup `status` continua onde estava. Existem porque um nó e2e que PULA
|
|
355
|
-
// não reprova o rollup — sem o status do nó e a razão LITERAL do skip, o
|
|
356
|
-
// gate-check lê 'pass' tanto quando o e2e rodou e passou quanto quando ele
|
|
357
|
-
// nunca rodou, e não tem como separar dispensa legítima ("o projeto não
|
|
358
|
-
// containeriza") de ausência de prova ("docker indisponível"). `e2eReason`
|
|
359
|
-
// é null quando o nó não pulou — e um null NUM SKIP é lido como dispensa
|
|
360
|
-
// sem motivo, que risk-detector.js recusa.
|
|
247
|
+
// `sha` ties the stamp to the exact commit (null without git: fail-open). `nodes` and `e2eReason`
|
|
248
|
+
// exist because a skipped e2e does not fail the rollup; a null reason ON A SKIP reads as a
|
|
249
|
+
// waiver without motive, which risk-detector.js refuses.
|
|
361
250
|
const stamp = {
|
|
362
251
|
status: result.status,
|
|
363
252
|
nodes: nodeStatuses(result),
|
|
364
253
|
e2eReason: result.nodes.e2e.status === 'skip' ? (result.nodes.e2e.reason ?? null) : null,
|
|
365
254
|
sha: ci ? ci.run.headSha : getHeadSha(process.cwd()),
|
|
366
255
|
ts: now,
|
|
367
|
-
scope: taskId ? `task:${taskId}` : 'feature',
|
|
368
256
|
...(ci ? { source: 'ci', ciRun: ci.run.url } : {}),
|
|
369
257
|
};
|
|
370
258
|
await updateFeature(featureName, 'verification', stamp);
|
|
371
259
|
}
|
|
372
260
|
|
|
373
|
-
// A
|
|
261
|
+
// A run over the exact tree of the previous one proves nothing
|
|
374
262
|
// new. Advisory only: the stamp and the exit code are the same either way.
|
|
375
263
|
let repeatOf = null;
|
|
376
264
|
try {
|
|
377
265
|
const cwd = process.cwd();
|
|
378
|
-
if (
|
|
266
|
+
if (persist && !ci) repeatOf = findRepeatedVerify(cwd, { feature: featureName, fingerprint });
|
|
379
267
|
const tree = resolve(cwd).replace(/\\/g, '/') === resolvePrimaryRoot(cwd) ? 'root' : 'worktree';
|
|
380
268
|
recordVerifyEvent(cwd, buildVerifyEvent(result, { sha: ci ? ci.run.headSha : getHeadSha(cwd), tree, persisted: persist, reason, fingerprint, source: ci ? 'ci' : 'local' }));
|
|
381
269
|
} catch { /* telemetry never changes the verdict */ }
|
|
@@ -406,7 +294,7 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
406
294
|
console.log(chalk.gray(`${mark} dotnet build-server shutdown: ${cleanup.status}${cleanup.reason ? ` (${cleanup.reason})` : ''}\n`));
|
|
407
295
|
}
|
|
408
296
|
if (repeatOf) {
|
|
409
|
-
console.log(chalk.yellow(`⚠️ Mesma árvore do verify de
|
|
297
|
+
console.log(chalk.yellow(`⚠️ Mesma árvore do verify de ${repeatOf.ts} (${repeatOf.status}): nada mudou fora de .morph/ desde então. `
|
|
410
298
|
+ 'Esta rodada não prova nada novo — reuse o resultado anterior, a menos que o ambiente (Docker, banco, serviço externo) tenha mudado.\n'));
|
|
411
299
|
}
|
|
412
300
|
if (result.baseline?.recorded) {
|
|
@@ -417,16 +305,13 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
417
305
|
`));
|
|
418
306
|
}
|
|
419
307
|
if (persist) {
|
|
420
|
-
console.log(chalk.gray(`Persisted: ${
|
|
308
|
+
console.log(chalk.gray(`Persisted: ${historyKey} (attempts: ${compact.attempts})\n`));
|
|
421
309
|
} else {
|
|
422
310
|
console.log(chalk.yellow(`⚠️ Not persisted (${reason})\n`));
|
|
423
311
|
}
|
|
424
312
|
}
|
|
425
313
|
|
|
426
|
-
// A recording
|
|
427
|
-
// invocation even when the run itself was green (a filtered run is green and
|
|
428
|
-
// unrecordable): exiting 0 there would report success for something that did
|
|
429
|
-
// not happen.
|
|
314
|
+
// A recording asked for and not made fails the invocation, even when the run was green.
|
|
430
315
|
const baselineRefused = Boolean(result.baseline && !result.baseline.recorded);
|
|
431
316
|
process.exit(result.status === 'pass' && !baselineRefused ? 0 : 1);
|
|
432
317
|
}
|
|
@@ -1,115 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Output Schema — Single Source of Truth (v5)
|
|
3
|
-
*
|
|
4
|
-
* ALL output type definitions live here. Edit ONLY this file to add, rename,
|
|
5
|
-
* or relocate output types.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { join } from 'path';
|
|
9
2
|
|
|
10
|
-
// ============================================================================
|
|
11
|
-
// Schema Definition
|
|
12
|
-
// ============================================================================
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Schema for output types produced by morph-spec NLH phases.
|
|
16
|
-
*
|
|
17
|
-
* Fields:
|
|
18
|
-
* filename - The file's basename (e.g. 'spec.md'). Single-file types only.
|
|
19
|
-
* phaseDir - Subdirectory within .morph/features/{feature}/
|
|
20
|
-
* phase - The morph-spec phase that produces this output
|
|
21
|
-
* protected - Whether the file requires an approval gate before editing
|
|
22
|
-
* approvalGate - (optional) Which gate protects this file
|
|
23
|
-
* mutableFields- (optional) For protected JSON files: top-level array-item
|
|
24
|
-
* field names that MAY still change after the gate is approved.
|
|
25
|
-
* Lets the implement phase update task progress without
|
|
26
|
-
* unfreezing the plan contract. When present, the protect
|
|
27
|
-
* hook does a semantic diff instead of a blanket block.
|
|
28
|
-
*
|
|
29
|
-
* Multi-file types (`multi: true`) hold N files in a directory instead of one
|
|
30
|
-
* fixed basename. They carry `dirname` + `filenamePattern` + `displayFilename`
|
|
31
|
-
* INSTEAD of `filename`, and every derivation keyed by basename
|
|
32
|
-
* (FILENAME_TO_OUTPUT, PROTECTED_SPEC_FILES, MUTABLE_SPEC_FIELDS) skips them —
|
|
33
|
-
* there is no single basename to key on. `getOutputPath` returns the DIRECTORY
|
|
34
|
-
* for these types.
|
|
35
|
-
*/
|
|
36
3
|
export const OUTPUT_SCHEMA = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
stackScan: { filename: 'stack-scan.md', phaseDir: '0-proposal', phase: 'proposal', protected: false },
|
|
4
|
+
plan: { filename: 'plan.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
5
|
+
checks: { filename: 'checks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
40
6
|
|
|
41
|
-
// 1-design/ (optional)
|
|
42
7
|
uiDesignSystem: { filename: 'design-system.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
43
8
|
uiMockups: { filename: 'mockups.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
44
9
|
uiComponents: { filename: 'components.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
45
10
|
uiFlows: { filename: 'flows.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
46
11
|
|
|
47
|
-
|
|
48
|
-
spec: { filename: 'spec.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
49
|
-
tasks: { filename: 'tasks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan', mutableFields: ['status', 'outputs', 'notes'] },
|
|
50
|
-
decisions: { filename: 'decisions.md', phaseDir: '2-plan', phase: 'plan', protected: false },
|
|
51
|
-
// Veredito REUSAR/ESTENDER/CRIAR por conceito que a feature introduz, com a
|
|
52
|
-
// justificativa de cada CRIAR. É o ARTEFATO do ciclo de reuso (a prosa está
|
|
53
|
-
// em morph-plan §2b, a cobrança em src/lib/validators/reuse/). NÃO protegido,
|
|
54
|
-
// como decisions.md: é registro vivo — o implementador que descobre um alvo
|
|
55
|
-
// melhor no meio da task precisa poder anotar, e congelá-lo transformaria a
|
|
56
|
-
// correção num bloqueio de gate.
|
|
57
|
-
// N files, one per system use case — the behaviour layer the plan distils
|
|
58
|
-
// BEFORE spec.md/tasks.json. Deliberately NOT protected: editing a use case
|
|
59
|
-
// after Gate 2 is the supported way to change behaviour and re-run implement;
|
|
60
|
-
// the audit trail lives in git.
|
|
61
|
-
usecases: {
|
|
62
|
-
dirname: 'usecases',
|
|
63
|
-
filenamePattern: '^UC-\\d+-[a-z0-9-]+\\.md$',
|
|
64
|
-
displayFilename: 'UC-{nn}-{slug}.md',
|
|
65
|
-
phaseDir: '2-plan',
|
|
66
|
-
phase: 'plan',
|
|
67
|
-
protected: false,
|
|
68
|
-
multi: true,
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
// 3-implement/
|
|
72
|
-
recap: { filename: 'recap.md', phaseDir: '3-implement', phase: 'implement', protected: false },
|
|
73
|
-
// N files, one per task whose doneCriteria judges what a MODEL answers
|
|
74
|
-
// (morph-plan §5). Runtime evidence: input, model, output, verdict — pointing
|
|
75
|
-
// at the `runner: "evals"` node when the stack declares one. CONDITIONAL by
|
|
76
|
-
// construction: never in phases.json requiredOutputs. Readers: morph-review
|
|
77
|
-
// dispatch prompt.
|
|
78
|
-
evidencias: { dirname: 'evidencias', filenamePattern: '^T\\d+\\.md$', displayFilename: '{taskId}.md',
|
|
12
|
+
evidencias: { dirname: 'evidencias', filenamePattern: '^C\\d+\\.md$', displayFilename: '{Cn}.md',
|
|
79
13
|
phaseDir: '3-implement', phase: 'implement', protected: false, multi: true },
|
|
80
14
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// (MORPH.md §6) — the orchestrator reads it and synthesizes reviewReport
|
|
85
|
-
// from it; the two never collide because one is raw sub-agent output and
|
|
86
|
-
// the other is the orchestrator's curated Gate 3 presentation.
|
|
87
|
-
evaluatorReport:{ filename: 'evaluator-report.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
15
|
+
verifierReport: { filename: 'verifier.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
16
|
+
reviewerReport: { filename: 'reviewer.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
17
|
+
securityReport: { filename: 'security.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
88
18
|
};
|
|
89
19
|
|
|
90
|
-
// ============================================================================
|
|
91
|
-
// Builder Functions (Windows-safe via path.join)
|
|
92
|
-
// ============================================================================
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Leaf segment of a schema entry: the basename for single-file types, the
|
|
96
|
-
* directory name for multi-file ones. Keeps every path builder from having to
|
|
97
|
-
* know about `multi` on its own.
|
|
98
|
-
*/
|
|
99
20
|
function leafSegment(entry) {
|
|
100
21
|
return entry.multi ? entry.dirname : entry.filename;
|
|
101
22
|
}
|
|
102
23
|
|
|
103
|
-
/**
|
|
104
|
-
* Get the morph-relative path for a specific output type of a feature.
|
|
105
|
-
*
|
|
106
|
-
* For `multi` types this is the DIRECTORY holding the N files
|
|
107
|
-
* (".morph/features/my-feature/2-plan/usecases"), not a single file.
|
|
108
|
-
*
|
|
109
|
-
* @param {string} featureName
|
|
110
|
-
* @param {string} outputType - camelCase output type key from OUTPUT_SCHEMA
|
|
111
|
-
* @returns {string} e.g. ".morph/features/my-feature/2-plan/spec.md"
|
|
112
|
-
*/
|
|
24
|
+
/** For `multi` types this is the directory holding the N files. */
|
|
113
25
|
export function getOutputPath(featureName, outputType) {
|
|
114
26
|
const entry = OUTPUT_SCHEMA[outputType];
|
|
115
27
|
if (!entry) {
|
|
@@ -119,10 +31,6 @@ export function getOutputPath(featureName, outputType) {
|
|
|
119
31
|
.replace(/\\/g, '/');
|
|
120
32
|
}
|
|
121
33
|
|
|
122
|
-
/**
|
|
123
|
-
* Get the absolute filesystem path for a specific output type.
|
|
124
|
-
* For `multi` types this is the directory — see {@link getOutputPath}.
|
|
125
|
-
*/
|
|
126
34
|
export function getAbsoluteOutputPath(projectRoot, featureName, outputType) {
|
|
127
35
|
const entry = OUTPUT_SCHEMA[outputType];
|
|
128
36
|
if (!entry) {
|
|
@@ -131,42 +39,20 @@ export function getAbsoluteOutputPath(projectRoot, featureName, outputType) {
|
|
|
131
39
|
return join(projectRoot, '.morph', 'features', featureName, entry.phaseDir, leafSegment(entry));
|
|
132
40
|
}
|
|
133
41
|
|
|
134
|
-
/**
|
|
135
|
-
* Get the feature base directory (morph-relative, forward slashes).
|
|
136
|
-
*/
|
|
137
42
|
export function getFeatureDir(featureName) {
|
|
138
43
|
return join('.morph', 'features', featureName).replace(/\\/g, '/');
|
|
139
44
|
}
|
|
140
45
|
|
|
141
|
-
/**
|
|
142
|
-
* Per-task sub-agent report — the durable "return value" channel for a
|
|
143
|
-
* dispatched implementation task (MORPH.md §6). A sub-agent's final text
|
|
144
|
-
* message may never reach the orchestrator (idle_notification with no text
|
|
145
|
-
* is an observed failure mode); the file it writes here always does. Doesn't
|
|
146
|
-
* fit OUTPUT_SCHEMA's one-fixed-filename-per-type model since the filename
|
|
147
|
-
* is parameterized by taskId, so it's a sibling helper instead of a schema
|
|
148
|
-
* entry.
|
|
149
|
-
*
|
|
150
|
-
* @param {string} featureName
|
|
151
|
-
* @param {string} taskId - e.g. "T7"
|
|
152
|
-
* @returns {string} morph-relative, forward-slash path
|
|
153
|
-
*/
|
|
154
46
|
export function getTaskReportPath(featureName, taskId) {
|
|
155
47
|
return join('.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`)
|
|
156
48
|
.replace(/\\/g, '/');
|
|
157
49
|
}
|
|
158
50
|
|
|
159
|
-
/** Absolute filesystem variant of {@link getTaskReportPath}. */
|
|
160
51
|
export function getAbsoluteTaskReportPath(projectRoot, featureName, taskId) {
|
|
161
52
|
return join(projectRoot, '.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`);
|
|
162
53
|
}
|
|
163
54
|
|
|
164
|
-
/**
|
|
165
|
-
* Get all output paths for a feature, ready for state.json `outputs` block.
|
|
166
|
-
*
|
|
167
|
-
* Multi-file types contribute their DIRECTORY, so `created` for them means
|
|
168
|
-
* "the folder exists" — the state index tracks presence, not file count.
|
|
169
|
-
*/
|
|
55
|
+
/** `multi` types contribute their directory, so `created` means "the folder exists". */
|
|
170
56
|
export function getAllOutputPaths(featureName) {
|
|
171
57
|
const outputs = {};
|
|
172
58
|
for (const [key] of Object.entries(OUTPUT_SCHEMA)) {
|
|
@@ -178,14 +64,6 @@ export function getAllOutputPaths(featureName) {
|
|
|
178
64
|
return outputs;
|
|
179
65
|
}
|
|
180
66
|
|
|
181
|
-
// ============================================================================
|
|
182
|
-
// Derived Constants
|
|
183
|
-
// ============================================================================
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* filename → output type. Multi-file types are absent by construction: they
|
|
187
|
-
* have N basenames, so there is nothing to key on.
|
|
188
|
-
*/
|
|
189
67
|
export const FILENAME_TO_OUTPUT = Object.fromEntries(
|
|
190
68
|
Object.entries(OUTPUT_SCHEMA)
|
|
191
69
|
.filter(([, entry]) => !entry.multi)
|
|
@@ -196,36 +74,12 @@ export const OUTPUT_PHASE_MAP = Object.fromEntries(
|
|
|
196
74
|
Object.entries(OUTPUT_SCHEMA).map(([key, entry]) => [key, entry.phase])
|
|
197
75
|
);
|
|
198
76
|
|
|
199
|
-
/**
|
|
200
|
-
* Protected spec files and the approval gate that locks them.
|
|
201
|
-
*/
|
|
202
77
|
export const PROTECTED_SPEC_FILES = Object.fromEntries(
|
|
203
78
|
Object.entries(OUTPUT_SCHEMA)
|
|
204
79
|
.filter(([, entry]) => entry.protected && entry.approvalGate && !entry.multi)
|
|
205
80
|
.map(([, entry]) => [entry.filename, entry.approvalGate])
|
|
206
81
|
);
|
|
207
82
|
|
|
208
|
-
/**
|
|
209
|
-
* For protected files that carry mutable progress fields: filename → the
|
|
210
|
-
* top-level array-item fields that may change after the gate is approved.
|
|
211
|
-
* The protect-spec-files hook uses this to allow progress updates (e.g.
|
|
212
|
-
* task status) while still freezing the plan contract.
|
|
213
|
-
*/
|
|
214
|
-
export const MUTABLE_SPEC_FIELDS = Object.fromEntries(
|
|
215
|
-
Object.entries(OUTPUT_SCHEMA)
|
|
216
|
-
.filter(([, entry]) => entry.protected && entry.approvalGate && entry.mutableFields && !entry.multi)
|
|
217
|
-
.map(([, entry]) => [entry.filename, entry.mutableFields])
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
// ============================================================================
|
|
221
|
-
// Backwards-compat shim (will be removed in v5.1)
|
|
222
|
-
// ============================================================================
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* v4 had separate contracts.cs / contracts.ts / contracts-vsa.cs output types.
|
|
226
|
-
* In v5 contracts live inside spec.md (or in source files for VSA). This
|
|
227
|
-
* function keeps callers happy until they're updated.
|
|
228
|
-
*/
|
|
229
83
|
export function resolveContractsOutputType() {
|
|
230
|
-
return '
|
|
84
|
+
return 'plan';
|
|
231
85
|
}
|
|
@@ -1,32 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Feature identity — the fields that keep a feature.json from being an
|
|
3
|
-
* anonymous shell (the {status, updatedAt}-only scaffolds nobody could later
|
|
4
|
-
* identify). name/createdAt/workType are auto-stamped at birth (explicit
|
|
5
|
-
* `morph-spec create` or the state-sync hook's implicit registration);
|
|
6
|
-
* `description` is the one field a human must supply.
|
|
7
|
-
*
|
|
8
|
-
* `missingIdentity` powers the audit surfaces (`list`, `doctor`) that flag
|
|
9
|
-
* pre-existing anonymous features LENIENTLY (warn, never invalidate old
|
|
10
|
-
* feature.json). `create` enforces `description` HARD at birth — the two-tier
|
|
11
|
-
* split the schema's `required` array can't express (adding fields there would
|
|
12
|
-
* invalidate every feature.json that predates this).
|
|
13
|
-
*
|
|
14
|
-
* Pure module — no filesystem/CLI side effects.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
1
|
export const IDENTITY_FIELDS = ['name', 'description', 'workType', 'createdAt'];
|
|
18
2
|
|
|
19
3
|
const blank = (v) => v === undefined || v === null || String(v).trim() === '';
|
|
20
4
|
|
|
21
|
-
|
|
22
|
-
* Identity fields absent from a feature.json (empty array = fully identified).
|
|
23
|
-
* `description` leads because its absence is the real anonymity signal.
|
|
24
|
-
* `workType` is intentionally excluded — absent workType defaults to 'feature',
|
|
25
|
-
* so it is never a marker of an anonymous shell.
|
|
26
|
-
*
|
|
27
|
-
* @param {object} feature - feature.json contents
|
|
28
|
-
* @returns {string[]} missing identity field names
|
|
29
|
-
*/
|
|
5
|
+
// `workType` is excluded on purpose: absent, it defaults to `feature`.
|
|
30
6
|
export function missingIdentity(feature) {
|
|
31
7
|
const missing = [];
|
|
32
8
|
if (blank(feature?.description)) missing.push('description');
|