@polymorphism-tech/morph-spec 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +4 -62
  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 +1 -1
  9. package/framework/MORPH.md +1 -1
  10. package/framework/agents.json +12 -4
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +6 -17
  14. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  15. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  16. package/framework/hooks/claude-code/core/chain.js +4 -65
  17. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  18. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  19. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  20. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  21. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  22. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  23. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  24. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  25. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  26. package/framework/hooks/claude-code/core/state-sync.js +11 -67
  27. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  29. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  30. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  31. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  32. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  33. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  34. package/framework/hooks/shared/activity-logger.js +9 -89
  35. package/framework/hooks/shared/agent-reap.js +2 -17
  36. package/framework/hooks/shared/build-check.js +6 -75
  37. package/framework/hooks/shared/claim-registry.js +9 -119
  38. package/framework/hooks/shared/command-tokens.js +6 -51
  39. package/framework/hooks/shared/comment-rules.js +5 -20
  40. package/framework/hooks/shared/comment-scanner.js +6 -25
  41. package/framework/hooks/shared/comment-stripper.js +11 -85
  42. package/framework/hooks/shared/e2e-check.js +0 -0
  43. package/framework/hooks/shared/errata.js +2 -15
  44. package/framework/hooks/shared/feature-locator.js +11 -95
  45. package/framework/hooks/shared/frontend-locations.js +1 -3
  46. package/framework/hooks/shared/gate-question.js +1 -7
  47. package/framework/hooks/shared/git-exclude.js +3 -20
  48. package/framework/hooks/shared/git-paths.js +8 -104
  49. package/framework/hooks/shared/graph-freshness.js +22 -166
  50. package/framework/hooks/shared/graph-refresh.js +21 -149
  51. package/framework/hooks/shared/heavy-command.js +4 -12
  52. package/framework/hooks/shared/hook-response.js +8 -83
  53. package/framework/hooks/shared/morph-cli.js +8 -52
  54. package/framework/hooks/shared/payload-utils.js +10 -132
  55. package/framework/hooks/shared/phase-utils.js +1 -42
  56. package/framework/hooks/shared/redact-secrets.js +4 -15
  57. package/framework/hooks/shared/reparse-scan.js +6 -66
  58. package/framework/hooks/shared/slice-progress.js +4 -23
  59. package/framework/hooks/shared/source-files.js +4 -17
  60. package/framework/hooks/shared/state-reader.js +13 -183
  61. package/framework/hooks/shared/state-writer.js +8 -52
  62. package/framework/hooks/shared/stdin-reader.js +1 -11
  63. package/framework/hooks/shared/telemetry-logger.js +7 -67
  64. package/framework/hooks/shared/transcript-usage.js +7 -29
  65. package/framework/hooks/shared/worktree-root.js +7 -29
  66. package/framework/rules/csharp-standards.md +1 -1
  67. package/framework/rules/frontend-standards.md +1 -0
  68. package/framework/rules/hangfire-standards.md +1 -1
  69. package/framework/rules/neon-standards.md +1 -1
  70. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  71. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  72. package/framework/skills/core/morph-apply/SKILL.md +2 -2
  73. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  74. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  75. package/framework/skills/core/morph-media/SKILL.md +1 -1
  76. package/framework/skills/core/morph-plan/SKILL.md +2 -2
  77. package/framework/skills/core/morph-review/SKILL.md +1 -1
  78. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  79. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  80. package/framework/standards/STANDARDS.json +34 -12
  81. package/framework/standards/STANDARDS.meta.json +16 -2
  82. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  83. package/framework/standards/ai-agents/agent-session.md +1 -1
  84. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  85. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  86. package/framework/standards/ai-agents/guardrails.md +1 -1
  87. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  88. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  89. package/framework/standards/ai-agents/production.md +1 -1
  90. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  91. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  92. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  93. package/framework/standards/ai-agents/setup.md +2 -2
  94. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  95. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  96. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  97. package/framework/standards/ai-agents/workflows.md +1 -1
  98. package/framework/standards/architecture/code-comments.md +1 -1
  99. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  100. package/framework/standards/backend/api/validation.md +1 -1
  101. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  102. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  103. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  104. package/framework/standards/data/nosql/cache/redis.md +1 -1
  105. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  106. package/framework/standards/frontend/admin-app/forms.md +1 -1
  107. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  108. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  109. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  110. package/framework/templates/CLAUDE.md.core.template +3 -3
  111. package/framework/templates/REGISTRY.json +9 -9
  112. package/framework/templates/docs/ui-mockups.md +4 -0
  113. package/package.json +1 -1
  114. package/src/commands/context/context-refresh.js +0 -16
  115. package/src/commands/dashboard/dashboard.js +2 -8
  116. package/src/commands/e2e/e2e.js +17 -104
  117. package/src/commands/graph/affected.js +0 -14
  118. package/src/commands/graph/build.js +11 -50
  119. package/src/commands/graph/explain.js +0 -16
  120. package/src/commands/graph/status.js +1 -12
  121. package/src/commands/mcp/mcp-setup.js +4 -62
  122. package/src/commands/observability/cost.js +5 -57
  123. package/src/commands/observability/telemetry.js +1 -14
  124. package/src/commands/project/branch.js +4 -45
  125. package/src/commands/project/claim.js +4 -48
  126. package/src/commands/project/doctor.js +48 -302
  127. package/src/commands/project/env.js +1 -27
  128. package/src/commands/project/fleet.js +3 -32
  129. package/src/commands/project/init.js +4 -68
  130. package/src/commands/project/install-plugin-cmd.js +0 -1
  131. package/src/commands/project/standards.js +1 -62
  132. package/src/commands/project/status.js +0 -53
  133. package/src/commands/project/update.js +31 -239
  134. package/src/commands/project/worktree.js +108 -882
  135. package/src/commands/scaffold/scaffold.js +0 -25
  136. package/src/commands/state/advance.js +14 -85
  137. package/src/commands/state/approve.js +14 -96
  138. package/src/commands/state/archive.js +12 -64
  139. package/src/commands/state/auto-approval.js +2 -5
  140. package/src/commands/state/create.js +22 -109
  141. package/src/commands/state/delete.js +11 -63
  142. package/src/commands/state/finish.js +98 -475
  143. package/src/commands/state/gate-check.js +22 -132
  144. package/src/commands/state/gate-decision.js +3 -39
  145. package/src/commands/state/handoff.js +3 -30
  146. package/src/commands/state/list.js +14 -84
  147. package/src/commands/state/report-check.js +8 -58
  148. package/src/commands/state/retype.js +4 -26
  149. package/src/commands/validation/index.js +0 -3
  150. package/src/commands/validation/mutate.js +2 -5
  151. package/src/commands/validation/validate-feature.js +5 -94
  152. package/src/commands/validation/verify.js +15 -98
  153. package/src/core/paths/output-schema.js +2 -101
  154. package/src/core/state/feature-identity.js +1 -25
  155. package/src/core/state/gate-order.js +0 -27
  156. package/src/core/state/lifecycle-audit.js +8 -100
  157. package/src/core/state/state-manager.js +22 -304
  158. package/src/core/templates/template-registry.js +2 -97
  159. package/src/core/templates/template-renderer.js +13 -93
  160. package/src/core/templates/template-seed.js +0 -1
  161. package/src/core/work-types.js +1 -36
  162. package/src/dashboard/app/App.jsx +1 -8
  163. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  164. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  165. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  166. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  167. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  168. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  169. package/src/dashboard/app/components/Markdown.jsx +0 -9
  170. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  171. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  172. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  173. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  174. package/src/dashboard/app/hooks/usePref.js +1 -5
  175. package/src/dashboard/app/lib/format.js +0 -2
  176. package/src/dashboard/app/lib/phases.js +1 -5
  177. package/src/dashboard/app/lib/project.js +0 -12
  178. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  179. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  180. package/src/dashboard/app/pages/Features.jsx +1 -8
  181. package/src/dashboard/app/pages/Guide.jsx +0 -1
  182. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  183. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  184. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  185. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  186. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  187. package/src/dashboard/app/pages/Standards.jsx +2 -17
  188. package/src/dashboard/scanner/activity-scanner.js +10 -69
  189. package/src/dashboard/scanner/agents-scanner.js +1 -23
  190. package/src/dashboard/scanner/docs-scanner.js +0 -6
  191. package/src/dashboard/scanner/feature-status.js +2 -5
  192. package/src/dashboard/scanner/features-scanner.js +10 -66
  193. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  194. package/src/dashboard/scanner/index.js +0 -4
  195. package/src/dashboard/scanner/portfolio.js +5 -29
  196. package/src/dashboard/scanner/project-api.js +8 -54
  197. package/src/dashboard/scanner/rules-scanner.js +1 -2
  198. package/src/dashboard/scanner/skills-scanner.js +2 -12
  199. package/src/dashboard/scanner/standards-scanner.js +2 -6
  200. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  201. package/src/lib/ai/ai-checks.js +7 -52
  202. package/src/lib/ai/ai-project-scan.js +14 -108
  203. package/src/lib/ai/code-audit.js +7 -63
  204. package/src/lib/ai/pin-drift.js +8 -51
  205. package/src/lib/ai/registry-audit.js +11 -90
  206. package/src/lib/ci/ci-verify.js +4 -19
  207. package/src/lib/ci/github-ci.js +9 -37
  208. package/src/lib/ci/workflow-scaffold.js +2 -10
  209. package/src/lib/detectors/claude-config-detector.js +13 -74
  210. package/src/lib/detectors/index.js +4 -50
  211. package/src/lib/e2e/compose-env.js +5 -66
  212. package/src/lib/e2e/stack-teardown.js +6 -40
  213. package/src/lib/generators/architecture-scanner.js +18 -156
  214. package/src/lib/generators/handoff-generator.js +9 -53
  215. package/src/lib/generators/scaffold-generator.js +5 -101
  216. package/src/lib/generators/settings-generator.js +4 -37
  217. package/src/lib/git/branch-classifier.js +11 -84
  218. package/src/lib/git/default-branch.js +14 -169
  219. package/src/lib/git/evidence-staleness.js +1 -0
  220. package/src/lib/git/gh.js +2 -17
  221. package/src/lib/git/pending-prs.js +6 -37
  222. package/src/lib/git/pr-files.js +3 -28
  223. package/src/lib/git/pr-index.js +3 -34
  224. package/src/lib/git/tree-fingerprint.js +3 -15
  225. package/src/lib/git/tree-state.js +6 -33
  226. package/src/lib/graph/god-nodes.js +2 -22
  227. package/src/lib/graph/graph-health.js +3 -24
  228. package/src/lib/graph/graph-loader.js +5 -56
  229. package/src/lib/graph/graphify-runtime.js +7 -37
  230. package/src/lib/graph/graphifyignore.js +8 -74
  231. package/src/lib/graph/sanitize.js +16 -136
  232. package/src/lib/heavy/cleanup.js +1 -1
  233. package/src/lib/installers/effects.js +8 -93
  234. package/src/lib/installers/mcp-installer.js +9 -169
  235. package/src/lib/installers/memory-installer.js +4 -27
  236. package/src/lib/installers/templates-reconcile.js +5 -60
  237. package/src/lib/mutation/dotnet-introspection.js +20 -106
  238. package/src/lib/mutation/subset.js +1 -4
  239. package/src/lib/plan/artifact-vocabulary.js +5 -25
  240. package/src/lib/plan/checks.js +5 -31
  241. package/src/lib/plan/plan-parser.js +1 -6
  242. package/src/lib/projects/registry.js +7 -44
  243. package/src/lib/review-reports.js +5 -23
  244. package/src/lib/risk-detector.js +39 -173
  245. package/src/lib/specs/adr-index.js +6 -34
  246. package/src/lib/specs/frontmatter.js +2 -15
  247. package/src/lib/specs/gfm-table.js +1 -7
  248. package/src/lib/specs/revocations.js +1 -14
  249. package/src/lib/specs/screen-checklist.js +47 -124
  250. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  251. package/src/lib/stack/stack-profile.js +1 -28
  252. package/src/lib/stack/stack-scan.js +1 -8
  253. package/src/lib/stack-filter.js +4 -27
  254. package/src/lib/standards/registry.js +1 -7
  255. package/src/lib/standards/sections.js +2 -13
  256. package/src/lib/standards/verified-against.js +19 -191
  257. package/src/lib/tasks/eval-node.js +21 -169
  258. package/src/lib/tasks/linux-runner.js +18 -142
  259. package/src/lib/tasks/test-nodes.js +43 -240
  260. package/src/lib/tasks/test-runner.js +43 -341
  261. package/src/lib/telemetry/metrics.js +7 -36
  262. package/src/lib/telemetry/telemetry-reader.js +13 -125
  263. package/src/lib/telemetry/verify-event.js +2 -30
  264. package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
  265. package/src/lib/validators/code-budget/index.js +0 -7
  266. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  267. package/src/lib/validators/contracts/index.js +0 -6
  268. package/src/lib/validators/nextjs/index.js +0 -5
  269. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  270. package/src/lib/validators/packages/index.js +0 -6
  271. package/src/lib/validators/packages/package-validator.js +6 -56
  272. package/src/lib/validators/reuse/index.js +0 -8
  273. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  274. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  275. package/src/lib/validators/traceability-check.js +5 -63
  276. package/src/lib/validators/validation-runner.js +20 -125
  277. package/src/lib/validators/verify-runner.js +52 -358
  278. package/src/lib/work-type-detector.js +5 -67
  279. package/src/lib/worktree/docker-audit.js +17 -136
  280. package/src/lib/worktree/env-allocator.js +10 -98
  281. package/src/lib/worktree/feature-truth.js +12 -115
  282. package/src/lib/worktree/finish-backup.js +32 -242
  283. package/src/lib/worktree/fleet-scanner.js +13 -95
  284. package/src/lib/worktree/ignored-triage.js +15 -91
  285. package/src/lib/worktree/vscode-settings.js +2 -3
  286. package/src/lib/worktree/worktree-audit.js +21 -213
  287. package/src/lib/worktree/worktree-include.js +3 -5
  288. package/src/scripts/global-install.js +3 -9
  289. package/src/scripts/install-plugin.js +1 -13
  290. package/src/scripts/setup-infra.js +17 -147
  291. package/src/utils/agents-installer.js +28 -275
  292. package/src/utils/banner.js +4 -25
  293. package/src/utils/claude-md-injector.js +1 -37
  294. package/src/utils/claude-settings-manager.js +1 -52
  295. package/src/utils/file-copier.js +11 -95
  296. package/src/utils/hooks-installer.js +21 -298
  297. package/src/utils/skills-installer.js +4 -101
  298. package/src/utils/version-checker.js +4 -83
  299. package/framework/commands/morph-proposal.md +0 -9
  300. package/src/commands/utils/index.js +0 -3
  301. package/src/lib/detectors/design-system-detector.js +0 -188
  302. package/src/lib/generators/design-system-generator.js +0 -508
  303. package/src/lib/specs/old-format.js +0 -68
  304. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,47 +1,10 @@
1
- /**
2
- * git-paths.js — pure-filesystem git common-dir resolution (no subprocess).
3
- *
4
- * `telemetry-log.js`'s claim heartbeat used to pay `execSync('git rev-parse
5
- * --git-common-dir')` on every tool call to learn where `claims.json` lives —
6
- * a ~34ms subprocess spawn, wasted on ~99% of calls because the heartbeat
7
- * write itself is throttled (see `claim-registry.js`'s `shouldHeartbeat`).
8
- * This module answers the SAME question — mirroring `git rev-parse
9
- * --git-common-dir` — by reading the two on-disk shapes git itself uses,
10
- * without spawning `git` at all:
11
- *
12
- * 1. PRIMARY checkout: `<root>/.git` is a DIRECTORY. That directory IS the
13
- * common dir. (A directory `.git` is never a linked worktree's own —
14
- * git always represents a worktree's `.git` as a FILE — so this case is
15
- * unambiguous.)
16
- * 2. LINKED WORKTREE (`git worktree add`): `<root>/.git` is a FILE
17
- * containing `gitdir: <path>`, pointing at this worktree's PRIVATE
18
- * gitdir (normally `<primary>/.git/worktrees/<name>`). Inside that
19
- * private gitdir, a `commondir` file holds a path — relative to the
20
- * private gitdir, or absolute — to the real common dir (normally
21
- * `../..`, i.e. back up to `<primary>/.git`).
22
- *
23
- * Verified against a real `git worktree add` fixture: `commondir` held
24
- * `../..`, `.git`'s `gitdir:` line held an absolute path, and the resolved
25
- * result matched `git rev-parse --git-common-dir` exactly (see
26
- * `test/hooks/git-paths.test.js`, which asserts that parity directly rather
27
- * than against a hand-computed path).
28
- *
29
- * Fail-open: any missing file, unreadable path, or shape that doesn't match
30
- * either case above resolves to `null`. Callers must treat `null` as "not
31
- * resolvable right now" and skip whatever git-dependent step they were
32
- * guarding — never throw.
33
- */
1
+ // Answers `git rev-parse` questions from the on-disk shapes git uses, without spawning git:
2
+ // a primary checkout's `.git` is a DIRECTORY, a linked worktree's is a FILE holding `gitdir:`.
3
+ // Every unreadable or unknown shape resolves to `null`, never a throw.
34
4
 
35
5
  import { readFileSync, statSync } from 'fs';
36
6
  import { dirname, isAbsolute, join, resolve } from 'path';
37
7
 
38
- /**
39
- * Find the nearest `.git` entry (file or directory) at or above `startDir`,
40
- * climbing exactly like git itself does to find the repo root.
41
- *
42
- * @param {string} startDir
43
- * @returns {{ path: string, isDirectory: boolean }|null}
44
- */
45
8
  function findDotGit(startDir) {
46
9
  let dir = resolve(startDir);
47
10
  for (;;) {
@@ -50,21 +13,13 @@ function findDotGit(startDir) {
50
13
  const st = statSync(candidate);
51
14
  return { path: candidate, isDirectory: st.isDirectory() };
52
15
  } catch {
53
- // not here — keep climbing
54
16
  }
55
17
  const parent = dirname(dir);
56
- if (parent === dir) return null; // reached the filesystem root
18
+ if (parent === dir) return null;
57
19
  dir = parent;
58
20
  }
59
21
  }
60
22
 
61
- /**
62
- * A linked worktree's `.git` FILE holds `gitdir: <path>` (absolute or relative
63
- * to the file) pointing at its private gitdir. `null` when the line is absent.
64
- *
65
- * @param {string} dotGitFile
66
- * @returns {string|null}
67
- */
68
23
  function privateGitDirOf(dotGitFile) {
69
24
  const match = readFileSync(dotGitFile, 'utf8').match(/^gitdir:\s*(.+?)\s*$/m);
70
25
  if (!match) return null;
@@ -73,15 +28,7 @@ function privateGitDirOf(dotGitFile) {
73
28
 
74
29
  const MORPH_BRANCH = /^ref:\s*refs\/heads\/morph\/([A-Za-z0-9._-]+)\s*$/;
75
30
 
76
- /**
77
- * The feature a checkout is bound to by its branch `morph/<feature>`, read from
78
- * `<root>/.git` exactly (no climbing) — the primary's `.git/HEAD` or, for a
79
- * linked worktree, `<gitdir>/HEAD`. `null` for any other branch (`morph-task/*`
80
- * included), a detached HEAD, or no `.git` at `root`.
81
- *
82
- * @param {string} root
83
- * @returns {string|null}
84
- */
31
+ /** Reads `<root>/.git` exactly, without climbing; `null` for any branch other than `morph/<feature>`. */
85
32
  export function morphBranchFeature(root) {
86
33
  try {
87
34
  const dotGit = join(root, '.git');
@@ -94,22 +41,7 @@ export function morphBranchFeature(root) {
94
41
  }
95
42
  }
96
43
 
97
- /**
98
- * Is `startDir` inside a LINKED (secondary) worktree?
99
- *
100
- * Same on-disk fact `resolveGitCommonDir` reads: git represents a linked
101
- * worktree's `.git` as a FILE holding a `gitdir:` pointer, and a primary
102
- * checkout's `.git` as a DIRECTORY. The e2e isolation rule keys on this —
103
- * a compose that cannot be isolated is a hard failure only where a second
104
- * stack could actually collide with the primary's (see e2e-check.js
105
- * `decideIsolation`).
106
- *
107
- * Fail-open: outside any repo, or on an unreadable path, answers `false` —
108
- * the lenient direction (warn instead of fail).
109
- *
110
- * @param {string} startDir
111
- * @returns {boolean}
112
- */
44
+ /** Outside any repo or on an unreadable path, answers `false`: the lenient direction for e2e isolation. */
113
45
  export function isSecondaryWorktree(startDir) {
114
46
  try {
115
47
  const found = findDotGit(startDir);
@@ -119,14 +51,6 @@ export function isSecondaryWorktree(startDir) {
119
51
  }
120
52
  }
121
53
 
122
- /**
123
- * Top of the working tree that contains `startDir` — the directory holding the
124
- * nearest `.git` (file or directory), which is what `git rev-parse
125
- * --show-toplevel` answers, without the subprocess. `null` outside any repo.
126
- *
127
- * @param {string} startDir
128
- * @returns {string|null}
129
- */
130
54
  export function workTreeTop(startDir) {
131
55
  try {
132
56
  const found = findDotGit(startDir);
@@ -136,16 +60,7 @@ export function workTreeTop(startDir) {
136
60
  }
137
61
  }
138
62
 
139
- /**
140
- * Resolve the git COMMON dir for `startDir` — pure filesystem, no subprocess.
141
- * Always returns an absolute path (unlike `git rev-parse --git-common-dir`,
142
- * which returns a path relative to the CALLER's cwd when run from the
143
- * primary checkout — callers of this function never need their own
144
- * `resolve(cwd, ...)` step afterward).
145
- *
146
- * @param {string} startDir
147
- * @returns {string|null}
148
- */
63
+ /** Always absolute, unlike `git rev-parse --git-common-dir`, which is relative to the caller's cwd in the primary. */
149
64
  export function resolveGitCommonDir(startDir) {
150
65
  try {
151
66
  const found = findDotGit(startDir);
@@ -158,8 +73,6 @@ export function resolveGitCommonDir(startDir) {
158
73
  const privateGitDir = privateGitDirOf(found.path);
159
74
  if (!privateGitDir) return null;
160
75
 
161
- // Inside the private gitdir, `commondir` points (relative to itself, or
162
- // absolute) at the real common dir.
163
76
  const commonRaw = readFileSync(join(privateGitDir, 'commondir'), 'utf8').trim();
164
77
  if (!commonRaw) return null;
165
78
  return isAbsolute(commonRaw) ? commonRaw : resolve(privateGitDir, commonRaw);
@@ -170,15 +83,7 @@ export function resolveGitCommonDir(startDir) {
170
83
 
171
84
  const SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
172
85
 
173
- /**
174
- * The commit HEAD points at in the PRIMARY checkout `root`, read from disk —
175
- * `git rev-parse HEAD` without the subprocess: a detached SHA, or the branch's
176
- * loose ref, then `packed-refs`. `null` for every shape it does not read (a
177
- * linked worktree, reftable, a branch with no commit yet): the caller asks git.
178
- *
179
- * @param {string} root
180
- * @returns {string|null}
181
- */
86
+ /** `null` for every shape it does not read (linked worktree, reftable, unborn branch): the caller asks git. */
182
87
  export function readPrimaryHead(root) {
183
88
  try {
184
89
  const gitDir = join(root, '.git');
@@ -191,7 +96,6 @@ export function readPrimaryHead(root) {
191
96
  const loose = readFileSync(join(gitDir, ref), 'utf8').trim();
192
97
  if (SHA.test(loose)) return loose;
193
98
  } catch {
194
- // not loose — packed
195
99
  }
196
100
  for (const line of readFileSync(join(gitDir, 'packed-refs'), 'utf8').split(/\r?\n/)) {
197
101
  const [sha, name] = line.split(' ');
@@ -1,41 +1,6 @@
1
- /**
2
- * graph-freshness.js — a regra ÚNICA de "o grafo desta árvore está em dia".
3
- *
4
- * Todo mundo que pergunta lê daqui: o refresh em background
5
- * (graph-refresh.js, decide se roda), o `graph status|explain|affected` e o
6
- * `doctor` (via src/lib/graph/graph-loader.js `getFreshness`) e o
7
- * `graph build|refresh` (grava o resultado). Mora em hooks/shared porque hook
8
- * não importa `src/`. Uma segunda cópia da regra é como o status dizia
9
- * "fresco" enquanto o refresh achava "velho", ou o contrário.
10
- *
11
- * Duas fontes, nesta ordem:
12
- * 1. o carimbo `built_at_commit` do graph.json contra o HEAD;
13
- * 2. o REGISTRO da última rodada, por árvore, em
14
- * `<git-common-dir>/morph-spec/graph-refresh-state.json`:
15
- * `{ [árvore]: { head, exit, ignoreHash?, at, unavailable?, seeded? } }`.
16
- *
17
- * O registro existe porque o `graphify update` do 0.9.50 NÃO recarimba quando
18
- * a topologia não muda — "No code-graph topology changes detected; outputs
19
- * left untouched" (watch.py): commit só de `.morph/`, corpo de método editado,
20
- * commit num arquivo excluído. O carimbo fica no commit antigo e, sem o
21
- * registro, o grafo parecia velho para sempre e cada disparo rodava o graphify
22
- * de novo (~80 s no MORPH_GHLBrain). Rodada com exit 0 no HEAD H vale como
23
- * grafo em dia em H: o 0.9.50 sai 1 quando recusa escrever (`_check_shrink`,
24
- * grafo ilegível), e "untouched" sai 0.
25
- *
26
- * As regras do `.graphifyignore` entram por CONTEÚDO, nunca pelo relógio: o
27
- * registro guarda o hash das regras sob as quais o grafo EM DISCO foi
28
- * extraído (uma rodada que falhou não o muda), e um hash diferente do das
29
- * regras que valem hoje é grafo velho — numa worktree sem arquivo próprio,
30
- * as da raiz, que são as que o `graph refresh` usa ali. Comentário, linha
31
- * em branco, BOM e CRLF não contam — trocar o cabeçalho não pede refresh.
32
- * Grafo sem registro das regras (anterior a este estado) com regras valendo é
33
- * `rules-unknown`: um refresh grava o registro e encerra.
34
- *
35
- * Fail-open: nada aqui lança.
36
- *
37
- * @module graph-freshness
38
- */
1
+ // Regra ÚNICA de "grafo em dia". O registro da rodada existe porque o `graphify
2
+ // update` não recarimba `built_at_commit` quando a topologia não muda; as regras
3
+ // do ignore entram por CONTEÚDO, nunca pelo relógio. Fail-open: nada aqui lança.
39
4
 
40
5
  import { createHash } from 'crypto';
41
6
  import { execFileSync } from 'child_process';
@@ -50,19 +15,12 @@ export const GRAPH_DIR = 'graphify-out';
50
15
  export const GRAPHIFY_IGNORE = '.graphifyignore';
51
16
  const STATE_FILE = 'graph-refresh-state.json';
52
17
 
53
- /** @param {string} tree */
54
18
  export function graphFile(tree) {
55
19
  return join(tree, GRAPH_DIR, 'graph.json');
56
20
  }
57
21
 
58
- /**
59
- * `built_at_commit` do graph.json. O graphify o grava como a ÚLTIMA chave, então
60
- * a leitura do fim do arquivo evita parsear 18 MB; a leitura inteira fica de
61
- * reserva para qualquer outra forma.
62
- *
63
- * @param {string} tree
64
- * @returns {string|null}
65
- */
22
+ // O graphify grava `built_at_commit` como a ÚLTIMA chave: ler o fim evita parsear
23
+ // o grafo inteiro, que fica de reserva para outra forma.
66
24
  export function readBuiltAtCommit(tree) {
67
25
  const file = graphFile(tree);
68
26
  let fd;
@@ -86,10 +44,6 @@ export function readBuiltAtCommit(tree) {
86
44
  }
87
45
  }
88
46
 
89
- /**
90
- * @param {string} tree
91
- * @returns {string|null}
92
- */
93
47
  export function readHead(tree) {
94
48
  try {
95
49
  return execFileSync('git', ['rev-parse', 'HEAD'], {
@@ -100,14 +54,7 @@ export function readHead(tree) {
100
54
  }
101
55
  }
102
56
 
103
- /**
104
- * As REGRAS de um `.graphifyignore`, sem o que o graphify não lê: comentário,
105
- * linha em branco, espaço no fim, BOM e CRLF. A ordem fica — no gitignore ela
106
- * muda o resultado.
107
- *
108
- * @param {string} text
109
- * @returns {string}
110
- */
57
+ // A ordem fica: no gitignore ela muda o resultado.
111
58
  export function ignoreRules(text) {
112
59
  return String(text ?? '')
113
60
  .replace(/^\uFEFF/, '')
@@ -117,37 +64,22 @@ export function ignoreRules(text) {
117
64
  .join('\n');
118
65
  }
119
66
 
120
- /**
121
- * @param {string|null} text
122
- * @returns {string|null} null quando não há arquivo
123
- */
124
67
  export function rulesHash(text) {
125
68
  if (text === null || text === undefined) return null;
126
69
  return createHash('sha256').update(ignoreRules(text)).digest('hex').slice(0, 16);
127
70
  }
128
71
 
129
- /**
130
- * A árvore principal do repositório — a única que o refresh em background
131
- * toca. `null` fora de um repositório e quando o common dir não é o `.git` de
132
- * uma árvore (repositório bare, `--separate-git-dir`): aí não há raiz.
133
- *
134
- * @param {string} dir - qualquer diretório do repositório, raiz ou worktree
135
- * @returns {string|null}
136
- */
72
+ /** `null` também quando o common dir não é o `.git` de uma árvore (bare, `--separate-git-dir`). */
137
73
  export function primaryTree(dir) {
138
74
  const common = dir ? resolveGitCommonDir(dir) : null;
139
75
  if (!common || basename(common) !== '.git') return null;
140
76
  return dirname(common);
141
77
  }
142
78
 
143
- /**
144
- * 1ª linha do `.graphifyignore` que o `graph refresh` põe numa worktree só
145
- * durante a rodada. Comentário não muda as regras nem o hash, e é por ele que a
146
- * sobra de uma rodada cujo processo morreu no meio é reconhecida.
147
- */
79
+ // 1ª linha da cópia temporária: reconhece a sobra de uma rodada que morreu no
80
+ // meio sem mudar as regras nem o hash.
148
81
  export const TEMP_IGNORE_MARKER = '# morph-spec: cópia temporária do graph refresh, apagada ao fim da rodada';
149
82
 
150
- /** @param {string|null} text */
151
83
  export function isTemporaryIgnore(text) {
152
84
  return String(text ?? '').replace(/^/, '').split(/\r?\n/, 1)[0] === TEMP_IGNORE_MARKER;
153
85
  }
@@ -160,14 +92,8 @@ function readText(path) {
160
92
  }
161
93
  }
162
94
 
163
- /**
164
- * O `.graphifyignore` com que o graphify roda nesta árvore: o do checkout;
165
- * numa worktree sem um (ou só com a sobra temporária), o da raiz, que é o que o
166
- * `graph refresh` põe no lugar durante a rodada.
167
- *
168
- * @param {string} tree
169
- * @returns {string|null} null quando nenhum arquivo vale
170
- */
95
+ // Numa worktree sem arquivo próprio valem as regras da raiz: é o que o
96
+ // `graph refresh` põe no lugar durante a rodada.
171
97
  export function ignoreTextOf(tree) {
172
98
  const own = readText(join(tree, GRAPHIFY_IGNORE));
173
99
  if (own !== null && !isTemporaryIgnore(own)) return own;
@@ -177,37 +103,18 @@ export function ignoreTextOf(tree) {
177
103
  return inherited !== null && !isTemporaryIgnore(inherited) ? inherited : null;
178
104
  }
179
105
 
180
- /**
181
- * @param {string} tree
182
- * @returns {string|null} null quando nenhum `.graphifyignore` vale para a árvore
183
- */
184
106
  export function ignoreRulesHash(tree) {
185
107
  return rulesHash(ignoreTextOf(tree));
186
108
  }
187
109
 
188
- /**
189
- * As linhas de `git status --porcelain` de `tree` sem a da sobra do
190
- * `.graphifyignore` temporário: ela é só uma cópia das regras da raiz, e o
191
- * `?? .graphifyignore` que deixa fazia o `finish` e o
192
- * `worktree remove --if-clean` recusarem. Só lê — quem confere a árvore chama
193
- * isto antes das recusas, e `dropLeftoverIgnore` só depois delas.
194
- *
195
- * @param {string} tree
196
- * @param {string[]} lines
197
- * @returns {string[]}
198
- */
110
+ // Só lê: quem confere a árvore chama isto antes das recusas, e
111
+ // `dropLeftoverIgnore` só depois delas.
199
112
  export function withoutLeftoverIgnore(tree, lines) {
200
113
  const leftover = `?? ${GRAPHIFY_IGNORE}`;
201
114
  if (!lines.includes(leftover) || !isTemporaryIgnore(readText(join(tree, GRAPHIFY_IGNORE)))) return lines;
202
115
  return lines.filter((line) => line !== leftover);
203
116
  }
204
117
 
205
- /**
206
- * Apaga a sobra do `.graphifyignore` temporário, reconhecida pelo marcador.
207
- *
208
- * @param {string} tree
209
- * @returns {boolean} apagou
210
- */
211
118
  export function dropLeftoverIgnore(tree) {
212
119
  const path = join(tree, GRAPHIFY_IGNORE);
213
120
  if (!isTemporaryIgnore(readText(path))) return false;
@@ -219,21 +126,12 @@ export function dropLeftoverIgnore(tree) {
219
126
  }
220
127
  }
221
128
 
222
- /**
223
- * Chave de uma árvore no registro. No win32 o caminho não distingue caixa, e
224
- * `R:\x` e `r:\x` são a mesma árvore.
225
- *
226
- * @param {string} tree
227
- */
129
+ // No win32 o caminho não distingue caixa.
228
130
  export function treeKey(tree) {
229
131
  const path = resolve(tree);
230
132
  return process.platform === 'win32' ? path.toLowerCase() : path;
231
133
  }
232
134
 
233
- /**
234
- * @param {string} dir - qualquer diretório do repositório
235
- * @returns {string|null}
236
- */
237
135
  export function refreshStatePath(dir) {
238
136
  const common = dir ? resolveGitCommonDir(dir) : null;
239
137
  return common ? join(common, 'morph-spec', STATE_FILE) : null;
@@ -248,10 +146,7 @@ function readState(path) {
248
146
  }
249
147
  }
250
148
 
251
- /**
252
- * @param {string} tree
253
- * @returns {{head: string|null, exit: number|null, ignoreHash?: string|null, at: string, unavailable?: boolean, seeded?: boolean}|null}
254
- */
149
+ /** @returns {{head: string|null, exit: number|null, ignoreHash?: string|null, at: string, unavailable?: boolean, seeded?: boolean}|null} */
255
150
  export function readRefreshRecord(tree) {
256
151
  const path = refreshStatePath(tree);
257
152
  if (!path) return null;
@@ -263,15 +158,7 @@ function pause(ms) {
263
158
  try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* sem espera */ }
264
159
  }
265
160
 
266
- /**
267
- * Grava o registro de `tree` (substitui o anterior). Por tmp + rename: quem lê
268
- * no meio vê o arquivo velho ou o novo, nunca meio. As árvores que não existem
269
- * mais saem na mesma escrita — uma worktree removida não deixa lixo.
270
- *
271
- * @param {string} tree
272
- * @param {object} record
273
- * @returns {object|null} o registro gravado, ou null (sem git, disco)
274
- */
161
+ // As árvores que não existem mais saem na mesma escrita.
275
162
  export function writeRefreshRecord(tree, record) {
276
163
  const path = refreshStatePath(tree);
277
164
  if (!path) return null;
@@ -286,8 +173,7 @@ export function writeRefreshRecord(tree, record) {
286
173
  }
287
174
  state[key] = entry;
288
175
  writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
289
- // No win32 o rename sobre um arquivo aberto por outro leitor falha por um
290
- // instante (EPERM/EBUSY); o registro vale umas tentativas.
176
+ // No win32 o rename sobre um arquivo aberto por outro leitor falha por um instante.
291
177
  for (let attempt = 0; ; attempt++) {
292
178
  try {
293
179
  renameSync(tmp, path);
@@ -303,15 +189,8 @@ export function writeRefreshRecord(tree, record) {
303
189
  }
304
190
  }
305
191
 
306
- /**
307
- * Registra uma rodada do graphify sobre `tree`. As regras só mudam com exit 0:
308
- * uma rodada que falhou não mexeu no grafo, e o hash descreve o grafo em
309
- * disco, não a tentativa.
310
- *
311
- * @param {string} tree
312
- * @param {{head: string|null, exit: number|null, ignoreHash: string|null, unavailable?: boolean}} run
313
- * `head` e `ignoreHash` lidos ANTES da rodada — é o que o graphify leu
314
- */
192
+ // O hash das regras só muda com exit 0: descreve o grafo em disco, não a
193
+ // tentativa. `head` e `ignoreHash` são lidos ANTES da rodada.
315
194
  export function recordRefreshRun(tree, { head, exit, ignoreHash, unavailable = false }) {
316
195
  const previous = readRefreshRecord(tree);
317
196
  let rules = {};
@@ -325,23 +204,7 @@ export function recordRefreshRun(tree, { head, exit, ignoreHash, unavailable = f
325
204
  });
326
205
  }
327
206
 
328
- /**
329
- * O frescor do grafo de `tree`.
330
- *
331
- * missing sem graph.json
332
- * unknown sem git, ou sem carimbo nem registro que diga o commit
333
- * stale atrás do HEAD (`reason` ausente), ou em dia no commit mas com as
334
- * regras do ignore mudadas (`ignore-changed`) ou desconhecidas
335
- * (`rules-unknown`)
336
- * fresh carimbo = HEAD, ou rodada com exit 0 neste HEAD — e as regras
337
- * que valem para a árvore são as do grafo (`ignoreTextOf`; nenhum
338
- * arquivo valendo não afirma nada)
339
- *
340
- * @param {string} tree - o diretório que contém `graphify-out/`
341
- * @param {{graph?: object, head?: string|null}} [known] o que o chamador já
342
- * leu: o grafo parseado (o carimbo sai dele) e o HEAD
343
- * @returns {{state: 'missing'|'unknown'|'stale'|'fresh', built: string|null, head: string|null, reason?: 'ignore-changed'|'rules-unknown', record: object|null}}
344
- */
207
+ /** @returns {{state: 'missing'|'unknown'|'stale'|'fresh', built: string|null, head: string|null, reason?: 'ignore-changed'|'rules-unknown', record: object|null}} */
345
208
  export function graphFreshness(tree, { graph, head } = {}) {
346
209
  if (!graph && !existsSync(graphFile(tree))) {
347
210
  return { state: 'missing', built: null, head: null, record: null };
@@ -363,15 +226,8 @@ export function graphFreshness(tree, { graph, head } = {}) {
363
226
  return { state: 'fresh', ...base };
364
227
  }
365
228
 
366
- /**
367
- * Um refresh em background resolveria? Grafo velho, e sem uma rodada que já
368
- * FALHOU neste mesmo HEAD — essa só se repete quando o HEAD andar (ou por
369
- * `morph-spec graph refresh`, que é explícito). "graphify ausente" não conta
370
- * como falha: instalado, ele roda no próximo disparo.
371
- *
372
- * @param {{state: string, head: string|null, record: object|null}} freshness
373
- * @returns {boolean}
374
- */
229
+ // Uma rodada que FALHOU neste HEAD só se repete quando o HEAD andar;
230
+ // "graphify ausente" não conta como falha.
375
231
  export function refreshPending(freshness) {
376
232
  if (freshness?.state !== 'stale') return false;
377
233
  const r = freshness.record;