@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,52 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* and how long each one lives.
|
|
5
|
-
*
|
|
6
|
-
* A backup exists so that removing a worktree (or shedding the root-side
|
|
7
|
-
* scaffold) never destroys the only exemplar of a file. That is a reason to keep
|
|
8
|
-
* the last ones, not all of them, and a worktree's `.env.e2e` — the e2e stack's
|
|
9
|
-
* credentials — has no business living inside `.git/` indefinitely. So every
|
|
10
|
-
* backup is subject to a retention policy, on by default:
|
|
11
|
-
*
|
|
12
|
-
* maxCount — the newest this many backups per namespace always stay
|
|
13
|
-
* maxAgeDays — no age cut unless configured: a quiet month must never
|
|
14
|
-
* empty the backups of the last features closed
|
|
15
|
-
* secretMaxAgeDays — `.env*` files inside a backup go away after this
|
|
16
|
-
*
|
|
17
|
-
* `config.json → finishBackup.retention` can change the numbers or turn the
|
|
18
|
-
* whole thing off (`false`); a project without the key gets the defaults, and a
|
|
19
|
-
* value that cannot be read skips the prune for that run.
|
|
20
|
-
*
|
|
21
|
-
* A backup without its manifest was written before this retention existed. The
|
|
22
|
-
* automatic prune (end of `finish`, end of `worktree remove`) never deletes it,
|
|
23
|
-
* never strips its `.env*` and does not count it against the budget — it only
|
|
24
|
-
* reports how many wait (`legacyPending`). The first prune of those is the
|
|
25
|
-
* explicit `doctor --worktrees --prune`, which ranks them with the rest and
|
|
26
|
-
* gives each one it keeps a manifest, so from then on they are ordinary backups.
|
|
27
|
-
*
|
|
28
|
-
* Layout, and what counts as ONE backup:
|
|
29
|
-
*
|
|
30
|
-
* {feature}-{stamp}/ the feature scope the shed discards
|
|
31
|
-
* {feature}-{stamp}-worktree-ignored/ the worktree's exclusive gitignored
|
|
32
|
-
* {folder}.manifest.json sidecar: what was copied, the secrets
|
|
33
|
-
* task/{name}-{stamp}-worktree-ignored/ a namespace (task isolation trees)
|
|
34
|
-
*
|
|
35
|
-
* One finish writes both folders with the SAME stamp, so they are one backup —
|
|
36
|
-
* the unit the count applies to; a folder with no same-stamp sibling counts
|
|
37
|
-
* alone. The `task/` namespace has its own budget: a fan-out of twenty
|
|
38
|
-
* sub-agent worktrees must not evict the backup of the feature finished a
|
|
39
|
-
* minute earlier. The backup the calling run just wrote is never pruned by it.
|
|
40
|
-
*
|
|
41
|
-
* Deletion is PROVE-or-DECLINE, as everywhere else near a worktree: a backup
|
|
42
|
-
* holding a reparse point is never removed, links are never descended, and
|
|
43
|
-
* nothing outside the backup directory is touched. Not because `rmSync` would
|
|
44
|
-
* follow a junction — it unlinks the link itself; what once emptied a primary
|
|
45
|
-
* root was `git worktree remove` — but because a link inside a backup is not
|
|
46
|
-
* something this code put there. A first-level entry that is neither a backup
|
|
47
|
-
* nor `task/` is left alone, even empty — it is not ours to judge.
|
|
48
|
-
*/
|
|
49
|
-
|
|
1
|
+
// Folders with the SAME stamp are one backup; `task/` has its own count budget.
|
|
2
|
+
// A backup without manifest predates retention: only the explicit `--prune` touches it.
|
|
3
|
+
// Deletion is prove-or-decline: a backup holding a reparse point is never removed.
|
|
50
4
|
import {
|
|
51
5
|
cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync,
|
|
52
6
|
rmdirSync, rmSync, unlinkSync,
|
|
@@ -62,41 +16,30 @@ export const FINISH_BACKUP_RETENTION_DEFAULTS = Object.freeze({
|
|
|
62
16
|
secretMaxAgeDays: 7,
|
|
63
17
|
});
|
|
64
18
|
|
|
65
|
-
/** The keys whose `null` means "no limit" rather than an invalid value. */
|
|
66
19
|
const NULLABLE_RETENTION_KEYS = new Set(['maxAgeDays']);
|
|
67
20
|
|
|
68
21
|
const DAY_MS = 86_400_000;
|
|
69
22
|
const WORKTREE_IGNORED_SUFFIX = '-worktree-ignored';
|
|
70
23
|
const MANIFEST_SUFFIX = '.manifest.json';
|
|
71
24
|
const NAME_RE = /^(.+)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z(-worktree-ignored)?$/;
|
|
72
|
-
/** How many names a payload carries; the counts are always complete. */
|
|
73
25
|
const SAMPLE = 5;
|
|
74
26
|
|
|
75
|
-
/** @param {string} root - the primary root */
|
|
76
27
|
export function morphSpecStoreDir(root) {
|
|
77
28
|
return join(root, '.git', 'morph-spec');
|
|
78
29
|
}
|
|
79
30
|
|
|
80
|
-
/** @param {string} root - the primary root */
|
|
81
31
|
export function finishBackupDir(root) {
|
|
82
32
|
return join(morphSpecStoreDir(root), 'finish-backup');
|
|
83
33
|
}
|
|
84
34
|
|
|
85
|
-
/** The stamp in a backup's name — filesystem-safe ISO-8601. */
|
|
86
35
|
export function newBackupStamp(date = new Date()) {
|
|
87
36
|
return date.toISOString().replace(/[:.]/g, '-');
|
|
88
37
|
}
|
|
89
38
|
|
|
90
|
-
/** `.env`, `.env.e2e`, `.env.local`… — the files that hold a stack's secrets. */
|
|
91
39
|
export function isSecretFileName(name) {
|
|
92
40
|
return name.startsWith('.env');
|
|
93
41
|
}
|
|
94
42
|
|
|
95
|
-
/**
|
|
96
|
-
* @param {string} name - one directory name under finish-backup/ (or under a namespace)
|
|
97
|
-
* @returns {{ base: string, feature: string, stamp: string,
|
|
98
|
-
* kind: 'feature-scope'|'worktree-ignored', createdAt: number }|null}
|
|
99
|
-
*/
|
|
100
43
|
export function parseBackupName(name) {
|
|
101
44
|
const m = NAME_RE.exec(name);
|
|
102
45
|
if (!m) return null;
|
|
@@ -122,16 +65,8 @@ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArr
|
|
|
122
65
|
const shown = (v) => JSON.stringify(v) ?? String(v);
|
|
123
66
|
const skippedPolicy = (reason) => ({ enabled: false, skipped: reason, ...FINISH_BACKUP_RETENTION_DEFAULTS });
|
|
124
67
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
* gets the defaults (retention on) and an explicit `false` turns it off. What
|
|
128
|
-
* this cannot recognise SKIPS the prune for the run instead: the operation
|
|
129
|
-
* deletes, and a value nobody can read — `{ enabled: false }`, `"false"`, a
|
|
130
|
-
* `maxCount` of `0` — may well have been meant as "keep everything".
|
|
131
|
-
*
|
|
132
|
-
* @param {object|null} config - null when there is no config.json at all
|
|
133
|
-
* @returns {{ enabled: boolean, skipped?: string, maxCount: number, maxAgeDays: number|null, secretMaxAgeDays: number }}
|
|
134
|
-
*/
|
|
68
|
+
// A value this cannot recognise SKIPS the prune instead of falling back to the
|
|
69
|
+
// defaults: the operation deletes, and `"false"` or `maxCount: 0` may mean "keep all".
|
|
135
70
|
export function resolveBackupRetention(config) {
|
|
136
71
|
const d = FINISH_BACKUP_RETENTION_DEFAULTS;
|
|
137
72
|
const skip = skippedPolicy;
|
|
@@ -156,13 +91,8 @@ export function resolveBackupRetention(config) {
|
|
|
156
91
|
return policy;
|
|
157
92
|
}
|
|
158
93
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
* not "no config": the defaults would turn on a prune its author may have
|
|
162
|
-
* switched off in the part that failed to parse.
|
|
163
|
-
*
|
|
164
|
-
* @param {string} root
|
|
165
|
-
*/
|
|
94
|
+
// Unlike `readMorphConfig`, an unparseable config.json is not "no config": the
|
|
95
|
+
// defaults would turn on a prune its author may have switched off.
|
|
166
96
|
export function readBackupRetention(root) {
|
|
167
97
|
const path = join(root, CONFIG_FILE_NAME);
|
|
168
98
|
if (!existsSync(path)) return resolveBackupRetention(null);
|
|
@@ -196,13 +126,6 @@ function isStrictlyInside(parent, child) {
|
|
|
196
126
|
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
197
127
|
}
|
|
198
128
|
|
|
199
|
-
/**
|
|
200
|
-
* Every `.env*` REAL file under `dir`, relative to it. Links are neither
|
|
201
|
-
* followed nor reported: a link is not bytes this backup holds.
|
|
202
|
-
*
|
|
203
|
-
* @param {string} dir
|
|
204
|
-
* @returns {string[]}
|
|
205
|
-
*/
|
|
206
129
|
export function secretFilesIn(dir) {
|
|
207
130
|
const out = [];
|
|
208
131
|
const stack = [''];
|
|
@@ -224,13 +147,7 @@ export function secretFilesIn(dir) {
|
|
|
224
147
|
return out.sort();
|
|
225
148
|
}
|
|
226
149
|
|
|
227
|
-
/**
|
|
228
|
-
* Bytes and files under `dir`, by lstat — a link counts as itself, never as
|
|
229
|
-
* what it points at. `null` when the walk could not start.
|
|
230
|
-
*
|
|
231
|
-
* @param {string} dir
|
|
232
|
-
* @returns {{ bytes: number, files: number }|null}
|
|
233
|
-
*/
|
|
150
|
+
/** By lstat: a link counts as itself. `null` when the walk could not start. */
|
|
234
151
|
export function measureDir(dir) {
|
|
235
152
|
if (!isRealDir(dir)) return null;
|
|
236
153
|
let bytes = 0;
|
|
@@ -247,7 +164,7 @@ export function measureDir(dir) {
|
|
|
247
164
|
for (const e of entries) {
|
|
248
165
|
const p = join(current, e.name);
|
|
249
166
|
if (e.isDirectory() && !e.isSymbolicLink()) { stack.push(p); continue; }
|
|
250
|
-
try { bytes += lstatSync(p).size; files += 1; } catch {
|
|
167
|
+
try { bytes += lstatSync(p).size; files += 1; } catch {}
|
|
251
168
|
}
|
|
252
169
|
}
|
|
253
170
|
return { bytes, files };
|
|
@@ -265,30 +182,16 @@ function readManifest(backupPath) {
|
|
|
265
182
|
}
|
|
266
183
|
}
|
|
267
184
|
|
|
268
|
-
|
|
269
|
-
* Bookkeeping, never a precondition: a backup whose bytes are on disk is a
|
|
270
|
-
* backup, with or without its manifest. One whose manifest failed to be written
|
|
271
|
-
* reads as legacy, which is the safe side — the automatic prune leaves it alone.
|
|
272
|
-
*/
|
|
185
|
+
// Fail-open: a backup whose manifest failed reads as legacy, the safe side.
|
|
273
186
|
function writeManifest(backupPath, manifest) {
|
|
274
187
|
try {
|
|
275
188
|
atomicWriteJson(manifestPathFor(backupPath), { version: 1, ...manifest });
|
|
276
|
-
} catch {
|
|
189
|
+
} catch {}
|
|
277
190
|
}
|
|
278
191
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
* entries: git reports an ignored directory as ONE entry when the directory
|
|
283
|
-
* itself matches a pattern, so an exclusive `apps/x/` would otherwise drag its
|
|
284
|
-
* `node_modules/` and `.next/` along. They are regenerable, and the removal
|
|
285
|
-
* deletes them either way.
|
|
286
|
-
*
|
|
287
|
-
* @param {{ worktreePath: string, root: string, feature: string, rels: string[], stamp?: string }} args
|
|
288
|
-
* @returns {string|null} the backup path, or null if ANY copy failed (the caller
|
|
289
|
-
* must then decline the removal — a partial backup authorizes nothing) or if
|
|
290
|
-
* none of `rels` exists anymore
|
|
291
|
-
*/
|
|
192
|
+
// Caches are excluded at every depth: git reports an ignored dir as ONE entry,
|
|
193
|
+
// so `apps/x/` would drag its `node_modules/` along.
|
|
194
|
+
/** `null` if ANY copy failed — the caller must then decline the removal. */
|
|
292
195
|
export function writeWorktreeIgnoredBackup({ worktreePath, root, feature, rels, stamp = newBackupStamp() }) {
|
|
293
196
|
const dest = join(finishBackupDir(root), `${feature}-${stamp}${WORKTREE_IGNORED_SUFFIX}`);
|
|
294
197
|
const copied = [];
|
|
@@ -297,7 +200,7 @@ export function writeWorktreeIgnoredBackup({ worktreePath, root, feature, rels,
|
|
|
297
200
|
try {
|
|
298
201
|
for (const rel of rels) {
|
|
299
202
|
const src = join(worktreePath, rel);
|
|
300
|
-
if (!existsSync(src)) continue;
|
|
203
|
+
if (!existsSync(src)) continue;
|
|
301
204
|
if (isDisposableBuildArtifact(relOf(src))) {
|
|
302
205
|
excludedCaches.push(relOf(src));
|
|
303
206
|
continue;
|
|
@@ -332,17 +235,8 @@ export function writeWorktreeIgnoredBackup({ worktreePath, root, feature, rels,
|
|
|
332
235
|
return dest;
|
|
333
236
|
}
|
|
334
237
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
* the shed can never be the reason something is lost.
|
|
338
|
-
*
|
|
339
|
-
* Lives under `.git/` on purpose: it is not a working-tree path, so it can never
|
|
340
|
-
* dirty `git status` (which the very next validation reads) and is never merged,
|
|
341
|
-
* committed, or cleaned by `git clean`.
|
|
342
|
-
*
|
|
343
|
-
* @param {{ root: string, feature: string, stamp?: string }} args
|
|
344
|
-
* @returns {string|null} absolute backup path, or null if the copy failed
|
|
345
|
-
*/
|
|
238
|
+
// Under `.git/` so it never dirties the `git status` the next validation reads.
|
|
239
|
+
/** `null` if the copy failed: the caller must not shed. */
|
|
346
240
|
export function writeFeatureScopeBackup({ root, feature, stamp = newBackupStamp() }) {
|
|
347
241
|
const src = join(root, '.morph', 'features', feature);
|
|
348
242
|
if (!existsSync(src)) return null;
|
|
@@ -351,7 +245,7 @@ export function writeFeatureScopeBackup({ root, feature, stamp = newBackupStamp(
|
|
|
351
245
|
mkdirSync(dirname(dest), { recursive: true });
|
|
352
246
|
cpSync(src, dest, { recursive: true });
|
|
353
247
|
} catch {
|
|
354
|
-
return null;
|
|
248
|
+
return null;
|
|
355
249
|
}
|
|
356
250
|
writeManifest(dest, {
|
|
357
251
|
kind: 'feature-scope',
|
|
@@ -363,26 +257,10 @@ export function writeFeatureScopeBackup({ root, feature, stamp = newBackupStamp(
|
|
|
363
257
|
return dest;
|
|
364
258
|
}
|
|
365
259
|
|
|
366
|
-
|
|
367
|
-
* The first-level directories that hold backups instead of being one: `task/`
|
|
368
|
-
* is where the `task/{name}` key of a task isolation tree (see
|
|
369
|
-
* `envKeyForWorktree` in worktree.js) lands. Any other first-level directory
|
|
370
|
-
* that is not a backup was put there by someone else — never listed, never
|
|
371
|
-
* removed, not even empty.
|
|
372
|
-
*/
|
|
260
|
+
// Any other first-level directory that is not a backup is not ours: never removed.
|
|
373
261
|
const BACKUP_NAMESPACES = new Set(['task']);
|
|
374
262
|
|
|
375
|
-
/**
|
|
376
|
-
* Every backup folder on disk: the first level, plus one level down inside a
|
|
377
|
-
* namespace of BACKUP_NAMESPACES. A link is never descended, at either level.
|
|
378
|
-
*
|
|
379
|
-
* @param {string} root
|
|
380
|
-
* `managed` is whether the folder has its manifest — the one criterion that
|
|
381
|
-
* tells a backup of this retention from one written before it.
|
|
382
|
-
*
|
|
383
|
-
* @returns {Array<{ name: string, path: string, namespace: string, set: string,
|
|
384
|
-
* feature: string, stamp: string, kind: string, createdAt: number, managed: boolean }>}
|
|
385
|
-
*/
|
|
263
|
+
/** `managed` = has its manifest; without one the backup predates retention. */
|
|
386
264
|
export function listFinishBackups(root) {
|
|
387
265
|
const base = finishBackupDir(root);
|
|
388
266
|
if (!isRealDir(base)) return [];
|
|
@@ -396,7 +274,7 @@ export function listFinishBackups(root) {
|
|
|
396
274
|
}
|
|
397
275
|
for (const e of entries) {
|
|
398
276
|
const path = join(dir, e.name);
|
|
399
|
-
if (!isRealDir(path)) continue;
|
|
277
|
+
if (!isRealDir(path)) continue;
|
|
400
278
|
const parsed = parseBackupName(e.name);
|
|
401
279
|
if (parsed) {
|
|
402
280
|
const prefix = namespace ? `${namespace}/` : '';
|
|
@@ -420,30 +298,8 @@ export function listFinishBackups(root) {
|
|
|
420
298
|
return out;
|
|
421
299
|
}
|
|
422
300
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
* Pure: the caller supplies the listing and the clock.
|
|
426
|
-
*
|
|
427
|
-
* Two kinds of set are never removed. The ones stamped with `protectStamps` —
|
|
428
|
-
* the backup the calling run just wrote: a retention that runs after a backup
|
|
429
|
-
* exists to make room for it, not to take it. And the ones stamped AFTER `now`
|
|
430
|
-
* (a clock that ran ahead and was corrected): their age is unknowable, so they
|
|
431
|
-
* stay and do not take a place in the count — otherwise a handful of them
|
|
432
|
-
* would push every new backup out the moment it is written.
|
|
433
|
-
*
|
|
434
|
-
* A set is legacy unless EVERY folder in it has its manifest. Without
|
|
435
|
-
* `includeLegacy` (the automatic prune) legacy sets are out of the plan
|
|
436
|
-
* altogether — no removal, no strip, no place in the count — and only counted
|
|
437
|
-
* in `legacy`. With it (the explicit prune) they are ranked like the rest, and
|
|
438
|
-
* the ones kept are listed in `adopt`.
|
|
439
|
-
*
|
|
440
|
-
* @param {ReturnType<typeof listFinishBackups>} backups
|
|
441
|
-
* @param {ReturnType<typeof resolveBackupRetention>} retention
|
|
442
|
-
* @param {number} now - epoch ms
|
|
443
|
-
* @param {{ protectStamps?: string[], includeLegacy?: boolean }} [opts]
|
|
444
|
-
* @returns {{ sets: number, legacy: number, remove: object[], stripSecrets: object[], adopt: object[] }}
|
|
445
|
-
* each entry a `{ set, namespace, stamp, createdAt, managed, members }`
|
|
446
|
-
*/
|
|
301
|
+
// Sets stamped after `now` (clock ran ahead) stay and take no place in the count,
|
|
302
|
+
// or a handful of them would evict every new backup the moment it is written.
|
|
447
303
|
export function planBackupRetention(backups, retention, now, { protectStamps = [], includeLegacy = false } = {}) {
|
|
448
304
|
const sets = new Map();
|
|
449
305
|
for (const b of backups) {
|
|
@@ -486,27 +342,8 @@ export function planBackupRetention(backups, retention, now, { protectStamps = [
|
|
|
486
342
|
return plan;
|
|
487
343
|
}
|
|
488
344
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
* the short retention, and record the stripped names in the manifest. A backup
|
|
492
|
-
* whose removal is declined (a reparse point inside, a locked file) is stripped
|
|
493
|
-
* instead: the policy wanted it gone, and its secrets are the part that must not
|
|
494
|
-
* linger. A policy that could not be read (`skipped`) prunes nothing. Never
|
|
495
|
-
* throws — a retention that fails must never be the reason a `finish` fails.
|
|
496
|
-
*
|
|
497
|
-
* Legacy backups (no manifest) are touched only with `includeLegacy` — the
|
|
498
|
-
* explicit `doctor --worktrees --prune`; otherwise they are counted in
|
|
499
|
-
* `legacyPending`. The explicit prune writes a manifest into every legacy
|
|
500
|
-
* backup it keeps (`adopted`), which makes it an ordinary backup from then on.
|
|
501
|
-
*
|
|
502
|
-
* @param {string} root
|
|
503
|
-
* @param {{ now?: number, retention?: ReturnType<typeof resolveBackupRetention>,
|
|
504
|
-
* protectStamps?: string[], includeLegacy?: boolean }} [opts]
|
|
505
|
-
* `protectStamps`: the stamp(s) the calling run wrote its backup under
|
|
506
|
-
* @returns {{ enabled: boolean, skipped?: string, removed: string[],
|
|
507
|
-
* secretsStripped: Array<{ backup: string, files: string[] }>, declined: Array<{ backup: string, reason: string }>,
|
|
508
|
-
* legacyPending: number, adopted: string[] }}
|
|
509
|
-
*/
|
|
345
|
+
// A backup whose removal is declined still loses its `.env*`. Never throws:
|
|
346
|
+
// a failed retention must never be the reason a `finish` fails.
|
|
510
347
|
export function applyBackupRetention(root, { now = Date.now(), retention, protectStamps = [], includeLegacy = false } = {}) {
|
|
511
348
|
const result = { enabled: true, removed: [], secretsStripped: [], declined: [], legacyPending: 0, adopted: [] };
|
|
512
349
|
try {
|
|
@@ -539,7 +376,7 @@ export function applyBackupRetention(root, { now = Date.now(), retention, protec
|
|
|
539
376
|
toStrip.push(m);
|
|
540
377
|
continue;
|
|
541
378
|
}
|
|
542
|
-
try { rmSync(manifestPathFor(m.path), { force: true }); } catch {
|
|
379
|
+
try { rmSync(manifestPathFor(m.path), { force: true }); } catch {}
|
|
543
380
|
result.removed.push(m.name);
|
|
544
381
|
}
|
|
545
382
|
}
|
|
@@ -550,7 +387,7 @@ export function applyBackupRetention(root, { now = Date.now(), retention, protec
|
|
|
550
387
|
try {
|
|
551
388
|
unlinkSync(join(m.path, rel));
|
|
552
389
|
stripped.push(rel);
|
|
553
|
-
} catch {
|
|
390
|
+
} catch {}
|
|
554
391
|
}
|
|
555
392
|
if (!stripped.length) continue;
|
|
556
393
|
const manifest = readManifest(m.path) ?? { kind: m.kind, feature: m.feature, createdAt: new Date(m.createdAt).toISOString() };
|
|
@@ -567,27 +404,16 @@ export function applyBackupRetention(root, { now = Date.now(), retention, protec
|
|
|
567
404
|
result.adopted.push(m.name);
|
|
568
405
|
}
|
|
569
406
|
|
|
570
|
-
// A namespace emptied by the removals goes too; rmdir refuses anything else.
|
|
571
407
|
for (const name of BACKUP_NAMESPACES) {
|
|
572
408
|
const p = join(base, name);
|
|
573
409
|
if (isRealDir(p)) {
|
|
574
|
-
try { rmdirSync(p); } catch {
|
|
410
|
+
try { rmdirSync(p); } catch {}
|
|
575
411
|
}
|
|
576
412
|
}
|
|
577
|
-
} catch {
|
|
413
|
+
} catch {}
|
|
578
414
|
return result;
|
|
579
415
|
}
|
|
580
416
|
|
|
581
|
-
/**
|
|
582
|
-
* What a prune WOULD do now, without doing it. `includeLegacy` picks which
|
|
583
|
-
* prune: without it, the automatic one (what `finish --dry-run` announces);
|
|
584
|
-
* with it, the explicit `doctor --worktrees --prune` (what the doctor reports).
|
|
585
|
-
*
|
|
586
|
-
* @param {string} root
|
|
587
|
-
* @param {{ now?: number, retention?: ReturnType<typeof resolveBackupRetention>, includeLegacy?: boolean }} [opts]
|
|
588
|
-
* @returns {{ policy: object, sets: number, folders: number, legacy: number,
|
|
589
|
-
* outsideRetention: number, overdueSecrets: string[] }}
|
|
590
|
-
*/
|
|
591
417
|
export function inspectFinishBackups(root, { now = Date.now(), retention, includeLegacy = false } = {}) {
|
|
592
418
|
const policy = retention ?? readBackupRetention(root);
|
|
593
419
|
const backups = listFinishBackups(root);
|
|
@@ -603,7 +429,6 @@ export function inspectFinishBackups(root, { now = Date.now(), retention, includ
|
|
|
603
429
|
};
|
|
604
430
|
}
|
|
605
431
|
|
|
606
|
-
/** The sentence both the audit and the store line say about what `--prune` would do. */
|
|
607
432
|
function pendingParts(report, { long = false } = {}) {
|
|
608
433
|
const { maxCount, maxAgeDays, secretMaxAgeDays } = report.policy;
|
|
609
434
|
const parts = [];
|
|
@@ -632,16 +457,7 @@ function formatSize(bytes) {
|
|
|
632
457
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
633
458
|
}
|
|
634
459
|
|
|
635
|
-
|
|
636
|
-
* The default `doctor`'s line about `.git/morph-spec/`: its size and the
|
|
637
|
-
* backups in it, in the check shape the doctor renders. The size rides in the
|
|
638
|
-
* NAME because an `ok` line prints only its name. `ok` or `warn`, never a
|
|
639
|
-
* failure; `null` — no line — when nothing was ever stored there.
|
|
640
|
-
*
|
|
641
|
-
* @param {string} root - the primary root
|
|
642
|
-
* @param {{ now?: number }} [opts]
|
|
643
|
-
* @returns {{ name: string, status: 'ok'|'warn', msg?: string }|null}
|
|
644
|
-
*/
|
|
460
|
+
// The size rides in the NAME because the doctor prints only the name of an `ok` line.
|
|
645
461
|
export function morphStoreCheck(root, { now = Date.now() } = {}) {
|
|
646
462
|
let store;
|
|
647
463
|
let report;
|
|
@@ -659,17 +475,6 @@ export function morphStoreCheck(root, { now = Date.now() } = {}) {
|
|
|
659
475
|
return { name, status: 'warn', msg: `${pending.join(', ')} — rode morph-spec doctor --worktrees --prune` };
|
|
660
476
|
}
|
|
661
477
|
|
|
662
|
-
/**
|
|
663
|
-
* The retention as an audit finding, in the shape `auditWorktrees` returns:
|
|
664
|
-
* what the explicit `--prune` would do, including the legacy backups only it
|
|
665
|
-
* prunes. `warn`, never a failure — backups waiting for a prune are
|
|
666
|
-
* housekeeping. A policy that could not be read is a finding too: until it is
|
|
667
|
-
* fixed, nothing is ever pruned.
|
|
668
|
-
*
|
|
669
|
-
* @param {string} root
|
|
670
|
-
* @param {{ now?: number }} [opts]
|
|
671
|
-
* @returns {Array<{ type: string, severity: string, detail: string, fix: string }>}
|
|
672
|
-
*/
|
|
673
478
|
export function auditFinishBackups(root, opts = {}) {
|
|
674
479
|
let report;
|
|
675
480
|
try {
|
|
@@ -697,14 +502,7 @@ export function auditFinishBackups(root, opts = {}) {
|
|
|
697
502
|
}];
|
|
698
503
|
}
|
|
699
504
|
|
|
700
|
-
/**
|
|
701
|
-
* A payload-sized account of one retention run: complete counts, a short
|
|
702
|
-
* sample of names; `{ skipped }` when the policy could not be read. `null` when
|
|
703
|
-
* the run changed nothing and nothing waits for the explicit prune.
|
|
704
|
-
*
|
|
705
|
-
* @param {ReturnType<typeof applyBackupRetention>} result
|
|
706
|
-
* @returns {object|null}
|
|
707
|
-
*/
|
|
505
|
+
/** `null` when the run changed nothing and nothing waits for the explicit prune. */
|
|
708
506
|
export function summarizeRetention(result) {
|
|
709
507
|
if (!result) return null;
|
|
710
508
|
if (result.skipped) return { skipped: result.skipped };
|
|
@@ -721,15 +519,7 @@ export function summarizeRetention(result) {
|
|
|
721
519
|
return Object.keys(out).length ? out : null;
|
|
722
520
|
}
|
|
723
521
|
|
|
724
|
-
/**
|
|
725
|
-
* The retention step every writer of backups runs after its own backup:
|
|
726
|
-
* apply + summarize, fail-open. `protectStamps` is the stamp that backup was
|
|
727
|
-
* written under — never pruned by this run.
|
|
728
|
-
*
|
|
729
|
-
* @param {string} root
|
|
730
|
-
* @param {{ protectStamps?: string[] }} [opts]
|
|
731
|
-
* @returns {object|null}
|
|
732
|
-
*/
|
|
522
|
+
/** `protectStamps`: the stamp the caller just wrote its backup under, never pruned by this run. */
|
|
733
523
|
export function enforceBackupRetention(root, { protectStamps = [] } = {}) {
|
|
734
524
|
try {
|
|
735
525
|
return summarizeRetention(applyBackupRetention(root, { protectStamps }));
|