@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,36 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* WHY THIS EXISTS: a squash-merge puts the WORK in `origin/master` while
|
|
6
|
-
* leaving none of the branch's own commits reachable from it. So
|
|
7
|
-
* `git branch --merged origin/master` does not list the branch, and
|
|
8
|
-
* `git branch -d` refuses it as unmerged. The only remaining command is
|
|
9
|
-
* `git branch -D`, which is byte-identical to the command that discards real,
|
|
10
|
-
* never-integrated work — the tool gives the user no way to tell the two apart,
|
|
11
|
-
* and after enough of these the habit becomes "just -D it", which is how work
|
|
12
|
-
* gets lost.
|
|
13
|
-
*
|
|
14
|
-
* Every branch lands in exactly one class, and only the two MERGED ones are
|
|
15
|
-
* ever deletable:
|
|
16
|
-
*
|
|
17
|
-
* merged-ancestral — git itself proves it: the branch tip is an ancestor of
|
|
18
|
-
* the default. A plain merge, or a fast-forward.
|
|
19
|
-
* merged-squash — git cannot see it, but the evidence is positive: either
|
|
20
|
-
* GitHub reports the PR as MERGED, or the branch's tree is
|
|
21
|
-
* IDENTICAL to the default's (every change it carries is
|
|
22
|
-
* already there under a different commit).
|
|
23
|
-
* open — a PR is OPEN for it. Never deleted: the PR is the whole
|
|
24
|
-
* reason the branch must survive.
|
|
25
|
-
* in-worktree — checked out by a live worktree. Never deleted.
|
|
26
|
-
* unmerged — none of the above could be PROVEN. This is the fail-safe
|
|
27
|
-
* default and covers "no `gh`", "no origin", and "genuinely
|
|
28
|
-
* unintegrated work" alike, because from here they are the
|
|
29
|
-
* same thing: not proven integrated.
|
|
30
|
-
*
|
|
31
|
-
* Nothing here mutates. `pruneBranches` in commands/project/branch.js does the
|
|
32
|
-
* deleting, and it deletes ONLY what this file classified as merged.
|
|
33
|
-
*/
|
|
1
|
+
// A squash-merge leaves the work in the default branch but none of the branch's commits,
|
|
2
|
+
// so `branch -d` refuses it and `-D` looks like discarding real work. Only the two
|
|
3
|
+
// `merged-*` classes are deletable; anything not PROVEN integrated is `unmerged`.
|
|
34
4
|
|
|
35
5
|
import { execFileSync } from 'child_process';
|
|
36
6
|
|
|
@@ -38,11 +8,7 @@ function git(args, cwd) {
|
|
|
38
8
|
return execFileSync('git', args, { cwd, stdio: 'pipe' }).toString();
|
|
39
9
|
}
|
|
40
10
|
|
|
41
|
-
/**
|
|
42
|
-
* Local branches with their tips. Excludes nothing — the caller decides.
|
|
43
|
-
*
|
|
44
|
-
* @returns {Array<{ name: string, sha: string, current: boolean, worktreePath: string|null }>}
|
|
45
|
-
*/
|
|
11
|
+
/** @returns {Array<{ name: string, sha: string, current: boolean, worktreePath: string|null }>} */
|
|
46
12
|
export function listLocalBranches(cwd) {
|
|
47
13
|
try {
|
|
48
14
|
const out = git(['for-each-ref', '--format=%(refname:short)%09%(objectname)%09%(HEAD)%09%(worktreepath)', 'refs/heads/'], cwd);
|
|
@@ -55,7 +21,6 @@ export function listLocalBranches(cwd) {
|
|
|
55
21
|
}
|
|
56
22
|
}
|
|
57
23
|
|
|
58
|
-
/** Is `ancestor` reachable from `descendant`? False on any git error. */
|
|
59
24
|
function isAncestor(ancestor, descendant, cwd) {
|
|
60
25
|
try {
|
|
61
26
|
git(['merge-base', '--is-ancestor', ancestor, descendant], cwd);
|
|
@@ -65,19 +30,8 @@ function isAncestor(ancestor, descendant, cwd) {
|
|
|
65
30
|
}
|
|
66
31
|
}
|
|
67
32
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
*
|
|
71
|
-
* `git diff --quiet <base> <branch>` compares the two TREES: an empty diff
|
|
72
|
-
* means every file the branch touched already reads exactly as the branch left
|
|
73
|
-
* it in the base. That is precisely the state a squash-merge produces, and it
|
|
74
|
-
* is a positive proof rather than an inference from history shape.
|
|
75
|
-
*
|
|
76
|
-
* Deliberately NOT `base...branch` (the three-dot form diffs against the merge
|
|
77
|
-
* base, which for a squash-merged branch still shows the branch's own changes).
|
|
78
|
-
*
|
|
79
|
-
* Fail-CLOSED: any git error reads as "introduces something", never as merged.
|
|
80
|
-
*/
|
|
33
|
+
// Two-dot tree diff, never `base...branch`: the three-dot form still shows a squash-merged
|
|
34
|
+
// branch's own changes. Fail-closed: a git error never reads as merged.
|
|
81
35
|
function treeIsIdentical(base, branch, cwd) {
|
|
82
36
|
try {
|
|
83
37
|
execFileSync('git', ['diff', '--quiet', base, branch], { cwd, stdio: 'pipe' });
|
|
@@ -87,13 +41,7 @@ function treeIsIdentical(base, branch, cwd) {
|
|
|
87
41
|
}
|
|
88
42
|
}
|
|
89
43
|
|
|
90
|
-
/**
|
|
91
|
-
* How many commits the branch carries that the base does not. Used only for
|
|
92
|
-
* REPORTING — the classification never turns on it, because a squash-merged
|
|
93
|
-
* branch legitimately has commits ahead while carrying no unintegrated work.
|
|
94
|
-
*
|
|
95
|
-
* @returns {number|null} null when it cannot be computed
|
|
96
|
-
*/
|
|
44
|
+
/** Reporting only: a squash-merged branch legitimately has commits ahead. @returns {number|null} */
|
|
97
45
|
export function commitsAhead(base, branch, cwd) {
|
|
98
46
|
try {
|
|
99
47
|
const out = git(['rev-list', '--count', `${base}..${branch}`], cwd).trim();
|
|
@@ -105,12 +53,8 @@ export function commitsAhead(base, branch, cwd) {
|
|
|
105
53
|
}
|
|
106
54
|
|
|
107
55
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* @returns {string|null} null when neither resolves — the caller must then
|
|
112
|
-
* classify everything as `unmerged`, because there is no baseline to prove
|
|
113
|
-
* integration against.
|
|
56
|
+
* `origin/<default>` when it exists, else the local branch.
|
|
57
|
+
* @returns {string|null} null = no baseline, so everything is `unmerged`.
|
|
114
58
|
*/
|
|
115
59
|
export function resolveBaseRef(defaultBranch, cwd) {
|
|
116
60
|
if (!defaultBranch) return null;
|
|
@@ -123,19 +67,8 @@ export function resolveBaseRef(defaultBranch, cwd) {
|
|
|
123
67
|
return null;
|
|
124
68
|
}
|
|
125
69
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
* table above is testable without a GitHub account.
|
|
129
|
-
*
|
|
130
|
-
* ORDER IS THE POLICY, and the two protective classes come FIRST: a branch that
|
|
131
|
-
* is checked out, or that carries an OPEN PR, is never deleted even if its tree
|
|
132
|
-
* happens to match the base — a PR being open means a human still has business
|
|
133
|
-
* with it.
|
|
134
|
-
*
|
|
135
|
-
* @param {object} b - from listLocalBranches
|
|
136
|
-
* @param {{ baseRef: string|null, cwd: string, pr: {number:number,state:string}|null|undefined }} ctx
|
|
137
|
-
* @returns {{ name: string, kind: string, reason: string, pr: number|null, ahead: number|null, deletable: boolean }}
|
|
138
|
-
*/
|
|
70
|
+
// Order is the policy: checked-out and open-PR branches are protected before any
|
|
71
|
+
// merged check, even when the tree matches the base.
|
|
139
72
|
export function classifyBranch(b, { baseRef, cwd, pr }) {
|
|
140
73
|
const prNum = pr?.number ?? null;
|
|
141
74
|
const mk = (kind, reason, deletable) => ({
|
|
@@ -163,12 +96,6 @@ export function classifyBranch(b, { baseRef, cwd, pr }) {
|
|
|
163
96
|
return mk('unmerged', `carrega mudanças que não estão em ${baseRef} e nenhum PR MERGED comprova o contrário`, false);
|
|
164
97
|
}
|
|
165
98
|
|
|
166
|
-
/**
|
|
167
|
-
* Classify every local branch except the default itself.
|
|
168
|
-
*
|
|
169
|
-
* @param {{ cwd: string, defaultBranch: string|null, prIndex: Map|null }} ctx
|
|
170
|
-
* @returns {Array} one entry per branch, in `for-each-ref` order
|
|
171
|
-
*/
|
|
172
99
|
export function classifyBranches({ cwd, defaultBranch, prIndex }) {
|
|
173
100
|
const baseRef = resolveBaseRef(defaultBranch, cwd);
|
|
174
101
|
return listLocalBranches(cwd)
|
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'child_process';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Git helpers shared by `create` (which branches `morph/{f}` off the default)
|
|
5
|
-
* and `finish` (which merges/PRs back into it). Kept tiny and pure so both
|
|
6
|
-
* call sites read the same source of truth for "what is the default branch"
|
|
7
|
-
* and "is there a remote to push to".
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Does a local branch exist? execFile (argv, never a cmd.exe string) so the
|
|
12
|
-
* name reaches git as one token — the same anti-injection discipline the rest
|
|
13
|
-
* of the worktree/finish path uses.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} branch
|
|
16
|
-
* @param {string} cwd
|
|
17
|
-
* @returns {boolean}
|
|
18
|
-
*/
|
|
19
3
|
function localBranchExists(branch, cwd) {
|
|
20
4
|
try {
|
|
21
5
|
execFileSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { cwd, stdio: 'pipe' });
|
|
@@ -26,26 +10,14 @@ function localBranchExists(branch, cwd) {
|
|
|
26
10
|
}
|
|
27
11
|
|
|
28
12
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* 1. `refs/remotes/origin/HEAD` symbolic ref — the remote's declared default,
|
|
32
|
-
* the authoritative answer when a remote is configured.
|
|
33
|
-
* 2. A local `main`, then `master` — the fallback for a repo with no remote
|
|
34
|
-
* (or one whose `origin/HEAD` was never set).
|
|
35
|
-
*
|
|
36
|
-
* Returns `null` when none of these resolve (a repo whose default is some other
|
|
37
|
-
* name, with no `origin/HEAD` set) — the caller decides: `create` fails open
|
|
38
|
-
* (no branch), `finish` refuses with a clear message.
|
|
39
|
-
*
|
|
40
|
-
* @param {string} [cwd]
|
|
41
|
-
* @returns {string|null} the default branch name, or null if undeterminable
|
|
13
|
+
* `origin/HEAD` first, then a local `main`, then `master`.
|
|
14
|
+
* @returns {string|null} null when none resolves; the caller decides.
|
|
42
15
|
*/
|
|
43
16
|
export function detectDefaultBranch(cwd = process.cwd()) {
|
|
44
17
|
try {
|
|
45
18
|
const ref = execFileSync('git', ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], { cwd, stdio: 'pipe' })
|
|
46
19
|
.toString()
|
|
47
20
|
.trim();
|
|
48
|
-
// e.g. "refs/remotes/origin/main" → "main"
|
|
49
21
|
const m = ref.match(/^refs\/remotes\/origin\/(.+)$/);
|
|
50
22
|
if (m && m[1]) return m[1];
|
|
51
23
|
} catch { /* no origin/HEAD — fall back to a local default */ }
|
|
@@ -56,13 +28,6 @@ export function detectDefaultBranch(cwd = process.cwd()) {
|
|
|
56
28
|
return null;
|
|
57
29
|
}
|
|
58
30
|
|
|
59
|
-
/**
|
|
60
|
-
* Is an `origin` remote configured? `--pr` needs one to push the feature branch;
|
|
61
|
-
* `--merge` is purely local and never calls this.
|
|
62
|
-
*
|
|
63
|
-
* @param {string} [cwd]
|
|
64
|
-
* @returns {boolean}
|
|
65
|
-
*/
|
|
66
31
|
export function hasOriginRemote(cwd = process.cwd()) {
|
|
67
32
|
try {
|
|
68
33
|
const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd, stdio: 'pipe' }).toString().trim();
|
|
@@ -72,18 +37,7 @@ export function hasOriginRemote(cwd = process.cwd()) {
|
|
|
72
37
|
}
|
|
73
38
|
}
|
|
74
39
|
|
|
75
|
-
/**
|
|
76
|
-
* Resolve HEAD's full commit SHA. Fail-open: null when cwd isn't a git repo,
|
|
77
|
-
* or the repo has no commits yet (unborn HEAD) — never throws.
|
|
78
|
-
*
|
|
79
|
-
* Shared by `morph-spec verify` (the `verification` stamp) and
|
|
80
|
-
* `morph-spec gate-check` (signals + staleness-vs-code checks) — both need
|
|
81
|
-
* the exact commit a result covers, mirroring the same fail-open pattern
|
|
82
|
-
* `reviewStaleness()` in approve.js uses for the same `git rev-parse HEAD` call.
|
|
83
|
-
*
|
|
84
|
-
* @param {string} [cwd]
|
|
85
|
-
* @returns {string|null}
|
|
86
|
-
*/
|
|
40
|
+
/** @returns {string|null} null outside a repo or on an unborn HEAD; never throws. */
|
|
87
41
|
export function getHeadSha(cwd = process.cwd()) {
|
|
88
42
|
try {
|
|
89
43
|
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
|
|
@@ -92,15 +46,6 @@ export function getHeadSha(cwd = process.cwd()) {
|
|
|
92
46
|
}
|
|
93
47
|
}
|
|
94
48
|
|
|
95
|
-
/**
|
|
96
|
-
* Parse `git diff --shortstat` output ("3 files changed, 45 insertions(+), 2
|
|
97
|
-
* deletions(-)") into counts. Any line missing (e.g. only insertions, no
|
|
98
|
-
* deletions) contributes 0 for that field. Empty input (no diff) is all-zero,
|
|
99
|
-
* not an error.
|
|
100
|
-
*
|
|
101
|
-
* @param {string} text
|
|
102
|
-
* @returns {{ filesChanged: number, insertions: number, deletions: number }}
|
|
103
|
-
*/
|
|
104
49
|
function parseShortstat(text) {
|
|
105
50
|
const files = /(\d+) files? changed/.exec(text || '');
|
|
106
51
|
const ins = /(\d+) insertions?\(\+\)/.exec(text || '');
|
|
@@ -112,33 +57,10 @@ function parseShortstat(text) {
|
|
|
112
57
|
};
|
|
113
58
|
}
|
|
114
59
|
|
|
115
|
-
/**
|
|
116
|
-
* Count brand-new UNTRACKED files (never `git add`-ed) — the gap plain
|
|
117
|
-
* `git diff --shortstat` never sees, tracked-edits-only by design. Fail-open:
|
|
118
|
-
* any error (not a repo, git missing) reads as 0 untracked, never throws —
|
|
119
|
-
* this must not take down the whole `getDiffStat` result over a probe that
|
|
120
|
-
* is secondary to the diff itself.
|
|
121
|
-
*
|
|
122
|
-
* `--exclude-standard` matches gitignore the same way `git status` does, so
|
|
123
|
-
* scratch/build output never inflates the count.
|
|
124
|
-
*
|
|
125
|
-
* @param {string} cwd
|
|
126
|
-
* @returns {number}
|
|
127
|
-
*/
|
|
128
60
|
function countUntrackedFiles(cwd) {
|
|
129
61
|
return listUntrackedFiles(cwd).length;
|
|
130
62
|
}
|
|
131
63
|
|
|
132
|
-
/**
|
|
133
|
-
* Split git's stdout into clean, forward-slashed, non-empty path lines.
|
|
134
|
-
*
|
|
135
|
-
* One helper for both probes because the two disagreeing about what a "line" is
|
|
136
|
-
* — a stray `\r` on Windows, a trailing empty line — would make the count and
|
|
137
|
-
* the listing describe different sets from the same output.
|
|
138
|
-
*
|
|
139
|
-
* @param {string} text
|
|
140
|
-
* @returns {string[]}
|
|
141
|
-
*/
|
|
142
64
|
function splitLines(text) {
|
|
143
65
|
return String(text)
|
|
144
66
|
.split('\n')
|
|
@@ -146,25 +68,8 @@ function splitLines(text) {
|
|
|
146
68
|
.filter(Boolean);
|
|
147
69
|
}
|
|
148
70
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* {@link countUntrackedFiles}, and the second half of {@link changedPathsAgainst}.
|
|
152
|
-
*
|
|
153
|
-
* The two callers need the same fact at different resolutions, and this is the
|
|
154
|
-
* ONLY place the probe is spelled: `git diff` never shows a file that was never
|
|
155
|
-
* `git add`-ed, regardless of ref or working-tree mode, so anything that answers
|
|
156
|
-
* "what changed" has to ask twice. A second copy of this command is exactly how
|
|
157
|
-
* the invisible-new-file bug would come back on one caller and not the other.
|
|
158
|
-
*
|
|
159
|
-
* `--exclude-standard` honours gitignore the way `git status` does, so build
|
|
160
|
-
* output never inflates either answer.
|
|
161
|
-
*
|
|
162
|
-
* Fail-open to `[]`: this probe is secondary to the diff and must never take the
|
|
163
|
-
* whole result down.
|
|
164
|
-
*
|
|
165
|
-
* @param {string} cwd
|
|
166
|
-
* @returns {string[]}
|
|
167
|
-
*/
|
|
71
|
+
// `git diff` never shows a file that was never `git add`-ed, so every "what changed"
|
|
72
|
+
// answer asks here too. The only spelling of this probe: a copy is how one caller goes blind.
|
|
168
73
|
function listUntrackedFiles(cwd) {
|
|
169
74
|
try {
|
|
170
75
|
return splitLines(
|
|
@@ -176,29 +81,8 @@ function listUntrackedFiles(cwd) {
|
|
|
176
81
|
}
|
|
177
82
|
|
|
178
83
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* deliberately not `baseRef...HEAD`, so a gate-check mid-implement still sees
|
|
182
|
-
* uncommitted work), PLUS a count of brand-new UNTRACKED files (never
|
|
183
|
-
* `git add`-ed) that plain `git diff` never shows regardless of commit vs
|
|
184
|
-
* working tree. `filesChanged` is the combined total (tracked-changed +
|
|
185
|
-
* untracked) so a caller reading just that one number gets the true scope of
|
|
186
|
-
* what changed; `untrackedFiles` is broken out separately so a caller that
|
|
187
|
-
* cares CAN tell diff-vs-new apart instead of the two being silently fused.
|
|
188
|
-
*
|
|
189
|
-
* Both git calls run with the same `cwd` — every current caller passes a
|
|
190
|
-
* feature/worktree root there, so `git diff`'s repo-wide default and
|
|
191
|
-
* `git ls-files`'s cwd-relative listing agree; a caller passing a
|
|
192
|
-
* sub-directory as `cwd` would see `untrackedFiles` scoped to that
|
|
193
|
-
* sub-directory while `insertions`/`deletions` stay repo-wide — not a
|
|
194
|
-
* concern today, but worth knowing if that assumption ever changes.
|
|
195
|
-
*
|
|
196
|
-
* Fail-open: null when `baseRef` is falsy, cwd isn't a git repo, or the diff
|
|
197
|
-
* itself fails (e.g. baseRef doesn't resolve). The untracked probe fails open
|
|
198
|
-
* on its own (see countUntrackedFiles) and never turns a good diff into null.
|
|
199
|
-
*
|
|
200
|
-
* @param {string|null} baseRef - typically `detectDefaultBranch(cwd)`
|
|
201
|
-
* @param {string} [cwd]
|
|
84
|
+
* Diff against the WORKING TREE (not `baseRef...HEAD`), so uncommitted work counts.
|
|
85
|
+
* `filesChanged` includes untracked files; `untrackedFiles` breaks them out.
|
|
202
86
|
* @returns {{ filesChanged: number, insertions: number, deletions: number, untrackedFiles: number }|null}
|
|
203
87
|
*/
|
|
204
88
|
export function getDiffStat(baseRef, cwd = process.cwd()) {
|
|
@@ -214,35 +98,9 @@ export function getDiffStat(baseRef, cwd = process.cwd()) {
|
|
|
214
98
|
}
|
|
215
99
|
|
|
216
100
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* WHY IT LIVES BESIDE getDiffStat, AND REUSES ITS UNTRACKED PROBE
|
|
221
|
-
*
|
|
222
|
-
* The two answer the same question at different resolutions ("how much changed"
|
|
223
|
-
* versus "what changed"), and both depend on the SAME fragile fact: `git diff`
|
|
224
|
-
* is tracked-edits-only, so a brand-new file is invisible to it no matter which
|
|
225
|
-
* ref is named. `getDiffStat`'s docblock has documented that for a while; a
|
|
226
|
-
* second copy of the probe here is precisely how the "new file is invisible"
|
|
227
|
-
* bug comes back on one of the two call sites and not the other. So
|
|
228
|
-
* `countUntrackedFiles`'s command is not retyped — the listing is.
|
|
229
|
-
*
|
|
230
|
-
* `git diff --name-only <baseRef>` against the WORKING TREE, deliberately not
|
|
231
|
-
* `baseRef...HEAD`: a gate-check in the middle of implement must see work that
|
|
232
|
-
* is not committed yet. Someone who has only staged the prompt change has still
|
|
233
|
-
* changed the prompt.
|
|
234
|
-
*
|
|
235
|
-
* FAIL-OPEN, and the caller must know what that means. `[]` comes back both for
|
|
236
|
-
* "nothing changed" and for "the ref did not resolve", exactly like
|
|
237
|
-
* `getDiffStat` returning null. The distinction that MATTERS to the caller is a
|
|
238
|
-
* different one — "the project declares no watched path" versus "it declares
|
|
239
|
-
* some and none were touched" — and that one is answered by the caller's own
|
|
240
|
-
* `declared` flag, never by the emptiness of this array.
|
|
241
|
-
*
|
|
242
|
-
* @param {string|null} baseRef - typically `detectDefaultBranch(cwd)`
|
|
243
|
-
* @param {string} [cwd]
|
|
244
|
-
* @returns {string[]} Repo-relative paths with forward slashes, deduplicated,
|
|
245
|
-
* sorted. Empty when `baseRef` is falsy, cwd is not a repo, or the diff fails.
|
|
101
|
+
* Paths changed in the working tree against `baseRef`, untracked included, sorted.
|
|
102
|
+
* `[]` means both "nothing changed" and "ref did not resolve".
|
|
103
|
+
* @returns {string[]}
|
|
246
104
|
*/
|
|
247
105
|
export function changedPathsAgainst(baseRef, cwd = process.cwd()) {
|
|
248
106
|
if (!baseRef) return [];
|
|
@@ -253,8 +111,6 @@ export function changedPathsAgainst(baseRef, cwd = process.cwd()) {
|
|
|
253
111
|
execFileSync('git', ['diff', '--name-only', baseRef], { cwd, stdio: 'pipe' }).toString()
|
|
254
112
|
);
|
|
255
113
|
} catch {
|
|
256
|
-
// A baseRef that does not resolve is not a reason to claim "nothing
|
|
257
|
-
// changed" loudly — the caller's policy is written to be inert on `[]`.
|
|
258
114
|
return [];
|
|
259
115
|
}
|
|
260
116
|
|
|
@@ -262,21 +118,10 @@ export function changedPathsAgainst(baseRef, cwd = process.cwd()) {
|
|
|
262
118
|
}
|
|
263
119
|
|
|
264
120
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* Measured from the merge-base, not the tip: a default branch that moved on
|
|
270
|
-
* after the feature branched must not lend its edits to the feature. `-M`
|
|
271
|
-
* keeps a moved file from reading as added. `--relative` gives paths relative
|
|
272
|
-
* to `cwd` and drops what lies outside it, the same base as the untracked
|
|
273
|
-
* listing: a project in a subfolder of the repo reads its own files.
|
|
274
|
-
*
|
|
275
|
-
* @param {string|null} baseRef - typically `detectDefaultBranch(cwd)`
|
|
276
|
-
* @param {string} [cwd]
|
|
277
|
-
* @returns {Map<string, number[]|null>|null} path → added line numbers, or
|
|
278
|
-
* `null` for a whole untracked file. `null` (not an empty map) when it cannot
|
|
279
|
-
* tell: falsy or unresolvable ref, not a repo, diff failed.
|
|
121
|
+
* Added lines per file, measured from the merge-base so a moved-on default branch
|
|
122
|
+
* lends nothing; paths relative to `cwd` (`--relative`), like the untracked listing.
|
|
123
|
+
* @returns {Map<string, number[]|null>|null} `null` value = whole untracked file;
|
|
124
|
+
* `null` result = cannot tell.
|
|
280
125
|
*/
|
|
281
126
|
export function addedLinesAgainst(baseRef, cwd = process.cwd()) {
|
|
282
127
|
if (!baseRef) return null;
|
|
@@ -7,7 +7,7 @@ const CONFIG_PATH = '.morph/config/config.json';
|
|
|
7
7
|
|
|
8
8
|
const gitOut = (cwd, args) => execFileSync('git', args, { cwd, stdio: 'pipe', maxBuffer: 64 * 1024 * 1024 }).toString();
|
|
9
9
|
|
|
10
|
-
function withoutBaseline(text) {
|
|
10
|
+
export function withoutBaseline(text) {
|
|
11
11
|
const config = JSON.parse(text);
|
|
12
12
|
if (Array.isArray(config?.project?.tests)) {
|
|
13
13
|
for (const node of config.project.tests) delete node?.expectedCount;
|
|
@@ -31,6 +31,7 @@ export function treeMovedSince(cwd, sha, head = getHeadSha(cwd)) {
|
|
|
31
31
|
if (!sha || !head || sha === head) return false;
|
|
32
32
|
let changed;
|
|
33
33
|
try {
|
|
34
|
+
// Without --no-renames a file moved into .morph/ shows only its new path and the deletion outside is lost.
|
|
34
35
|
changed = gitOut(cwd, ['diff', '--name-only', '--no-renames', '-z', sha, head]).split('\0').filter(Boolean);
|
|
35
36
|
} catch {
|
|
36
37
|
return true;
|
package/src/lib/git/gh.js
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Extraído de `finish.js`, onde nasceu, porque o `pr-index.js` chamava `'gh'`
|
|
5
|
-
* cru: o caminho de SUCESSO do índice era intestável, já que não havia por
|
|
6
|
-
* onde injetar um stub. Duas formas de chamar o mesmo binário também são duas
|
|
7
|
-
* formas de discordar sobre qual binário é.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Resolve a invocação do `gh`. Por padrão, o `gh` do PATH. Sobrescrevível via
|
|
12
|
-
* MORPH_GH_BIN: uma string simples nomeia outro binário (um wrapper corporativo,
|
|
13
|
-
* `hub`, …); um array JSON é um prefixo de argv COMPLETO — por exemplo
|
|
14
|
-
* `["/path/to/node","/path/to/gh-shim.js"]` — que é como os testes injetam um
|
|
15
|
-
* stub cross-platform que o execFileSync consegue spawnar (Node ≥20 recusa
|
|
16
|
-
* rodar shims `.cmd`/`.bat` sem `shell:true`, e `shell:true` reabriria o buraco
|
|
17
|
-
* de injeção de argumento que estes caminhos evitam).
|
|
18
|
-
*
|
|
2
|
+
* MORPH_GH_BIN: string = outro binário; array JSON = prefixo de argv completo
|
|
3
|
+
* (Node ≥20 recusa shim `.cmd` sem `shell:true`, e `shell:true` reabre injeção de argumento).
|
|
19
4
|
* @returns {{ bin: string, prefix: string[] }}
|
|
20
5
|
*/
|
|
21
6
|
export function resolveGh() {
|
|
@@ -1,60 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* `finish --pr` deixa o root primário sem vestígio da feature: o
|
|
5
|
-
* `shedRootFeatureScope` apaga `.morph/features/{f}` e o archive foi commitado
|
|
6
|
-
* na branch `morph/{f}`, chegando na default apenas no merge. Entre abrir e
|
|
7
|
-
* mergear, portanto, nenhuma superfície local sabe que aquela branch tem dono —
|
|
8
|
-
* foi assim que uma branch com PR #51 aberto quase foi varrida.
|
|
9
|
-
*
|
|
10
|
-
* Este módulo devolve o dono. Não há estado persistido a consultar, e é de
|
|
11
|
-
* propósito: o PR aberto no GitHub É o registro, a branch é sempre
|
|
12
|
-
* `morph/{feature}`, e o `pr-index.js` já é o leitor único desse fato. Lembrar
|
|
13
|
-
* o número numa segunda cópia só criaria como divergir dele.
|
|
14
|
-
*
|
|
15
|
-
* `null` (do índice ou daqui) significa DESCONHECIDO e jamais "não há
|
|
16
|
-
* pendência" — a mesma regra que torna seguros os caminhos destrutivos que
|
|
17
|
-
* leem o índice.
|
|
18
|
-
*/
|
|
1
|
+
// Depois do `finish --pr` a raiz não guarda nada da feature até o merge: o PR aberto
|
|
2
|
+
// é o único registro de que a branch tem dono. `null` = DESCONHECIDO, nunca "sem pendência".
|
|
19
3
|
|
|
20
4
|
import { existsSync } from 'fs';
|
|
21
5
|
import { join } from 'path';
|
|
22
6
|
import { buildPrIndex } from './pr-index.js';
|
|
23
7
|
|
|
24
|
-
/** As branches que o framework cria por feature. */
|
|
25
8
|
export const MORPH_BRANCH_RE = /^morph\/(.+)$/;
|
|
26
9
|
|
|
27
10
|
const DAY_MS = 86400000;
|
|
28
11
|
|
|
29
|
-
/**
|
|
30
|
-
* O PR da branch canônica de uma feature.
|
|
31
|
-
*
|
|
32
|
-
* @param {Map<string,object>|null} index - de buildPrIndex
|
|
33
|
-
* @param {string} feature
|
|
34
|
-
* @returns {{number:number,state:string,mergedAt:string|null,url:string|null,createdAt:string|null}|null}
|
|
35
|
-
* null = desconhecido OU sem PR; quem precisa distinguir os dois olha o índice.
|
|
36
|
-
*/
|
|
12
|
+
/** @returns {object|null} null = desconhecido OU sem PR; quem precisa distinguir olha o índice. */
|
|
37
13
|
export function prForFeature(index, feature) {
|
|
38
14
|
if (!index) return null;
|
|
39
15
|
return index.get(`morph/${feature}`) || null;
|
|
40
16
|
}
|
|
41
17
|
|
|
42
|
-
/** A feature tem pasta (ativa ou arquivada) nesta árvore? */
|
|
43
18
|
function hasLocalFolder(root, feature) {
|
|
44
19
|
return existsSync(join(root, '.morph', 'features', feature))
|
|
45
20
|
|| existsSync(join(root, '.morph', 'archive', feature));
|
|
46
21
|
}
|
|
47
22
|
|
|
48
23
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* @param {string} root - a raiz a inspecionar (e de onde o `gh` roda)
|
|
53
|
-
* @param {{ index?: Map<string,object>|null, now?: Date }} [opts]
|
|
54
|
-
* `index` injeta um índice já construído (o `list`/`fleet` constroem um por
|
|
55
|
-
* processo e reaproveitam; os testes passam um sintético). Ausente = constrói.
|
|
56
|
-
* @returns {Array<{feature:string,branch:string,number:number,state:string,url:string|null,createdAt:string|null,ageDays:number|null}>|null}
|
|
57
|
-
* null = DESCONHECIDO. Nunca lista vazia por falta de informação.
|
|
24
|
+
* PR OPEN numa branch `morph/*` cujo slug não tem pasta nesta árvore.
|
|
25
|
+
* @param {{ index?: Map<string,object>|null, now?: Date }} [opts] `index` ausente = constrói.
|
|
26
|
+
* @returns {Array<object>|null} null = DESCONHECIDO, nunca lista vazia por falta de informação.
|
|
58
27
|
*/
|
|
59
28
|
export function pendingPrFeatures(root, opts = {}) {
|
|
60
29
|
const index = opts.index !== undefined ? opts.index : buildPrIndex(root);
|
package/src/lib/git/pr-files.js
CHANGED
|
@@ -1,37 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pr-files.js — o diff de um PR carregou uma feature morph-spec?
|
|
3
|
-
*
|
|
4
|
-
* `morph/{f}` é a convenção de nome das branches que o framework cria, não
|
|
5
|
-
* prova de que a branch carregou uma feature: nada impede um PR só de docs
|
|
6
|
-
* numa `morph/x`. O `doctor` lia o nome como prova e acusava
|
|
7
|
-
* `pr-merged-not-landed` para sempre sobre PRs que nunca tiveram archive para
|
|
8
|
-
* pousar (medido no MORPH_GHLBrain: #5, um único `docs/*.md`, e #23, a onda 2
|
|
9
|
-
* de outra feature — nenhum caminho `.morph/` no diff de nenhum dos dois).
|
|
10
|
-
*
|
|
11
|
-
* A pergunta é feita ao diff, e só para o candidato: o `doctor` chama isto
|
|
12
|
-
* depois de já saber que o PR mergeou e que `archive/{f}` falta aqui, então o
|
|
13
|
-
* custo (~650 ms por `gh pr view`) não recai sobre quem não tem pendência. O
|
|
14
|
-
* índice (`pr-index.js`) segue sem o campo de propósito: pedir `mergeCommit` no
|
|
15
|
-
* `pr list` custou ~400 ms a mais em CADA chamada, paga pelos quatro leitores.
|
|
16
|
-
*
|
|
17
|
-
* Três respostas, nunca duas: `true` (o diff tocou `.morph/{features,archive}/{f}/`),
|
|
18
|
-
* `false` (o diff foi lido inteiro e não tocou) e `null` (DESCONHECIDO — sem
|
|
19
|
-
* `gh`, falha, JSON ilegível, ou o diff bateu no teto de arquivos do `gh` sem
|
|
20
|
-
* achar). Só `false` autoriza calar um aviso.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
1
|
import { execFileSync } from 'child_process';
|
|
24
2
|
import { resolveGh } from './gh.js';
|
|
25
3
|
|
|
26
|
-
/** Quantos arquivos o `gh pr view --json files` devolve, no máximo. */
|
|
27
4
|
export const GH_FILES_CAP = 100;
|
|
28
5
|
|
|
29
6
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @
|
|
33
|
-
* @param {{ timeout?: number }} [opts]
|
|
34
|
-
* @returns {boolean|null} null = DESCONHECIDO, nunca "não carregou"
|
|
7
|
+
* Uma branch `morph/{f}` não prova que o PR carregou a feature: só o diff tocando
|
|
8
|
+
* `.morph/{features,archive}/{f}/` prova.
|
|
9
|
+
* @returns {boolean|null} null = DESCONHECIDO; só `false` autoriza calar um aviso.
|
|
35
10
|
*/
|
|
36
11
|
export function prCarriesFeature(root, number, feature, opts = {}) {
|
|
37
12
|
const timeout = opts.timeout ?? 8000;
|
package/src/lib/git/pr-index.js
CHANGED
|
@@ -1,42 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pr-index.js — one `gh pr list` per process, shared by every caller that needs
|
|
3
|
-
* to answer "does a branch have a PR, and in what state".
|
|
4
|
-
*
|
|
5
|
-
* Extracted from `fleet-scanner.js` because three independent leaders now ask
|
|
6
|
-
* the SAME question and must get the SAME answer:
|
|
7
|
-
*
|
|
8
|
-
* • `morph-spec fleet` — the PR column
|
|
9
|
-
* • `morph-spec branch prune` — a branch with an OPEN PR is never deleted,
|
|
10
|
-
* and a MERGED one is deletable even when git
|
|
11
|
-
* cannot see the merge (squash-merge)
|
|
12
|
-
* • `morph-spec doctor --worktrees` — a worktree whose branch carries an open
|
|
13
|
-
* PR must NOT be offered up for removal
|
|
14
|
-
*
|
|
15
|
-
* Fail-open by construction: no `gh`, not authenticated, not GitHub, or a
|
|
16
|
-
* timeout all yield `null`. `null` means "unknown", NEVER "no PR" — every
|
|
17
|
-
* caller must treat the two differently, because the destructive paths
|
|
18
|
-
* (prune, remove) are only safe when the answer is a positive fact.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
1
|
import { execFileSync } from 'child_process';
|
|
22
2
|
import { resolveGh } from './gh.js';
|
|
23
3
|
|
|
24
4
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* Gated on the origin URL actually pointing at github.com: `gh` against any
|
|
28
|
-
* other host is a guaranteed error, and paying its start-up cost to learn that
|
|
29
|
-
* on every call is pure latency.
|
|
30
|
-
*
|
|
31
|
-
* `--state all` on purpose: a MERGED PR is exactly the evidence a squash-merge
|
|
32
|
-
* erases from git history, and it is the whole reason `branch prune` exists.
|
|
33
|
-
* First wins — `gh` lists newest-first, so an OPEN PR shadows an older closed
|
|
34
|
-
* one on the same branch.
|
|
35
|
-
*
|
|
36
|
-
* @param {string} root - repository root to run `gh` from
|
|
37
|
-
* @param {{ limit?: number, timeout?: number }} [opts]
|
|
5
|
+
* Branch → PR. `--state all`: a MERGED PR is the evidence a squash-merge erases from git.
|
|
6
|
+
* `gh` lists newest-first, so the first entry per branch wins.
|
|
38
7
|
* @returns {Map<string,{number:number,state:string,mergedAt:string|null,url:string|null,createdAt:string|null}>|null}
|
|
39
|
-
* null =
|
|
8
|
+
* null = unknown, NEVER "no PR": destructive callers act only on a positive fact.
|
|
40
9
|
*/
|
|
41
10
|
export function buildPrIndex(root, opts = {}) {
|
|
42
11
|
const limit = String(opts.limit ?? 200);
|