@north-light/crouter 0.3.227 → 0.3.228

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 (286) hide show
  1. package/dist/api/client.d.ts +5 -5
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/dto/broker-ops.d.ts +8 -0
  4. package/dist/api/dto/memory.d.ts +8 -7
  5. package/dist/api/dto/memory.js +2 -2
  6. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
  7. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
  8. package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
  9. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
  10. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
  11. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
  12. package/dist/builtin-memory/design/guide.md +38 -16
  13. package/dist/builtin-memory/design/roadmap.md +4 -4
  14. package/dist/builtin-memory/insights/init.md +5 -5
  15. package/dist/builtin-memory/internal/INDEX.md +1 -1
  16. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  17. package/dist/builtin-memory/internal/memory-loading.md +6 -6
  18. package/dist/builtin-memory/internal/plugins.md +1 -1
  19. package/dist/builtin-memory/plan/guide.md +53 -0
  20. package/dist/builtin-memory/plan/roadmap.md +10 -8
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
  22. package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
  23. package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
  24. package/dist/clients/attach/chrome/inbox-strip.js +11 -7
  25. package/dist/clients/attach/session/pane-tag.d.ts +28 -2
  26. package/dist/clients/attach/session/pane-tag.js +48 -22
  27. package/dist/clients/attach/session/profile-files.d.ts +6 -0
  28. package/dist/clients/attach/session/profile-files.js +44 -11
  29. package/dist/clients/attach/slash/dispatch.js +4 -7
  30. package/dist/clients/attach/viewer.js +588 -596
  31. package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
  32. package/dist/commands/api-client.d.ts +3 -7
  33. package/dist/commands/api-client.js +7 -12
  34. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
  35. package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
  36. package/dist/commands/memory/delete.js +51 -24
  37. package/dist/commands/memory/edit.js +50 -10
  38. package/dist/commands/memory/find.js +105 -56
  39. package/dist/commands/memory/history.js +51 -31
  40. package/dist/commands/memory/lint.d.ts +0 -9
  41. package/dist/commands/memory/lint.js +78 -322
  42. package/dist/commands/memory/list.d.ts +12 -4
  43. package/dist/commands/memory/list.js +64 -25
  44. package/dist/commands/memory/move.js +103 -73
  45. package/dist/commands/memory/origin.js +35 -5
  46. package/dist/commands/memory/read.d.ts +4 -0
  47. package/dist/commands/memory/read.js +132 -80
  48. package/dist/commands/memory/shared.d.ts +115 -25
  49. package/dist/commands/memory/shared.js +331 -74
  50. package/dist/commands/memory/write.js +72 -21
  51. package/dist/commands/memory.js +2 -2
  52. package/dist/commands/node/lifecycle.js +2 -2
  53. package/dist/commands/pkg/market-manage.js +25 -24
  54. package/dist/commands/pkg/plugin-manage.d.ts +22 -6
  55. package/dist/commands/pkg/plugin-manage.js +118 -25
  56. package/dist/commands/pkg/shared.d.ts +8 -0
  57. package/dist/commands/pkg/shared.js +23 -20
  58. package/dist/commands/surface-reload.d.ts +5 -0
  59. package/dist/commands/surface-reload.js +9 -1
  60. package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
  61. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
  62. package/dist/commands/sys/migrate.js +126 -140
  63. package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
  64. package/dist/commands/sys/panels/profiles-panel.js +599 -0
  65. package/dist/commands/sys/settings-shell.d.ts +4 -2
  66. package/dist/commands/sys/settings-shell.js +35 -4
  67. package/dist/commands/sys/settings.js +21 -5
  68. package/dist/commands/sys/sync-deps.d.ts +2 -0
  69. package/dist/commands/sys/sync-deps.js +26 -15
  70. package/dist/commands/sys/sync-project-guidance.js +228 -145
  71. package/dist/commands/sys/sync-skills.js +28 -17
  72. package/dist/commands/sys/update.js +11 -3
  73. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
  74. package/dist/core/__tests__/config-change-delta.test.js +106 -0
  75. package/dist/core/__tests__/context-intro.test.js +13 -9
  76. package/dist/core/__tests__/daemon-boot.test.js +22 -16
  77. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
  78. package/dist/core/__tests__/human-deliver.test.js +19 -0
  79. package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
  80. package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
  81. package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
  82. package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
  83. package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
  84. package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
  85. package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
  86. package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
  87. package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
  88. package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
  89. package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
  90. package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
  91. package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
  92. package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
  93. package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
  94. package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
  95. package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
  96. package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
  97. package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
  98. package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
  99. package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
  100. package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
  101. package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
  102. package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
  103. package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
  104. package/dist/core/__tests__/kickoff.test.js +16 -5
  105. package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
  106. package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
  107. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  108. package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
  109. package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
  110. package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
  111. package/dist/core/__tests__/repository-association.test.d.ts +1 -0
  112. package/dist/core/__tests__/repository-association.test.js +153 -0
  113. package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
  114. package/dist/core/__tests__/repository-root-identity.test.js +219 -0
  115. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
  116. package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
  117. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
  118. package/dist/core/canvas/canvas.d.ts +3 -0
  119. package/dist/core/canvas/canvas.js +30 -10
  120. package/dist/core/canvas/db.js +28 -1
  121. package/dist/core/canvas/paths.d.ts +4 -6
  122. package/dist/core/canvas/paths.js +8 -6
  123. package/dist/core/canvas/render-source.js +2 -2
  124. package/dist/core/canvas/types.d.ts +9 -13
  125. package/dist/core/exclusive-lock.d.ts +2 -0
  126. package/dist/core/exclusive-lock.js +21 -0
  127. package/dist/core/git.d.ts +0 -1
  128. package/dist/core/git.js +0 -3
  129. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
  130. package/dist/core/human/feedback-companion.js +3 -0
  131. package/dist/core/human/scan.js +8 -1
  132. package/dist/core/keybindings/catalog.d.ts +2 -2
  133. package/dist/core/keybindings/catalog.js +2 -1
  134. package/dist/core/memory/doc-link-grammar.js +2 -2
  135. package/dist/core/memory/history.d.ts +24 -0
  136. package/dist/core/memory/history.js +66 -1
  137. package/dist/core/memory/identity.d.ts +65 -0
  138. package/dist/core/memory/identity.js +185 -0
  139. package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
  140. package/dist/core/memory/inline-ref-guidance.js +1 -1
  141. package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
  142. package/dist/core/memory/inline-ref-inventory.js +30 -62
  143. package/dist/core/memory/lint.d.ts +129 -0
  144. package/dist/core/memory/lint.js +517 -0
  145. package/dist/core/memory/project-namespace.d.ts +31 -0
  146. package/dist/core/memory/project-namespace.js +85 -0
  147. package/dist/core/memory/repository-association.d.ts +31 -0
  148. package/dist/core/memory/repository-association.js +129 -0
  149. package/dist/core/memory/tree.d.ts +39 -0
  150. package/dist/core/memory/tree.js +93 -0
  151. package/dist/core/memory-resolver.d.ts +193 -90
  152. package/dist/core/memory-resolver.js +461 -375
  153. package/dist/core/nested-stores.js +6 -13
  154. package/dist/core/profiles/select.d.ts +4 -1
  155. package/dist/core/profiles/select.js +108 -48
  156. package/dist/core/review/__tests__/capture-origin.test.js +2 -2
  157. package/dist/core/review/__tests__/stage-identity.test.js +2 -2
  158. package/dist/core/review/companion.js +11 -2
  159. package/dist/core/runtime/bearings.d.ts +2 -2
  160. package/dist/core/runtime/bearings.js +3 -3
  161. package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
  162. package/dist/core/runtime/broker/rebind.js +5 -0
  163. package/dist/core/runtime/broker-extension-render.d.ts +7 -3
  164. package/dist/core/runtime/broker-extension-render.js +10 -5
  165. package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
  166. package/dist/core/runtime/broker-persona-guidance.js +122 -27
  167. package/dist/core/runtime/deliver-live.d.ts +16 -4
  168. package/dist/core/runtime/deliver-live.js +29 -15
  169. package/dist/core/runtime/kickoff.d.ts +3 -3
  170. package/dist/core/runtime/kickoff.js +8 -16
  171. package/dist/core/runtime/lifecycle.js +2 -3
  172. package/dist/core/runtime/nodes.d.ts +3 -4
  173. package/dist/core/runtime/nodes.js +3 -4
  174. package/dist/core/runtime/persona.d.ts +8 -12
  175. package/dist/core/runtime/persona.js +24 -96
  176. package/dist/core/runtime/promote.d.ts +2 -2
  177. package/dist/core/runtime/promote.js +8 -21
  178. package/dist/core/runtime/revive.js +20 -17
  179. package/dist/core/runtime/spawn.js +18 -7
  180. package/dist/core/runtime/tmux-bindings.js +2 -3
  181. package/dist/core/runtime/warm-pool.js +3 -4
  182. package/dist/core/scope.js +2 -0
  183. package/dist/core/self-update.d.ts +0 -2
  184. package/dist/core/self-update.js +2 -35
  185. package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
  186. package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
  187. package/dist/core/substrate/index.d.ts +2 -2
  188. package/dist/core/substrate/index.js +1 -1
  189. package/dist/core/substrate/injected-store.d.ts +43 -27
  190. package/dist/core/substrate/injected-store.js +208 -104
  191. package/dist/core/substrate/listings.d.ts +19 -12
  192. package/dist/core/substrate/listings.js +75 -52
  193. package/dist/core/substrate/on-read-node.d.ts +4 -7
  194. package/dist/core/substrate/on-read-node.js +6 -8
  195. package/dist/core/substrate/on-read.d.ts +22 -25
  196. package/dist/core/substrate/on-read.js +103 -147
  197. package/dist/core/substrate/render-node.d.ts +4 -7
  198. package/dist/core/substrate/render-node.js +5 -7
  199. package/dist/core/substrate/render.d.ts +21 -3
  200. package/dist/core/substrate/render.js +291 -223
  201. package/dist/core/substrate/schema.d.ts +1 -13
  202. package/dist/core/substrate/schema.js +5 -40
  203. package/dist/core/substrate/session-cache.d.ts +14 -4
  204. package/dist/core/substrate/session-cache.js +40 -22
  205. package/dist/core/substrate/surface-match.d.ts +9 -7
  206. package/dist/core/substrate/surface-match.js +26 -25
  207. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
  208. package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
  209. package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
  210. package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
  211. package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +21 -5
  212. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  213. package/dist/daemon/api/handlers/memory.js +2 -0
  214. package/dist/daemon/crtrd.js +2 -0
  215. package/dist/daemon/human/finish.js +8 -1
  216. package/dist/daemon/manage.d.ts +0 -1
  217. package/dist/daemon/manage.js +7 -16
  218. package/dist/daemon/startup-policy.d.ts +1 -0
  219. package/dist/daemon/startup-policy.js +1 -0
  220. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  221. package/dist/migrations/002-profile-project-memory.js +1 -0
  222. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  223. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  224. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  225. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  226. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  227. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  228. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  229. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  230. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  231. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  232. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  233. package/dist/migrations/__tests__/activation.test.js +149 -0
  234. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  235. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  236. package/dist/migrations/activation.d.ts +16 -0
  237. package/dist/migrations/activation.js +78 -0
  238. package/dist/migrations/convergent.d.ts +14 -3
  239. package/dist/migrations/convergent.js +21 -10
  240. package/dist/migrations/corpus.d.ts +78 -0
  241. package/dist/migrations/corpus.js +497 -0
  242. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  243. package/dist/migrations/frontmatter-splice.js +176 -0
  244. package/dist/migrations/registry.d.ts +6 -1
  245. package/dist/migrations/registry.js +7 -2
  246. package/dist/migrations/runner.d.ts +41 -0
  247. package/dist/migrations/runner.js +81 -0
  248. package/dist/migrations/types.d.ts +148 -9
  249. package/dist/migrations/types.js +9 -2
  250. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  251. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  252. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  253. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  254. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  255. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  256. package/dist/pi-extensions/canvas-stophook.js +6 -13
  257. package/dist/shared/generated-context.d.ts +0 -3
  258. package/dist/shared/generated-context.js +0 -57
  259. package/dist/shared/tool-groups.js +2 -3
  260. package/package.json +5 -4
  261. package/runtime.lock.json +2 -2
  262. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  263. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  264. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  265. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  266. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  267. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  268. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  269. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  270. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  271. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  272. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  273. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  274. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  275. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  276. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  277. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  278. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  279. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  280. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  281. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  282. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  283. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  284. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  285. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  286. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -0,0 +1,497 @@
1
+ // The corpus-lane runner: discover EVERY memory store a machine can reach,
2
+ // snapshot them together, let each corpus migration plan across the whole set,
3
+ // then apply in two global phases.
4
+ //
5
+ // Why global: a document's canonical identity is an ADDRESS other stores point
6
+ // at through `[[links]]` and `memory-read` routes. A per-store lane can rename
7
+ // an identity it owns but cannot see the consumers that break, so the corpus
8
+ // lane plans over every store at once — and one blocker anywhere writes nothing
9
+ // anywhere, because a half-applied identity cut leaves dangling addresses.
10
+ //
11
+ // Why two phases: consumers are rewritten first, identities second, both across
12
+ // the ENTIRE corpus. A store whose identity changed must not be readable at its
13
+ // new address before every pointer to the old one has moved.
14
+ //
15
+ // Discovery is filesystem-only and never opens canvas.db. A migration runs
16
+ // BEFORE the daemon on a cold home (Northlight's entrypoint does exactly that),
17
+ // so node target metadata comes from each node's durable meta.json — the source
18
+ // of truth the database index is rebuilt from — through its existing read-only
19
+ // helper. Missing or corrupt metadata remains a global blocker.
20
+ import { readdirSync } from 'node:fs';
21
+ import { dirname, join } from 'node:path';
22
+ import { readMetaObject } from '../core/canvas/meta-file.js';
23
+ import { nodeMetaPath, nodesRoot } from '../core/canvas/paths.js';
24
+ import { atomicWriteText, listDirs, pathExists, realpathOrSelf, removePath, walkFiles } from '../core/fs-utils.js';
25
+ import { memoryStoresInPrecedence, } from '../core/memory-resolver.js';
26
+ import { descendantStoreRootsExhaustive } from '../core/nested-stores.js';
27
+ import { profileMemoryDir } from '../core/profiles/manifest.js';
28
+ import { listInstalledPlugins, listInstalledPluginsInRoot } from '../core/resolver.js';
29
+ import { memoryDir as nodeMemoryDir } from '../core/runtime/memory.js';
30
+ import { NEUTRAL_PROJECT_MEMORY, pluginMemoryDir, projectScopeRoots, scopeRoot } from '../core/scope.js';
31
+ import { CRTR_DIR_NAME } from '../types.js';
32
+ import { foldDocChange, loadStoreSnapshot } from './convergent.js';
33
+ import { STATE_MIGRATIONS } from './registry.js';
34
+ function firstLine(e) {
35
+ return (e instanceof Error ? e.message : String(e)).split('\n')[0] ?? '';
36
+ }
37
+ function targetId(target) {
38
+ return `${target.profileId ?? '-'}:${target.nodeId ?? '-'}:${realpathOrSelf(target.cwd)}`;
39
+ }
40
+ function pluginStoreDescriptor(plugin) {
41
+ const storeRoot = pluginMemoryDir(plugin);
42
+ return {
43
+ scope: plugin.scope,
44
+ storeRoot,
45
+ namespace: plugin.name,
46
+ plugin: plugin.name,
47
+ mountStatus: pathExists(storeRoot) ? 'ready' : 'absent',
48
+ projectMemory: NEUTRAL_PROJECT_MEMORY,
49
+ };
50
+ }
51
+ function nodeStoreDescriptor(storeRoot) {
52
+ return {
53
+ scope: 'node',
54
+ storeRoot,
55
+ namespace: '',
56
+ mountStatus: pathExists(storeRoot) ? 'ready' : 'absent',
57
+ projectMemory: NEUTRAL_PROJECT_MEMORY,
58
+ };
59
+ }
60
+ /** Node stores holding at least one document. An empty node store contributes
61
+ * no identity and no reference, so it stays out of the corpus — which is what
62
+ * keeps discovery cheap across thousands of nodes AND keeps the daemon
63
+ * optional on a home whose nodes have never written memory. */
64
+ function nodeStoresWithDocs() {
65
+ const out = [];
66
+ for (const nodeId of listDirs(nodesRoot())) {
67
+ const storeRoot = nodeMemoryDir(nodeId);
68
+ if (!pathExists(storeRoot))
69
+ continue;
70
+ const docs = walkFiles(storeRoot, (n) => n.endsWith('.md') && n !== 'SKILL.md', (d) => d.startsWith('.'));
71
+ if (docs.length > 0)
72
+ out.push({ nodeId, storeRoot });
73
+ }
74
+ return out;
75
+ }
76
+ /** Discover the whole reachable corpus: every store, the targets that mount
77
+ * them, and the frontier searched to find them. `profiles` are the manifest
78
+ * lane's post-migration snapshots — a pre-migration manifest reads as corrupt,
79
+ * so discovery must run after that lane, not against `listProfiles()`. */
80
+ export async function discoverCorpus(opts) {
81
+ const cwd = realpathOrSelf(opts.cwd ?? process.cwd());
82
+ const selectedProfileId = process.env['CRTR_PROFILE_ID'] || null;
83
+ const explicitDirs = opts.explicitDirs ?? [];
84
+ const roots = [];
85
+ const rootSeen = new Set();
86
+ const addRoot = (row) => {
87
+ const key = `${row.reason}\u0000${realpathOrSelf(row.root)}\u0000${row.detail ?? ''}`;
88
+ if (rootSeen.has(key))
89
+ return;
90
+ rootSeen.add(key);
91
+ roots.push(row);
92
+ };
93
+ const projectRoots = new Map();
94
+ const addProjectRoot = (root, reason, profileId) => {
95
+ const key = realpathOrSelf(root);
96
+ let entry = projectRoots.get(key);
97
+ if (entry === undefined) {
98
+ entry = { root, reason, profileIds: [] };
99
+ projectRoots.set(key, entry);
100
+ }
101
+ if (!entry.profileIds.includes(profileId))
102
+ entry.profileIds.push(profileId);
103
+ };
104
+ const contexts = [
105
+ { reason: 'cwd-ancestor', profileId: selectedProfileId, roots: projectScopeRoots(cwd, selectedProfileId) },
106
+ ...opts.profiles.map(({ profileId, manifest }) => ({
107
+ reason: 'profile-project',
108
+ profileId,
109
+ detail: profileId,
110
+ roots: [
111
+ ...new Set(manifest.projects.flatMap((project) => [
112
+ join(project.path, CRTR_DIR_NAME),
113
+ ...projectScopeRoots(project.path, null),
114
+ ])),
115
+ ],
116
+ })),
117
+ ...explicitDirs.map((dir) => ({
118
+ reason: 'explicit-dir',
119
+ profileId: selectedProfileId,
120
+ detail: dir,
121
+ roots: [join(realpathOrSelf(dir), CRTR_DIR_NAME)],
122
+ })),
123
+ ];
124
+ const descendantCache = new Map();
125
+ /** Memoized by the root set: two profiles naming the same projects, or a
126
+ * target-discovered root already walked, must not pay for the walk twice. */
127
+ const descendantsUnder = (rootsIn) => {
128
+ const key = rootsIn.map((root) => realpathOrSelf(root)).sort().join('\n');
129
+ let found = descendantCache.get(key);
130
+ if (found === undefined) {
131
+ found = descendantStoreRootsExhaustive([...rootsIn]);
132
+ descendantCache.set(key, found);
133
+ }
134
+ return found;
135
+ };
136
+ /** Take one set of project roots into discovery: the roots themselves, then
137
+ * every store nested below them. Both kinds are project roots in their own
138
+ * right — each earns a project target whose own precedence walk can reach
139
+ * further roots, which is why the target loop below calls this again. */
140
+ const expandRoots = (rootsIn, reason, profileId, detail) => {
141
+ const detailRow = detail === undefined ? {} : { detail };
142
+ for (const root of rootsIn) {
143
+ addRoot({ reason, root, boundary: reason === 'explicit-dir' ? 'store' : 'ancestors', ...detailRow });
144
+ addProjectRoot(root, reason, profileId);
145
+ }
146
+ for (const descendant of descendantsUnder(rootsIn)) {
147
+ addRoot({ reason: 'descendant', root: descendant, boundary: 'descendants', ...detailRow });
148
+ addProjectRoot(descendant, 'descendant', profileId);
149
+ }
150
+ };
151
+ for (const context of contexts)
152
+ expandRoots(context.roots, context.reason, context.profileId, context.detail);
153
+ const blockers = [];
154
+ const stores = new Map();
155
+ const register = (descriptor, discovery, target) => {
156
+ const storeRoot = descriptor.storeRoot;
157
+ if (storeRoot === '' || !pathExists(storeRoot))
158
+ return;
159
+ const key = realpathOrSelf(storeRoot);
160
+ let record = stores.get(key);
161
+ if (record === undefined) {
162
+ record = {
163
+ key,
164
+ // First descriptor wins, matching the resolver's first-position dedup.
165
+ descriptor,
166
+ discovery: [],
167
+ discoverySeen: new Set(),
168
+ targets: [],
169
+ writable: descriptor.scope !== 'builtin',
170
+ };
171
+ stores.set(key, record);
172
+ try {
173
+ readdirSync(key);
174
+ }
175
+ catch (e) {
176
+ blockers.push({
177
+ kind: 'inaccessible-root',
178
+ message: `memory store cannot be listed (${firstLine(e)}) — discovery cannot see its documents; fix the permissions and re-run`,
179
+ storeRoot: key,
180
+ });
181
+ }
182
+ }
183
+ const discoveryKey = `${discovery.reason}\u0000${discovery.root}\u0000${discovery.targetId ?? ''}`;
184
+ if (!record.discoverySeen.has(discoveryKey)) {
185
+ record.discoverySeen.add(discoveryKey);
186
+ record.discovery.push(discovery);
187
+ }
188
+ if (target !== undefined) {
189
+ if (!record.targets.includes(target.id))
190
+ record.targets.push(target.id);
191
+ if (!target.storeKeys.includes(key))
192
+ target.storeKeys.push(key);
193
+ }
194
+ };
195
+ const targets = [];
196
+ const targetsById = new Map();
197
+ const addTarget = (kind, origin, target) => {
198
+ const id = targetId(target);
199
+ if (targetsById.has(id))
200
+ return;
201
+ const record = { id, kind, origin, target, storeKeys: [] };
202
+ targetsById.set(id, record);
203
+ targets.push(record);
204
+ };
205
+ addTarget('cwd', cwd, { cwd, profileId: selectedProfileId, nodeId: null });
206
+ for (const { profileId, manifest } of opts.profiles) {
207
+ const home = manifest.home !== null && manifest.home !== '' ? manifest.home : null;
208
+ addTarget('profile', profileId, {
209
+ cwd: home ?? manifest.projects[0]?.path ?? cwd,
210
+ profileId,
211
+ nodeId: null,
212
+ });
213
+ }
214
+ /** One project target per (root, profile) membership. Re-run whenever the
215
+ * root set grows: `addTarget` dedupes by target id. */
216
+ const addProjectTargets = () => {
217
+ for (const { root, profileIds } of [...projectRoots.values()]) {
218
+ for (const profileId of [...profileIds]) {
219
+ addTarget('project', dirname(root), { cwd: dirname(root), profileId, nodeId: null });
220
+ }
221
+ }
222
+ };
223
+ const nodeStores = nodeStoresWithDocs();
224
+ if (pathExists(nodesRoot()))
225
+ addRoot({ reason: 'node', root: nodesRoot(), boundary: 'nodes' });
226
+ const nodeMetadata = { available: true, nodes: 0 };
227
+ for (const { nodeId, storeRoot } of nodeStores) {
228
+ const metaPath = nodeMetaPath(nodeId);
229
+ const meta = readMetaObject(metaPath);
230
+ if (meta === null) {
231
+ nodeMetadata.available = false;
232
+ nodeMetadata.error ??= `unreadable metadata: ${metaPath}`;
233
+ blockers.push({
234
+ kind: 'inaccessible-target',
235
+ message: `node ${nodeId} holds memory documents but its durable metadata is unreadable at ${metaPath} — restore or repair that file, then re-run`,
236
+ storeRoot: realpathOrSelf(storeRoot),
237
+ path: metaPath,
238
+ });
239
+ register(nodeStoreDescriptor(storeRoot), { reason: 'node', root: storeRoot });
240
+ continue;
241
+ }
242
+ nodeMetadata.nodes += 1;
243
+ const nodeCwd = typeof meta['cwd'] === 'string' && meta['cwd'] !== '' ? meta['cwd'] : cwd;
244
+ const profileId = typeof meta['profile_id'] === 'string' && meta['profile_id'] !== '' ? meta['profile_id'] : null;
245
+ addTarget('node', nodeId, { cwd: nodeCwd, profileId, nodeId });
246
+ }
247
+ // Runtime default `includeDescendants: false`: this is the store stack a link
248
+ // or route actually resolves through. A descendant store is reached by its
249
+ // own owner-dir target instead.
250
+ const walkTarget = (target) => {
251
+ for (const descriptor of memoryStoresInPrecedence(target.target)) {
252
+ if (descriptor.plugin !== undefined) {
253
+ addRoot({ reason: 'plugin', root: descriptor.storeRoot, boundary: 'store', detail: descriptor.plugin });
254
+ register(descriptor, { reason: 'plugin', root: descriptor.storeRoot, targetId: target.id }, target);
255
+ continue;
256
+ }
257
+ if (descriptor.scope === 'project') {
258
+ const crtrRoot = dirname(descriptor.storeRoot);
259
+ const known = projectRoots.get(realpathOrSelf(crtrRoot));
260
+ if (known === undefined) {
261
+ // Reached only through this target's own ancestor walk — a node's cwd
262
+ // or a profile home outside every manifest project. It enters
263
+ // discovery WHOLE: the stores nested below it are corpus too, and the
264
+ // loop below gives it and them their own project targets.
265
+ expandRoots([crtrRoot], 'cwd-ancestor', target.target.profileId, target.id);
266
+ }
267
+ register(descriptor, { reason: known?.reason ?? 'cwd-ancestor', root: crtrRoot, targetId: target.id }, target);
268
+ continue;
269
+ }
270
+ const reason = descriptor.scope === 'user'
271
+ ? 'user'
272
+ : descriptor.scope === 'profile'
273
+ ? 'profile'
274
+ : descriptor.scope === 'node'
275
+ ? 'node'
276
+ : 'builtin';
277
+ addRoot({ reason, root: descriptor.storeRoot, boundary: 'store' });
278
+ register(descriptor, { reason, root: descriptor.storeRoot, targetId: target.id }, target);
279
+ }
280
+ };
281
+ // Discovery runs to a FIXPOINT: a target's store walk can name a project root
282
+ // no manifest and no ancestor walk knew, that root's descendants are corpus
283
+ // too, and each of them gets a target whose own walk can name another root.
284
+ // Terminates because a root expands once (`projectRoots` dedupes by realpath)
285
+ // and a target is walked once.
286
+ const walkedTargets = new Set();
287
+ for (;;) {
288
+ addProjectTargets();
289
+ const pending = targets.filter((target) => !walkedTargets.has(target.id));
290
+ if (pending.length === 0)
291
+ break;
292
+ for (const target of pending) {
293
+ walkedTargets.add(target.id);
294
+ walkTarget(target);
295
+ }
296
+ }
297
+ // Stores no target walk reaches. A DISABLED plugin's documents still hold
298
+ // identities and still point at other stores, so the corpus must see them —
299
+ // `memoryStoresInPrecedence` mounts only enabled ones.
300
+ const userRoot = scopeRoot('user');
301
+ if (userRoot !== null)
302
+ addRoot({ reason: 'plugin', root: userRoot, boundary: 'plugins' });
303
+ const extraPlugins = [...listInstalledPlugins('user')];
304
+ for (const { root } of projectRoots.values()) {
305
+ addRoot({ reason: 'plugin', root, boundary: 'plugins' });
306
+ extraPlugins.push(...listInstalledPluginsInRoot('project', root));
307
+ }
308
+ for (const plugin of extraPlugins) {
309
+ const descriptor = pluginStoreDescriptor(plugin);
310
+ addRoot({ reason: 'plugin', root: descriptor.storeRoot, boundary: 'store', detail: plugin.name });
311
+ register(descriptor, { reason: 'plugin', root: descriptor.storeRoot });
312
+ }
313
+ // A profile store the selected profile never mounts (no target selects it).
314
+ for (const { profileId } of opts.profiles) {
315
+ const storeRoot = profileMemoryDir(profileId);
316
+ addRoot({ reason: 'profile', root: storeRoot, boundary: 'store', detail: profileId });
317
+ register({ scope: 'profile', storeRoot, namespace: '', mountStatus: pathExists(storeRoot) ? 'ready' : 'absent', projectMemory: NEUTRAL_PROJECT_MEMORY }, { reason: 'profile', root: storeRoot });
318
+ }
319
+ return {
320
+ roots,
321
+ stores: [...stores.values()].map((record) => ({
322
+ key: record.key,
323
+ descriptor: record.descriptor,
324
+ discovery: record.discovery,
325
+ targets: record.targets,
326
+ writable: record.writable,
327
+ })),
328
+ targets: targets.map((record) => ({
329
+ id: record.id,
330
+ kind: record.kind,
331
+ origin: record.origin,
332
+ target: record.target,
333
+ storeKeys: record.storeKeys,
334
+ })),
335
+ blockers,
336
+ nodeMetadata,
337
+ };
338
+ }
339
+ // ---------------------------------------------------------------------------
340
+ // Snapshots
341
+ // ---------------------------------------------------------------------------
342
+ /** Read every discovered store's documents. `preloaded` supplies the per-store
343
+ * convergent lane's FOLDED snapshots so the corpus plans against the bytes a
344
+ * write run leaves behind — identical under `--dry-run`, where the same fold
345
+ * ran and nothing was written. */
346
+ export function loadCorpusSnapshots(discovery, opts = {}) {
347
+ const snapshots = new Map();
348
+ const skipped = [];
349
+ for (const store of discovery.stores) {
350
+ const preloaded = opts.preloaded?.get(store.key);
351
+ if (preloaded !== undefined) {
352
+ snapshots.set(store.key, preloaded);
353
+ continue;
354
+ }
355
+ const loaded = loadStoreSnapshot(store.descriptor.storeRoot);
356
+ snapshots.set(store.key, loaded.snapshot);
357
+ for (const row of loaded.skipped)
358
+ skipped.push({ storeRoot: store.key, ...row });
359
+ }
360
+ return { snapshots, skipped };
361
+ }
362
+ export function parseFailureBlockers(skipped) {
363
+ return skipped.map((row) => ({
364
+ kind: 'parse-failure',
365
+ message: `${row.relPath}: frontmatter is not valid YAML (${row.error}) — the document's identity cannot be read; fix the file and re-run`,
366
+ storeRoot: row.storeRoot,
367
+ path: join(row.storeRoot, row.relPath),
368
+ }));
369
+ }
370
+ function makeCorpusSnapshot(stores, targets, roots) {
371
+ const byKey = new Map(stores.map((store) => [store.key, store]));
372
+ const byTarget = new Map(targets.map((target) => [target.id, target]));
373
+ return {
374
+ stores,
375
+ targets,
376
+ roots,
377
+ store: (key) => byKey.get(key),
378
+ storesForTarget: (id) => (byTarget.get(id)?.storeKeys ?? [])
379
+ .map((key) => byKey.get(key))
380
+ .filter((store) => store !== undefined),
381
+ };
382
+ }
383
+ export function buildCorpusSnapshot(discovery, snapshots) {
384
+ const stores = discovery.stores.map((store) => ({
385
+ ...store,
386
+ snapshot: snapshots.get(store.key) ?? { root: store.descriptor.storeRoot, docs: [] },
387
+ }));
388
+ return makeCorpusSnapshot(stores, discovery.targets, discovery.roots);
389
+ }
390
+ // ---------------------------------------------------------------------------
391
+ // The runner
392
+ // ---------------------------------------------------------------------------
393
+ function cloneCorpus(corpus) {
394
+ const stores = corpus.stores.map((store) => ({
395
+ ...store,
396
+ snapshot: { root: store.snapshot.root, docs: [...store.snapshot.docs] },
397
+ }));
398
+ return makeCorpusSnapshot(stores, corpus.targets, corpus.roots);
399
+ }
400
+ /** Fold a planned change into the working corpus so migration k+1 plans against
401
+ * post-change state. A change outside every store is a migration defect, except
402
+ * an identity change creating this plan's repository-root declaration: absent
403
+ * root stores are intentionally not synthesized during planning. */
404
+ function foldCorpusChange(corpus, change, allowedOutsidePaths = new Set()) {
405
+ let owner;
406
+ for (const store of corpus.stores) {
407
+ if (!change.path.startsWith(`${store.snapshot.root}/`))
408
+ continue;
409
+ if (owner === undefined || store.snapshot.root.length > owner.snapshot.root.length)
410
+ owner = store;
411
+ }
412
+ if (owner === undefined) {
413
+ if (allowedOutsidePaths.has(change.path))
414
+ return;
415
+ throw new Error(`corpus migration: planned change outside every discovered store: ${change.path}`);
416
+ }
417
+ foldDocChange(owner.snapshot, change);
418
+ }
419
+ /** Plan every corpus migration over the whole corpus, then — only when the
420
+ * global plan is blocker-free — write every migration's consumer rewrites
421
+ * before any identity rewrite. `rebuild` re-reads the corpus from file truth
422
+ * for the post-write `verify` pass. */
423
+ export function runCorpusMigrations(corpus, opts = {}) {
424
+ const migrations = (opts.migrations ?? STATE_MIGRATIONS).filter((m) => m.lane === 'corpus');
425
+ const blockers = [...(opts.blockers ?? [])];
426
+ const planned = [];
427
+ const repositories = [];
428
+ const storePrefixes = [];
429
+ const renames = [];
430
+ const references = [];
431
+ const consumers = [];
432
+ const identities = [];
433
+ const result = (verified, wrote) => ({
434
+ migrations: migrations.length,
435
+ blocked: blockers.length > 0 && migrations.length > 0,
436
+ blockers,
437
+ planned,
438
+ repositories,
439
+ storePrefixes,
440
+ renames,
441
+ references,
442
+ wrote,
443
+ verified,
444
+ });
445
+ const noWrites = { consumers: [], identities: [] };
446
+ if (migrations.length === 0)
447
+ return result(false, noWrites);
448
+ const working = cloneCorpus(corpus);
449
+ for (const migration of migrations) {
450
+ const plan = migration.plan(working);
451
+ blockers.push(...plan.blockers);
452
+ repositories.push(...plan.repositories);
453
+ storePrefixes.push(...plan.storePrefixes);
454
+ renames.push(...plan.renames);
455
+ references.push(...plan.references);
456
+ for (const change of plan.consumers)
457
+ foldCorpusChange(working, change);
458
+ const allowedOutsidePaths = new Set(plan.repositories.map((repository) => repository.declarationPath));
459
+ for (const change of plan.identities)
460
+ foldCorpusChange(working, change, allowedOutsidePaths);
461
+ consumers.push(...plan.consumers);
462
+ identities.push(...plan.identities);
463
+ planned.push({
464
+ migration: migration.description,
465
+ consumers: plan.consumers.map((c) => c.path),
466
+ identities: plan.identities.map((c) => c.path),
467
+ });
468
+ }
469
+ const wrote = { consumers: consumers.map((c) => c.path), identities: identities.map((c) => c.path) };
470
+ if (blockers.length > 0)
471
+ return result(false, noWrites);
472
+ if (opts.dryRun === true)
473
+ return result(false, wrote);
474
+ // The phase barrier is GLOBAL: every migration's consumer rewrites land
475
+ // before any identity rewrite, so no pointer resolves to a moved address.
476
+ for (const change of consumers) {
477
+ if (change.after === null)
478
+ removePath(change.path);
479
+ else
480
+ atomicWriteText(change.path, change.after);
481
+ }
482
+ for (const change of identities) {
483
+ if (change.after === null)
484
+ removePath(change.path);
485
+ else
486
+ atomicWriteText(change.path, change.after);
487
+ }
488
+ if (opts.rebuild === undefined)
489
+ return result(false, wrote);
490
+ const rebuilt = opts.rebuild();
491
+ for (const migration of migrations) {
492
+ for (const finding of migration.verify?.(rebuilt) ?? []) {
493
+ blockers.push({ ...finding, kind: 'verification' });
494
+ }
495
+ }
496
+ return result(true, wrote);
497
+ }
@@ -0,0 +1,15 @@
1
+ /** Split a complete document into its raw frontmatter YAML and byte-preserved body. */
2
+ export declare function splitDocSource(source: string, docPath: string): {
3
+ rawYaml: string;
4
+ body: string;
5
+ };
6
+ /** Remove complete top-level mapping-key lines, including multiline values. */
7
+ export declare function dropTopLevelKeys(rawYaml: string, keys: readonly string[], docPath: string): string;
8
+ /** Remove selected items from a top-level block sequence without re-rendering retained bytes. */
9
+ export declare function dropSequenceItems(rawYaml: string, key: string, drop: (item: unknown) => boolean, docPath: string): string;
10
+ /** Append a rendered top-level mapping pair while retaining existing YAML bytes. */
11
+ export declare function appendRenderedKey(rawYaml: string, key: string, value: unknown, docPath: string): string;
12
+ /** Prove that a transformed YAML block parses to exactly the expected mapping. */
13
+ export declare function proveFrontmatterRoundTrip(after: string, expected: Record<string, unknown>, docPath: string): void;
14
+ /** Replace only the YAML block in a complete document; the body remains byte-for-byte unchanged. */
15
+ export declare function rebuildDocSource(source: string, newYaml: string, docPath: string): string;
@@ -0,0 +1,176 @@
1
+ import { isMap, isSeq, parse as parseYaml, parseDocument, stringify } from 'yaml';
2
+ import { parseFrontmatterGeneric } from '../core/frontmatter.js';
3
+ function deepEqual(a, b) {
4
+ if (a === b)
5
+ return true;
6
+ if (Array.isArray(a) || Array.isArray(b)) {
7
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
8
+ return false;
9
+ return a.every((value, index) => deepEqual(value, b[index]));
10
+ }
11
+ if (a !== null && b !== null && typeof a === 'object' && typeof b === 'object') {
12
+ const ak = Object.keys(a);
13
+ const bk = Object.keys(b);
14
+ if (ak.length !== bk.length)
15
+ return false;
16
+ return ak.every((key) => deepEqual(a[key], b[key]));
17
+ }
18
+ return false;
19
+ }
20
+ function yamlMap(rawYaml, docPath) {
21
+ if (rawYaml.trim() === '')
22
+ return [];
23
+ const document = parseDocument(rawYaml);
24
+ if (document.errors.length > 0 || !isMap(document.contents)) {
25
+ throw new Error(`frontmatter splice: unparseable frontmatter in ${docPath}`);
26
+ }
27
+ return document.contents.items;
28
+ }
29
+ function keyLineSpan(rawYaml, pair, docPath) {
30
+ const keyRange = pair.key?.range;
31
+ const valueRange = pair.value?.range;
32
+ if (keyRange === undefined)
33
+ throw new Error(`frontmatter splice: rangeless key in ${docPath}`);
34
+ const start = rawYaml.lastIndexOf('\n', keyRange[0] - 1) + 1;
35
+ let end = valueRange?.[2] ?? keyRange[2];
36
+ if (!(end > 0 && rawYaml[end - 1] === '\n')) {
37
+ while (end < rawYaml.length && (rawYaml[end] === ' ' || rawYaml[end] === '\t' || rawYaml[end] === '\r'))
38
+ end++;
39
+ if (rawYaml[end] === '\n')
40
+ end++;
41
+ }
42
+ return [start, end];
43
+ }
44
+ function parseRecord(rawYaml, docPath) {
45
+ yamlMap(rawYaml, docPath);
46
+ let parsed;
47
+ try {
48
+ parsed = parseYaml(rawYaml);
49
+ }
50
+ catch {
51
+ throw new Error(`frontmatter splice: unparseable frontmatter in ${docPath}`);
52
+ }
53
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
54
+ throw new Error(`frontmatter splice: frontmatter is not a mapping in ${docPath}`);
55
+ }
56
+ return parsed;
57
+ }
58
+ /** Split a complete document into its raw frontmatter YAML and byte-preserved body. */
59
+ export function splitDocSource(source, docPath) {
60
+ let parsed;
61
+ try {
62
+ parsed = parseFrontmatterGeneric(source);
63
+ }
64
+ catch (error) {
65
+ throw new Error(`frontmatter splice: unparseable frontmatter in ${docPath}`, { cause: error });
66
+ }
67
+ if (parsed.raw === '' && parsed.body === source) {
68
+ throw new Error(`frontmatter splice: cannot locate frontmatter block in ${docPath}`);
69
+ }
70
+ return { rawYaml: parsed.raw, body: parsed.body };
71
+ }
72
+ /** Remove complete top-level mapping-key lines, including multiline values. */
73
+ export function dropTopLevelKeys(rawYaml, keys, docPath) {
74
+ const pairs = yamlMap(rawYaml, docPath);
75
+ const names = new Set(keys);
76
+ const spans = pairs
77
+ .filter((pair) => names.has(String(pair.key)))
78
+ .map((pair) => keyLineSpan(rawYaml, pair, docPath))
79
+ .sort((a, b) => b[0] - a[0]);
80
+ let after = rawYaml;
81
+ for (const [start, end] of spans)
82
+ after = after.slice(0, start) + after.slice(end);
83
+ const expected = parseRecord(rawYaml, docPath);
84
+ for (const key of keys)
85
+ delete expected[key];
86
+ proveFrontmatterRoundTrip(after, expected, docPath);
87
+ return after;
88
+ }
89
+ /** Remove selected items from a top-level block sequence without re-rendering retained bytes. */
90
+ export function dropSequenceItems(rawYaml, key, drop, docPath) {
91
+ const pairs = yamlMap(rawYaml, docPath);
92
+ const pair = pairs.find((candidate) => String(candidate.key) === key);
93
+ if (pair === undefined)
94
+ return rawYaml;
95
+ if (!isSeq(pair.value))
96
+ throw new Error(`frontmatter splice: ${key} is not a sequence in ${docPath}`);
97
+ const items = pair.value.items;
98
+ const dropped = items.map((item) => drop(item.toJSON?.()));
99
+ if (!dropped.some(Boolean))
100
+ return rawYaml;
101
+ const expected = parseRecord(rawYaml, docPath);
102
+ const original = expected[key];
103
+ if (!Array.isArray(original))
104
+ throw new Error(`frontmatter splice: ${key} is not a sequence in ${docPath}`);
105
+ const retained = original.filter((_, index) => !dropped[index]);
106
+ if (retained.length === 0) {
107
+ delete expected[key];
108
+ const after = dropTopLevelKeys(rawYaml, [key], docPath);
109
+ proveFrontmatterRoundTrip(after, expected, docPath);
110
+ return after;
111
+ }
112
+ const sequenceRange = pair.value.range;
113
+ if (sequenceRange === undefined)
114
+ throw new Error(`frontmatter splice: rangeless sequence in ${docPath}`);
115
+ const lineStarts = items.map((item) => {
116
+ const range = item.range;
117
+ if (range === undefined)
118
+ throw new Error(`frontmatter splice: rangeless sequence item in ${docPath}`);
119
+ return rawYaml.lastIndexOf('\n', range[0] - 1) + 1;
120
+ });
121
+ if (new Set(lineStarts).size !== lineStarts.length) {
122
+ throw new Error(`frontmatter splice: flow sequence cannot be spliced byte-precisely in ${docPath}`);
123
+ }
124
+ const spans = [];
125
+ for (let index = 0; index < items.length; index++) {
126
+ if (!dropped[index])
127
+ continue;
128
+ const start = lineStarts[index];
129
+ const end = lineStarts[index + 1] ?? sequenceRange[2];
130
+ spans.push([start, end]);
131
+ }
132
+ spans.sort((a, b) => b[0] - a[0]);
133
+ let after = rawYaml;
134
+ for (const [start, end] of spans)
135
+ after = after.slice(0, start) + after.slice(end);
136
+ expected[key] = retained;
137
+ proveFrontmatterRoundTrip(after, expected, docPath);
138
+ return after;
139
+ }
140
+ /** Append a rendered top-level mapping pair while retaining existing YAML bytes. */
141
+ export function appendRenderedKey(rawYaml, key, value, docPath) {
142
+ const expected = parseRecord(rawYaml, docPath);
143
+ expected[key] = value;
144
+ const block = stringify({ [key]: value }, { lineWidth: 0 });
145
+ let after = rawYaml === '' ? block : (rawYaml.endsWith('\n') ? rawYaml : rawYaml + '\n') + block;
146
+ after = after.replace(/\r?\n$/, '');
147
+ proveFrontmatterRoundTrip(after, expected, docPath);
148
+ return after;
149
+ }
150
+ /** Prove that a transformed YAML block parses to exactly the expected mapping. */
151
+ export function proveFrontmatterRoundTrip(after, expected, docPath) {
152
+ let actual;
153
+ try {
154
+ actual = parseYaml(after === '' ? '{}' : after);
155
+ }
156
+ catch (error) {
157
+ throw new Error(`frontmatter splice: transformed frontmatter does not parse in ${docPath}`, { cause: error });
158
+ }
159
+ if (!deepEqual(actual ?? {}, expected)) {
160
+ throw new Error(`frontmatter splice: spliced frontmatter does not round-trip in ${docPath}`);
161
+ }
162
+ }
163
+ /** Replace only the YAML block in a complete document; the body remains byte-for-byte unchanged. */
164
+ export function rebuildDocSource(source, newYaml, docPath) {
165
+ const { rawYaml, body } = splitDocSource(source, docPath);
166
+ const bodyStart = source.length - body.length;
167
+ const firstNewline = source.indexOf('\n');
168
+ const rawStart = firstNewline < 0 ? -1 : firstNewline + 1;
169
+ const rawEnd = rawStart < 0 ? -1 : rawStart + rawYaml.length;
170
+ if (rawYaml === '' && bodyStart === source.length) {
171
+ throw new Error(`frontmatter splice: cannot locate frontmatter block in ${docPath}`);
172
+ }
173
+ if (rawStart < 0 || rawEnd > bodyStart)
174
+ throw new Error(`frontmatter splice: cannot locate frontmatter block in ${docPath}`);
175
+ return source.slice(0, rawStart) + newYaml + source.slice(rawEnd);
176
+ }