@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
@@ -1,26 +1,30 @@
1
- // Regression matrix for `resolveMemoryDoc`'s precedence rule: exact identity
2
- // and direct physical-path matches are resolved NEAREST-FIRST across the whole
3
- // scope stack before bare-leaf fallback is considered. This prevents a nested
4
- // doc's coincidental leaf from hiding a farther canonical identity while still
5
- // ensuring a nearer worktree's own physical `AGENTS.md` beats a farther doc
6
- // named exactly `AGENTS`, even when the nearer file's explicit frontmatter
7
- // identity has diverged from its physical filename.
1
+ // Regression matrix for exact canonical memory identity: a project store
2
+ // declares a namespace and every document in it answers ONLY at
3
+ // `<namespace>/<local name>`. Nothing else addresses it — not the old
4
+ // unqualified name, not the physical path, not a unique leaf, not a
5
+ // scope-prefixed spelling, and not a trailing `/INDEX` (a directory's own
6
+ // document answers at the directory name). Linked worktrees that declare the
7
+ // same repository namespace expose the same canonical identities: source order
8
+ // picks the address winner, every candidate stays ordered and reachable through
9
+ // its exact store.
8
10
  //
9
- // Fixture shape: two nested project scopes on disk —
10
- // root/.crouter/memory/... (farther project scope)
11
- // root/child/.crouter/memory/... (nearer project scope; cwd lives here)
12
- // `findProjectScopeRoots` walks every ancestor `.crouter/` from cwd upward, so
13
- // from `root/child` the resolved stack is exactly [child, root], nearest first
14
- // — no profile indirection needed to exercise multi-scope precedence.
11
+ // Fixture shape: one repository's main checkout plus a linked worktree, both
12
+ // declaring `namespace: root`. The cwd is the linked worktree and the main
13
+ // checkout enters the precedence stack through the selected profile:
14
+ // main/.crouter/memory/... (farther store, namespace root)
15
+ // linked/.crouter/memory/... (nearer store, namespace root; cwd)
16
+ // linked/grandchild/.crouter/memory/... (metadata-free nested store)
15
17
  //
16
- // Run: node --import tsx/esm --test src/core/__tests__/memory-resolver-precedence.test.ts
18
+ // Run: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/memory-resolver-precedence.test.ts
17
19
  import { test, before, beforeEach, after } from 'node:test';
18
20
  import assert from 'node:assert/strict';
19
21
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync, realpathSync } from 'node:fs';
20
22
  import { tmpdir } from 'node:os';
21
23
  import { join } from 'node:path';
24
+ import { gitSync } from '../git.js';
25
+ import { createProfile } from '../profiles/manifest.js';
22
26
  import { resetScopeCache } from '../scope.js';
23
- import { resolveMemoryDoc, resolveMemoryDocs } from '../memory-resolver.js';
27
+ import { loadMemoryStoreView, openProjectMemoryStore, resolveMemoryCandidates, resolveMemoryDoc, resolveMemoryDocInStore, resolveMemoryDocs, } from '../memory-resolver.js';
24
28
  let home;
25
29
  let base;
26
30
  let root;
@@ -28,42 +32,58 @@ let child;
28
32
  let grandchild;
29
33
  let prevCwd;
30
34
  let prevHome;
31
- function writeDoc(path, body, frontmatterName) {
35
+ let prevProfile;
36
+ function writeDoc(path, body, frontmatter) {
32
37
  mkdirSync(join(path, '..'), { recursive: true });
33
- const fm = frontmatterName ? `---\nname: ${frontmatterName}\n---\n` : '';
38
+ const fm = frontmatter ? `---\n${frontmatter}\n---\n` : '';
34
39
  writeFileSync(path, `${fm}${body}\n`, 'utf8');
35
40
  }
41
+ function initWorktreeRepository(main, linked) {
42
+ mkdirSync(main, { recursive: true });
43
+ assert.equal(gitSync(['-C', main, 'init', '-q'], undefined).status, 0);
44
+ writeFileSync(join(main, 'README.md'), 'fixture\n', 'utf8');
45
+ assert.equal(gitSync(['-C', main, 'add', 'README.md'], undefined).status, 0);
46
+ assert.equal(gitSync(['-C', main, '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.com', 'commit', '-qm', 'initial'], undefined).status, 0);
47
+ assert.equal(gitSync(['-C', main, 'worktree', 'add', '-q', '-b', 'linked', linked], undefined).status, 0);
48
+ }
49
+ function assertNotFound(name) {
50
+ assert.throws(() => resolveMemoryDoc(name), (err) => err.code === 'not_found', `${name} must not resolve`);
51
+ }
36
52
  before(() => {
37
53
  home = mkdtempSync(join(tmpdir(), 'crtr-resolver-precedence-home-'));
38
54
  prevHome = process.env.HOME;
55
+ prevProfile = process.env.CRTR_PROFILE_ID;
39
56
  process.env.HOME = home;
40
- // A farther user-scope preference doc, shared across every test below (the
41
- // fixture for the --kind shadowing regression case).
57
+ // A wider user-scope doc whose name a project doc used to shadow.
42
58
  mkdirSync(join(home, '.crouter', 'memory'), { recursive: true });
43
59
  writeFileSync(join(home, '.crouter', 'memory', 'testing-stance.md'), '---\nkind: preference\n---\nUSER PREFERENCE BODY\n', 'utf8');
44
60
  });
45
61
  beforeEach(() => {
46
62
  resetScopeCache();
47
63
  base = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-resolver-precedence-')));
48
- root = join(base, 'root');
49
- child = join(root, 'child');
64
+ root = join(base, 'main');
65
+ child = join(base, 'linked');
50
66
  grandchild = join(child, 'grandchild');
51
- mkdirSync(root, { recursive: true });
52
- mkdirSync(child, { recursive: true });
67
+ initWorktreeRepository(root, child);
53
68
  mkdirSync(grandchild, { recursive: true });
54
- // --- root (FARTHER project scope) ---
55
- writeDoc(join(root, '.crouter', 'memory', 'AGENTS.md'), 'ROOT AGENTS BODY'); // derived name "AGENTS"
56
- writeDoc(join(root, '.crouter', 'memory', 'shared.md'), 'ROOT SHARED BODY'); // derived name "shared"
57
- writeDoc(join(root, '.crouter', 'memory', 'taste', 'document-substrate.md'), 'ROOT TASTE BODY'); // derived "taste/document-substrate"
58
- writeDoc(join(root, '.crouter', 'memory', 'taste', 'INDEX.md'), 'ROOT TASTE INDEX'); // derived "taste/INDEX"
59
- // --- child (NEARER project scope; cwd resolves here) ---
60
- writeDoc(join(child, '.crouter', 'memory', 'AGENTS.md'), 'CHILD AGENTS BODY', 'crouter'); // physical AGENTS.md, identity diverged to "crouter"
61
- writeDoc(join(child, '.crouter', 'memory', 'sub', 'shared.md'), 'CHILD SUB SHARED BODY'); // derived "sub/shared" — leaf "shared"
62
- writeDoc(join(child, '.crouter', 'memory', 'taste', 'document-substrate.md'), 'CHILD TASTE BODY'); // derived "taste/document-substrate"
63
- writeDoc(join(child, '.crouter', 'memory', 'taste', 'INDEX.md'), 'CHILD TASTE INDEX'); // derived "taste/INDEX"
64
- writeDoc(join(child, '.crouter', 'memory', 'testing-stance.md'), 'CHILD TESTING STANCE BODY'); // frontmatter-less, kind defaults to "knowledge"
65
- // --- grandchild (nearest still; own migrated slashed identity) ---
66
- writeDoc(join(grandchild, '.crouter', 'memory', 'AGENTS.md'), 'GRANDCHILD SUBDIR BODY', 'root/subdir');
69
+ // --- main (FARTHER store, namespace root) ---
70
+ writeDoc(join(root, '.crouter', 'memory', 'INDEX.md'), 'ROOT STORE INDEX', 'namespace: root');
71
+ writeDoc(join(root, '.crouter', 'memory', 'shared.md'), 'ROOT SHARED BODY'); // root/shared
72
+ writeDoc(join(root, '.crouter', 'memory', 'taste', 'INDEX.md'), 'ROOT TASTE INDEX'); // root/taste
73
+ writeDoc(join(root, '.crouter', 'memory', 'taste', 'document-substrate.md'), 'ROOT TASTE BODY');
74
+ writeDoc(join(root, '.crouter', 'memory', 'dup.md'), 'ROOT DUP PREFERENCE', 'kind: preference'); // root/dup
75
+ // --- linked (NEARER store; the same repository namespace means its docs are
76
+ // alternate physical candidates for the SAME canonical identities) ---
77
+ writeDoc(join(child, '.crouter', 'memory', 'INDEX.md'), 'CHILD STORE INDEX', 'namespace: root');
78
+ writeDoc(join(child, '.crouter', 'memory', 'dup.md'), 'CHILD DUP KNOWLEDGE'); // root/dup
79
+ writeDoc(join(child, '.crouter', 'memory', 'taste', 'document-substrate.md'), 'CHILD TASTE BODY');
80
+ writeDoc(join(child, '.crouter', 'memory', 'testing-stance.md'), 'CHILD TESTING STANCE BODY');
81
+ writeDoc(join(child, '.crouter', 'memory', 'AGENTS.md'), 'CHILD AGENTS BODY', 'name: notes/agents');
82
+ // --- grandchild (metadata-free nested store: identity comes from linked's
83
+ // repository-root declaration) ---
84
+ writeDoc(join(grandchild, '.crouter', 'memory', 'orphan.md'), 'ORPHAN BODY');
85
+ process.env.CRTR_PROFILE_ID = createProfile('precedence', [{ path: root, memory: 'content' }]).profileId;
86
+ resetScopeCache();
67
87
  prevCwd = process.cwd();
68
88
  });
69
89
  after(() => {
@@ -75,119 +95,130 @@ after(() => {
75
95
  delete process.env.HOME;
76
96
  else
77
97
  process.env.HOME = prevHome;
98
+ if (prevProfile === undefined)
99
+ delete process.env.CRTR_PROFILE_ID;
100
+ else
101
+ process.env.CRTR_PROFILE_ID = prevProfile;
78
102
  });
79
- test('a physical-path query whose nearest match diverged in identity from a farther same-derived-name doc resolves to the NEAREST physical file (the AGENTS shadowing case)', () => {
103
+ test('a project doc answers only at its full canonical name', () => {
80
104
  process.chdir(child);
81
105
  resetScopeCache();
82
106
  try {
83
- const byPhysicalName = resolveMemoryDoc('AGENTS');
84
- assert.equal(byPhysicalName.path, join(child, '.crouter', 'memory', 'AGENTS.md'));
85
- assert.match(byPhysicalName.body, /CHILD AGENTS BODY/);
86
- const byExplicitIdentity = resolveMemoryDoc('crouter');
87
- assert.equal(byExplicitIdentity.path, join(child, '.crouter', 'memory', 'AGENTS.md'));
88
- assert.match(byExplicitIdentity.body, /CHILD AGENTS BODY/);
107
+ const doc = resolveMemoryDoc('root/taste/document-substrate');
108
+ assert.equal(doc.path, join(child, '.crouter', 'memory', 'taste', 'document-substrate.md'));
109
+ assert.equal(doc.localName, 'taste/document-substrate');
110
+ assert.match(doc.body, /CHILD TASTE BODY/);
111
+ assertNotFound('taste/document-substrate'); // the old unqualified project name
112
+ assertNotFound('document-substrate'); // a unique leaf
113
+ assertNotFound('project/root/taste/document-substrate'); // a scope-prefixed spelling
114
+ assertNotFound('root/taste/document-substrate.md'); // a physical path spelling
89
115
  }
90
116
  finally {
91
117
  process.chdir(prevCwd);
92
118
  }
93
119
  });
94
- test('batch resolution preserves direct-path precedence over a farther matching identity', () => {
120
+ test("a directory's own document answers at the directory name, a store root at its namespace", () => {
95
121
  process.chdir(child);
96
122
  resetScopeCache();
97
123
  try {
98
- const doc = resolveMemoryDocs(['AGENTS']).get('AGENTS');
99
- assert.equal(doc?.path, join(child, '.crouter', 'memory', 'AGENTS.md'));
100
- assert.match(doc?.body ?? '', /CHILD AGENTS BODY/);
124
+ const dir = resolveMemoryDoc('root/taste');
125
+ assert.equal(dir.path, join(root, '.crouter', 'memory', 'taste', 'INDEX.md'));
126
+ assert.equal(dir.representation, 'directory-index');
127
+ assert.equal(dir.routingAnchor, 'root/taste');
128
+ assert.match(dir.body, /ROOT TASTE INDEX/);
129
+ const storeRoot = resolveMemoryDoc('root');
130
+ assert.equal(storeRoot.path, join(child, '.crouter', 'memory', 'INDEX.md'));
131
+ assert.equal(storeRoot.localName, '');
132
+ assert.match(storeRoot.body, /CHILD STORE INDEX/);
133
+ assertNotFound('root/taste/INDEX');
134
+ assertNotFound('root/INDEX');
101
135
  }
102
136
  finally {
103
137
  process.chdir(prevCwd);
104
138
  }
105
139
  });
106
- test('a migrated nested doc resolves by its explicit slashed identity', () => {
107
- process.chdir(grandchild);
140
+ test('a namespaced project doc no longer shadows a wider unqualified name', () => {
141
+ process.chdir(child);
108
142
  resetScopeCache();
109
143
  try {
110
- const doc = resolveMemoryDoc('root/subdir');
111
- assert.equal(doc.path, join(grandchild, '.crouter', 'memory', 'AGENTS.md'));
112
- assert.equal(doc.name, 'root/subdir');
113
- assert.match(doc.body, /GRANDCHILD SUBDIR BODY/);
144
+ const wider = resolveMemoryDoc('testing-stance');
145
+ assert.equal(wider.scope, 'user');
146
+ assert.match(wider.body, /USER PREFERENCE BODY/);
147
+ const project = resolveMemoryDoc('root/testing-stance');
148
+ assert.equal(project.scope, 'project');
149
+ assert.match(project.body, /CHILD TESTING STANCE BODY/);
114
150
  }
115
151
  finally {
116
152
  process.chdir(prevCwd);
117
153
  }
118
154
  });
119
- test('a plain path-named doc with no explicit name resolves to the nearest copy', () => {
155
+ test('equal canonical identities keep every candidate in source order and stay exact-store targetable', () => {
120
156
  process.chdir(child);
121
157
  resetScopeCache();
122
158
  try {
123
- const doc = resolveMemoryDoc('taste/document-substrate');
124
- assert.equal(doc.path, join(child, '.crouter', 'memory', 'taste', 'document-substrate.md'));
125
- assert.match(doc.body, /CHILD TASTE BODY/);
159
+ const set = resolveMemoryCandidates('root/dup');
160
+ assert.deepEqual(set.candidates.map((d) => d.path), [join(child, '.crouter', 'memory', 'dup.md'), join(root, '.crouter', 'memory', 'dup.md')], 'nearest-first candidate order, both linked-worktree stores retained');
161
+ assert.equal(set.winner.path, join(child, '.crouter', 'memory', 'dup.md'));
162
+ const store = openProjectMemoryStore(root);
163
+ assert.equal(store.namespace, 'root');
164
+ assert.equal(store.mountStatus, 'ready');
165
+ const nonWinner = resolveMemoryDocInStore('root/dup', store);
166
+ assert.equal(nonWinner.path, join(root, '.crouter', 'memory', 'dup.md'));
167
+ assert.match(nonWinner.body, /ROOT DUP PREFERENCE/);
126
168
  }
127
169
  finally {
128
170
  process.chdir(prevCwd);
129
171
  }
130
172
  });
131
- test('a bare-directory query does not resolve to a doc — directories answer with listings, not INDEX.md', () => {
173
+ test('--kind is a constraint across equal-canonical candidates, not a post-filter', () => {
132
174
  process.chdir(child);
133
175
  resetScopeCache();
134
176
  try {
135
- assert.throws(() => resolveMemoryDoc('taste'), (err) => {
136
- assert.equal(err.code, 'not_found');
137
- return true;
138
- });
177
+ // The nearer candidate is frontmatter-less (kind defaults to knowledge) and
178
+ // must be SKIPPED, not returned and not not_found.
179
+ const doc = resolveMemoryDoc('root/dup', { kind: 'preference' });
180
+ assert.equal(doc.path, join(root, '.crouter', 'memory', 'dup.md'));
181
+ assert.match(doc.body, /ROOT DUP PREFERENCE/);
139
182
  }
140
183
  finally {
141
184
  process.chdir(prevCwd);
142
185
  }
143
186
  });
144
- test("an explicit '<dir>/INDEX' identity resolves to that physical INDEX.md", () => {
145
- // The assembled memory tree addresses a directory INDEX by its extensionless
146
- // resolver identity, so `<dir>/INDEX` must resolve to the physical INDEX.md
147
- // and report back the same identifier it was asked for.
187
+ test('an explicit frontmatter name is store-local and composes under the namespace', () => {
148
188
  process.chdir(child);
149
189
  resetScopeCache();
150
190
  try {
151
- const doc = resolveMemoryDoc('taste/INDEX');
152
- assert.equal(doc.path, join(child, '.crouter', 'memory', 'taste', 'INDEX.md'));
153
- assert.equal(doc.name, 'taste/INDEX');
154
- assert.match(doc.body, /CHILD TASTE INDEX/);
191
+ const doc = resolveMemoryDoc('root/notes/agents');
192
+ assert.equal(doc.path, join(child, '.crouter', 'memory', 'AGENTS.md'));
193
+ assert.equal(doc.localName, 'notes/agents');
194
+ assertNotFound('notes/agents'); // the explicit name is not an address by itself
195
+ assertNotFound('root/AGENTS'); // nor is the physical filename
155
196
  }
156
197
  finally {
157
198
  process.chdir(prevCwd);
158
199
  }
159
200
  });
160
- test('--kind skips a nearer wrong-kind (frontmatter-less, kind-defaulted) doc and resolves the farther matching-kind doc', () => {
161
- process.chdir(child);
201
+ test('a metadata-free nested project store mounts under its derived prefix', () => {
202
+ process.chdir(grandchild);
162
203
  resetScopeCache();
163
204
  try {
164
- // No --kind: nearest scope's frontmatter-less copy wins, as always.
165
- const noKind = resolveMemoryDoc('testing-stance');
166
- assert.equal(noKind.scope, 'project');
167
- assert.match(noKind.body, /CHILD TESTING STANCE BODY/);
168
- // --kind preference: the nearer project doc defaults to kind "knowledge"
169
- // and must be SKIPPED (not returned, not not_found) so resolution keeps
170
- // walking outward to the farther user-scope doc that actually carries
171
- // kind: preference.
172
- const withKind = resolveMemoryDoc('testing-stance', { kind: 'preference' });
173
- assert.equal(withKind.scope, 'user');
174
- assert.match(withKind.body, /USER PREFERENCE BODY/);
205
+ const store = openProjectMemoryStore(grandchild);
206
+ assert.equal(store.mountStatus, 'ready');
207
+ assert.equal(store.namespace, 'root/grandchild');
208
+ assert.equal(resolveMemoryDoc('root/grandchild/orphan').path, join(grandchild, '.crouter', 'memory', 'orphan.md'));
209
+ assert.deepEqual(loadMemoryStoreView(store, true).docs.map((d) => d.name), ['root/grandchild/orphan'], 'exact-store inspection uses the same derived identity');
175
210
  }
176
211
  finally {
177
212
  process.chdir(prevCwd);
178
213
  }
179
214
  });
180
- test('an exact identity match beats a nearer document reached only by leaf fallback', () => {
215
+ test('batch resolution takes canonical names only', () => {
181
216
  process.chdir(child);
182
217
  resetScopeCache();
183
218
  try {
184
- // Nearest scope's doc lives at "sub/shared.md" (derived name "sub/shared",
185
- // leaf "shared"); the farther scope's doc lives at top-level "shared.md"
186
- // (derived name "shared", an exact identity match for the bare query).
187
- const doc = resolveMemoryDoc('shared');
188
- assert.equal(doc.path, join(root, '.crouter', 'memory', 'shared.md'));
189
- assert.equal(doc.name, 'shared');
190
- assert.match(doc.body, /ROOT SHARED BODY/);
219
+ const resolved = resolveMemoryDocs(['root/shared', 'shared']);
220
+ assert.equal(resolved.get('root/shared')?.path, join(root, '.crouter', 'memory', 'shared.md'));
221
+ assert.equal(resolved.get('shared'), undefined);
191
222
  }
192
223
  finally {
193
224
  process.chdir(prevCwd);
@@ -72,16 +72,18 @@ test('non-git owner: the walk finds a store at the depth cap and stops below it'
72
72
  test('resolveMemoryDocForTarget resolves a nested doc only with includeDescendants', () => {
73
73
  const proj = join(fakeHome, 'proj');
74
74
  mkdirSync(join(proj, '.crouter', 'memory'), { recursive: true }); // project scope engages
75
+ writeFileSync(join(proj, '.crouter', 'memory', 'INDEX.md'), '---\nnamespace: proj\n---\nPROJ STORE\n');
75
76
  const nestedMemory = join(proj, 'sub', '.crouter', 'memory');
76
77
  mkdirSync(nestedMemory, { recursive: true });
78
+ writeFileSync(join(nestedMemory, 'INDEX.md'), '# nested store\n');
77
79
  writeFileSync(join(nestedMemory, 'guide.md'), '---\nkind: knowledge\n' +
78
80
  'name: nested-guide\n' +
79
81
  'when-and-why-to-read: When working in sub, this knowledge should be read because it carries the subsystem constraints.\n---\n' +
80
82
  'NESTED GUIDE BODY\n');
81
83
  const target = { cwd: proj, profileId: null, nodeId: null };
82
- assert.throws(() => resolveMemoryDocForTarget('nested-guide', target, {}), (e) => e instanceof CrtrError && e.code === 'not_found', 'without the opt-in the flat ancestor stack does not see the nested store');
83
- const doc = resolveMemoryDocForTarget('nested-guide', target, { includeDescendants: true });
84
- assert.equal(doc.name, 'nested-guide', 'resolves by explicit frontmatter name');
84
+ assert.throws(() => resolveMemoryDocForTarget('proj/sub/nested-guide', target, {}), (e) => e instanceof CrtrError && e.code === 'not_found', 'without the opt-in the flat ancestor stack does not see the nested store');
85
+ const doc = resolveMemoryDocForTarget('proj/sub/nested-guide', target, { includeDescendants: true });
86
+ assert.equal(doc.name, 'proj/sub/nested-guide', 'the root namespace composes with the nested owner path and local name');
85
87
  assert.equal(doc.scope, 'project');
86
88
  assert.ok(doc.body.includes('NESTED GUIDE BODY'));
87
89
  });
@@ -14,6 +14,7 @@ import { closeDb } from '../canvas/db.js';
14
14
  import { resetScopeCache } from '../scope.js';
15
15
  import { spawnNode } from '../runtime/nodes.js';
16
16
  import { renderOnReadDocs } from '../substrate/on-read-node.js';
17
+ import { emptyContextExposureState, exposureTarget } from '../substrate/injected-store.js';
17
18
  let canvasHome;
18
19
  let fakeHome;
19
20
  let prevHomeEnv;
@@ -58,7 +59,7 @@ test('a store nested in the crouter home does not leak into a canvas-artifact re
58
59
  const reportFile = join(fakeHome, '.crouter', 'canvas', 'nodes', 'somenode', 'reports', 'r.md');
59
60
  mkdirSync(join(fakeHome, '.crouter', 'canvas', 'nodes', 'somenode', 'reports'), { recursive: true });
60
61
  writeFileSync(reportFile, '# a node report\n');
61
- const rendered = renderOnReadDocs(node, reportFile, new Map());
62
+ const rendered = renderOnReadDocs(node, reportFile, exposureTarget(emptyContextExposureState(), 'transcript'));
62
63
  assert.ok(!rendered.includes('PRIVATE ASSISTANT MEMORY MUST NOT LEAK'), `crouter-home-nested store must not surface on a canvas read; got: ${rendered}`);
63
64
  assert.ok(!rendered.includes('private-thing'), `crouter-home-nested store name must not surface on a canvas read; got: ${rendered}`);
64
65
  });
@@ -1,8 +1,8 @@
1
- // On-read context surfaces at most once per conversation. The dedup set lives
2
- // at nodes/<id>/injected-docs.json so a process resumed onto the same transcript
3
- // rehydrates it, while a fresh transcript clears it. The same loader resolves a
4
- // doc's identity with the explicit-name-then-path-fallback rule the boot resolver
5
- // uses (schema.ts's resolveDocName), never deriving `name` from the path alone.
1
+ // On-read context surfaces once per loaded context at the requested rung. The
2
+ // exposure ledger lives at nodes/<id>/injected-docs.json, survives a strict
3
+ // resume, and resets at a fresh-context boundary. A project store delivers
4
+ // only once it declares its namespace, and an explicit frontmatter `name` is
5
+ // store-local: the injected envelope carries the composed canonical name.
6
6
  //
7
7
  // Run: node --import tsx/esm --test src/core/__tests__/on-read-dedup-resume.test.ts
8
8
  import { test, before, beforeEach, after } from 'node:test';
@@ -14,7 +14,18 @@ import { closeDb } from '../canvas/db.js';
14
14
  import { resetScopeCache } from '../scope.js';
15
15
  import { spawnNode } from '../runtime/nodes.js';
16
16
  import { renderOnReadDocs } from '../substrate/on-read-node.js';
17
- import { loadInjectedDocs, saveInjectedDocs, clearInjectedDocs, } from '../substrate/injected-store.js';
17
+ import { clearContextExposureState, emptyContextExposureState, exposureTarget, loadContextExposureState, saveContextExposureState, } from '../substrate/injected-store.js';
18
+ function freshTranscriptTarget() {
19
+ return exposureTarget(emptyContextExposureState(), 'transcript');
20
+ }
21
+ /** A repository-root project store mounts only once its root INDEX declares
22
+ * the namespace its documents compose under. */
23
+ function declareRepositoryRoot(memDir) {
24
+ mkdirSync(memDir, { recursive: true });
25
+ writeFileSync(join(memDir, 'INDEX.md'), '---\nnamespace: work\nkind: knowledge\n' +
26
+ 'when-and-why-to-read: When working in this workspace, this knowledge should be read because it is the front door.\n---\n' +
27
+ 'FRONT DOOR BODY\n');
28
+ }
18
29
  const FIXTURE_BODY = 'Fixture body that must surface exactly once.';
19
30
  let home;
20
31
  let work;
@@ -56,25 +67,25 @@ test('project and user memory docs without read surfaces do not fire on file rea
56
67
  'when-and-why-to-read: When reading any file, this preference should be read because this fixture must not fire without an explicit route\n---\n' +
57
68
  'NOISY USER BODY\n');
58
69
  const projectMemDir = join(fakeHome, 'work', '.crouter', 'memory');
59
- mkdirSync(projectMemDir, { recursive: true });
70
+ declareRepositoryRoot(projectMemDir);
60
71
  writeFileSync(join(projectMemDir, 'local.md'), '---\nkind: knowledge\n' +
61
72
  'when-and-why-to-read: When reading project files, this reference should be read because this fixture proves explicit file routing\n---\n' +
62
73
  'LOCAL PROJECT BODY\n');
63
74
  const readFile = join(fakeHome, 'work', 'ideas.md');
64
75
  writeFileSync(readFile, 'turn off xml closer\n');
65
- const rendered = renderOnReadDocs(node, readFile, new Map());
76
+ const rendered = renderOnReadDocs(node, readFile, freshTranscriptTarget());
66
77
  assert.ok(!rendered.includes('NOISY USER BODY'), 'user-global doc has no explicit file route');
67
78
  assert.ok(!rendered.includes('LOCAL PROJECT BODY'), 'project doc also has no implicit file route');
68
79
  assert.equal(rendered, '', 'no file context renders without an explicit trigger');
69
80
  rmSync(fakeHome, { recursive: true, force: true });
70
81
  });
71
- test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfaces after a fresh launch', () => {
82
+ test('on-read exposure survives a strict resume and resets for a fresh context', () => {
72
83
  const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
73
84
  // A file-routed substrate doc in a `.crouter/memory/` ancestor. The
74
85
  // `./`-anchored glob pins the repo-relative anchor: `src/file.ts` matches
75
86
  // only relative to the doc's owning repo dir, never absolutely.
76
87
  const memDir = join(work, '.crouter', 'memory');
77
- mkdirSync(memDir, { recursive: true });
88
+ declareRepositoryRoot(memDir);
78
89
  writeFileSync(join(memDir, 'onread-fixture.md'), '---\nkind: knowledge\n' +
79
90
  'when-and-why-to-read: When reading work files, this reference should be read because it is the on-read regression fixture\n' +
80
91
  'surfaces:\n' +
@@ -84,25 +95,26 @@ test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfa
84
95
  mkdirSync(join(work, 'src'), { recursive: true });
85
96
  writeFileSync(readFile, 'export const x = 1;\n');
86
97
  // --- pi process #1: cold load; first read surfaces the doc ---
87
- const seen = loadInjectedDocs(node);
88
- const first = renderOnReadDocs(node, readFile, seen);
98
+ const state = loadContextExposureState(node);
99
+ const transcript = exposureTarget(state, 'transcript');
100
+ const first = renderOnReadDocs(node, readFile, transcript);
89
101
  assert.match(first, new RegExp(FIXTURE_BODY.replace(/[.]/g, '\\.')), 'first read surfaces the doc body');
90
- saveInjectedDocs(node, seen);
91
- // Same process, repeat read of the same file → deduped in-memory.
92
- assert.ok(!renderOnReadDocs(node, readFile, seen).includes(FIXTURE_BODY), 'same-process repeat read is deduped');
93
- // --- dormancy → revive(resume): NEW process rehydrates the set from disk ---
94
- const seenAfterRevive = loadInjectedDocs(node);
95
- assert.deepEqual(seenAfterRevive, seen, 'revive(resume) rehydrates the persisted dedup set');
96
- assert.ok(!renderOnReadDocs(node, readFile, seenAfterRevive).includes(FIXTURE_BODY), 'resumed process does not re-inject the doc');
97
- // --- fresh launch (revive resume=false, e.g. a refresh-yield) clears the set ---
98
- clearInjectedDocs(node);
99
- const seenFresh = loadInjectedDocs(node);
100
- assert.ok(renderOnReadDocs(node, readFile, seenFresh).includes(FIXTURE_BODY), 'a fresh transcript surfaces the doc again');
102
+ saveContextExposureState(node, state);
103
+ // The active ledger suppresses an equal-rung repeat.
104
+ assert.ok(!renderOnReadDocs(node, readFile, transcript).includes(FIXTURE_BODY), 'same-context repeat read is already exposed');
105
+ // --- dormancy → revive(resume): NEW process rehydrates the state from disk ---
106
+ const stateAfterRevive = loadContextExposureState(node);
107
+ assert.deepEqual(stateAfterRevive, state, 'revive(resume) rehydrates the persisted context state');
108
+ assert.ok(!renderOnReadDocs(node, readFile, exposureTarget(stateAfterRevive, 'transcript')).includes(FIXTURE_BODY), 'strict resume does not re-deliver the document');
109
+ // --- fresh launch (revive resume=false, e.g. a refresh-yield) clears the state ---
110
+ clearContextExposureState(node);
111
+ const stateFresh = loadContextExposureState(node);
112
+ assert.ok(renderOnReadDocs(node, readFile, exposureTarget(stateFresh, 'transcript')).includes(FIXTURE_BODY), 'a fresh transcript surfaces the doc again');
101
113
  });
102
114
  test('an explicitly routed doc injects on-read under its frontmatter name, not the path-derived one', () => {
103
115
  const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
104
116
  const memDir = join(work, '.crouter', 'memory');
105
- mkdirSync(memDir, { recursive: true });
117
+ declareRepositoryRoot(memDir);
106
118
  writeFileSync(join(memDir, 'physical-guide.md'), '---\nkind: knowledge\n' +
107
119
  'name: acme-project\n' +
108
120
  'when-and-why-to-read: When reading acme source, this knowledge should be read because it carries the project-specific implementation constraints.\n' +
@@ -112,8 +124,8 @@ test('an explicitly routed doc injects on-read under its frontmatter name, not t
112
124
  const readFile = join(work, 'src', 'file.ts');
113
125
  mkdirSync(join(work, 'src'), { recursive: true });
114
126
  writeFileSync(readFile, 'export const x = 1;\n');
115
- const rendered = renderOnReadDocs(node, readFile, new Map());
127
+ const rendered = renderOnReadDocs(node, readFile, freshTranscriptTarget());
116
128
  assert.ok(rendered.includes('ACME PROJECT OPERATING GUIDE'), 'the matching explicit route fires');
117
- assert.ok(rendered.includes('<memory kind="knowledge" name="acme-project"'), `expected explicit frontmatter name "acme-project" in the injected envelope, got: ${rendered}`);
118
- assert.ok(!rendered.includes('name="physical-guide"'), 'must not fall back to the path-derived name when frontmatter sets one');
129
+ assert.ok(rendered.includes('<memory kind="knowledge" name="work/acme-project"'), `expected the explicit local name composed under the store's namespace, got: ${rendered}`);
130
+ assert.ok(!rendered.includes('work/physical-guide'), 'must not fall back to the path-derived name when frontmatter sets one');
119
131
  });
@@ -1,7 +1,7 @@
1
1
  // A `.crouter/memory` store nested BELOW a mounted project root is delivered
2
2
  // by the read path, not by boot: its `./**`-routed docs surface on the first
3
- // read beneath the store's owning dir, deduplicated against the
4
- // workspace-open render through the shared transcript dedup set. The `./`
3
+ // read beneath the store's owning dir and share the active transcript exposure
4
+ // ledger with the workspace-open render. The `./`
5
5
  // anchor stays fenced to project-store docs — a hand-authored `./` glob in a
6
6
  // user-scope doc must never match a read.
7
7
  //
@@ -16,6 +16,10 @@ import { resetScopeCache } from '../scope.js';
16
16
  import { spawnNode } from '../runtime/nodes.js';
17
17
  import { renderOnReadDocs, renderWorkspaceOpenDocs } from '../substrate/on-read-node.js';
18
18
  import { clearSessionCache } from '../substrate/session-cache.js';
19
+ import { emptyContextExposureState, exposureTarget, } from '../substrate/injected-store.js';
20
+ function freshTranscriptTarget() {
21
+ return exposureTarget(emptyContextExposureState(), 'transcript');
22
+ }
19
23
  const ROOT_BODY = 'ROOT WORKSPACE FRONT DOOR BODY';
20
24
  const NESTED_BODY = 'NESTED PKG FRONT DOOR BODY';
21
25
  let canvasHome;
@@ -44,14 +48,14 @@ after(() => {
44
48
  else
45
49
  process.env['HOME'] = prevHomeEnv;
46
50
  });
47
- /** A work tree with a root front door and a nested store front door, both
48
- * carrying the explicit front-door pair (workspace-open + `./**` read).
49
- * Returns the work dir. */
51
+ /** A work tree with a root front door declaring the repository namespace and a
52
+ * nested store front door carrying the explicit front-door pair (workspace-open
53
+ * + `./**` read), but no nested namespace declaration. Returns the work dir. */
50
54
  function makeWorkTree() {
51
55
  const work = join(fakeHome, 'work');
52
56
  const rootStore = join(work, '.crouter', 'memory');
53
57
  mkdirSync(rootStore, { recursive: true });
54
- writeFileSync(join(rootStore, 'INDEX.md'), '---\nkind: knowledge\n' +
58
+ writeFileSync(join(rootStore, 'INDEX.md'), '---\nnamespace: work\nkind: knowledge\n' +
55
59
  'when-and-why-to-read: When working in this workspace, this knowledge should be read because it is the operating guide.\n' +
56
60
  'surfaces:\n' +
57
61
  ' - {on: workspace-open, at: content}\n' +
@@ -73,26 +77,28 @@ test('a nested store front door surfaces on a read beneath its owning dir, not o
73
77
  const inside = join(work, 'pkg', 'lib', 'file.ts');
74
78
  mkdirSync(join(work, 'pkg', 'lib'), { recursive: true });
75
79
  writeFileSync(inside, 'export const x = 1;\n');
76
- const rendered = renderOnReadDocs(node, inside, new Map());
80
+ const rendered = renderOnReadDocs(node, inside, freshTranscriptTarget());
77
81
  assert.ok(rendered.includes(NESTED_BODY), `read beneath pkg surfaces the nested front door; got: ${rendered}`);
78
82
  const outside = join(work, 'docs', 'notes.ts');
79
83
  mkdirSync(join(work, 'docs'), { recursive: true });
80
84
  writeFileSync(outside, 'export const y = 2;\n');
81
- const renderedOutside = renderOnReadDocs(node, outside, new Map());
85
+ const renderedOutside = renderOnReadDocs(node, outside, freshTranscriptTarget());
82
86
  assert.ok(!renderedOutside.includes(NESTED_BODY), `read outside pkg must not surface the nested front door; got: ${renderedOutside}`);
83
87
  });
84
- test('workspace-open and on-read share one dedup set: root front door once, nested store on first read beneath it', () => {
88
+ test('workspace-open and on-read share one exposure ledger', () => {
85
89
  const work = makeWorkTree();
86
90
  const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
87
- const seen = new Map();
88
- const opened = renderWorkspaceOpenDocs(node, seen);
91
+ const transcript = freshTranscriptTarget();
92
+ const opened = renderWorkspaceOpenDocs(node, transcript);
89
93
  assert.ok(opened.includes(ROOT_BODY), 'workspace open delivers the mounted root front door');
94
+ assert.ok(opened.includes('name="work"'), `a store's root INDEX answers at its declared namespace, with no trailing INDEX segment; got: ${opened}`);
90
95
  assert.ok(!opened.includes(NESTED_BODY), 'boot stays flat: the nested store is not in the mounted stack');
91
96
  const readFile = join(work, 'pkg', 'lib', 'file.ts');
92
97
  mkdirSync(join(work, 'pkg', 'lib'), { recursive: true });
93
98
  writeFileSync(readFile, 'export const x = 1;\n');
94
- const onRead = renderOnReadDocs(node, readFile, seen);
99
+ const onRead = renderOnReadDocs(node, readFile, transcript);
95
100
  assert.ok(onRead.includes(NESTED_BODY), 'the nested front door delivers on the first read beneath it');
101
+ assert.ok(onRead.includes('name="work/pkg"'), 'the nested front door answers under the root namespace plus owner path');
96
102
  assert.ok(!onRead.includes(ROOT_BODY), 'the root front door already delivered at workspace open — never twice');
97
103
  });
98
104
  test('a hand-authored `./`-anchored read glob in a user-scope doc never matches a read', () => {
@@ -114,7 +120,7 @@ test('a hand-authored `./`-anchored read glob in a user-scope doc never matches
114
120
  const readFile = join(fakeHome, 'notes', 'thing.md');
115
121
  mkdirSync(join(fakeHome, 'notes'), { recursive: true });
116
122
  writeFileSync(readFile, '# a note\n');
117
- const rendered = renderOnReadDocs(node, readFile, new Map());
123
+ const rendered = renderOnReadDocs(node, readFile, freshTranscriptTarget());
118
124
  assert.ok(rendered.includes('USER CONTROL BODY'), 'the glob-routed user doc proves the user store is in the corpus');
119
125
  assert.ok(!rendered.includes('USER DOT BODY MUST NOT FIRE'), `the \`./\` anchor is project-store only — a user doc's owning root is ~; got: ${rendered}`);
120
126
  });