@north-light/crouter 0.3.227 → 0.3.228

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/dist/api/client.d.ts +5 -5
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/dto/broker-ops.d.ts +8 -0
  4. package/dist/api/dto/memory.d.ts +8 -7
  5. package/dist/api/dto/memory.js +2 -2
  6. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
  7. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
  8. package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
  9. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
  10. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
  11. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
  12. package/dist/builtin-memory/design/guide.md +38 -16
  13. package/dist/builtin-memory/design/roadmap.md +4 -4
  14. package/dist/builtin-memory/insights/init.md +5 -5
  15. package/dist/builtin-memory/internal/INDEX.md +1 -1
  16. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  17. package/dist/builtin-memory/internal/memory-loading.md +6 -6
  18. package/dist/builtin-memory/internal/plugins.md +1 -1
  19. package/dist/builtin-memory/plan/guide.md +53 -0
  20. package/dist/builtin-memory/plan/roadmap.md +10 -8
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
  22. package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
  23. package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
  24. package/dist/clients/attach/chrome/inbox-strip.js +11 -7
  25. package/dist/clients/attach/session/pane-tag.d.ts +28 -2
  26. package/dist/clients/attach/session/pane-tag.js +48 -22
  27. package/dist/clients/attach/session/profile-files.d.ts +6 -0
  28. package/dist/clients/attach/session/profile-files.js +44 -11
  29. package/dist/clients/attach/slash/dispatch.js +4 -7
  30. package/dist/clients/attach/viewer.js +588 -596
  31. package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
  32. package/dist/commands/api-client.d.ts +3 -7
  33. package/dist/commands/api-client.js +7 -12
  34. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
  35. package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
  36. package/dist/commands/memory/delete.js +51 -24
  37. package/dist/commands/memory/edit.js +50 -10
  38. package/dist/commands/memory/find.js +105 -56
  39. package/dist/commands/memory/history.js +51 -31
  40. package/dist/commands/memory/lint.d.ts +0 -9
  41. package/dist/commands/memory/lint.js +78 -322
  42. package/dist/commands/memory/list.d.ts +12 -4
  43. package/dist/commands/memory/list.js +64 -25
  44. package/dist/commands/memory/move.js +103 -73
  45. package/dist/commands/memory/origin.js +35 -5
  46. package/dist/commands/memory/read.d.ts +4 -0
  47. package/dist/commands/memory/read.js +132 -80
  48. package/dist/commands/memory/shared.d.ts +115 -25
  49. package/dist/commands/memory/shared.js +331 -74
  50. package/dist/commands/memory/write.js +72 -21
  51. package/dist/commands/memory.js +2 -2
  52. package/dist/commands/node/lifecycle.js +2 -2
  53. package/dist/commands/pkg/market-manage.js +25 -24
  54. package/dist/commands/pkg/plugin-manage.d.ts +22 -6
  55. package/dist/commands/pkg/plugin-manage.js +118 -25
  56. package/dist/commands/pkg/shared.d.ts +8 -0
  57. package/dist/commands/pkg/shared.js +23 -20
  58. package/dist/commands/surface-reload.d.ts +5 -0
  59. package/dist/commands/surface-reload.js +9 -1
  60. package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
  61. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
  62. package/dist/commands/sys/migrate.js +126 -140
  63. package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
  64. package/dist/commands/sys/panels/profiles-panel.js +599 -0
  65. package/dist/commands/sys/settings-shell.d.ts +4 -2
  66. package/dist/commands/sys/settings-shell.js +35 -4
  67. package/dist/commands/sys/settings.js +21 -5
  68. package/dist/commands/sys/sync-deps.d.ts +2 -0
  69. package/dist/commands/sys/sync-deps.js +26 -15
  70. package/dist/commands/sys/sync-project-guidance.js +228 -145
  71. package/dist/commands/sys/sync-skills.js +28 -17
  72. package/dist/commands/sys/update.js +11 -3
  73. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
  74. package/dist/core/__tests__/config-change-delta.test.js +106 -0
  75. package/dist/core/__tests__/context-intro.test.js +13 -9
  76. package/dist/core/__tests__/daemon-boot.test.js +22 -16
  77. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
  78. package/dist/core/__tests__/human-deliver.test.js +19 -0
  79. package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
  80. package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
  81. package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
  82. package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
  83. package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
  84. package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
  85. package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
  86. package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
  87. package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
  88. package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
  89. package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
  90. package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
  91. package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
  92. package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
  93. package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
  94. package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
  95. package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
  96. package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
  97. package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
  98. package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
  99. package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
  100. package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
  101. package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
  102. package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
  103. package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
  104. package/dist/core/__tests__/kickoff.test.js +16 -5
  105. package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
  106. package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
  107. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  108. package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
  109. package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
  110. package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
  111. package/dist/core/__tests__/repository-association.test.d.ts +1 -0
  112. package/dist/core/__tests__/repository-association.test.js +153 -0
  113. package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
  114. package/dist/core/__tests__/repository-root-identity.test.js +219 -0
  115. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
  116. package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
  117. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
  118. package/dist/core/canvas/canvas.d.ts +3 -0
  119. package/dist/core/canvas/canvas.js +30 -10
  120. package/dist/core/canvas/db.js +28 -1
  121. package/dist/core/canvas/paths.d.ts +4 -6
  122. package/dist/core/canvas/paths.js +8 -6
  123. package/dist/core/canvas/render-source.js +2 -2
  124. package/dist/core/canvas/types.d.ts +9 -13
  125. package/dist/core/exclusive-lock.d.ts +2 -0
  126. package/dist/core/exclusive-lock.js +21 -0
  127. package/dist/core/git.d.ts +0 -1
  128. package/dist/core/git.js +0 -3
  129. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
  130. package/dist/core/human/feedback-companion.js +3 -0
  131. package/dist/core/human/scan.js +8 -1
  132. package/dist/core/keybindings/catalog.d.ts +2 -2
  133. package/dist/core/keybindings/catalog.js +2 -1
  134. package/dist/core/memory/doc-link-grammar.js +2 -2
  135. package/dist/core/memory/history.d.ts +24 -0
  136. package/dist/core/memory/history.js +66 -1
  137. package/dist/core/memory/identity.d.ts +65 -0
  138. package/dist/core/memory/identity.js +185 -0
  139. package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
  140. package/dist/core/memory/inline-ref-guidance.js +1 -1
  141. package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
  142. package/dist/core/memory/inline-ref-inventory.js +30 -62
  143. package/dist/core/memory/lint.d.ts +129 -0
  144. package/dist/core/memory/lint.js +517 -0
  145. package/dist/core/memory/project-namespace.d.ts +31 -0
  146. package/dist/core/memory/project-namespace.js +85 -0
  147. package/dist/core/memory/repository-association.d.ts +31 -0
  148. package/dist/core/memory/repository-association.js +129 -0
  149. package/dist/core/memory/tree.d.ts +39 -0
  150. package/dist/core/memory/tree.js +93 -0
  151. package/dist/core/memory-resolver.d.ts +193 -90
  152. package/dist/core/memory-resolver.js +461 -375
  153. package/dist/core/nested-stores.js +6 -13
  154. package/dist/core/profiles/select.d.ts +4 -1
  155. package/dist/core/profiles/select.js +108 -48
  156. package/dist/core/review/__tests__/capture-origin.test.js +2 -2
  157. package/dist/core/review/__tests__/stage-identity.test.js +2 -2
  158. package/dist/core/review/companion.js +11 -2
  159. package/dist/core/runtime/bearings.d.ts +2 -2
  160. package/dist/core/runtime/bearings.js +3 -3
  161. package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
  162. package/dist/core/runtime/broker/rebind.js +5 -0
  163. package/dist/core/runtime/broker-extension-render.d.ts +7 -3
  164. package/dist/core/runtime/broker-extension-render.js +10 -5
  165. package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
  166. package/dist/core/runtime/broker-persona-guidance.js +122 -27
  167. package/dist/core/runtime/deliver-live.d.ts +16 -4
  168. package/dist/core/runtime/deliver-live.js +29 -15
  169. package/dist/core/runtime/kickoff.d.ts +3 -3
  170. package/dist/core/runtime/kickoff.js +8 -16
  171. package/dist/core/runtime/lifecycle.js +2 -3
  172. package/dist/core/runtime/nodes.d.ts +3 -4
  173. package/dist/core/runtime/nodes.js +3 -4
  174. package/dist/core/runtime/persona.d.ts +8 -12
  175. package/dist/core/runtime/persona.js +24 -96
  176. package/dist/core/runtime/promote.d.ts +2 -2
  177. package/dist/core/runtime/promote.js +8 -21
  178. package/dist/core/runtime/revive.js +20 -17
  179. package/dist/core/runtime/spawn.js +18 -7
  180. package/dist/core/runtime/tmux-bindings.js +2 -3
  181. package/dist/core/runtime/warm-pool.js +3 -4
  182. package/dist/core/scope.js +2 -0
  183. package/dist/core/self-update.d.ts +0 -2
  184. package/dist/core/self-update.js +2 -35
  185. package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
  186. package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
  187. package/dist/core/substrate/index.d.ts +2 -2
  188. package/dist/core/substrate/index.js +1 -1
  189. package/dist/core/substrate/injected-store.d.ts +43 -27
  190. package/dist/core/substrate/injected-store.js +208 -104
  191. package/dist/core/substrate/listings.d.ts +19 -12
  192. package/dist/core/substrate/listings.js +75 -52
  193. package/dist/core/substrate/on-read-node.d.ts +4 -7
  194. package/dist/core/substrate/on-read-node.js +6 -8
  195. package/dist/core/substrate/on-read.d.ts +22 -25
  196. package/dist/core/substrate/on-read.js +103 -147
  197. package/dist/core/substrate/render-node.d.ts +4 -7
  198. package/dist/core/substrate/render-node.js +5 -7
  199. package/dist/core/substrate/render.d.ts +21 -3
  200. package/dist/core/substrate/render.js +291 -223
  201. package/dist/core/substrate/schema.d.ts +1 -13
  202. package/dist/core/substrate/schema.js +5 -40
  203. package/dist/core/substrate/session-cache.d.ts +14 -4
  204. package/dist/core/substrate/session-cache.js +40 -22
  205. package/dist/core/substrate/surface-match.d.ts +9 -7
  206. package/dist/core/substrate/surface-match.js +26 -25
  207. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
  208. package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
  209. package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
  210. package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
  211. package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +21 -5
  212. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  213. package/dist/daemon/api/handlers/memory.js +2 -0
  214. package/dist/daemon/crtrd.js +2 -0
  215. package/dist/daemon/human/finish.js +8 -1
  216. package/dist/daemon/manage.d.ts +0 -1
  217. package/dist/daemon/manage.js +7 -16
  218. package/dist/daemon/startup-policy.d.ts +1 -0
  219. package/dist/daemon/startup-policy.js +1 -0
  220. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  221. package/dist/migrations/002-profile-project-memory.js +1 -0
  222. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  223. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  224. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  225. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  226. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  227. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  228. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  229. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  230. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  231. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  232. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  233. package/dist/migrations/__tests__/activation.test.js +149 -0
  234. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  235. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  236. package/dist/migrations/activation.d.ts +16 -0
  237. package/dist/migrations/activation.js +78 -0
  238. package/dist/migrations/convergent.d.ts +14 -3
  239. package/dist/migrations/convergent.js +21 -10
  240. package/dist/migrations/corpus.d.ts +78 -0
  241. package/dist/migrations/corpus.js +497 -0
  242. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  243. package/dist/migrations/frontmatter-splice.js +176 -0
  244. package/dist/migrations/registry.d.ts +6 -1
  245. package/dist/migrations/registry.js +7 -2
  246. package/dist/migrations/runner.d.ts +41 -0
  247. package/dist/migrations/runner.js +81 -0
  248. package/dist/migrations/types.d.ts +148 -9
  249. package/dist/migrations/types.js +9 -2
  250. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  251. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  252. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  253. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  254. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  255. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  256. package/dist/pi-extensions/canvas-stophook.js +6 -13
  257. package/dist/shared/generated-context.d.ts +0 -3
  258. package/dist/shared/generated-context.js +0 -57
  259. package/dist/shared/tool-groups.js +2 -3
  260. package/package.json +5 -4
  261. package/runtime.lock.json +2 -2
  262. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  263. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  264. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  265. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  266. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  267. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  268. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  269. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  270. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  271. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  272. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  273. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  274. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  275. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  276. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  277. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  278. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  279. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  280. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  281. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  282. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  283. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  284. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  285. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  286. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -1,121 +1,225 @@
1
- // injected-store.ts — durable per-node dedup map for substrate doc delivery.
2
- //
3
- // Delivery channels (boot render seeding, workspace-open, on-read, memory-read
4
- // listings and events, command events) dedup so a given doc surfaces at most
5
- // once per conversation AT A GIVEN DISCLOSURE. The map keys on realpath and
6
- // carries the highest rung rank delivered so far; higher rungs pierce lower —
7
- // a listing line (preview rank) must not block a later explicit content
8
- // delivery, while a content delivery silences everything after it. Directory
9
- // listings participate under the directory's own realpath (a directory never
10
- // collides with a file's realpath).
11
- //
12
- // The map USED to live only in the pi process heap, cleared on session_start.
13
- // But a node's logical session — the .jsonl transcript — spans MULTIPLE pi
14
- // processes: a dormancy → revive(resume) cycle exits the old process and
15
- // launches a fresh `pi --session` that REUSES the same transcript. This module
16
- // persists the map to `nodes/<id>/injected-docs.json` so the resumed process
17
- // rehydrates it and skips docs already present in the transcript. The one
18
- // launch path that begins a FRESH transcript (reviveNode with resume=false, in
19
- // runtime/revive.ts) calls clearInjectedDocs(), so a new conversation starts
20
- // empty. CLI processes (the `memory read` leaf) load/save the same file.
21
- //
22
- // Disk format: `{"v":2,"docs":{"<realpath>":<rank 1|2|3>}}` (rank =
23
- // rungRank of the delivered rung). A legacy v1 string-array rehydrates at
24
- // content rank — its entries were recorded under never-re-deliver semantics.
25
- //
26
- // All ops are best-effort: a failed read/write degrades to a possible
27
- // re-inject, never a crash — a dedup miss must never break a read or a revive.
28
- import { readFileSync, writeFileSync, rmSync } from 'node:fs';
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
29
3
  import { injectedDocsPath } from '../canvas/paths.js';
4
+ import { atomicWriteJson, realpathOrSelf } from '../fs-utils.js';
30
5
  import { rungRank } from './schema.js';
31
- /** Already delivered at `rung` or above? — the skip test every channel runs
32
- * before rendering a doc (or a listing line, at preview) again. */
33
- export function deliveredAtOrAbove(seen, realpath, rung) {
34
- const have = seen.get(realpath);
35
- return have !== undefined && have >= rungRank(rung);
36
- }
37
- /** Record a delivery at `rung`, keeping the highest rank seen. */
38
- export function recordDelivery(seen, realpath, rung) {
6
+ const CONTENT_RANK = rungRank('content');
7
+ const cache = new Map();
8
+ export function emptyContextExposureState() {
9
+ return { preferenceSnapshot: null, exposures: new Map() };
10
+ }
11
+ export function exposureTarget(state, source) {
12
+ return { state, source };
13
+ }
14
+ function validRank(value) {
15
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= CONTENT_RANK;
16
+ }
17
+ function activeRank(ranks) {
18
+ return Math.max(ranks?.system ?? 0, ranks?.transcript ?? 0);
19
+ }
20
+ export function exposedAtOrAbove(state, identity, rung) {
21
+ return activeRank(state.exposures.get(identity)) >= rungRank(rung);
22
+ }
23
+ /** The sole delivery mutation: max-fold one loaded identity into its source rank. */
24
+ export function registerExposure(target, identity, rung) {
39
25
  const rank = rungRank(rung);
40
- const have = seen.get(realpath);
41
- if (have === undefined || have < rank)
42
- seen.set(realpath, rank);
43
- }
44
- /** Max-merge `source` into `target` — used to fold an out-of-process delivery
45
- * (the daemon's warm-spare bearings push) into a live process's shared map. */
46
- export function mergeInjectedDocs(target, source) {
47
- for (const [real, rank] of source) {
48
- const have = target.get(real);
49
- if (have === undefined || have < rank)
50
- target.set(real, rank);
26
+ if (rank === 0)
27
+ return;
28
+ const current = target.state.exposures.get(identity) ?? {};
29
+ if ((current[target.source] ?? 0) >= rank)
30
+ return;
31
+ target.state.exposures.set(identity, { ...current, [target.source]: rank });
32
+ }
33
+ export function contentExposureIdentity(body) {
34
+ const canonical = body.trim();
35
+ return canonical === ''
36
+ ? null
37
+ : `content-sha256:${createHash('sha256').update(canonical).digest('hex')}`;
38
+ }
39
+ export function documentExposedAtOrAbove(state, path, body, rung) {
40
+ if (exposedAtOrAbove(state, realpathOrSelf(path), rung))
41
+ return true;
42
+ const contentIdentity = contentExposureIdentity(body);
43
+ return contentIdentity !== null && exposedAtOrAbove(state, contentIdentity, rung);
44
+ }
45
+ /** Register both the physical document and its non-empty content alias. */
46
+ export function registerDocumentExposure(target, path, body, rung) {
47
+ registerExposure(target, realpathOrSelf(path), rung);
48
+ const contentIdentity = contentExposureIdentity(body);
49
+ if (contentIdentity !== null)
50
+ registerExposure(target, contentIdentity, rung);
51
+ }
52
+ export function freezePreferenceSnapshot(state, rendered) {
53
+ if (state.preferenceSnapshot === null)
54
+ state.preferenceSnapshot = rendered;
55
+ return state.preferenceSnapshot;
56
+ }
57
+ export function cloneContextExposureState(source) {
58
+ return {
59
+ preferenceSnapshot: source.preferenceSnapshot,
60
+ exposures: new Map([...source.exposures].map(([identity, ranks]) => [identity, { ...ranks }])),
61
+ };
62
+ }
63
+ export function hasTranscriptExposure(state) {
64
+ return [...state.exposures.values()].some((ranks) => validRank(ranks.transcript));
65
+ }
66
+ /** The context copied into a fork excludes the source's system snapshot. */
67
+ export function cloneTranscriptExposureState(source) {
68
+ const clone = emptyContextExposureState();
69
+ const transcript = exposureTarget(clone, 'transcript');
70
+ for (const [identity, ranks] of source.exposures) {
71
+ const rank = ranks.transcript;
72
+ if (!validRank(rank))
73
+ continue;
74
+ registerExposure(transcript, identity, rank === 1 ? 'name' : rank === 2 ? 'preview' : 'content');
51
75
  }
76
+ return clone;
52
77
  }
53
- /** Per-process cache behind `sharedInjectedDocs`: one Map instance per node,
54
- * shared by every extension in the pi process (context-intro's workspace-open
55
- * render and doc-substrate's on-read hook), so a doc delivered by one hook is
56
- * already in the map the other dedups against. */
57
- const cache = new Map();
58
- /** The process-shared dedup map for a node: loaded from disk once, then the
59
- * SAME Map instance on every call, so all hooks in this pi process grow and
60
- * consult one map. Daemon-side deliveries (a warm-spare claim) write the disk
61
- * file from another process; consumers that must see those merge the plain
62
- * `loadInjectedDocs` result in before rendering. */
63
- export function sharedInjectedDocs(nodeId) {
64
- let seen = cache.get(nodeId);
65
- if (seen === undefined) {
66
- seen = loadInjectedDocs(nodeId);
67
- cache.set(nodeId, seen);
78
+ export function replaceContextExposureState(target, source) {
79
+ target.preferenceSnapshot = source.preferenceSnapshot;
80
+ target.exposures.clear();
81
+ for (const [identity, ranks] of source.exposures) {
82
+ target.exposures.set(identity, { ...ranks });
68
83
  }
69
- return seen;
70
84
  }
71
- const CONTENT_RANK = rungRank('content');
72
- /** Rehydrate a node's dedup map from disk. Returns an empty map when the file
73
- * is absent, unreadable, or malformed (a fresh transcript, or a node that has
74
- * not yet surfaced any doc). */
75
- export function loadInjectedDocs(nodeId) {
85
+ export function contextExposureStatesEqual(left, right) {
86
+ if (left.preferenceSnapshot !== right.preferenceSnapshot || left.exposures.size !== right.exposures.size)
87
+ return false;
88
+ for (const [identity, ranks] of left.exposures) {
89
+ const other = right.exposures.get(identity);
90
+ if (ranks.system !== other?.system || ranks.transcript !== other?.transcript)
91
+ return false;
92
+ }
93
+ return true;
94
+ }
95
+ /** Fold state written by another process into the live context state. */
96
+ export function mergeContextExposureState(target, source) {
97
+ if (target.preferenceSnapshot !== null &&
98
+ source.preferenceSnapshot !== null &&
99
+ target.preferenceSnapshot !== source.preferenceSnapshot) {
100
+ throw new Error('context exposure state contains conflicting preference snapshots');
101
+ }
102
+ if (target.preferenceSnapshot === null && source.preferenceSnapshot !== null) {
103
+ target.preferenceSnapshot = source.preferenceSnapshot;
104
+ }
105
+ for (const [identity, ranks] of source.exposures) {
106
+ const current = target.exposures.get(identity) ?? {};
107
+ const system = Math.max(current.system ?? 0, ranks.system ?? 0);
108
+ const transcript = Math.max(current.transcript ?? 0, ranks.transcript ?? 0);
109
+ target.exposures.set(identity, {
110
+ ...(system > 0 ? { system } : {}),
111
+ ...(transcript > 0 ? { transcript } : {}),
112
+ });
113
+ }
114
+ }
115
+ export function sharedContextExposureState(nodeId) {
116
+ let state = cache.get(nodeId);
117
+ if (state === undefined) {
118
+ state = loadContextExposureState(nodeId);
119
+ cache.set(nodeId, state);
120
+ }
121
+ return state;
122
+ }
123
+ /** v1/v2 ranks predate provenance and conservatively become transcript exposure. */
124
+ export function loadContextExposureState(nodeId) {
125
+ const path = injectedDocsPath(nodeId);
126
+ let raw;
76
127
  try {
77
- const raw = JSON.parse(readFileSync(injectedDocsPath(nodeId), 'utf8'));
78
- const out = new Map();
79
- if (Array.isArray(raw)) {
80
- // legacy v1: bare realpath array, never-re-deliver semantics
81
- for (const x of raw)
82
- if (typeof x === 'string')
83
- out.set(x, CONTENT_RANK);
84
- return out;
128
+ raw = JSON.parse(readFileSync(path, 'utf8'));
129
+ }
130
+ catch (error) {
131
+ if (error.code === 'ENOENT')
132
+ return emptyContextExposureState();
133
+ throw new Error(`failed to read context exposure state at ${path}: ${error instanceof Error ? error.message : String(error)}`);
134
+ }
135
+ const state = emptyContextExposureState();
136
+ const transcript = exposureTarget(state, 'transcript');
137
+ if (Array.isArray(raw)) {
138
+ for (const identity of raw) {
139
+ if (typeof identity !== 'string')
140
+ throw new Error(`invalid v1 context exposure state at ${path}`);
141
+ registerExposure(transcript, identity, 'content');
142
+ }
143
+ return state;
144
+ }
145
+ if (raw === null || typeof raw !== 'object') {
146
+ throw new Error(`invalid context exposure state at ${path}`);
147
+ }
148
+ const record = raw;
149
+ if (record['v'] === 3) {
150
+ const preferenceSnapshot = record['preferenceSnapshot'];
151
+ const exposures = record['exposures'];
152
+ if ((preferenceSnapshot !== null && typeof preferenceSnapshot !== 'string') ||
153
+ exposures === null ||
154
+ typeof exposures !== 'object' ||
155
+ Array.isArray(exposures)) {
156
+ throw new Error(`invalid v3 context exposure state at ${path}`);
85
157
  }
86
- if (raw !== null && typeof raw === 'object') {
87
- const docs = raw.docs;
88
- if (docs !== null && typeof docs === 'object' && !Array.isArray(docs)) {
89
- for (const [real, rank] of Object.entries(docs)) {
90
- if (typeof rank === 'number' && rank >= 1 && rank <= CONTENT_RANK)
91
- out.set(real, rank);
92
- }
158
+ state.preferenceSnapshot = preferenceSnapshot;
159
+ for (const [identity, value] of Object.entries(exposures)) {
160
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
161
+ throw new Error(`invalid v3 exposure entry '${identity}' at ${path}`);
162
+ }
163
+ const ranks = value;
164
+ const system = ranks['system'];
165
+ const transcriptRank = ranks['transcript'];
166
+ if ((system !== undefined && !validRank(system)) ||
167
+ (transcriptRank !== undefined && !validRank(transcriptRank)) ||
168
+ (system === undefined && transcriptRank === undefined)) {
169
+ throw new Error(`invalid v3 exposure ranks for '${identity}' at ${path}`);
93
170
  }
171
+ state.exposures.set(identity, {
172
+ ...(validRank(system) ? { system } : {}),
173
+ ...(validRank(transcriptRank) ? { transcript: transcriptRank } : {}),
174
+ });
94
175
  }
95
- return out;
96
- }
97
- catch {
98
- return new Map();
176
+ return state;
99
177
  }
100
- }
101
- /** Persist a node's dedup map. Called after each delivery that grew it, so
102
- * the grown map survives a later dormancy. */
103
- export function saveInjectedDocs(nodeId, seen) {
104
- try {
105
- writeFileSync(injectedDocsPath(nodeId), JSON.stringify({ v: 2, docs: Object.fromEntries(seen) }));
178
+ if (record['v'] !== 2)
179
+ throw new Error(`unsupported context exposure state at ${path}`);
180
+ const docs = record['docs'];
181
+ if (docs === null || typeof docs !== 'object' || Array.isArray(docs)) {
182
+ throw new Error(`invalid v2 context exposure state at ${path}`);
106
183
  }
107
- catch {
108
- // best-effort — a failed persist only risks a re-inject, never a crash
184
+ for (const [identity, rank] of Object.entries(docs)) {
185
+ if (!validRank(rank))
186
+ throw new Error(`invalid v2 exposure rank for '${identity}' at ${path}`);
187
+ registerExposure(transcript, identity, rank === 1 ? 'name' : rank === 2 ? 'preview' : 'content');
109
188
  }
189
+ return state;
110
190
  }
111
- /** Drop a node's persisted dedup map. Called by the launch paths that start a
112
- * FRESH transcript, so the new conversation surfaces docs from scratch. */
113
- export function clearInjectedDocs(nodeId) {
114
- cache.delete(nodeId);
115
- try {
116
- rmSync(injectedDocsPath(nodeId), { force: true });
117
- }
118
- catch {
119
- // ignore — absence is the desired state anyway
191
+ export function saveContextExposureState(nodeId, state) {
192
+ atomicWriteJson(injectedDocsPath(nodeId), {
193
+ v: 3,
194
+ preferenceSnapshot: state.preferenceSnapshot,
195
+ exposures: Object.fromEntries(state.exposures),
196
+ });
197
+ }
198
+ /** Clear in place so every extension closure observes the new context boundary. */
199
+ export function clearContextExposureState(nodeId) {
200
+ const live = cache.get(nodeId);
201
+ if (live !== undefined)
202
+ replaceContextExposureState(live, emptyContextExposureState());
203
+ rmSync(injectedDocsPath(nodeId), { force: true });
204
+ }
205
+ /** Seed a fork with only context copied in the source transcript. */
206
+ export function initializeForkContextExposure(destinationNodeId, sourceNodeId) {
207
+ if (existsSync(injectedDocsPath(destinationNodeId))) {
208
+ const existing = loadContextExposureState(destinationNodeId);
209
+ const live = cache.get(destinationNodeId);
210
+ if (live === undefined)
211
+ cache.set(destinationNodeId, existing);
212
+ else
213
+ replaceContextExposureState(live, existing);
214
+ return;
120
215
  }
216
+ const destination = sourceNodeId === null
217
+ ? emptyContextExposureState()
218
+ : cloneTranscriptExposureState(loadContextExposureState(sourceNodeId));
219
+ const live = cache.get(destinationNodeId);
220
+ if (live === undefined)
221
+ cache.set(destinationNodeId, destination);
222
+ else
223
+ replaceContextExposureState(live, destination);
224
+ saveContextExposureState(destinationNodeId, destination);
121
225
  }
@@ -1,21 +1,28 @@
1
1
  import type { MemoryDoc } from '../memory-resolver.js';
2
- import { type InjectedDocs } from './injected-store.js';
3
- /** Dedup key for a whole directory's listing. Realpath keys start with `/`,
4
- * so the scheme prefix never collides with a doc entry. */
2
+ import { type ExposureTarget } from './injected-store.js';
3
+ /** Exposure identity for a whole directory listing. Realpath identities start
4
+ * with `/`, so the scheme prefix never collides with a document. */
5
5
  export declare function dirDedupKey(dir: string): string;
6
6
  /** First-occurrence-wins name map over a precedence-ordered corpus: the doc
7
7
  * that renders a name's line is the doc a read of that name would return. */
8
8
  export declare function docsByName(corpus: readonly MemoryDoc[]): Map<string, MemoryDoc>;
9
- /** Is `name` a directory in the merged name tree — i.e. does any doc live
10
- * beneath it? The corpus root ('') is never a directory here: store roots
11
- * merge into the whole catalog, which `memory list` owns. */
9
+ /** Is `name` a directory in the merged name tree — i.e. does it have members?
10
+ * A doc that fronts a directory answers true here and still returns its own
11
+ * body, because one node carries both. The corpus root ('') is never a
12
+ * directory here: store roots merge into the whole catalog, which
13
+ * `memory list` owns. */
12
14
  export declare function isDirName(byName: ReadonlyMap<string, MemoryDoc>, name: string): boolean;
13
15
  /** The dirs a doc read discloses: the doc's own directory, then each ancestor
14
16
  * walking up, the root excluded. `a/b/c` → `['a/b', 'a']`. */
15
17
  export declare function ancestorDirsOf(name: string): string[];
16
- /** Render one directory's listing lines: `[[name]]: <when-and-why>` per
17
- * member doc (bare `[[name]]` when the doc carries no routing line), a bare
18
- * `[[name]]` per subdirectory. `unlisted` members are omitted always;
19
- * `filterDelivered` additionally drops members already delivered at preview
20
- * rank or higher. Rendered members record at preview rank into `seen`. */
21
- export declare function renderDirListing(byName: ReadonlyMap<string, MemoryDoc>, dir: string, seen: InjectedDocs | null, filterDelivered: boolean): string[];
18
+ /** The listings a read of `name` discloses, in render order: the node's own
19
+ * listing first when it has members (a directory document's members belong
20
+ * with its body), then each ancestor walking up. */
21
+ export declare function docListingDirs(byName: ReadonlyMap<string, MemoryDoc>, name: string): string[];
22
+ /** Render one directory's listing lines: `[[name]]: <when-and-why>` per member
23
+ * (bare `[[name]]` when it carries no routing line), childless members first
24
+ * and subdirectories after. `unlisted` members are omitted always;
25
+ * `filterDelivered` additionally drops members already exposed at preview rank
26
+ * or higher. A subdirectory always keeps its row — it advertises a subtree, so
27
+ * an omitted preview leaves the bare name. */
28
+ export declare function renderDirListing(byName: ReadonlyMap<string, MemoryDoc>, dir: string, target: ExposureTarget | null, filterDelivered: boolean): string[];
@@ -1,24 +1,26 @@
1
- // listings.ts — system-owned directory listings over the merged name tree.
1
+ // listings.ts — system-owned directory listings over the canonical tree.
2
2
  //
3
3
  // Reading a doc discloses where it sits: `ls` semantics over the corpus
4
4
  // namespace (directories merge across stores), consulting no frontmatter
5
- // except `unlisted`. A listing renders one when-and-why line per member doc
6
- // and one bare name per subdirectory — never recursive contents, never gates.
5
+ // except `unlisted`. A listing renders one when-and-why line per member,
6
+ // including a subdirectory that carries its own document — never recursive
7
+ // contents, never gates. Structure comes from `memory/tree.ts`, so a directory
8
+ // document and its members are one row rather than two.
7
9
  //
8
10
  // Two render modes share one function. An EXPLICIT directory read (`crtr
9
11
  // memory read <dir>`) is a deliberate act, so every line renders (unlisted
10
12
  // excluded) — mirroring how an explicit doc read always returns content. The
11
13
  // AUTOMATIC neighborhood listings a doc read prepends are unsolicited, so
12
- // each member line dedup-filters against the transcript set and whole
13
- // directories dedup under a synthetic `dir://<name>` key (a name-tree
14
+ // each member line filters against the active exposure ledger and each whole
15
+ // directory registers a synthetic `dir://<name>` identity (a name-tree
14
16
  // directory spans stores, so no single physical realpath identifies it).
15
- // Either mode records rendered member docs at preview rank — a listing line
16
- // IS a preview-rank render — when a dedup map is supplied.
17
- import { realpathOrSelf } from '../fs-utils.js';
17
+ // Either mode registers rendered member docs at preview rank when a target is
18
+ // supplied, because a listing line is a preview-rank render.
19
+ import { buildMemoryTree } from '../memory/tree.js';
18
20
  import { parseSubstrateDoc, previewLine } from './schema.js';
19
- import { deliveredAtOrAbove, recordDelivery } from './injected-store.js';
20
- /** Dedup key for a whole directory's listing. Realpath keys start with `/`,
21
- * so the scheme prefix never collides with a doc entry. */
21
+ import { documentExposedAtOrAbove, registerDocumentExposure, } from './injected-store.js';
22
+ /** Exposure identity for a whole directory listing. Realpath identities start
23
+ * with `/`, so the scheme prefix never collides with a document. */
22
24
  export function dirDedupKey(dir) {
23
25
  return `dir://${dir}`;
24
26
  }
@@ -31,17 +33,25 @@ export function docsByName(corpus) {
31
33
  byName.set(d.name, d);
32
34
  return byName;
33
35
  }
34
- /** Is `name` a directory in the merged name tree — i.e. does any doc live
35
- * beneath it? The corpus root ('') is never a directory here: store roots
36
- * merge into the whole catalog, which `memory list` owns. */
36
+ /** One tree per winner map. A single read asks about its own directory, each
37
+ * ancestor, and every `[[link]]` in its body; rebuilding the tree per question
38
+ * would walk the whole corpus each time. */
39
+ const trees = new WeakMap();
40
+ function treeFor(byName) {
41
+ const cached = trees.get(byName);
42
+ if (cached !== undefined)
43
+ return cached;
44
+ const tree = buildMemoryTree([...byName.values()]);
45
+ trees.set(byName, tree);
46
+ return tree;
47
+ }
48
+ /** Is `name` a directory in the merged name tree — i.e. does it have members?
49
+ * A doc that fronts a directory answers true here and still returns its own
50
+ * body, because one node carries both. The corpus root ('') is never a
51
+ * directory here: store roots merge into the whole catalog, which
52
+ * `memory list` owns. */
37
53
  export function isDirName(byName, name) {
38
- if (name === '')
39
- return false;
40
- const prefix = name + '/';
41
- for (const n of byName.keys())
42
- if (n.startsWith(prefix))
43
- return true;
44
- return false;
54
+ return name !== '' && treeFor(byName).isDirectory(name);
45
55
  }
46
56
  /** The dirs a doc read discloses: the doc's own directory, then each ancestor
47
57
  * walking up, the root excluded. `a/b/c` → `['a/b', 'a']`. */
@@ -52,37 +62,50 @@ export function ancestorDirsOf(name) {
52
62
  dirs.push(segs.slice(0, i).join('/'));
53
63
  return dirs;
54
64
  }
55
- /** Render one directory's listing lines: `[[name]]: <when-and-why>` per
56
- * member doc (bare `[[name]]` when the doc carries no routing line), a bare
57
- * `[[name]]` per subdirectory. `unlisted` members are omitted always;
58
- * `filterDelivered` additionally drops members already delivered at preview
59
- * rank or higher. Rendered members record at preview rank into `seen`. */
60
- export function renderDirListing(byName, dir, seen, filterDelivered) {
61
- const prefix = dir + '/';
62
- const memberLines = new Map();
63
- const subdirs = new Set();
64
- for (const [name, doc] of byName) {
65
- if (!name.startsWith(prefix))
66
- continue;
67
- const rest = name.slice(prefix.length);
68
- const slash = rest.indexOf('/');
69
- if (slash >= 0) {
70
- subdirs.add(rest.slice(0, slash));
71
- continue;
72
- }
73
- const sub = parseSubstrateDoc(doc);
74
- if (sub?.unlisted)
75
- continue;
76
- const real = realpathOrSelf(doc.path);
77
- if (filterDelivered && seen !== null && deliveredAtOrAbove(seen, real, 'preview'))
65
+ /** The listings a read of `name` discloses, in render order: the node's own
66
+ * listing first when it has members (a directory document's members belong
67
+ * with its body), then each ancestor walking up. */
68
+ export function docListingDirs(byName, name) {
69
+ return [...(isDirName(byName, name) ? [name] : []), ...ancestorDirsOf(name)];
70
+ }
71
+ /** The when-and-why suffix a doc contributes to its line, or null when it must
72
+ * not preview here: `unlisted` always, and under `filterDelivered` a doc
73
+ * already in this context at preview rank or higher. A rendered preview IS a
74
+ * preview-rank delivery, so it registers. */
75
+ function previewSuffix(doc, target, filterDelivered) {
76
+ const sub = parseSubstrateDoc(doc);
77
+ if (sub?.unlisted)
78
+ return null;
79
+ if (filterDelivered &&
80
+ target !== null &&
81
+ documentExposedAtOrAbove(target.state, doc.path, doc.body, 'preview'))
82
+ return null;
83
+ if (target !== null)
84
+ registerDocumentExposure(target, doc.path, doc.body, 'preview');
85
+ return sub === null ? '' : previewLine(sub);
86
+ }
87
+ function line(name, suffix) {
88
+ return suffix === null || suffix === '' ? `[[${name}]]` : `[[${name}]]: ${suffix}`;
89
+ }
90
+ /** Render one directory's listing lines: `[[name]]: <when-and-why>` per member
91
+ * (bare `[[name]]` when it carries no routing line), childless members first
92
+ * and subdirectories after. `unlisted` members are omitted always;
93
+ * `filterDelivered` additionally drops members already exposed at preview rank
94
+ * or higher. A subdirectory always keeps its row — it advertises a subtree, so
95
+ * an omitted preview leaves the bare name. */
96
+ export function renderDirListing(byName, dir, target, filterDelivered) {
97
+ const node = treeFor(byName).node(dir);
98
+ if (node === null)
99
+ return [];
100
+ const lines = [];
101
+ for (const member of node.childDocuments) {
102
+ const suffix = previewSuffix(member.document, target, filterDelivered);
103
+ if (suffix === null)
78
104
  continue;
79
- const line = sub === null ? '' : previewLine(sub);
80
- memberLines.set(rest, line === '' ? `[[${name}]]` : `[[${name}]]: ${line}`);
81
- if (seen !== null)
82
- recordDelivery(seen, real, 'preview');
105
+ lines.push(line(member.name, suffix));
106
+ }
107
+ for (const sub of node.childDirectories) {
108
+ lines.push(line(sub.name, sub.document === null ? null : previewSuffix(sub.document, target, filterDelivered)));
83
109
  }
84
- return [
85
- ...[...memberLines.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, l]) => l),
86
- ...[...subdirs].sort((a, b) => a.localeCompare(b)).map((s) => `[[${prefix}${s}]]`),
87
- ];
110
+ return lines;
88
111
  }
@@ -1,9 +1,6 @@
1
- import type { InjectedDocs } from './injected-store.js';
1
+ import { type ExposureTarget } from './injected-store.js';
2
2
  /** Surface docs matched by a successful read tool call. An unknown node id or
3
3
  * an unreadable canvas renders nothing. */
4
- export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: InjectedDocs): string;
5
- /** Surface workspace-wide docs during first-message assembly, using the node's
6
- * recorded cwd and selected profile: docs in mounted project stores carrying
7
- * a `workspace-open` surfaces entry deliver here. `seen` is the
8
- * transcript-scoped dedup set shared with the on-read hook. */
9
- export declare function renderWorkspaceOpenDocs(nodeId: string, seen?: InjectedDocs): string;
4
+ export declare function renderOnReadDocs(nodeId: string, readFilePath: string, target?: ExposureTarget): string;
5
+ /** Surface matching workspace-open docs from the node's mounted project stores. */
6
+ export declare function renderWorkspaceOpenDocs(nodeId: string, target?: ExposureTarget): string;
@@ -4,11 +4,12 @@
4
4
  // that already holds a subject imports on-read.js without subject.js's
5
5
  // canvas-db reach.
6
6
  import { getNode } from '../canvas/index.js';
7
+ import { emptyContextExposureState, exposureTarget, } from './injected-store.js';
7
8
  import { assembleNodeSubject } from './subject.js';
8
9
  import { renderOnReadDocsForSubject, renderWorkspaceOpenDocsForSubject } from './on-read.js';
9
10
  /** Surface docs matched by a successful read tool call. An unknown node id or
10
11
  * an unreadable canvas renders nothing. */
11
- export function renderOnReadDocs(nodeId, readFilePath, seen = new Map()) {
12
+ export function renderOnReadDocs(nodeId, readFilePath, target = exposureTarget(emptyContextExposureState(), 'transcript')) {
12
13
  let subject;
13
14
  try {
14
15
  subject = assembleNodeSubject(nodeId);
@@ -18,19 +19,16 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Map()) {
18
19
  }
19
20
  if (subject === null)
20
21
  return '';
21
- return renderOnReadDocsForSubject(subject, readFilePath, seen);
22
+ return renderOnReadDocsForSubject(subject, readFilePath, target);
22
23
  }
23
- /** Surface workspace-wide docs during first-message assembly, using the node's
24
- * recorded cwd and selected profile: docs in mounted project stores carrying
25
- * a `workspace-open` surfaces entry deliver here. `seen` is the
26
- * transcript-scoped dedup set shared with the on-read hook. */
27
- export function renderWorkspaceOpenDocs(nodeId, seen = new Map()) {
24
+ /** Surface matching workspace-open docs from the node's mounted project stores. */
25
+ export function renderWorkspaceOpenDocs(nodeId, target = exposureTarget(emptyContextExposureState(), 'transcript')) {
28
26
  try {
29
27
  const subject = assembleNodeSubject(nodeId);
30
28
  const node = getNode(nodeId);
31
29
  if (subject === null || node === null)
32
30
  return '';
33
- return renderWorkspaceOpenDocsForSubject(subject, node.cwd, node.profile_id ?? null, seen);
31
+ return renderWorkspaceOpenDocsForSubject(subject, node.cwd, node.profile_id ?? null, target);
34
32
  }
35
33
  catch {
36
34
  return '';