@north-light/crouter 0.3.226 → 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 (288) 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 +37 -9
  212. package/dist/daemon/api/bridge.d.ts +35 -4
  213. package/dist/daemon/api/bridge.js +50 -8
  214. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  215. package/dist/daemon/api/handlers/memory.js +2 -0
  216. package/dist/daemon/crtrd.js +2 -0
  217. package/dist/daemon/human/finish.js +8 -1
  218. package/dist/daemon/manage.d.ts +0 -1
  219. package/dist/daemon/manage.js +7 -16
  220. package/dist/daemon/startup-policy.d.ts +1 -0
  221. package/dist/daemon/startup-policy.js +1 -0
  222. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  223. package/dist/migrations/002-profile-project-memory.js +1 -0
  224. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  225. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  226. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  227. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  228. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  229. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  230. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  231. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  232. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  233. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  234. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  235. package/dist/migrations/__tests__/activation.test.js +149 -0
  236. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  237. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  238. package/dist/migrations/activation.d.ts +16 -0
  239. package/dist/migrations/activation.js +78 -0
  240. package/dist/migrations/convergent.d.ts +14 -3
  241. package/dist/migrations/convergent.js +21 -10
  242. package/dist/migrations/corpus.d.ts +78 -0
  243. package/dist/migrations/corpus.js +497 -0
  244. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  245. package/dist/migrations/frontmatter-splice.js +176 -0
  246. package/dist/migrations/registry.d.ts +6 -1
  247. package/dist/migrations/registry.js +7 -2
  248. package/dist/migrations/runner.d.ts +41 -0
  249. package/dist/migrations/runner.js +81 -0
  250. package/dist/migrations/types.d.ts +148 -9
  251. package/dist/migrations/types.js +9 -2
  252. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  253. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  254. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  255. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  256. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  257. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  258. package/dist/pi-extensions/canvas-stophook.js +6 -13
  259. package/dist/shared/generated-context.d.ts +0 -3
  260. package/dist/shared/generated-context.js +0 -57
  261. package/dist/shared/tool-groups.js +2 -3
  262. package/package.json +5 -4
  263. package/runtime.lock.json +2 -2
  264. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  265. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  266. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  267. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  268. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  269. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  270. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  271. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  272. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  273. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  274. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  275. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  276. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  277. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  278. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  279. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  280. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  281. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  282. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  283. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  284. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  285. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  286. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  287. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  288. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -0,0 +1,349 @@
1
+ import { dirname } from 'node:path';
2
+ import { realpathOrSelf, readTextIfExists } from '../../core/fs-utils.js';
3
+ import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
4
+ import { composeCanonicalName, explicitLocalName, localNameFromPath, validateExplicitLocalName, validateNamespace, } from '../../core/memory/identity.js';
5
+ import { deriveProjectNamespace, } from '../../core/memory/project-namespace.js';
6
+ import { associateRepository, } from '../../core/memory/repository-association.js';
7
+ const SYNTHETIC_NAMESPACE = '__CROUTER_SYNTHETIC_NAMESPACE__';
8
+ const SYNTHETIC_NAMESPACE_SCALAR = '__CROUTER_SYNTHETIC_NAMESPACE_SCALAR__';
9
+ /** Exact source emitted by the superseded migration 003, with only its namespace
10
+ * value parameterized. It is deliberately byte-shaped rather than prose-shaped:
11
+ * a substantive nested guide is never deleted because it merely resembles this. */
12
+ const SYNTHETIC_INDEX_TEMPLATE = [
13
+ '---',
14
+ 'kind: knowledge',
15
+ `namespace: ${SYNTHETIC_NAMESPACE_SCALAR}`,
16
+ `when-and-why-to-read: When working in \`${SYNTHETIC_NAMESPACE}\`, this knowledge should be read because it names the namespace every document in this project answers under.`,
17
+ 'surfaces:',
18
+ ' - on: workspace-open',
19
+ ' at: content',
20
+ ' - on: read',
21
+ ' match: "./**"',
22
+ ' at: content',
23
+ '---',
24
+ `# ${SYNTHETIC_NAMESPACE}`,
25
+ '',
26
+ `This project's memory store. Every document here answers at \`${SYNTHETIC_NAMESPACE}/<local name>\`.`,
27
+ '',
28
+ ].join('\n');
29
+ export function planRepositories(corpus) {
30
+ const blockers = [];
31
+ const associated = [];
32
+ const groups = new Map();
33
+ const rootsInCorpusOrder = [];
34
+ for (const store of corpus.stores) {
35
+ if (!isNativeProjectStore(store))
36
+ continue;
37
+ const ownerDir = realpathOrSelf(ownerDirOf(store));
38
+ const result = associateRepository(ownerDir);
39
+ if (!result.ok) {
40
+ blockers.push({
41
+ kind: 'association',
42
+ message: `project memory store at ${store.key} owned by ${ownerDir} cannot be associated with a repository (${result.reason}) — repair the repository boundary and re-run`,
43
+ storeRoot: store.key,
44
+ });
45
+ continue;
46
+ }
47
+ const row = {
48
+ store,
49
+ ownerDir,
50
+ association: result.association,
51
+ rootDoc: store.snapshot.docs.find((doc) => localNameFromPath(doc.relPath) === '') ?? null,
52
+ };
53
+ associated.push(row);
54
+ const groupingKey = result.association.kind === 'git'
55
+ ? result.association.repositoryKey
56
+ : `path:${result.association.repositoryRoot}`;
57
+ let group = groups.get(groupingKey);
58
+ if (group === undefined) {
59
+ group = {
60
+ repositoryKey: result.association.repositoryKey,
61
+ roots: new Map(),
62
+ };
63
+ groups.set(groupingKey, group);
64
+ }
65
+ let root = group.roots.get(result.association.repositoryRoot);
66
+ if (root === undefined) {
67
+ root = {
68
+ association: result.association,
69
+ stores: [],
70
+ rootStore: null,
71
+ index: missingRootIndex(),
72
+ declaration: { state: 'missing' },
73
+ };
74
+ group.roots.set(result.association.repositoryRoot, root);
75
+ rootsInCorpusOrder.push(root);
76
+ }
77
+ root.stores.push(row);
78
+ if (result.association.ownerRelativePath === '' && root.rootStore === null)
79
+ root.rootStore = row;
80
+ }
81
+ const nestedByStore = new Map();
82
+ for (const row of associated) {
83
+ if (row.association.ownerRelativePath === '') {
84
+ nestedByStore.set(row.store.key, { declaration: { state: 'missing' }, action: 'none' });
85
+ continue;
86
+ }
87
+ const declaration = legacyNestedDeclarationFacts(row.rootDoc);
88
+ let action = 'none';
89
+ if (declaration.state === 'invalid') {
90
+ blockers.push(invalidDeclarationBlocker(row.store.key, row.rootDoc?.path ?? row.store.key, declaration, 'nested project memory store'));
91
+ }
92
+ else if (declaration.state === 'valid') {
93
+ action = row.rootDoc !== null && isSupersededSyntheticIndex(row.rootDoc.source, declaration.value)
94
+ ? 'synthetic-index-removed'
95
+ : 'removed';
96
+ }
97
+ nestedByStore.set(row.store.key, { declaration, action });
98
+ }
99
+ for (const root of rootsInCorpusOrder) {
100
+ root.index = readRootIndex(root);
101
+ root.declaration = root.index.error === null
102
+ ? declarationFacts(root.index.frontmatter?.['namespace'])
103
+ : { state: 'invalid', reason: root.index.error, raw: undefined };
104
+ if (root.declaration.state === 'invalid') {
105
+ blockers.push(invalidDeclarationBlocker(root.rootStore?.store.key ?? dirname(root.association.declarationPath), root.association.declarationPath, root.declaration, 'repository root'));
106
+ }
107
+ }
108
+ const selectedByRoot = new Map();
109
+ for (const group of groups.values())
110
+ selectGroup(group, selectedByRoot, blockers);
111
+ const repositories = [];
112
+ for (const root of rootsInCorpusOrder) {
113
+ const selected = selectedByRoot.get(root.association.repositoryRoot);
114
+ if (selected !== undefined)
115
+ repositories.push(selected.decision);
116
+ }
117
+ const prefixes = new Map();
118
+ for (const row of associated) {
119
+ const selected = selectedByRoot.get(row.association.repositoryRoot);
120
+ const nested = nestedByStore.get(row.store.key);
121
+ if (selected === undefined || nested.declaration.state === 'invalid')
122
+ continue;
123
+ const isRoot = row.association.ownerRelativePath === '';
124
+ const effectivePrefix = composeCanonicalName(selected.decision.namespace, row.association.ownerRelativePath);
125
+ const { oldPrefix, oldRule } = oldAuthority(row.store, isRoot, selected.declaration, nested.declaration, effectivePrefix);
126
+ prefixes.set(row.store.key, {
127
+ store: row.store,
128
+ storeKey: row.store.key,
129
+ storeRoot: row.store.key,
130
+ ownerDir: row.ownerDir,
131
+ repositoryKey: row.association.repositoryKey,
132
+ repositoryRoot: row.association.repositoryRoot,
133
+ ownerRelativePath: row.association.ownerRelativePath,
134
+ oldPrefix,
135
+ oldRule,
136
+ effectivePrefix,
137
+ nestedDeclarationAction: nested.action,
138
+ legacyDeclaration: !isRoot && nested.declaration.state === 'valid' ? nested.declaration.value : null,
139
+ });
140
+ }
141
+ addOwnerPathCollisionBlockers(associated, selectedByRoot, blockers);
142
+ return { repositories, prefixes, blockers };
143
+ }
144
+ /** What a store's documents answered under BEFORE this run, from file truth
145
+ * alone. A store's own valid declaration proves its old prefix outright.
146
+ *
147
+ * A nested store that declares nothing is in one of two states, and they are not
148
+ * the same authority: pre-003, where nothing up the tree declared anything and
149
+ * its documents answered at bare local names; or already cut, where the
150
+ * repository root's kept declaration is exactly what gave them their prefix. The
151
+ * kept root declaration plus the absence of any explicit `name:` this cut would
152
+ * have rewritten identifies the second — that pair is the corpus this migration
153
+ * leaves behind, so reading it as pre-003 would report an identity rename for
154
+ * every document that never moves. */
155
+ function oldAuthority(store, isRoot, rootDeclaration, nestedDeclaration, effectivePrefix) {
156
+ const own = isRoot ? rootDeclaration : nestedDeclaration;
157
+ if (own.state === 'valid')
158
+ return { oldPrefix: own.value, oldRule: 'per-store-declaration' };
159
+ if (!isRoot && rootDeclaration.state === 'valid' && !carriesLegacyExplicitIdentity(store, effectivePrefix)) {
160
+ return { oldPrefix: effectivePrefix, oldRule: 'per-store-declaration' };
161
+ }
162
+ return { oldPrefix: '', oldRule: 'pre-003' };
163
+ }
164
+ /** An explicit `name:` this cut never leaves behind: one on a store root
165
+ * document, or one that is not a store-local name under the store's own prefix —
166
+ * a fully qualified pre-003 name is the shape that matters. Either proves the
167
+ * store still carries pre-003 identity even though it declares nothing. */
168
+ function carriesLegacyExplicitIdentity(store, effectivePrefix) {
169
+ return store.snapshot.docs.some((doc) => {
170
+ const explicit = explicitLocalName(doc.frontmatter);
171
+ if (explicit === null)
172
+ return false;
173
+ return localNameFromPath(doc.relPath) === '' || !validateExplicitLocalName(explicit, effectivePrefix).ok;
174
+ });
175
+ }
176
+ function selectGroup(group, selectedByRoot, blockers) {
177
+ const roots = [...group.roots.values()];
178
+ if (roots.some((root) => root.declaration.state === 'invalid'))
179
+ return;
180
+ const declared = roots.filter((root) => root.declaration.state === 'valid');
181
+ const values = [...new Set(declared.map((root) => root.declaration.value))];
182
+ if (values.length > 1) {
183
+ const paths = declared.map((root) => root.association.declarationPath);
184
+ blockers.push({
185
+ kind: 'namespace',
186
+ message: `linked repository roots for ${group.repositoryKey} declare different namespaces (${values.map((value) => `\`${value}\``).join(', ')}) — make every root declaration agree and re-run`,
187
+ paths,
188
+ });
189
+ return;
190
+ }
191
+ if (values.length === 1) {
192
+ const namespace = values[0];
193
+ for (const root of roots) {
194
+ const action = root.declaration.state === 'valid' ? 'kept' : 'filled-linked-root';
195
+ selectedByRoot.set(root.association.repositoryRoot, {
196
+ declaration: root.declaration,
197
+ decision: repositoryDecision(group, root, namespace, 'declared', action),
198
+ });
199
+ }
200
+ return;
201
+ }
202
+ const derivingRoot = roots[0];
203
+ const derived = deriveProjectNamespace({
204
+ ownerDir: derivingRoot.association.repositoryRoot,
205
+ rootFrontmatter: derivingRoot.index.frontmatter,
206
+ ...(derivingRoot.index.exists ? { rootBody: derivingRoot.index.body } : {}),
207
+ });
208
+ if (!derived.ok) {
209
+ blockers.push({
210
+ kind: 'namespace',
211
+ message: `repository at ${derivingRoot.association.repositoryRoot} cannot derive a namespace (${derived.reason}) — declare a valid \`namespace:\` on ${derivingRoot.association.declarationPath} and re-run`,
212
+ storeRoot: derivingRoot.rootStore?.store.key ?? dirname(derivingRoot.association.declarationPath),
213
+ ...(derivingRoot.index.exists ? { path: derivingRoot.association.declarationPath } : {}),
214
+ });
215
+ return;
216
+ }
217
+ for (const root of roots) {
218
+ selectedByRoot.set(root.association.repositoryRoot, {
219
+ declaration: root.declaration,
220
+ decision: repositoryDecision(group, root, derived.namespace, derived.source, root.index.exists ? 'kept' : 'created'),
221
+ });
222
+ }
223
+ }
224
+ function repositoryDecision(group, root, namespace, source, action) {
225
+ return {
226
+ repositoryKey: group.repositoryKey,
227
+ repositoryRoot: root.association.repositoryRoot,
228
+ declarationPath: root.association.declarationPath,
229
+ namespace,
230
+ source,
231
+ action,
232
+ storeKeys: root.stores.map((row) => row.store.key),
233
+ };
234
+ }
235
+ function readRootIndex(root) {
236
+ const declarationRealPath = realpathOrSelf(root.association.declarationPath);
237
+ const declarationDoc = root.rootStore?.store.snapshot.docs.find((doc) => realpathOrSelf(doc.path) === declarationRealPath);
238
+ if (declarationDoc !== undefined) {
239
+ return {
240
+ exists: true,
241
+ frontmatter: declarationDoc.frontmatter,
242
+ body: declarationDoc.body,
243
+ error: null,
244
+ };
245
+ }
246
+ try {
247
+ const source = readTextIfExists(root.association.declarationPath);
248
+ if (source === null)
249
+ return missingRootIndex();
250
+ const parsed = parseFrontmatterGeneric(source);
251
+ return {
252
+ exists: true,
253
+ frontmatter: parsed.data,
254
+ body: parsed.body,
255
+ error: null,
256
+ };
257
+ }
258
+ catch (error) {
259
+ return {
260
+ exists: true,
261
+ frontmatter: null,
262
+ body: '',
263
+ error: `cannot read or parse ${root.association.declarationPath} (${firstLine(error)})`,
264
+ };
265
+ }
266
+ }
267
+ function missingRootIndex() {
268
+ return { exists: false, frontmatter: null, body: '', error: null };
269
+ }
270
+ function legacyNestedDeclarationFacts(rootDoc) {
271
+ if (rootDoc?.frontmatter === null
272
+ || rootDoc?.frontmatter === undefined
273
+ || !Object.prototype.hasOwnProperty.call(rootDoc.frontmatter, 'namespace')) {
274
+ return { state: 'missing' };
275
+ }
276
+ return authoredDeclarationFacts(rootDoc.frontmatter['namespace']);
277
+ }
278
+ function declarationFacts(raw) {
279
+ if (raw === undefined || raw === null || raw === '')
280
+ return { state: 'missing' };
281
+ return authoredDeclarationFacts(raw);
282
+ }
283
+ function authoredDeclarationFacts(raw) {
284
+ if (typeof raw !== 'string') {
285
+ return {
286
+ state: 'invalid',
287
+ reason: `\`namespace:\` is non-string (${JSON.stringify(raw)})`,
288
+ raw,
289
+ };
290
+ }
291
+ const valid = validateNamespace(raw);
292
+ return valid.ok
293
+ ? { state: 'valid', value: raw }
294
+ : { state: 'invalid', reason: `\`namespace: ${raw}\` is invalid (${valid.reason})`, raw };
295
+ }
296
+ function invalidDeclarationBlocker(storeRoot, path, declaration, subject) {
297
+ return {
298
+ kind: 'namespace',
299
+ message: `${subject} has an invalid namespace declaration at ${path} (${declaration.reason}) — correct it and re-run; migration never replaces an unprovable identity`,
300
+ storeRoot,
301
+ path,
302
+ ...(typeof declaration.raw === 'string' ? { name: declaration.raw } : {}),
303
+ };
304
+ }
305
+ function addOwnerPathCollisionBlockers(associated, selectedByRoot, blockers) {
306
+ const collisions = new Map();
307
+ for (const row of associated) {
308
+ const key = `${row.association.repositoryRoot}\u0000${row.association.ownerRelativePath}`;
309
+ const bucket = collisions.get(key);
310
+ if (bucket === undefined)
311
+ collisions.set(key, [row]);
312
+ else
313
+ bucket.push(row);
314
+ }
315
+ for (const rows of collisions.values()) {
316
+ if (rows.length < 2)
317
+ continue;
318
+ const first = rows[0];
319
+ const selected = selectedByRoot.get(first.association.repositoryRoot);
320
+ const effectivePrefix = selected === undefined
321
+ ? first.association.ownerRelativePath
322
+ : composeCanonicalName(selected.decision.namespace, first.association.ownerRelativePath);
323
+ const paths = rows.map((row) => row.ownerDir);
324
+ blockers.push({
325
+ kind: 'collision',
326
+ message: `directories ${paths.join(', ')} in repository ${first.association.repositoryRoot} normalize to the same owner path \`${first.association.ownerRelativePath}\` and effective prefix \`${effectivePrefix}\` — rename one directory and re-run`,
327
+ storeRoot: first.store.key,
328
+ name: effectivePrefix,
329
+ paths,
330
+ });
331
+ }
332
+ }
333
+ function isSupersededSyntheticIndex(source, namespace) {
334
+ return source === SYNTHETIC_INDEX_TEMPLATE
335
+ .replaceAll(SYNTHETIC_NAMESPACE_SCALAR, renderScalar(namespace))
336
+ .replaceAll(SYNTHETIC_NAMESPACE, namespace);
337
+ }
338
+ function renderScalar(value) {
339
+ return /^[A-Za-z][A-Za-z0-9._\-/]*$/.test(value) ? value : JSON.stringify(value);
340
+ }
341
+ function ownerDirOf(store) {
342
+ return store.descriptor.ownerDir ?? dirname(dirname(store.descriptor.storeRoot));
343
+ }
344
+ function isNativeProjectStore(store) {
345
+ return store.descriptor.scope === 'project' && store.descriptor.plugin === undefined;
346
+ }
347
+ function firstLine(error) {
348
+ return (error instanceof Error ? error.message : String(error)).split('\n')[0] ?? String(error);
349
+ }
@@ -0,0 +1,145 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync, mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import test from 'node:test';
8
+ import { withExclusiveDirectoryLockAsync } from '../../core/exclusive-lock.js';
9
+ import { activateOnDiskMigrations, runForcedOnDiskMigrations } from '../activation.js';
10
+ function result() {
11
+ return {
12
+ blockers: [],
13
+ finalVerificationSucceeded: true,
14
+ wrote: { profileManifests: [], documents: [], corpusConsumers: [], corpusIdentities: [] },
15
+ };
16
+ }
17
+ function deferred() {
18
+ let resolve;
19
+ const promise = new Promise((done) => { resolve = done; });
20
+ return { promise, resolve };
21
+ }
22
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
23
+ async function waitFor(check) {
24
+ const deadline = Date.now() + 2_000;
25
+ while (!check()) {
26
+ if (Date.now() >= deadline)
27
+ throw new Error('timed out waiting for migration race fixture');
28
+ await delay(5);
29
+ }
30
+ }
31
+ function childActivation(home, release) {
32
+ const moduleUrl = pathToFileURL(join(process.cwd(), 'src/migrations/activation.ts')).href;
33
+ const script = `
34
+ const { existsSync, writeFileSync } = await import('node:fs');
35
+ const { join } = await import('node:path');
36
+ const { activateOnDiskMigrations } = await import(${JSON.stringify(moduleUrl)});
37
+ await activateOnDiskMigrations({
38
+ timeoutMs: 5000,
39
+ runner: async () => {
40
+ writeFileSync(join(${JSON.stringify(home)}, 'entered.' + process.pid), '');
41
+ while (!existsSync(${JSON.stringify(release)})) await new Promise((resolve) => setTimeout(resolve, 5));
42
+ return { blockers: [], finalVerificationSucceeded: true, wrote: { profileManifests: [], documents: [], corpusConsumers: [], corpusIdentities: [] } };
43
+ },
44
+ });
45
+ `;
46
+ const child = spawn(process.execPath, ['--conditions=crtr-src', '--import', 'tsx/esm', '--input-type=module', '-e', script], {
47
+ cwd: process.cwd(),
48
+ env: { ...process.env, CRTR_HOME: home },
49
+ stdio: ['ignore', 'ignore', 'pipe'],
50
+ });
51
+ let stderr = '';
52
+ child.stderr?.setEncoding('utf8').on('data', (chunk) => { stderr += String(chunk); });
53
+ const done = new Promise((resolve, reject) => {
54
+ child.once('error', reject);
55
+ child.once('close', (code) => code === 0 ? resolve() : reject(new Error(`activation child exited ${code}: ${stderr}`)));
56
+ });
57
+ return done;
58
+ }
59
+ test('the asynchronous lock remains owned until its operation promise settles', async () => {
60
+ const home = mkdtempSync(join(tmpdir(), 'crtr-async-lock-'));
61
+ const lock = join(home, 'exclusive.lock');
62
+ const gate = deferred();
63
+ const entered = deferred();
64
+ let secondEntered = false;
65
+ try {
66
+ const first = withExclusiveDirectoryLockAsync(lock, async () => {
67
+ entered.resolve();
68
+ await gate.promise;
69
+ }, { timeoutMs: 1_000 });
70
+ await entered.promise;
71
+ assert.equal(existsSync(lock), true);
72
+ const second = withExclusiveDirectoryLockAsync(lock, async () => { secondEntered = true; }, { timeoutMs: 1_000 });
73
+ await delay(30);
74
+ assert.equal(secondEntered, false);
75
+ assert.equal(existsSync(lock), true);
76
+ gate.resolve();
77
+ await Promise.all([first, second]);
78
+ assert.equal(secondEntered, true);
79
+ assert.equal(existsSync(lock), false);
80
+ }
81
+ finally {
82
+ rmSync(home, { recursive: true, force: true });
83
+ }
84
+ });
85
+ test('activation and a forced migration serialize, and forced still runs after the marker write', async () => {
86
+ const home = mkdtempSync(join(tmpdir(), 'crtr-activation-forced-'));
87
+ const previous = process.env['CRTR_HOME'];
88
+ process.env['CRTR_HOME'] = home;
89
+ const gate = deferred();
90
+ const entered = deferred();
91
+ let calls = 0;
92
+ let active = 0;
93
+ let maxActive = 0;
94
+ const runner = async () => {
95
+ calls += 1;
96
+ active += 1;
97
+ maxActive = Math.max(maxActive, active);
98
+ try {
99
+ if (calls === 1) {
100
+ entered.resolve();
101
+ await gate.promise;
102
+ }
103
+ return result();
104
+ }
105
+ finally {
106
+ active -= 1;
107
+ }
108
+ };
109
+ try {
110
+ const activation = activateOnDiskMigrations({ timeoutMs: 1_000, runner });
111
+ await entered.promise;
112
+ const forced = runForcedOnDiskMigrations({ timeoutMs: 1_000, runner });
113
+ await delay(30);
114
+ assert.equal(calls, 1);
115
+ gate.resolve();
116
+ await Promise.all([activation, forced]);
117
+ assert.equal(calls, 2);
118
+ assert.equal(maxActive, 1);
119
+ }
120
+ finally {
121
+ if (previous === undefined)
122
+ delete process.env['CRTR_HOME'];
123
+ else
124
+ process.env['CRTR_HOME'] = previous;
125
+ rmSync(home, { recursive: true, force: true });
126
+ }
127
+ });
128
+ test('a waiting process rechecks the marker so only one process runs migration', async () => {
129
+ const home = mkdtempSync(join(tmpdir(), 'crtr-activation-process-race-'));
130
+ const release = join(home, 'release');
131
+ mkdirSync(home, { recursive: true });
132
+ try {
133
+ const first = childActivation(home, release);
134
+ await waitFor(() => readdirSync(home).some((entry) => entry.startsWith('entered.')));
135
+ const second = childActivation(home, release);
136
+ await delay(50);
137
+ assert.equal(readdirSync(home).filter((entry) => entry.startsWith('entered.')).length, 1);
138
+ writeFileSync(release, '');
139
+ await Promise.all([first, second]);
140
+ assert.equal(readdirSync(home).filter((entry) => entry.startsWith('entered.')).length, 1);
141
+ }
142
+ finally {
143
+ rmSync(home, { recursive: true, force: true });
144
+ }
145
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,149 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+ import { onDiskMigrationMarkerPath } from '../../core/canvas/paths.js';
7
+ import { activateOnDiskMigrations, runForcedOnDiskMigrations } from '../activation.js';
8
+ import { onDiskMigrationIdentity, STATE_MIGRATIONS } from '../registry.js';
9
+ function result(options = {}) {
10
+ const writes = options.writes ?? [];
11
+ return {
12
+ blockers: options.blockers ?? [],
13
+ finalVerificationSucceeded: options.verified ?? true,
14
+ wrote: { profileManifests: writes, documents: [], corpusConsumers: [], corpusIdentities: [] },
15
+ };
16
+ }
17
+ async function inHome(run) {
18
+ const home = mkdtempSync(join(tmpdir(), 'crtr-migration-activation-'));
19
+ const previous = process.env['CRTR_HOME'];
20
+ process.env['CRTR_HOME'] = home;
21
+ try {
22
+ await run(home);
23
+ }
24
+ finally {
25
+ if (previous === undefined)
26
+ delete process.env['CRTR_HOME'];
27
+ else
28
+ process.env['CRTR_HOME'] = previous;
29
+ rmSync(home, { recursive: true, force: true });
30
+ }
31
+ }
32
+ function marker() {
33
+ return JSON.parse(readFileSync(onDiskMigrationMarkerPath(), 'utf8'));
34
+ }
35
+ test('activation skips only an exact current marker', async () => {
36
+ await inHome(async () => {
37
+ let runs = 0;
38
+ const runner = async () => { runs += 1; return result(); };
39
+ writeFileSync(onDiskMigrationMarkerPath(), JSON.stringify(onDiskMigrationIdentity()));
40
+ assert.equal(await activateOnDiskMigrations({ timeoutMs: 1_000, runner }), null);
41
+ assert.equal(runs, 0);
42
+ for (const invalid of [undefined, '{broken', JSON.stringify(onDiskMigrationIdentity().slice(0, -1))]) {
43
+ rmSync(onDiskMigrationMarkerPath(), { force: true });
44
+ if (invalid !== undefined)
45
+ writeFileSync(onDiskMigrationMarkerPath(), invalid);
46
+ assert.notEqual(await activateOnDiskMigrations({ timeoutMs: 1_000, runner }), null);
47
+ }
48
+ assert.equal(runs, 3);
49
+ assert.deepEqual(marker(), onDiskMigrationIdentity());
50
+ });
51
+ });
52
+ // The repository-root cut renamed migration 003, and the rename is the whole
53
+ // invalidation mechanism: every home carrying the superseded identity must run
54
+ // the corpus lane again. These ids are written literally rather than through
55
+ // `onDiskMigrationIdentity()` so a rename that silently keeps the old marker
56
+ // valid fails here.
57
+ test('a superseded 003 identity reruns the corpus lane and the repository-root identity skips it', async () => {
58
+ await inHome(async () => {
59
+ let runs = 0;
60
+ const runner = async () => { runs += 1; return result(); };
61
+ writeFileSync(onDiskMigrationMarkerPath(), JSON.stringify([
62
+ { id: '001-surfaces-frontmatter', lane: 'convergent' },
63
+ { id: '002-profile-project-memory', lane: 'profile-manifest' },
64
+ { id: '003-workspace-qualified-memory-identity', lane: 'corpus' },
65
+ ]));
66
+ assert.notEqual(await activateOnDiskMigrations({ timeoutMs: 1_000, runner }), null, 'the superseded 003 identity must not satisfy activation');
67
+ assert.equal(runs, 1);
68
+ const repositoryRootIdentity = [
69
+ { id: '001-surfaces-frontmatter', lane: 'convergent' },
70
+ { id: '002-profile-project-memory', lane: 'profile-manifest' },
71
+ { id: '003-repository-root-memory-identity', lane: 'corpus' },
72
+ ];
73
+ assert.deepEqual(marker(), repositoryRootIdentity, 'the rerun records the repository-root identity in order');
74
+ writeFileSync(onDiskMigrationMarkerPath(), JSON.stringify(repositoryRootIdentity));
75
+ assert.equal(await activateOnDiskMigrations({ timeoutMs: 1_000, runner }), null);
76
+ assert.equal(runs, 1, 'the repository-root marker skips the run');
77
+ });
78
+ });
79
+ test('journaled database entries do not change the on-disk marker identity', () => {
80
+ const before = onDiskMigrationIdentity();
81
+ const registry = STATE_MIGRATIONS;
82
+ registry.push({ lane: 'journaled', description: 'test database migration', apply: () => undefined });
83
+ try {
84
+ assert.deepEqual(onDiskMigrationIdentity(), before);
85
+ }
86
+ finally {
87
+ registry.pop();
88
+ }
89
+ });
90
+ test('forced runs ignore current markers while dry runs leave marker bytes untouched', async () => {
91
+ await inHome(async () => {
92
+ const current = JSON.stringify(onDiskMigrationIdentity());
93
+ writeFileSync(onDiskMigrationMarkerPath(), current);
94
+ let runs = 0;
95
+ await runForcedOnDiskMigrations({
96
+ timeoutMs: 1_000,
97
+ runner: async () => { runs += 1; return result(); },
98
+ });
99
+ assert.equal(runs, 1);
100
+ assert.deepEqual(marker(), onDiskMigrationIdentity());
101
+ writeFileSync(onDiskMigrationMarkerPath(), '{malformed but preserved');
102
+ await runForcedOnDiskMigrations({
103
+ timeoutMs: 1_000,
104
+ dryRun: true,
105
+ runner: async () => { runs += 1; return result({ verified: false }); },
106
+ });
107
+ assert.equal(readFileSync(onDiskMigrationMarkerPath(), 'utf8'), '{malformed but preserved');
108
+ assert.equal(runs, 2);
109
+ });
110
+ });
111
+ test('failure leaves the marker absent and a later activation converges', async () => {
112
+ await inHome(async () => {
113
+ await assert.rejects(activateOnDiskMigrations({ timeoutMs: 1_000, runner: async () => { throw new Error('interrupted'); } }), /interrupted/);
114
+ assert.equal(existsSync(onDiskMigrationMarkerPath()), false);
115
+ await activateOnDiskMigrations({ timeoutMs: 1_000, runner: async () => result() });
116
+ assert.deepEqual(marker(), onDiskMigrationIdentity());
117
+ });
118
+ });
119
+ test('an unreadable profile blocker aborts startup effects with its path and repair command', async () => {
120
+ await inHome(async (home) => {
121
+ const blockedPath = join(home, 'profiles', 'bad-deadbeef', 'profile.json');
122
+ const writtenPath = join(home, 'profiles', 'root', 'profile.json');
123
+ const effects = { database: false, pidfile: false, api: false, broker: false };
124
+ const startup = async () => {
125
+ await activateOnDiskMigrations({
126
+ timeoutMs: 1_000,
127
+ runner: async () => result({
128
+ verified: false,
129
+ writes: [writtenPath],
130
+ blockers: [{ kind: 'parse-failure', message: 'profile manifest is not valid JSON', path: blockedPath }],
131
+ }),
132
+ });
133
+ effects.database = true;
134
+ effects.pidfile = true;
135
+ effects.api = true;
136
+ effects.broker = true;
137
+ };
138
+ await assert.rejects(startup(), (error) => {
139
+ assert.ok(error instanceof Error);
140
+ assert.match(error.message, /on-disk migration blocked by 1 finding/);
141
+ assert.match(error.message, new RegExp(blockedPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
142
+ assert.match(error.message, new RegExp(writtenPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
143
+ assert.match(error.message, /Repair: crtr sys migrate/);
144
+ return true;
145
+ });
146
+ assert.deepEqual(effects, { database: false, pidfile: false, api: false, broker: false });
147
+ assert.equal(existsSync(onDiskMigrationMarkerPath()), false);
148
+ });
149
+ });