@polymorphism-tech/morph-spec 9.0.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/rules/nextjs-standards.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +10 -4
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -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;
|
|
@@ -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);
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* untracked files, plus `config.json` without `expectedCount` (it names the test commands).
|
|
4
|
-
* The rest of `.morph/` stays out, as in `treeMovedSince`: committing verify's own
|
|
5
|
-
* bookkeeping must not make the same code look different.
|
|
6
|
-
*/
|
|
1
|
+
// `.morph/` stays out except `config.json` minus `expectedCount`, as in `treeMovedSince`:
|
|
2
|
+
// committing verify's own bookkeeping must not make the same code look different.
|
|
7
3
|
|
|
8
4
|
import { execFileSync } from 'child_process';
|
|
9
5
|
import { createHash } from 'crypto';
|
|
@@ -11,33 +7,25 @@ import { existsSync, readFileSync, statSync } from 'fs';
|
|
|
11
7
|
import { join } from 'path';
|
|
12
8
|
import { withoutBaseline } from './evidence-staleness.js';
|
|
13
9
|
|
|
14
|
-
/** Pathspec for "the project minus its `.morph/` bookkeeping". */
|
|
15
10
|
export const OUTSIDE_MORPH = ['--', '.', ':(exclude).morph'];
|
|
16
11
|
const MAX_OUTPUT = 64 * 1024 * 1024;
|
|
17
12
|
|
|
18
|
-
/** git output as a Buffer; stderr dropped, throws on a non-zero exit. */
|
|
19
13
|
export function git(projectPath, args, input) {
|
|
20
14
|
return execFileSync('git', args, {
|
|
21
15
|
cwd: projectPath, stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'ignore'], maxBuffer: MAX_OUTPUT, input,
|
|
22
16
|
});
|
|
23
17
|
}
|
|
24
18
|
|
|
25
|
-
/**
|
|
26
|
-
* @param {string} projectPath
|
|
27
|
-
* @returns {string|null} sha256 hex, or null outside a git repo / on any git error
|
|
28
|
-
*/
|
|
19
|
+
/** @returns {string|null} sha256 hex, or null outside a git repo / on any git error */
|
|
29
20
|
export function treeFingerprint(projectPath) {
|
|
30
21
|
try {
|
|
31
22
|
const hash = createHash('sha256');
|
|
32
23
|
let head = null;
|
|
33
24
|
try { head = git(projectPath, ['rev-parse', 'HEAD']).toString().trim(); } catch { /* no commit yet */ }
|
|
34
|
-
// Tree ids are content-addressed: the top-level entries minus .morph change only with code.
|
|
35
25
|
const tree = head
|
|
36
26
|
? git(projectPath, ['ls-tree', '-z', 'HEAD']).toString().split('\0').filter((e) => e && e.split('\t')[1] !== '.morph')
|
|
37
27
|
: ['no-commit'];
|
|
38
28
|
hash.update(`tree\0${tree.join('\0')}\0`);
|
|
39
|
-
// Before the first commit there is no HEAD to diff against: every file is
|
|
40
|
-
// untracked, and the listing below covers them.
|
|
41
29
|
if (head) hash.update(git(projectPath, ['diff', 'HEAD', '--binary', ...OUTSIDE_MORPH]));
|
|
42
30
|
hash.update('\0untracked\0');
|
|
43
31
|
const untracked = git(projectPath, ['ls-files', '--others', '--exclude-standard', '-z', ...OUTSIDE_MORPH])
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The working tree as the orchestrator needs it when it takes control back
|
|
3
|
-
* from a dispatch, in one call instead of `git status` + `git diff --stat` +
|
|
4
|
-
* `git log` + a marker grep. Everything is read outside `.morph/`, the same
|
|
5
|
-
* cut the tree fingerprint makes.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { existsSync } from 'fs';
|
|
9
2
|
import { join } from 'path';
|
|
10
3
|
import { OUTSIDE_MORPH, treeFingerprint, git as gitBuffer } from './tree-fingerprint.js';
|
|
11
4
|
import { detectDefaultBranch } from './default-branch.js';
|
|
12
5
|
import { resolveBaseRef } from './branch-classifier.js';
|
|
13
6
|
|
|
14
|
-
/** What the verifier writes next to a mutation it has not reverted yet. */
|
|
15
7
|
export const MUTATION_MARKER = 'MUTACAO';
|
|
16
8
|
const MAX_LISTED = 50;
|
|
17
9
|
const MAX_COMMITS = 10;
|
|
@@ -24,13 +16,11 @@ function headSha(cwd) {
|
|
|
24
16
|
try { return git(cwd, ['rev-parse', 'HEAD']).trim(); } catch { return null; }
|
|
25
17
|
}
|
|
26
18
|
|
|
27
|
-
/** Every file that differs from HEAD outside `.morph/`, untracked ones one by one. */
|
|
28
19
|
function changedFiles(cwd) {
|
|
29
20
|
return parsePorcelain(git(cwd, ['status', '--porcelain=v1', '-z', '--untracked-files=all', ...OUTSIDE_MORPH]))
|
|
30
21
|
.map((e) => e.path);
|
|
31
22
|
}
|
|
32
23
|
|
|
33
|
-
/** Blob id of each file as it is on disk; null for a file that is gone. */
|
|
34
24
|
function diskBlobs(cwd, paths) {
|
|
35
25
|
const present = paths.filter((p) => existsSync(join(cwd, p)));
|
|
36
26
|
const ids = present.length ? git(cwd, ['hash-object', '--stdin-paths'], present.join('\n')).split('\n') : [];
|
|
@@ -39,7 +29,6 @@ function diskBlobs(cwd, paths) {
|
|
|
39
29
|
return out;
|
|
40
30
|
}
|
|
41
31
|
|
|
42
|
-
/** Blob id of each path in a commit; null where the commit does not have it. */
|
|
43
32
|
function commitBlobs(cwd, commit, paths) {
|
|
44
33
|
const out = Object.fromEntries(paths.map((p) => [p, null]));
|
|
45
34
|
if (!commit || !paths.length) return out;
|
|
@@ -49,10 +38,8 @@ function commitBlobs(cwd, commit, paths) {
|
|
|
49
38
|
}
|
|
50
39
|
|
|
51
40
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* differs from it. Kept on disk, so a session that resumes after the agent
|
|
55
|
-
* died compares against the tree before the dispatch, not its own start.
|
|
41
|
+
* HEAD plus the blob of every file that differs from it, kept on disk so a resumed
|
|
42
|
+
* session compares against the tree before the dispatch, not its own start.
|
|
56
43
|
* @returns {{ at: string, head: string|null, files: Object<string, string|null> }|null}
|
|
57
44
|
*/
|
|
58
45
|
export function snapshotTree(cwd) {
|
|
@@ -64,9 +51,8 @@ export function snapshotTree(cwd) {
|
|
|
64
51
|
}
|
|
65
52
|
|
|
66
53
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* unchanged does not.
|
|
54
|
+
* Compares content, not status: a mutation committed since shows up, and a file
|
|
55
|
+
* committed unchanged does not.
|
|
70
56
|
* @returns {{ at: string, headChanged: boolean, changed: string[], changedCount: number }}
|
|
71
57
|
*/
|
|
72
58
|
export function compareToSnapshot(cwd, snapshot) {
|
|
@@ -85,7 +71,7 @@ export function compareToSnapshot(cwd, snapshot) {
|
|
|
85
71
|
return { at: snapshot.at, headChanged: snapshot.head !== head, changed: changed.slice(0, MAX_LISTED), changedCount: changed.length };
|
|
86
72
|
}
|
|
87
73
|
|
|
88
|
-
|
|
74
|
+
// With `-z`, a rename or copy carries its old path as an extra entry.
|
|
89
75
|
function parsePorcelain(out) {
|
|
90
76
|
const entries = [];
|
|
91
77
|
const parts = out.split('\0');
|
|
@@ -118,7 +104,6 @@ function commitsOnTop(cwd) {
|
|
|
118
104
|
.split('\n').filter(Boolean);
|
|
119
105
|
}
|
|
120
106
|
|
|
121
|
-
/** Untracked files count too; gitignored ones (build output) do not. */
|
|
122
107
|
function markers(cwd) {
|
|
123
108
|
try {
|
|
124
109
|
const out = git(cwd, ['grep', '-n', '-I', '--untracked', '-F', '-e', MUTATION_MARKER, ...OUTSIDE_MORPH]);
|
|
@@ -132,19 +117,7 @@ function markers(cwd) {
|
|
|
132
117
|
}
|
|
133
118
|
}
|
|
134
119
|
|
|
135
|
-
/**
|
|
136
|
-
* @param {string} cwd
|
|
137
|
-
* @param {{ snapshot?: object|null }} [opts] - a `snapshotTree()` to compare with
|
|
138
|
-
* @returns {{ head: string|null, branch: string|null, fingerprint: string|null,
|
|
139
|
-
* dirty: {status: string, path: string}[], dirtyCount: number,
|
|
140
|
-
* diffstat: {files: number, insertions: number, deletions: number}|null,
|
|
141
|
-
* commits: string[]|null, markers: {file: string, line: number}[],
|
|
142
|
-
* sinceBeforeEval: ReturnType<typeof compareToSnapshot>|null }|null}
|
|
143
|
-
* null when git cannot answer (outside a repo, or an error): unknown, never
|
|
144
|
-
* a clean tree. `commits` is null when there is no default branch to count
|
|
145
|
-
* from; `diffstat` is null before the first commit; `sinceBeforeEval` is
|
|
146
|
-
* null without a snapshot.
|
|
147
|
-
*/
|
|
120
|
+
/** @returns {object|null} null when git cannot answer: unknown, never a clean tree. */
|
|
148
121
|
export function treeState(cwd, { snapshot = null } = {}) {
|
|
149
122
|
try {
|
|
150
123
|
const dirty = parsePorcelain(git(cwd, ['status', '--porcelain=v1', '-z', ...OUTSIDE_MORPH]));
|
|
@@ -1,31 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* O inventário do architecture-scanner diz o que EXISTE; os god nodes dizem o
|
|
5
|
-
* que é CENTRAL. No ProspectPRO, seis linhas (`Result` 223, `Abstractions` 217,
|
|
6
|
-
* `Repository` 156, `cn()` 141) orientam melhor que 665 de inventário — sem
|
|
7
|
-
* substituí-lo, porque exaustividade é um valor que o grafo não tem.
|
|
8
|
-
*
|
|
9
|
-
* Só entram nós COM source_file: um god node sem arquivo não orienta ninguém.
|
|
10
|
-
* E nada de teste: dublês e fixtures estão no grafo para serem achados, não
|
|
11
|
-
* para disputar centralidade — sem o filtro o `FakeGhlToolbelt` entra no top
|
|
12
|
-
* 10 do MORPH_GHLBrain e as arestas vindas de teste inflam o grau de `Result`
|
|
13
|
-
* de 380 para 443. Aresta com uma ponta em teste não conta para ninguém, e
|
|
14
|
-
* comunidade só de teste some da lista.
|
|
15
|
-
*
|
|
16
|
-
* @module god-nodes
|
|
17
|
-
*/
|
|
1
|
+
// Só nós com source_file, e nada de teste: dublês e fixtures estão no grafo para serem achados, não para
|
|
2
|
+
// disputar centralidade. Aresta com uma ponta em teste não conta, e comunidade só de teste some.
|
|
18
3
|
import { loadGraph } from './graph-loader.js';
|
|
19
4
|
import { buildIndex, resolveEdgeTarget, linksOf, isTestPath } from './sanitize.js';
|
|
20
5
|
|
|
21
6
|
/** Sem `graphify label`, as comunidades se chamam Community_N — ruído puro. */
|
|
22
7
|
const PLACEHOLDER_LABEL = /^Community[_ ]?\d+$/i;
|
|
23
8
|
|
|
24
|
-
/**
|
|
25
|
-
* @param {string} projectPath
|
|
26
|
-
* @param {{top?: number, topCommunities?: number}} [opts]
|
|
27
|
-
* @returns {Promise<{available: boolean, godNodes: Array<{label: string, source_file: string, degree: number}>, communities: Array<{id: number, label: string, size: number, labeled: boolean}>}>}
|
|
28
|
-
*/
|
|
29
9
|
export async function scanGraph(projectPath, opts = {}) {
|
|
30
10
|
const top = opts.top ?? 10;
|
|
31
11
|
const topCommunities = opts.topCommunities ?? 10;
|