@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.
Files changed (345) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +11 -63
  4. package/bin/validate.js +1 -39
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -1
  9. package/framework/MORPH.md +3 -3
  10. package/framework/agents.json +19 -11
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +22 -31
  14. package/framework/evals/review-checklist.md +12 -2
  15. package/framework/hooks/README.md +8 -44
  16. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  17. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  18. package/framework/hooks/claude-code/core/chain.js +4 -65
  19. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  20. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  21. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  22. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  23. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  24. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  25. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  26. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  27. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  28. package/framework/hooks/claude-code/core/state-sync.js +12 -68
  29. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  30. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  31. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  32. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  33. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  34. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  35. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  36. package/framework/hooks/claude-code/statusline.sh +11 -11
  37. package/framework/hooks/shared/activity-logger.js +9 -89
  38. package/framework/hooks/shared/agent-reap.js +2 -17
  39. package/framework/hooks/shared/build-check.js +6 -75
  40. package/framework/hooks/shared/claim-registry.js +9 -119
  41. package/framework/hooks/shared/command-tokens.js +6 -51
  42. package/framework/hooks/shared/comment-rules.js +5 -20
  43. package/framework/hooks/shared/comment-scanner.js +6 -25
  44. package/framework/hooks/shared/comment-stripper.js +11 -85
  45. package/framework/hooks/shared/e2e-check.js +0 -0
  46. package/framework/hooks/shared/errata.js +2 -15
  47. package/framework/hooks/shared/feature-locator.js +11 -95
  48. package/framework/hooks/shared/frontend-locations.js +1 -3
  49. package/framework/hooks/shared/gate-question.js +1 -7
  50. package/framework/hooks/shared/git-exclude.js +3 -20
  51. package/framework/hooks/shared/git-paths.js +8 -104
  52. package/framework/hooks/shared/graph-freshness.js +22 -166
  53. package/framework/hooks/shared/graph-refresh.js +21 -149
  54. package/framework/hooks/shared/heavy-command.js +4 -12
  55. package/framework/hooks/shared/hook-response.js +8 -83
  56. package/framework/hooks/shared/morph-cli.js +8 -52
  57. package/framework/hooks/shared/payload-utils.js +10 -132
  58. package/framework/hooks/shared/phase-utils.js +1 -42
  59. package/framework/hooks/shared/redact-secrets.js +4 -15
  60. package/framework/hooks/shared/reparse-scan.js +6 -66
  61. package/framework/hooks/shared/slice-progress.js +4 -23
  62. package/framework/hooks/shared/source-files.js +4 -17
  63. package/framework/hooks/shared/state-reader.js +13 -183
  64. package/framework/hooks/shared/state-writer.js +8 -52
  65. package/framework/hooks/shared/stdin-reader.js +1 -11
  66. package/framework/hooks/shared/telemetry-logger.js +7 -67
  67. package/framework/hooks/shared/transcript-usage.js +7 -29
  68. package/framework/hooks/shared/worktree-root.js +7 -29
  69. package/framework/rules/csharp-standards.md +1 -1
  70. package/framework/rules/frontend-standards.md +1 -0
  71. package/framework/rules/hangfire-standards.md +1 -1
  72. package/framework/rules/neon-standards.md +1 -1
  73. package/framework/rules/nextjs-standards.md +1 -1
  74. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  75. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  76. package/framework/skills/core/morph-apply/SKILL.md +4 -4
  77. package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
  78. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  79. package/framework/skills/core/morph-media/SKILL.md +1 -1
  80. package/framework/skills/core/morph-plan/SKILL.md +9 -7
  81. package/framework/skills/core/morph-review/SKILL.md +9 -6
  82. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  83. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  84. package/framework/standards/STANDARDS.json +36 -14
  85. package/framework/standards/STANDARDS.meta.json +16 -2
  86. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  87. package/framework/standards/ai-agents/agent-session.md +1 -1
  88. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  89. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  90. package/framework/standards/ai-agents/guardrails.md +1 -1
  91. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  92. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  93. package/framework/standards/ai-agents/production.md +1 -1
  94. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  95. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  96. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  97. package/framework/standards/ai-agents/setup.md +2 -2
  98. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  99. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  100. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  101. package/framework/standards/ai-agents/workflows.md +1 -1
  102. package/framework/standards/architecture/code-comments.md +1 -1
  103. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  104. package/framework/standards/backend/api/validation.md +1 -1
  105. package/framework/standards/backend/dotnet/testing.md +10 -4
  106. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  107. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  108. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  109. package/framework/standards/data/nosql/cache/redis.md +1 -1
  110. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  111. package/framework/standards/frontend/admin-app/forms.md +1 -1
  112. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  113. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  114. package/framework/standards/frontend/nextjs/testing.md +10 -6
  115. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  116. package/framework/templates/.idea/morph-templates.xml +92 -0
  117. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  118. package/framework/templates/CLAUDE.md.core.template +3 -3
  119. package/framework/templates/IDE-SNIPPETS.md +262 -262
  120. package/framework/templates/REGISTRY.json +9 -9
  121. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  122. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  123. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  124. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  125. package/framework/templates/docs/plan.md +3 -1
  126. package/framework/templates/docs/ui-mockups.md +4 -0
  127. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  128. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  129. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  130. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  131. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  132. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  133. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  134. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  135. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  136. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  137. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  138. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  139. package/framework/templates/state.template.json +17 -17
  140. package/framework/templates/ui/design-system.css +226 -226
  141. package/package.json +1 -1
  142. package/src/commands/context/context-refresh.js +0 -16
  143. package/src/commands/dashboard/dashboard.js +2 -8
  144. package/src/commands/e2e/e2e.js +17 -104
  145. package/src/commands/graph/affected.js +0 -14
  146. package/src/commands/graph/build.js +11 -50
  147. package/src/commands/graph/explain.js +0 -16
  148. package/src/commands/graph/status.js +1 -12
  149. package/src/commands/mcp/mcp-setup.js +4 -62
  150. package/src/commands/observability/cost.js +5 -57
  151. package/src/commands/observability/telemetry.js +1 -14
  152. package/src/commands/project/branch.js +4 -45
  153. package/src/commands/project/claim.js +4 -48
  154. package/src/commands/project/doctor.js +48 -302
  155. package/src/commands/project/env.js +1 -27
  156. package/src/commands/project/fleet.js +3 -32
  157. package/src/commands/project/init.js +4 -68
  158. package/src/commands/project/install-plugin-cmd.js +0 -1
  159. package/src/commands/project/standards.js +1 -62
  160. package/src/commands/project/status.js +0 -53
  161. package/src/commands/project/update.js +31 -239
  162. package/src/commands/project/worktree.js +108 -882
  163. package/src/commands/scaffold/scaffold.js +0 -25
  164. package/src/commands/state/advance.js +14 -85
  165. package/src/commands/state/approve.js +15 -97
  166. package/src/commands/state/archive.js +12 -64
  167. package/src/commands/state/auto-approval.js +2 -5
  168. package/src/commands/state/create.js +22 -109
  169. package/src/commands/state/delete.js +11 -63
  170. package/src/commands/state/finish.js +98 -475
  171. package/src/commands/state/gate-check.js +22 -132
  172. package/src/commands/state/gate-decision.js +3 -39
  173. package/src/commands/state/handoff.js +3 -30
  174. package/src/commands/state/list.js +14 -84
  175. package/src/commands/state/report-check.js +8 -58
  176. package/src/commands/state/retype.js +4 -26
  177. package/src/commands/validation/index.js +0 -3
  178. package/src/commands/validation/mutate.js +11 -11
  179. package/src/commands/validation/plan-check.js +4 -2
  180. package/src/commands/validation/tests-audit.js +7 -0
  181. package/src/commands/validation/validate-feature.js +10 -97
  182. package/src/commands/validation/verify.js +30 -113
  183. package/src/core/paths/output-schema.js +2 -101
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +0 -27
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +22 -304
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +0 -1
  191. package/src/core/work-types.js +1 -36
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  194. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +1 -5
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/main.jsx +13 -13
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  211. package/src/dashboard/app/pages/Features.jsx +1 -8
  212. package/src/dashboard/app/pages/Guide.jsx +0 -1
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +10 -69
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +0 -6
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +10 -66
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +2 -6
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/dashboard/vite.config.js +24 -24
  233. package/src/lib/ai/ai-checks.js +7 -52
  234. package/src/lib/ai/ai-project-scan.js +14 -108
  235. package/src/lib/ai/code-audit.js +7 -63
  236. package/src/lib/ai/pin-drift.js +8 -51
  237. package/src/lib/ai/registry-audit.js +11 -90
  238. package/src/lib/ci/ci-verify.js +4 -19
  239. package/src/lib/ci/github-ci.js +9 -37
  240. package/src/lib/ci/workflow-scaffold.js +14 -10
  241. package/src/lib/detectors/claude-config-detector.js +13 -74
  242. package/src/lib/detectors/index.js +4 -50
  243. package/src/lib/e2e/applicability.js +37 -0
  244. package/src/lib/e2e/compose-env.js +5 -66
  245. package/src/lib/e2e/stack-teardown.js +6 -40
  246. package/src/lib/generators/architecture-scanner.js +18 -156
  247. package/src/lib/generators/handoff-generator.js +9 -53
  248. package/src/lib/generators/scaffold-generator.js +5 -101
  249. package/src/lib/generators/settings-generator.js +4 -37
  250. package/src/lib/git/branch-classifier.js +11 -84
  251. package/src/lib/git/default-branch.js +14 -169
  252. package/src/lib/git/evidence-staleness.js +1 -0
  253. package/src/lib/git/gh.js +2 -17
  254. package/src/lib/git/pending-prs.js +6 -37
  255. package/src/lib/git/pr-files.js +3 -28
  256. package/src/lib/git/pr-index.js +3 -34
  257. package/src/lib/git/tree-fingerprint.js +3 -15
  258. package/src/lib/git/tree-state.js +6 -33
  259. package/src/lib/graph/god-nodes.js +2 -22
  260. package/src/lib/graph/graph-health.js +3 -24
  261. package/src/lib/graph/graph-loader.js +5 -56
  262. package/src/lib/graph/graphify-runtime.js +7 -37
  263. package/src/lib/graph/graphifyignore.js +8 -74
  264. package/src/lib/graph/sanitize.js +16 -136
  265. package/src/lib/heavy/cleanup.js +1 -1
  266. package/src/lib/installers/effects.js +8 -93
  267. package/src/lib/installers/mcp-installer.js +9 -169
  268. package/src/lib/installers/memory-installer.js +4 -27
  269. package/src/lib/installers/templates-reconcile.js +5 -60
  270. package/src/lib/mutation/adapters.js +6 -17
  271. package/src/lib/mutation/dotnet-introspection.js +20 -106
  272. package/src/lib/mutation/runner.js +23 -75
  273. package/src/lib/mutation/subset.js +1 -4
  274. package/src/lib/mutation/trx.js +0 -1
  275. package/src/lib/plan/artifact-vocabulary.js +5 -25
  276. package/src/lib/plan/checks.js +34 -33
  277. package/src/lib/plan/plan-parser.js +7 -8
  278. package/src/lib/projects/registry.js +7 -44
  279. package/src/lib/review-reports.js +11 -26
  280. package/src/lib/risk-detector.js +41 -173
  281. package/src/lib/specs/adr-index.js +6 -34
  282. package/src/lib/specs/frontmatter.js +2 -15
  283. package/src/lib/specs/gfm-table.js +1 -7
  284. package/src/lib/specs/revocations.js +1 -14
  285. package/src/lib/specs/screen-checklist.js +47 -124
  286. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +1 -8
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +1 -7
  291. package/src/lib/standards/sections.js +2 -13
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +21 -169
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -240
  296. package/src/lib/tasks/test-runner.js +43 -341
  297. package/src/lib/telemetry/metrics.js +7 -36
  298. package/src/lib/telemetry/telemetry-reader.js +13 -125
  299. package/src/lib/telemetry/verify-event.js +2 -30
  300. package/src/lib/tests-audit/audit.js +96 -0
  301. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  302. package/src/lib/validators/code-budget/index.js +0 -7
  303. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  304. package/src/lib/validators/contracts/index.js +0 -6
  305. package/src/lib/validators/nextjs/index.js +0 -5
  306. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  307. package/src/lib/validators/packages/index.js +0 -6
  308. package/src/lib/validators/packages/package-validator.js +6 -56
  309. package/src/lib/validators/reuse/index.js +0 -8
  310. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  311. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  312. package/src/lib/validators/traceability-check.js +5 -63
  313. package/src/lib/validators/validation-runner.js +21 -158
  314. package/src/lib/validators/verify-runner.js +85 -376
  315. package/src/lib/work-type-detector.js +5 -67
  316. package/src/lib/worktree/docker-audit.js +17 -136
  317. package/src/lib/worktree/env-allocator.js +10 -98
  318. package/src/lib/worktree/feature-truth.js +12 -115
  319. package/src/lib/worktree/finish-backup.js +32 -242
  320. package/src/lib/worktree/fleet-scanner.js +13 -95
  321. package/src/lib/worktree/ignored-triage.js +15 -91
  322. package/src/lib/worktree/vscode-settings.js +2 -3
  323. package/src/lib/worktree/worktree-audit.js +21 -213
  324. package/src/lib/worktree/worktree-include.js +3 -5
  325. package/src/scripts/global-install.js +3 -9
  326. package/src/scripts/install-plugin.js +1 -13
  327. package/src/scripts/setup-infra.js +17 -147
  328. package/src/utils/agents-installer.js +29 -276
  329. package/src/utils/banner.js +4 -25
  330. package/src/utils/claude-md-injector.js +1 -37
  331. package/src/utils/claude-settings-manager.js +1 -52
  332. package/src/utils/file-copier.js +11 -95
  333. package/src/utils/hooks-installer.js +21 -298
  334. package/src/utils/skills-installer.js +4 -101
  335. package/src/utils/version-checker.js +4 -83
  336. package/framework/commands/morph-proposal.md +0 -9
  337. package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
  338. package/framework/hooks/git/pre-commit/agents.sh +0 -25
  339. package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
  340. package/framework/hooks/git/pre-push/run-tests.sh +0 -44
  341. package/src/commands/utils/index.js +0 -3
  342. package/src/lib/detectors/design-system-detector.js +0 -188
  343. package/src/lib/generators/design-system-generator.js +0 -508
  344. package/src/lib/specs/old-format.js +0 -68
  345. 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
- * Resolve the repository's default branch NAME (e.g. `main`), never assuming
30
- * `master`. Strategy, in order:
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
- * The brand-new UNTRACKED files, by NAME — the listing behind
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
- * `git diff --shortstat <baseRef>` against the CURRENT WORKING TREE (tracked
180
- * edits — staged, unstaged, and everything committed since baseRef —
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
- * Every path this working tree CHANGED against `baseRef` — tracked edits plus
218
- * brand-new untracked files.
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
- * The lines this working tree ADDED against `baseRef`, per file: the new-side
266
- * line numbers of `git diff -U0`, plus every brand-new untracked file whole
267
- * (the same probe as {@link changedPathsAgainst}).
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
- * gh.js — como o `gh` é invocado, num lugar só.
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
- * pending-prs.js — as features que existem SÓ como PR aberto.
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
- * As features aguardando merge: PR OPEN numa branch `morph/*` cujo slug não
50
- * tem pasta nesta árvore.
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);
@@ -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
- * @param {string} root - de onde o `gh` roda (resolve o repositório pelo origin)
31
- * @param {number} number - número do PR
32
- * @param {string} feature - o `{f}` de `morph/{f}`
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;
@@ -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
- * Build the branch → PR index for a repository.
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 = DESCONHECIDO (sem gh / não é GitHub / falhou), não "sem PRs"
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
- * A hash of what a verify run reads: the committed tree, the uncommitted changes and the
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
- * What the tree is before a dispatch whose work must leave it unchanged (the
53
- * verifier mutates and reverts): HEAD plus the blob of every file that
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
- * Files whose content on disk is not what it was in the snapshot. Content,
68
- * not status: a mutation committed since shows up, and a file committed
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
- /** `git status --porcelain -z`: a rename carries its old path as an extra entry. */
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
- * God nodes e comunidades para o contexto de arquitetura.
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;