@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- 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 +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- 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 +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- 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.sh +11 -11
- 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 +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- 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 +1 -42
- 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 +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- 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/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/rules/nextjs-standards.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- 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/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 +2 -2
- 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 +1 -1
- 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/testing.md +10 -4
- 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/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- 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/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- 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/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- 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/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- 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 +5 -57
- 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.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- 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 +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- 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 +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- 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 +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- 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 +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- 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 +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- 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 +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- 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 +1 -0
- 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 +3 -15
- 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 +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- 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 +2 -24
- 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 +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- 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 +13 -95
- 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 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -14,7 +14,6 @@ import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
|
|
|
14
14
|
import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
15
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('✗');
|
|
@@ -69,27 +68,9 @@ function formatVerifyResult(result) {
|
|
|
69
68
|
|
|
70
69
|
const samePath = (a, b) => canonicalPath(a).replace(/\\/g, '/') === canonicalPath(b).replace(/\\/g, '/');
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* the PRIMARY ROOT so every tree gets the same answer. Any other tree (a
|
|
76
|
-
* sub-agent's `morph-task/*` tree branched from `morph/{f}`, a scratch
|
|
77
|
-
* checkout) must never write feature.json: its copy is not the one the gates
|
|
78
|
-
* read, and the write would merge-conflict with the owner's.
|
|
79
|
-
*
|
|
80
|
-
* The old rule read only the root's `worktree.path` pointer, which a feature
|
|
81
|
-
* created INSIDE its worktree never has: a `worktrees/homolog-v2` on
|
|
82
|
-
* `morph/chat-demo-simulacao` measured `unregistered-worktree` on 11/11 runs,
|
|
83
|
-
* and `repeatOf` (computed only when persisting) never fired. Git's worktree
|
|
84
|
-
* registry names that tree as the owner.
|
|
85
|
-
*
|
|
86
|
-
* The root is not special: with a live worktree owning the feature, the
|
|
87
|
-
* root's copy is the birth-instant skeleton and a stamp there is drift —
|
|
88
|
-
* `owner-tree-elsewhere`, the verify side of `assertOwnerTree`.
|
|
89
|
-
*
|
|
90
|
-
* @param {{ cwd: string, featureName: string, noPersist: boolean }} params
|
|
91
|
-
* @returns {{ persist: boolean, reason: string|null }}
|
|
92
|
-
*/
|
|
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.
|
|
93
74
|
function decidePersistence({ cwd, featureName, noPersist }) {
|
|
94
75
|
if (noPersist) return { persist: false, reason: 'no-persist-flag' };
|
|
95
76
|
|
|
@@ -100,23 +81,12 @@ function decidePersistence({ cwd, featureName, noPersist }) {
|
|
|
100
81
|
}
|
|
101
82
|
|
|
102
83
|
function baselineStampBlock(baseline) {
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
if (fell.length === 0) return null;
|
|
106
|
-
return `record-baseline: a contagem caiu em ${fell.map((u) => `${u.id} (${u.from} → ${u.to})`).join(', ')}`;
|
|
84
|
+
if (baseline?.recorded) return null;
|
|
85
|
+
return baseline?.reason ? `record-baseline: ${baseline.reason}` : 'record-baseline';
|
|
107
86
|
}
|
|
108
87
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* writers of it — the history entry (`verifications.feature`) and the
|
|
112
|
-
* singular gate stamp (`verification.nodes`). One function because they were
|
|
113
|
-
* two literal copies twenty-five lines apart: a fifth node meant remembering
|
|
114
|
-
* both, and a stamp missing a node the history has is precisely the kind of
|
|
115
|
-
* half-truth the gate reads as fact.
|
|
116
|
-
*
|
|
117
|
-
* @param {{ nodes: Record<string, { status: string }> }} result - runVerify() output
|
|
118
|
-
* @returns {{ build: string, tests: string, validators: string, e2e: string }}
|
|
119
|
-
*/
|
|
88
|
+
// The single writer of both node maps the schema fixes (`verification.nodes` and the history
|
|
89
|
+
// entry): a stamp missing a node the history has is a half-truth the gate reads as fact.
|
|
120
90
|
function nodeStatuses(result) {
|
|
121
91
|
return {
|
|
122
92
|
build: result.nodes.build.status,
|
|
@@ -127,25 +97,8 @@ function nodeStatuses(result) {
|
|
|
127
97
|
};
|
|
128
98
|
}
|
|
129
99
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
* The rollup cannot answer the question Gate 3 asks. A red eval and a red unit
|
|
134
|
-
* test both arrive as `tests: 'fail'`, and — worse — an eval node that was
|
|
135
|
-
* SKIPPED (`--filter`) leaves the rollup green, so a gate reading `tests` alone
|
|
136
|
-
* cannot tell "the eval ran and passed" from "the eval never ran".
|
|
137
|
-
*
|
|
138
|
-
* Four values, and the distinction between the last two is the whole point:
|
|
139
|
-
* - `'pass'` / `'fail'`: every declared eval node agreed, or at least one did
|
|
140
|
-
* not. Same pessimistic rollup rule the tests node already uses;
|
|
141
|
-
* - `'skip'`: eval nodes ARE declared, but this run did not execute them;
|
|
142
|
-
* - `null`: this project declares NO eval node. Not a skip — there is nothing
|
|
143
|
-
* to skip. `risk-detector.js` pauses on the two for different reasons,
|
|
144
|
-
* because they call for different actions.
|
|
145
|
-
*
|
|
146
|
-
* @param {{nodes: {tests: {runs?: Array}}}} result - runVerify() output
|
|
147
|
-
* @returns {'pass'|'fail'|'skip'|null}
|
|
148
|
-
*/
|
|
100
|
+
// `'skip'` = eval nodes declared but not run; `null` = no eval node declared. A skipped eval
|
|
101
|
+
// leaves the tests rollup green, so the gate needs this split to tell them apart.
|
|
149
102
|
function evalsStatusOf(result) {
|
|
150
103
|
const runs = (result.nodes.tests?.runs || []).filter((run) => run.runner === EVAL_RUNNER);
|
|
151
104
|
if (runs.length === 0) return null;
|
|
@@ -154,10 +107,7 @@ function evalsStatusOf(result) {
|
|
|
154
107
|
return 'skip';
|
|
155
108
|
}
|
|
156
109
|
|
|
157
|
-
|
|
158
|
-
* The options that make a run partial, each with the `reason` it
|
|
159
|
-
* reports when it keeps the stamp from being written — first match wins.
|
|
160
|
-
*/
|
|
110
|
+
// First match wins; each keeps the stamp from being written, with its `reason`.
|
|
161
111
|
const PARTIAL_RUN_FLAGS = [['filter', 'filtered'], ['skipTests', 'skip-tests'], ['skipBuild', 'skip-build']];
|
|
162
112
|
|
|
163
113
|
const CI_EXCLUSIVE_FLAGS = [
|
|
@@ -181,9 +131,7 @@ async function runLocalVerify(featureName, options) {
|
|
|
181
131
|
linuxImage: options.linuxImage,
|
|
182
132
|
});
|
|
183
133
|
} catch (err) {
|
|
184
|
-
//
|
|
185
|
-
// verdict. `--filter` that selects nothing must not print green (or red):
|
|
186
|
-
// there is no verification to report, only a command to fix.
|
|
134
|
+
// Exit 2, never a verdict: a `--filter` that selects nothing must not print green or red.
|
|
187
135
|
if (err instanceof VerifyUsageError) {
|
|
188
136
|
console.error(chalk.red(`\n❌ ${err.message}\n`));
|
|
189
137
|
process.exit(2);
|
|
@@ -204,25 +152,19 @@ async function runLocalVerify(featureName, options) {
|
|
|
204
152
|
}
|
|
205
153
|
}
|
|
206
154
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
* - `--filter` that matches no declared node (a filtered-to-empty run is
|
|
221
|
-
* green and proves nothing);
|
|
222
|
-
* - `--record-baseline` with nowhere to write (no `config.json`);
|
|
223
|
-
* - `--linux` without Docker.
|
|
224
|
-
* The last three arrive as `VerifyUsageError` from the runner.
|
|
225
|
-
*/
|
|
155
|
+
function exitWithReusedStamp(featureName, reused, why, options) {
|
|
156
|
+
console.error(chalk.green(`\n↪ ${why} (sha ${reused.sha.slice(0, 8)}, ${reused.ts}): nada mudou fora de .morph/ desde aquela rodada verde.\n`));
|
|
157
|
+
if (options.json) {
|
|
158
|
+
console.log(JSON.stringify({
|
|
159
|
+
feature: featureName, status: reused.status, nodes: reused.nodes, e2eReason: reused.e2eReason ?? null,
|
|
160
|
+
reused: { sha: reused.sha, ts: reused.ts, source: reused.source ?? 'local' }, persisted: false, reason: 'stamp-reused',
|
|
161
|
+
}, null, 2));
|
|
162
|
+
}
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Exit 2 is "the command cannot mean what you typed" (unknown feature, `--filter` matching
|
|
167
|
+
// nothing, `--record-baseline` without config.json, `--linux` without Docker): never a verdict.
|
|
226
168
|
export async function verifyCommand(featureName, options = {}) {
|
|
227
169
|
let feature;
|
|
228
170
|
try {
|
|
@@ -245,6 +187,8 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
245
187
|
|
|
246
188
|
let ci = null;
|
|
247
189
|
if (options.ci) {
|
|
190
|
+
const fresh = reusableLocalStamp(process.cwd(), feature.verification);
|
|
191
|
+
if (fresh) exitWithReusedStamp(featureName, fresh, 'stamp verde e fresco — o CI não roda de novo', options);
|
|
248
192
|
try {
|
|
249
193
|
const outcome = await runCiVerify(process.cwd(), featureName, readMorphConfig(process.cwd()), {
|
|
250
194
|
log: (line) => console.error(chalk.gray(` ${line}`)),
|
|
@@ -256,17 +200,6 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
256
200
|
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`));
|
|
257
201
|
process.exit(3);
|
|
258
202
|
} else {
|
|
259
|
-
const reused = reusableLocalStamp(process.cwd(), feature.verification);
|
|
260
|
-
if (reused) {
|
|
261
|
-
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`));
|
|
262
|
-
if (options.json) {
|
|
263
|
-
console.log(JSON.stringify({
|
|
264
|
-
feature: featureName, status: reused.status, nodes: reused.nodes, e2eReason: reused.e2eReason ?? null,
|
|
265
|
-
reused: { sha: reused.sha, ts: reused.ts, source: reused.source ?? 'local' }, persisted: false, reason: 'stamp-reused',
|
|
266
|
-
}, null, 2));
|
|
267
|
-
}
|
|
268
|
-
process.exit(0);
|
|
269
|
-
}
|
|
270
203
|
console.error(chalk.yellow(`\n↪ CI indisponível (${outcome.reason}) — verify local.\n`));
|
|
271
204
|
}
|
|
272
205
|
} catch (err) {
|
|
@@ -311,22 +244,9 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
311
244
|
};
|
|
312
245
|
await updateFeature(featureName, historyKey, compact);
|
|
313
246
|
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
// EXACT commit it covers, the same way a review report's
|
|
318
|
-
// `**Reviewed-SHA:**` token lets approve.js detect a stale review. null
|
|
319
|
-
// in a non-git checkout (or before the first commit) — fail-open, never
|
|
320
|
-
// blocks persistence.
|
|
321
|
-
//
|
|
322
|
-
// `nodes` + `e2eReason` são acréscimo de campo, não mudança de forma: o
|
|
323
|
-
// rollup `status` continua onde estava. Existem porque um nó e2e que PULA
|
|
324
|
-
// não reprova o rollup — sem o status do nó e a razão LITERAL do skip, o
|
|
325
|
-
// gate-check lê 'pass' tanto quando o e2e rodou e passou quanto quando ele
|
|
326
|
-
// nunca rodou, e não tem como separar dispensa legítima ("o projeto não
|
|
327
|
-
// containeriza") de ausência de prova ("docker indisponível"). `e2eReason`
|
|
328
|
-
// é null quando o nó não pulou — e um null NUM SKIP é lido como dispensa
|
|
329
|
-
// 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.
|
|
330
250
|
const stamp = {
|
|
331
251
|
status: result.status,
|
|
332
252
|
nodes: nodeStatuses(result),
|
|
@@ -391,10 +311,7 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
391
311
|
}
|
|
392
312
|
}
|
|
393
313
|
|
|
394
|
-
// A recording
|
|
395
|
-
// invocation even when the run itself was green (a filtered run is green and
|
|
396
|
-
// unrecordable): exiting 0 there would report success for something that did
|
|
397
|
-
// not happen.
|
|
314
|
+
// A recording asked for and not made fails the invocation, even when the run was green.
|
|
398
315
|
const baselineRefused = Boolean(result.baseline && !result.baseline.recorded);
|
|
399
316
|
process.exit(result.status === 'pass' && !baselineRefused ? 0 : 1);
|
|
400
317
|
}
|
|
@@ -1,84 +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. 'plan.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
|
-
*
|
|
24
|
-
* Multi-file types (`multi: true`) hold N files in a directory instead of one
|
|
25
|
-
* fixed basename. They carry `dirname` + `filenamePattern` + `displayFilename`
|
|
26
|
-
* INSTEAD of `filename`, and every derivation keyed by basename
|
|
27
|
-
* (FILENAME_TO_OUTPUT, PROTECTED_SPEC_FILES) skips them —
|
|
28
|
-
* there is no single basename to key on. `getOutputPath` returns the DIRECTORY
|
|
29
|
-
* for these types.
|
|
30
|
-
*/
|
|
31
3
|
export const OUTPUT_SCHEMA = {
|
|
32
|
-
// 2-plan/ — the plan gate freezes both: plan.md accepts only an append in
|
|
33
|
-
// `## Errata`, checks.json only a rewrite by `morph-spec plan-check`.
|
|
34
4
|
plan: { filename: 'plan.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
35
5
|
checks: { filename: 'checks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
36
6
|
|
|
37
|
-
// 1-design/ (optional)
|
|
38
7
|
uiDesignSystem: { filename: 'design-system.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
39
8
|
uiMockups: { filename: 'mockups.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
40
9
|
uiComponents: { filename: 'components.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
41
10
|
uiFlows: { filename: 'flows.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
42
11
|
|
|
43
|
-
// 3-implement/ — the slice reports (`reports/F{n}.md`, see getTaskReportPath) are the record.
|
|
44
|
-
// N files, one per scenario whose proof judges what a MODEL answers
|
|
45
|
-
// (`prova: eval` in the plan). Runtime evidence: input, model, output, verdict — pointing
|
|
46
|
-
// at the `runner: "evals"` node when the stack declares one. CONDITIONAL by
|
|
47
|
-
// construction: never in phases.json requiredOutputs. Readers: morph-review
|
|
48
|
-
// dispatch prompt.
|
|
49
12
|
evidencias: { dirname: 'evidencias', filenamePattern: '^C\\d+\\.md$', displayFilename: '{Cn}.md',
|
|
50
13
|
phaseDir: '3-implement', phase: 'implement', protected: false, multi: true },
|
|
51
14
|
|
|
52
|
-
// 4-review/
|
|
53
|
-
// Um relatório por papel; o canônico da fase é o do verificador.
|
|
54
15
|
verifierReport: { filename: 'verifier.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
55
16
|
reviewerReport: { filename: 'reviewer.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
56
17
|
securityReport: { filename: 'security.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
57
18
|
};
|
|
58
19
|
|
|
59
|
-
// ============================================================================
|
|
60
|
-
// Builder Functions (Windows-safe via path.join)
|
|
61
|
-
// ============================================================================
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Leaf segment of a schema entry: the basename for single-file types, the
|
|
65
|
-
* directory name for multi-file ones. Keeps every path builder from having to
|
|
66
|
-
* know about `multi` on its own.
|
|
67
|
-
*/
|
|
68
20
|
function leafSegment(entry) {
|
|
69
21
|
return entry.multi ? entry.dirname : entry.filename;
|
|
70
22
|
}
|
|
71
23
|
|
|
72
|
-
/**
|
|
73
|
-
* Get the morph-relative path for a specific output type of a feature.
|
|
74
|
-
*
|
|
75
|
-
* For `multi` types this is the DIRECTORY holding the N files
|
|
76
|
-
* (".morph/features/my-feature/3-implement/evidencias"), not a single file.
|
|
77
|
-
*
|
|
78
|
-
* @param {string} featureName
|
|
79
|
-
* @param {string} outputType - camelCase output type key from OUTPUT_SCHEMA
|
|
80
|
-
* @returns {string} e.g. ".morph/features/my-feature/2-plan/plan.md"
|
|
81
|
-
*/
|
|
24
|
+
/** For `multi` types this is the directory holding the N files. */
|
|
82
25
|
export function getOutputPath(featureName, outputType) {
|
|
83
26
|
const entry = OUTPUT_SCHEMA[outputType];
|
|
84
27
|
if (!entry) {
|
|
@@ -88,10 +31,6 @@ export function getOutputPath(featureName, outputType) {
|
|
|
88
31
|
.replace(/\\/g, '/');
|
|
89
32
|
}
|
|
90
33
|
|
|
91
|
-
/**
|
|
92
|
-
* Get the absolute filesystem path for a specific output type.
|
|
93
|
-
* For `multi` types this is the directory — see {@link getOutputPath}.
|
|
94
|
-
*/
|
|
95
34
|
export function getAbsoluteOutputPath(projectRoot, featureName, outputType) {
|
|
96
35
|
const entry = OUTPUT_SCHEMA[outputType];
|
|
97
36
|
if (!entry) {
|
|
@@ -100,42 +39,20 @@ export function getAbsoluteOutputPath(projectRoot, featureName, outputType) {
|
|
|
100
39
|
return join(projectRoot, '.morph', 'features', featureName, entry.phaseDir, leafSegment(entry));
|
|
101
40
|
}
|
|
102
41
|
|
|
103
|
-
/**
|
|
104
|
-
* Get the feature base directory (morph-relative, forward slashes).
|
|
105
|
-
*/
|
|
106
42
|
export function getFeatureDir(featureName) {
|
|
107
43
|
return join('.morph', 'features', featureName).replace(/\\/g, '/');
|
|
108
44
|
}
|
|
109
45
|
|
|
110
|
-
/**
|
|
111
|
-
* Per-task sub-agent report — the durable "return value" channel for a
|
|
112
|
-
* dispatched implementation task (MORPH.md §6). A sub-agent's final text
|
|
113
|
-
* message may never reach the orchestrator (idle_notification with no text
|
|
114
|
-
* is an observed failure mode); the file it writes here always does. Doesn't
|
|
115
|
-
* fit OUTPUT_SCHEMA's one-fixed-filename-per-type model since the filename
|
|
116
|
-
* is parameterized by taskId, so it's a sibling helper instead of a schema
|
|
117
|
-
* entry.
|
|
118
|
-
*
|
|
119
|
-
* @param {string} featureName
|
|
120
|
-
* @param {string} taskId - e.g. "T7"
|
|
121
|
-
* @returns {string} morph-relative, forward-slash path
|
|
122
|
-
*/
|
|
123
46
|
export function getTaskReportPath(featureName, taskId) {
|
|
124
47
|
return join('.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`)
|
|
125
48
|
.replace(/\\/g, '/');
|
|
126
49
|
}
|
|
127
50
|
|
|
128
|
-
/** Absolute filesystem variant of {@link getTaskReportPath}. */
|
|
129
51
|
export function getAbsoluteTaskReportPath(projectRoot, featureName, taskId) {
|
|
130
52
|
return join(projectRoot, '.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`);
|
|
131
53
|
}
|
|
132
54
|
|
|
133
|
-
/**
|
|
134
|
-
* Get all output paths for a feature, ready for state.json `outputs` block.
|
|
135
|
-
*
|
|
136
|
-
* Multi-file types contribute their DIRECTORY, so `created` for them means
|
|
137
|
-
* "the folder exists" — the state index tracks presence, not file count.
|
|
138
|
-
*/
|
|
55
|
+
/** `multi` types contribute their directory, so `created` means "the folder exists". */
|
|
139
56
|
export function getAllOutputPaths(featureName) {
|
|
140
57
|
const outputs = {};
|
|
141
58
|
for (const [key] of Object.entries(OUTPUT_SCHEMA)) {
|
|
@@ -147,14 +64,6 @@ export function getAllOutputPaths(featureName) {
|
|
|
147
64
|
return outputs;
|
|
148
65
|
}
|
|
149
66
|
|
|
150
|
-
// ============================================================================
|
|
151
|
-
// Derived Constants
|
|
152
|
-
// ============================================================================
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* filename → output type. Multi-file types are absent by construction: they
|
|
156
|
-
* have N basenames, so there is nothing to key on.
|
|
157
|
-
*/
|
|
158
67
|
export const FILENAME_TO_OUTPUT = Object.fromEntries(
|
|
159
68
|
Object.entries(OUTPUT_SCHEMA)
|
|
160
69
|
.filter(([, entry]) => !entry.multi)
|
|
@@ -165,20 +74,12 @@ export const OUTPUT_PHASE_MAP = Object.fromEntries(
|
|
|
165
74
|
Object.entries(OUTPUT_SCHEMA).map(([key, entry]) => [key, entry.phase])
|
|
166
75
|
);
|
|
167
76
|
|
|
168
|
-
/**
|
|
169
|
-
* Protected spec files and the approval gate that locks them.
|
|
170
|
-
*/
|
|
171
77
|
export const PROTECTED_SPEC_FILES = Object.fromEntries(
|
|
172
78
|
Object.entries(OUTPUT_SCHEMA)
|
|
173
79
|
.filter(([, entry]) => entry.protected && entry.approvalGate && !entry.multi)
|
|
174
80
|
.map(([, entry]) => [entry.filename, entry.approvalGate])
|
|
175
81
|
);
|
|
176
82
|
|
|
177
|
-
// ============================================================================
|
|
178
|
-
// Contracts
|
|
179
|
-
// ============================================================================
|
|
180
|
-
|
|
181
|
-
/** The output type whose code blocks declare the contracts the code must honour. */
|
|
182
83
|
export function resolveContractsOutputType() {
|
|
183
84
|
return 'plan';
|
|
184
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');
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gate monotonicity — single source of truth.
|
|
3
|
-
*
|
|
4
|
-
* A gate may only be approved once its predecessors are approved. The strict
|
|
5
|
-
* chain is plan → review. uiux is optional: it requires plan but gates nothing
|
|
6
|
-
* downstream, so a feature that skips UI/UX is never blocked.
|
|
7
|
-
*
|
|
8
|
-
* Policy-stamped gates (approvedBy: "policy:{type}", written at birth by
|
|
9
|
-
* `morph-spec create` for a work type's skippedGates) count as approved, so
|
|
10
|
-
* chore/hotfix (plan pre-stamped) satisfy prereqs naturally.
|
|
11
|
-
*
|
|
12
|
-
* Pure module — no filesystem/CLI side effects. Consumed by
|
|
13
|
-
* `approve`/`advance` (refuse), the state-sync hook (warn), and `list`/`doctor`
|
|
14
|
-
* (report).
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
1
|
export const GATE_PREREQ = {
|
|
18
2
|
plan: [],
|
|
19
3
|
uiux: ['plan'],
|
|
@@ -22,22 +6,11 @@ export const GATE_PREREQ = {
|
|
|
22
6
|
|
|
23
7
|
const isApproved = (gates, name) => gates?.[name]?.approved === true;
|
|
24
8
|
|
|
25
|
-
/**
|
|
26
|
-
* Prerequisite gates NOT yet approved that block approving `gate`.
|
|
27
|
-
* @param {string} gate - gate about to be approved
|
|
28
|
-
* @param {object} gates - feature.approvalGates
|
|
29
|
-
* @returns {string[]} unmet prerequisite gate names (empty = clear to approve)
|
|
30
|
-
*/
|
|
31
9
|
export function unmetPrereqs(gate, gates) {
|
|
32
10
|
const prereqs = GATE_PREREQ[gate] || [];
|
|
33
11
|
return prereqs.filter((p) => !isApproved(gates, p));
|
|
34
12
|
}
|
|
35
13
|
|
|
36
|
-
/**
|
|
37
|
-
* All monotonicity violations currently present in a feature's gates.
|
|
38
|
-
* @param {object} gates - feature.approvalGates
|
|
39
|
-
* @returns {Array<{gate: string, missing: string[]}>} empty when in order
|
|
40
|
-
*/
|
|
41
14
|
export function gateOrderViolations(gates) {
|
|
42
15
|
if (!gates || typeof gates !== 'object') return [];
|
|
43
16
|
const out = [];
|
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Feature-lifecycle audit — the consolidated inconsistency scanner behind
|
|
3
|
-
* `morph-spec doctor`. Read-only, non-throwing: scans `.morph/features/*` and
|
|
4
|
-
* the thin root index and reports what's already wrong in the repo, without
|
|
5
|
-
* touching state (never calls loadState — a diagnostic must not mutate).
|
|
6
|
-
*
|
|
7
|
-
* Issue types:
|
|
8
|
-
* • gate-order — a gate approved with a predecessor pending (item 4)
|
|
9
|
-
* • identity — a feature missing description/name/createdAt (item 3)
|
|
10
|
-
* • orphan-folder — a feature folder with no feature.json
|
|
11
|
-
* • stale-index — a root-index entry with no feature folder (auto-heals on
|
|
12
|
-
* the next loadState, but surfaced so it's not a mystery)
|
|
13
|
-
* • branch-drift — the root's copy of a feature.json disagrees with the copy
|
|
14
|
-
* committed on `morph/{f}` (B5 — the root's view lags)
|
|
15
|
-
* • abandoned-scaffold — an `in_progress` feature that never produced
|
|
16
|
-
* anything: no artifacts, no approved gate, and a branch
|
|
17
|
-
* with no commit of its own (see abandonedScaffold)
|
|
18
|
-
* • corrupt — a feature.json that won't parse
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
1
|
import { execFileSync } from 'child_process';
|
|
22
2
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
23
3
|
import { join } from 'path';
|
|
@@ -26,40 +6,19 @@ import { missingIdentity } from './feature-identity.js';
|
|
|
26
6
|
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
27
7
|
import { commitsAhead, resolveBaseRef } from '../../lib/git/branch-classifier.js';
|
|
28
8
|
|
|
29
|
-
/** The operational truth of a feature.json: its status and which gates are approved. */
|
|
30
9
|
function operationalShape(fj) {
|
|
31
10
|
const gates = {};
|
|
32
11
|
for (const [g, v] of Object.entries(fj?.approvalGates || {})) gates[g] = v?.approved === true;
|
|
33
12
|
return JSON.stringify({ status: fj?.status ?? null, gates });
|
|
34
13
|
}
|
|
35
14
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* COMMITTED on that feature's own branch?
|
|
39
|
-
*
|
|
40
|
-
* `.morph/state.json` is gitignored and derived, and every worktree keeps its
|
|
41
|
-
* own — so "the index diverges between worktrees" is by design, not a defect to
|
|
42
|
-
* fix by making the index authoritative (that would undo the whole per-feature
|
|
43
|
-
* state model). The defect is that the divergence was INVISIBLE: since v8.4 the
|
|
44
|
-
* authoritative feature.json travels committed on `morph/{f}`, so a primary root
|
|
45
|
-
* parked on the default holds a copy that silently lags, and everything derived
|
|
46
|
-
* from it (status, gates, and the index rebuilt from it) lags with it.
|
|
47
|
-
*
|
|
48
|
-
* Comparison is on the OPERATIONAL fields only — status and approved-gate flags
|
|
49
|
-
* — not on bytes: reformatting or a timestamp bump is not a divergence anyone
|
|
50
|
-
* needs to hear about.
|
|
51
|
-
*
|
|
52
|
-
* Fail-open at every step: no repo, no branch, no blob, unparseable JSON → no
|
|
53
|
-
* issue. A diagnostic that invents problems is worse than one that misses some.
|
|
54
|
-
*
|
|
55
|
-
* @returns {string|null} the issue detail, or null when the copies agree
|
|
56
|
-
*/
|
|
15
|
+
// Compares only status and approved gates (bytes and timestamps are not drift).
|
|
16
|
+
// Fail-open: no repo, branch, blob or parseable JSON means no issue.
|
|
57
17
|
function branchDrift(baseDir, feature, rootFj) {
|
|
58
18
|
const branch = `morph/${feature}`;
|
|
59
19
|
try {
|
|
60
20
|
const current = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: baseDir, stdio: 'pipe' })
|
|
61
21
|
.toString().trim();
|
|
62
|
-
// On the feature's own branch the working tree IS the branch — nothing to compare.
|
|
63
22
|
if (current === branch) return null;
|
|
64
23
|
|
|
65
24
|
execFileSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { cwd: baseDir, stdio: 'pipe' });
|
|
@@ -77,18 +36,9 @@ function branchDrift(baseDir, feature, rootFj) {
|
|
|
77
36
|
}
|
|
78
37
|
}
|
|
79
38
|
|
|
80
|
-
|
|
81
|
-
* How long a scaffold has to sit untouched before it counts as abandoned.
|
|
82
|
-
*
|
|
83
|
-
* A dead scaffold and a feature born five minutes ago have the SAME shape:
|
|
84
|
-
* `create` forks `morph/{f}` off the default branch without committing, so at
|
|
85
|
-
* birth the branch has no commit of its own and the folder holds nothing but
|
|
86
|
-
* `feature.json`. Only elapsed time separates the two — and a check that fires
|
|
87
|
-
* on every fresh `create` is one people learn to scroll past.
|
|
88
|
-
*/
|
|
39
|
+
// A dead scaffold and a feature born minutes ago have the same shape; only age tells them apart.
|
|
89
40
|
const ABANDONED_SCAFFOLD_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
90
41
|
|
|
91
|
-
/** Newest timestamp the feature carries, in ms; falls back to the folder's mtime. */
|
|
92
42
|
function lastTouched(fdir, fj) {
|
|
93
43
|
let newest = 0;
|
|
94
44
|
for (const ts of [fj?.updatedAt, fj?.createdAt]) {
|
|
@@ -99,12 +49,7 @@ function lastTouched(fdir, fj) {
|
|
|
99
49
|
try { return statSync(fdir).mtimeMs; } catch { return 0; }
|
|
100
50
|
}
|
|
101
51
|
|
|
102
|
-
|
|
103
|
-
* Does the feature dir hold any FILE other than its own `feature.json`?
|
|
104
|
-
*
|
|
105
|
-
* Files, not entries: `create` scaffolds the phase folders empty, so an empty
|
|
106
|
-
* `2-plan/` is the absence of work, not evidence of it.
|
|
107
|
-
*/
|
|
52
|
+
// Files, not entries: `create` scaffolds the phase folders empty.
|
|
108
53
|
function hasArtifacts(fdir) {
|
|
109
54
|
let entries;
|
|
110
55
|
try { entries = readdirSync(fdir, { withFileTypes: true }); } catch { return false; }
|
|
@@ -119,22 +64,7 @@ function hasArtifacts(fdir) {
|
|
|
119
64
|
return false;
|
|
120
65
|
}
|
|
121
66
|
|
|
122
|
-
|
|
123
|
-
* Commits on the feature's branch that the base does not already contain, or
|
|
124
|
-
* `null` when git cannot answer (no repo, no resolvable base, no branch, no
|
|
125
|
-
* rev-list). "Cannot prove" must never read as "proved zero" — the predicate
|
|
126
|
-
* below hinges on this number, so an unanswerable repo has to stay silent.
|
|
127
|
-
*
|
|
128
|
-
* The counting itself is `branch-classifier.js`'s `commitsAhead` against its
|
|
129
|
-
* `resolveBaseRef`, NOT a second copy: `origin/{default}` is the authority on
|
|
130
|
-
* what actually landed, and two drifting answers to "does this branch carry
|
|
131
|
-
* anything of its own?" is exactly what the `doctor`/`branch prune` split was
|
|
132
|
-
* unified to avoid.
|
|
133
|
-
*
|
|
134
|
-
* Note the field evidence this exists for: the branch pointed at the merge
|
|
135
|
-
* commit of somebody else's PR. That is the base's own tip — reachable from
|
|
136
|
-
* the base, and therefore zero commits of its own.
|
|
137
|
-
*/
|
|
67
|
+
// `null` when git cannot answer: "cannot prove" must never read as "proved zero".
|
|
138
68
|
function ownCommitCount(baseDir, branch) {
|
|
139
69
|
try {
|
|
140
70
|
execFileSync('git', ['rev-parse', '--git-dir'], { cwd: baseDir, stdio: 'pipe' });
|
|
@@ -146,31 +76,15 @@ function ownCommitCount(baseDir, branch) {
|
|
|
146
76
|
try {
|
|
147
77
|
execFileSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { cwd: baseDir, stdio: 'pipe' });
|
|
148
78
|
} catch {
|
|
149
|
-
return 0; // the branch never existed
|
|
79
|
+
return 0; // the branch never existed: nothing was committed on it
|
|
150
80
|
}
|
|
151
81
|
return commitsAhead(baseRef, branch, baseDir);
|
|
152
82
|
}
|
|
153
83
|
|
|
154
|
-
/**
|
|
155
|
-
* The orphan skeleton `create --worktree` can leave behind: a feature.json on
|
|
156
|
-
* its own, `status: in_progress`, no tasks, no gates, and a branch parked on
|
|
157
|
-
* somebody else's commit. Every one of those alone is normal at some point in
|
|
158
|
-
* a feature's life; together, and only after the feature has gone stale, they
|
|
159
|
-
* mean nobody ever came back to it.
|
|
160
|
-
*
|
|
161
|
-
* The artifact test is deliberately broader than the "0 tasks" the report
|
|
162
|
-
* named: a plan drafted and waiting on its gate also has zero tasks and is
|
|
163
|
-
* very much alive. What a dead scaffold has is NOTHING — checked in the root
|
|
164
|
-
* and, if the feature registered one, in its worktree, where uncommitted work
|
|
165
|
-
* would otherwise be invisible to both the disk scan and `rev-list`.
|
|
166
|
-
*
|
|
167
|
-
* @returns {string|null} the issue detail, or null when the feature is alive
|
|
168
|
-
*/
|
|
169
84
|
function abandonedScaffold(baseDir, feature, fdir, fj) {
|
|
170
85
|
if (fj?.status !== 'in_progress') return null;
|
|
171
86
|
|
|
172
|
-
//
|
|
173
|
-
// never a decision anyone took, so it must not read as progress.
|
|
87
|
+
// A gate stamped `policy:{workType}` at birth was never a decision, so it is not progress.
|
|
174
88
|
const decided = Object.values(fj.approvalGates || {}).some(
|
|
175
89
|
(g) => g?.approved === true && !String(g?.approvedBy ?? '').startsWith('policy:'),
|
|
176
90
|
);
|
|
@@ -185,8 +99,6 @@ function abandonedScaffold(baseDir, feature, fdir, fj) {
|
|
|
185
99
|
const age = Date.now() - lastTouched(fdir, fj);
|
|
186
100
|
if (age < ABANDONED_SCAFFOLD_MIN_AGE_MS) return null;
|
|
187
101
|
|
|
188
|
-
// The registered branch when the feature has one, `morph/{f}` otherwise —
|
|
189
|
-
// same name `branchDrift` above assumes.
|
|
190
102
|
const branch = fj.worktree?.branch || `morph/${feature}`;
|
|
191
103
|
if (ownCommitCount(baseDir, branch) !== 0) return null;
|
|
192
104
|
|
|
@@ -196,10 +108,6 @@ function abandonedScaffold(baseDir, feature, fdir, fj) {
|
|
|
196
108
|
+ 'ou retome a feature';
|
|
197
109
|
}
|
|
198
110
|
|
|
199
|
-
/**
|
|
200
|
-
* @param {string} [baseDir] - project root (defaults to cwd)
|
|
201
|
-
* @returns {Array<{ feature: string, type: string, detail: string }>} issues
|
|
202
|
-
*/
|
|
203
111
|
export function auditFeatureLifecycle(baseDir = process.cwd()) {
|
|
204
112
|
const issues = [];
|
|
205
113
|
const featuresDir = join(baseDir, '.morph', 'features');
|
|
@@ -256,7 +164,7 @@ export function auditFeatureLifecycle(baseDir = process.cwd()) {
|
|
|
256
164
|
issues.push({ feature: name, type: 'stale-index', detail: 'in state.json index but no feature folder' });
|
|
257
165
|
}
|
|
258
166
|
}
|
|
259
|
-
} catch {
|
|
167
|
+
} catch { }
|
|
260
168
|
}
|
|
261
169
|
|
|
262
170
|
return issues;
|