@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,4 +1,5 @@
1
- // render.ts — the pure boot-render functions for the document substrate.
1
+ // render.ts — the pure render functions for the document substrate: two boot
2
+ // targets plus the config-change delta.
2
3
  //
3
4
  // Two boot targets, one shape per kind: each kind renders as ONE wrapped block —
4
5
  // intro prose → GROUPED content → an update directive. The render is grouped
@@ -6,12 +7,13 @@
6
7
  // clean prose bodies (verbatim, no tree chrome, no name label — this is what
7
8
  // makes content-rung the persona-prose mechanism), in general→specific order;
8
9
  // then the `preview`+`name` docs render as ONE catalog tree, each at its own
9
- // rung (preview → a `# read when:` routing line; name → a bare entry); a doc
10
- // with NO boot entry leaks only into its dir's `[+N more]` count, never a
11
- // name — EXCEPT a node-local doc with no boot entry: renderKnowledgeForSubject
12
- // floors it up to `name` so it still shows its bare name rather than
13
- // collapsing into a hidden count. A node-local doc with boot entries remains
14
- // absent when none of those entries participates.
10
+ // rung (preview → a `# read when:` routing line; name → a bare entry), a
11
+ // directory that carries its own document rendering that entry on the
12
+ // directory's own row; a doc with NO boot entry leaks only into its dir's
13
+ // `[+N more]` count, never a name — EXCEPT a node-store doc with no boot entry:
14
+ // renderKnowledgeForSubject floors it up to `name` so it still shows its bare
15
+ // name rather than collapsing into a hidden count. A node-store doc with boot
16
+ // entries remains absent when none of those entries participates.
15
17
  //
16
18
  // • the SYSTEM-PROMPT half — `<memory kind="preference">`
17
19
  // (renderPreferencesSection) — the always-present memory-usage guidance plus
@@ -20,51 +22,59 @@
20
22
  // `\n\nGuidelines:` anchor;
21
23
  // • the first-message half — `<memory kind="knowledge">`
22
24
  // (renderKnowledgeBlock) — the consultable catalog (every `kind: knowledge`
23
- // doc ∪ node-local) the string bearings.ts embeds in the session_start
25
+ // doc ∪ the node's own store) the string bearings.ts embeds in the session_start
24
26
  // message as a SIBLING of `<crtr-bearings>` (never nested under the context
25
27
  // dir — the catalog spans all scopes and belongs to no one directory).
26
28
  //
29
+ // renderConfigChangeDelta runs the same winner assemblies after a configuration
30
+ // change and appends only identities newly visible or raised above the active
31
+ // context rank. It never rewrites the frozen system snapshot.
32
+ //
27
33
  // The pipeline is explicitly TWO-STEP and must never run in the other order:
28
34
  // 1. SELECT WINNERS — MemoryDoc → parseSubstrateDoc → (null-filter
29
35
  // non-substrate) → per-doc boot rung (`bootRung`, the max `at` over
30
36
  // participating `on: boot` surfaces entries) → gatePasses → first-wins DEDUP by
31
37
  // name over the resolver's precedence order (nearest project > user >
32
- // builtin). Node-local docs follow the same gate rule, but the
33
- // knowledge-block render floors only a node-local doc with no boot entry to
34
- // `name` — the one rung-floor exemption, scoped to the node-local store
35
- // only.
38
+ // builtin). Node-store docs follow the same gate rule, but the
39
+ // knowledge-block render floors only a node-store doc with no boot entry to
40
+ // `name` — the one rung-floor exemption, scoped to the node store only.
36
41
  // 2. RENDER IN DISPLAY ORDER — split winners into content vs. the rest;
37
42
  // order content general→specific (builtin, user, project outermost→
38
- // nearest, node-local last) then by tree position then filename; place
43
+ // nearest, node store last) then by tree position then filename; place
39
44
  // the rest into the catalog tree (position-ordered, as before).
40
45
  // Sorting into display order BEFORE step 1's dedup would let a
41
46
  // later-in-display-order builtin doc's array position mask a project/user
42
47
  // override — dedup precedence and display order are deliberately different
43
48
  // orderings and must not be conflated.
44
49
  //
45
- // Pure + defensive: reads the resolver, canvas-db (subject assembly), and the
46
- // node-local memory dir; performs no writes and no side effects beyond the
47
- // caller-supplied `seen` delivery map (write-only seeding — boot docs render
48
- // every session and are never skipped). A single malformed doc never throws
49
- // the whole render — parsing is isolated upstream (parseSubstrateDoc returns
50
- // null for a non-substrate doc; per-file node-local loads are wrapped), and
51
- // tree construction is pure string work.
52
- import { relative, sep } from 'node:path';
50
+ // Pure + defensive: reads the resolver (the node's own store included) and
51
+ // canvas-db (subject assembly); performs no writes and no side effects beyond
52
+ // the caller-supplied exposure target. Baseline renders register their snapshot
53
+ // source; transition renders query the active maximum and register transcript
54
+ // deliveries. A single malformed doc never throws the whole render — the
55
+ // resolver's loader isolates an unparseable file and parseSubstrateDoc returns
56
+ // null for a non-substrate doc.
57
+ //
58
+ // STRUCTURE comes from the canonical directory tree (core/memory/tree.ts) and
59
+ // ORDER from each descriptor's stored physical path, never from the canonical
60
+ // name's segments: a project store's namespace has no physical counterpart and
61
+ // an explicit `name:` detaches identity from the path. A directory that carries
62
+ // its own document is ONE row — the document renders at its boot rung on the
63
+ // directory's own line, with the directory's members nested beneath it.
64
+ import { sep } from 'node:path';
53
65
  import { interpolateNodePaths } from '../canvas/paths.js';
54
- import { listAllMemoryDocs, resolveMemoryDoc } from '../memory-resolver.js';
66
+ import { listAllMemoryDocs, loadMemoryTargetView, resolveMemoryDoc } from '../memory-resolver.js';
67
+ import { buildMemoryTree } from '../memory/tree.js';
55
68
  import { subKindsAvailableTo } from '../config.js';
56
- import { parseFrontmatterGeneric } from '../frontmatter.js';
57
- import { pathExists, readText, realpathOrSelf, walkFiles } from '../fs-utils.js';
58
- import { memoryDir } from '../runtime/memory.js';
59
- import { NEUTRAL_PROJECT_MEMORY, projectScopeRoots } from '../scope.js';
69
+ import { projectScopeRoots } from '../scope.js';
60
70
  import { gatePasses } from './gate.js';
61
71
  // De-barreled to LEAF modules on purpose: importing from './index.js' would
62
72
  // pull subject.js (canvas-db) transitively, re-tainting every CLI consumer of
63
73
  // the pure render fns. The canvas-db wrappers (renderPreferencesSection /
64
74
  // renderKnowledgeBlock) live in render-node.js instead.
65
- import { recordDelivery } from './injected-store.js';
66
- import { bootRung, minRung, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, rungRank, } from './schema.js';
67
- import { cachedSubstrateDocs } from './session-cache.js';
75
+ import { documentExposedAtOrAbove, registerDocumentExposure, } from './injected-store.js';
76
+ import { bootRung, minRung, parseSubstrateDoc, previewLine, rungRank, } from './schema.js';
77
+ import { cachedAllMemoryDocs, cachedSubstrateDocs } from './session-cache.js';
68
78
  /** First-wins dedup by (already-normalized) name, preserving the input's
69
79
  * precedence order. The array's ORDER is the precedence signal — callers must
70
80
  * pass docs already ordered highest-precedence-first; this function never
@@ -96,34 +106,54 @@ function dedupeFirstWins(docs) {
96
106
  * surviving doc's authored boot rung is clamped to its cap before dedup, the
97
107
  * session cache, and delivery seeding see it. */
98
108
  function selectWinners(subject, kind) {
99
- let docs;
109
+ let corpus;
100
110
  try {
101
- docs = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
111
+ corpus = bootCorpus();
102
112
  }
103
113
  catch {
104
114
  return [];
105
115
  }
106
- const eligible = docs
107
- .filter((d) => d.kind === kind)
108
- .filter((d) => d.projectMemory !== 'none')
109
- .filter((d) => gatePasses(d, subject))
110
- .map((d) => ({ ...d, bootRung: minRung(bootRung(d, subject), d.projectMemory) }));
116
+ const eligible = [];
117
+ for (const d of corpus.docs) {
118
+ if (d.kind !== kind || d.projectMemory === 'none' || !gatePasses(d, subject))
119
+ continue;
120
+ const source = corpus.sources.get(d.path);
121
+ if (source === undefined)
122
+ continue;
123
+ eligible.push({ ...d, source, bootRung: minRung(bootRung(d, subject), d.projectMemory) });
124
+ }
111
125
  return dedupeFirstWins(eligible);
112
126
  }
127
+ const bootSources = new WeakMap();
128
+ /** The session's boot corpus, each parsed doc joined back to its descriptor.
129
+ * `cachedSubstrateDocs` is a filtered map of `cachedAllMemoryDocs`, and an
130
+ * absolute path names exactly one document in a corpus (store roots are
131
+ * realpath-deduped), so the path is the join key. The join is memoized per
132
+ * cached array because one session renders preferences, knowledge, and any
133
+ * config-change delta off the same corpus. */
134
+ function bootCorpus() {
135
+ const docs = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
136
+ let sources = bootSources.get(docs);
137
+ if (sources === undefined) {
138
+ sources = new Map(cachedAllMemoryDocs(listAllMemoryDocs).map((d) => [d.path, d]));
139
+ bootSources.set(docs, sources);
140
+ }
141
+ return { docs, sources };
142
+ }
113
143
  // ---------------------------------------------------------------------------
114
- // Boot-delivery seeding — the injected-docs map grows at render time.
144
+ // Exposure registration
115
145
  // ---------------------------------------------------------------------------
116
- /** Record every VISIBLY rendered winner into the caller's transcript dedup map
117
- * at its displayed rung (write-only — boot renders every session and never
118
- * skips). Seeding is what lets a later listing line or on-read injection skip
119
- * a doc the boot catalog already discloses at that rank. */
120
- function seedBootDelivery(seen, docs) {
121
- if (seen === undefined)
146
+ function rendersAtBoot(doc) {
147
+ return isVisible(doc.bootRung) && (doc.bootRung !== 'content' || doc.body.trim() !== '');
148
+ }
149
+ /** Register every visibly rendered winner at its displayed rung. */
150
+ function recordRendered(target, docs) {
151
+ if (target === undefined)
122
152
  return;
123
153
  for (const d of docs) {
124
- if (!isVisible(d.bootRung))
154
+ if (!rendersAtBoot(d))
125
155
  continue;
126
- recordDelivery(seen, realpathOrSelf(d.path), d.bootRung);
156
+ registerDocumentExposure(target, d.path, d.body, d.bootRung);
127
157
  }
128
158
  }
129
159
  // ---------------------------------------------------------------------------
@@ -133,14 +163,14 @@ function seedBootDelivery(seen, docs) {
133
163
  * profile=2, project ranks start at 3 and climb by root nearness — the
134
164
  * OUTERMOST ancestor `.crouter/` root ranks lowest (renders first), the
135
165
  * NEAREST root ranks highest (renders last) — the mirror of the resolver's
136
- * nearest-first dedup precedence. `isNodeLocal` docs rank last of all (the
137
- * most specific, this-node-only store). This is display order ONLY — the
166
+ * nearest-first dedup precedence. Node-scope docs rank last of all (the most
167
+ * specific, this-node-only store). This is display order ONLY — the
138
168
  * resolver's REPLACEMENT precedence (first-wins dedup, step 1) is the
139
169
  * opposite direction for project/profile: project stack > profile > user >
140
170
  * builtin, nearest project strongest; the two orderings are deliberately
141
171
  * different (see the module comment). */
142
- function scopeGeneralityRank(doc, isNodeLocal) {
143
- if (isNodeLocal)
172
+ function scopeGeneralityRank(doc) {
173
+ if (doc.scope === 'node')
144
174
  return 1000;
145
175
  if (doc.scope === 'builtin')
146
176
  return 0;
@@ -155,18 +185,17 @@ function scopeGeneralityRank(doc, isNodeLocal) {
155
185
  const idx = nearFirstIdx === -1 ? 0 : nearFirstIdx;
156
186
  return 3 + (roots.length - 1 - idx); // outermost (largest idx) -> smallest rank
157
187
  }
158
- /** The doc's RAW physical path segments (numeric prefix intact), recovered
159
- * from its absolute `path` using its (already-normalized) `name`'s segment
160
- * COUNT — normalization strips a prefix from a segment but never merges or
161
- * drops a segment, so the last N components of `path` are always the doc's
162
- * physical segments. This is what lets `tree position` sort respect an
163
- * author's `NN-` ordering pin even though the pin is invisible in `name`. */
188
+ /** The doc's stored physical path segments, `NN-` ordering prefixes intact —
189
+ * the descriptor's store-relative path, which is what lets `tree position`
190
+ * sort respect an author's ordering pin even though the pin is invisible in
191
+ * the canonical name. A directory document's `INDEX` filename names its
192
+ * directory rather than a segment of its own, so it sorts AS its directory,
193
+ * ahead of everything nested under it. */
164
194
  function physicalSegments(doc) {
165
- const n = doc.name.split('/').length;
166
- const raw = doc.path.split(sep).slice(-n);
167
- if (raw.length > 0)
168
- raw[raw.length - 1] = raw[raw.length - 1].replace(/\.md$/i, '');
169
- return raw;
195
+ const segments = doc.source.physicalRelativePath.replace(/\.md$/i, '').split('/');
196
+ if (doc.source.representation === 'directory-index')
197
+ segments.pop();
198
+ return segments;
170
199
  }
171
200
  /** "Tree position" comparator for content-prose ordering within a store:
172
201
  * compares two docs' RAW physical segments element-by-element (a shallower
@@ -193,10 +222,8 @@ function isAgentEditable(doc) {
193
222
  * display order: `content` docs first, as clean prose (general→specific,
194
223
  * then tree position, then filename — no tree chrome); every
195
224
  * other winner (preview/name/hidden-bootless) folds into ONE catalog tree
196
- * exactly as before. `nodeLocalSet` marks which winners came from the
197
- * node-local store (ranked most-specific in content ordering); omit it for a
198
- * pure resolver render. */
199
- function renderGrouped(winners, rootLabel, nodeLocalSet = new Set()) {
225
+ * exactly as before. */
226
+ function renderGrouped(winners, rootLabel) {
200
227
  const content = [];
201
228
  const rest = [];
202
229
  for (const d of winners) {
@@ -205,7 +232,7 @@ function renderGrouped(winners, rootLabel, nodeLocalSet = new Set()) {
205
232
  else
206
233
  rest.push(d);
207
234
  }
208
- content.sort((a, b) => scopeGeneralityRank(a, nodeLocalSet.has(a)) - scopeGeneralityRank(b, nodeLocalSet.has(b)) ||
235
+ content.sort((a, b) => scopeGeneralityRank(a) - scopeGeneralityRank(b) ||
209
236
  compareTreePosition(a, b) ||
210
237
  a.name.localeCompare(b.name));
211
238
  const contentProse = content
@@ -214,82 +241,40 @@ function renderGrouped(winners, rootLabel, nodeLocalSet = new Set()) {
214
241
  .join('\n\n');
215
242
  return { contentProse, tree: buildTree(rest, rootLabel), contentDocs: content };
216
243
  }
217
- /** The node-local memory docs eligible at boot. Node-local memory lives in the
218
- * canvas home (`nodes/<id>/context/memory/`) and is OUTSIDE the scope resolver,
219
- * so it is loaded directly here, its raw frontmatter run through
220
- * parseSubstrateFrontmatter (mirroring the resolver pipeline), then gate-passed.
221
- * Non-substrate files (those with no `kind`) parse to null and drop out.
222
- * Returned across ALL kinds — node-local is the catch-all this-node store and
223
- * rides into the knowledge block.
244
+ /** The node's own store (`nodes/<id>/context/memory/`), loaded through the
245
+ * resolver's descriptor loader like every other store, then gate-passed.
246
+ * Returned across ALL kinds — the node store is the catch-all this-node store
247
+ * and rides into the knowledge block.
248
+ *
249
+ * The store is addressed by the EXPLICIT `nodeId` rather than the host
250
+ * process's, because one process renders for other nodes (crtrd claiming a
251
+ * warm spare, `sys prompt-review` walking the canvas). The boot resolver
252
+ * corpus deliberately excludes node scope (`cachedAllMemoryDocs`), so this is
253
+ * the one path that carries node docs into a boot render and they cannot
254
+ * double-surface.
224
255
  *
225
- * Node-local docs follow the same GATE rule as every other store. A doc with
226
- * no boot entry is retained here; renderKnowledgeForSubject floors it to
227
- * `name` so it shows as a bare name instead of collapsing into `[+N more]`. */
228
- function nodeLocalDocs(nodeId, subject) {
229
- const dir = memoryDir(nodeId);
230
- if (!pathExists(dir))
256
+ * Node docs follow the same GATE rule as every other store. A doc with no boot
257
+ * entry is retained here; renderKnowledgeForSubject floors it to `name` so it
258
+ * shows as a bare name instead of collapsing into `[+N more]`. */
259
+ function nodeStoreDocs(nodeId, subject) {
260
+ let docs;
261
+ try {
262
+ // Narrowed to the node scope, so cwd and profile select no store.
263
+ docs = loadMemoryTargetView({ cwd: process.cwd(), profileId: null, nodeId }, { scope: 'node', quiet: true }).docs;
264
+ }
265
+ catch {
231
266
  return [];
267
+ }
232
268
  const out = [];
233
- for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
234
- const raw = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
235
- const fallbackName = normalizeDocName(raw);
236
- if (!fallbackName)
237
- continue;
238
- try {
239
- const { data, body } = parseFrontmatterGeneric(readText(file));
240
- const schema = parseSubstrateFrontmatter(data);
241
- if (schema === null)
242
- continue;
243
- // ONE substrate-identity rule (schema.ts's resolveDocName), shared with
244
- // the resolver and explicit file-context loader: frontmatter `name` wins
245
- // over the physical-path-derived fallback.
246
- const name = resolveDocName(data, fallbackName);
247
- // node-local is NOT a resolver scope; `scope` is a placeholder never read
248
- // by gate eval (keyed off the NODE subject, not the doc) nor by the
249
- // renderers (keyed off name / body / rung).
250
- // Node-local is not a project store: no profile relationship names it,
251
- // so it carries the neutral ceiling.
252
- out.push({ ...schema, name, scope: 'user', path: file, body, projectMemory: NEUTRAL_PROJECT_MEMORY });
253
- }
254
- catch {
255
- // A single malformed file is skipped, never fatal to the render.
269
+ for (const source of docs) {
270
+ const parsed = parseSubstrateDoc(source);
271
+ if (parsed === null || !gatePasses(parsed, subject))
256
272
  continue;
257
- }
258
- }
259
- return out.filter((d) => gatePasses(d, subject));
260
- }
261
- function newDir(path) {
262
- const segs = path.split('/');
263
- return {
264
- segment: path === '' ? '' : segs[segs.length - 1],
265
- path,
266
- children: new Map(),
267
- leaves: [],
268
- hiddenHere: 0,
269
- renders: false,
270
- ownCount: 0,
271
- };
272
- }
273
- /** Walk/create the dir node at `path`, building intermediates. */
274
- function ensureDir(root, path) {
275
- if (path === '')
276
- return root;
277
- let cur = root;
278
- let acc = '';
279
- for (const s of path.split('/')) {
280
- acc = acc === '' ? s : `${acc}/${s}`;
281
- let child = cur.children.get(s);
282
- if (!child) {
283
- child = newDir(acc);
284
- cur.children.set(s, child);
285
- }
286
- cur = child;
273
+ const rung = bootRung(parsed, subject);
274
+ const floored = isVisible(rung) || parsed.surfaces.some((e) => e.on === 'boot') ? rung : 'name';
275
+ out.push({ ...parsed, source, bootRung: floored });
287
276
  }
288
- return cur;
289
- }
290
- function parentDirOf(name) {
291
- const i = name.lastIndexOf('/');
292
- return i === -1 ? '' : name.slice(0, i);
277
+ return out;
293
278
  }
294
279
  function leafSegment(name) {
295
280
  const i = name.lastIndexOf('/');
@@ -298,74 +283,89 @@ function leafSegment(name) {
298
283
  function isVisible(rung) {
299
284
  return rungRank(rung) >= rungRank('name');
300
285
  }
286
+ /** Project one canonical tree node into a render row. `byPath` maps a
287
+ * descriptor back to the winner carrying its boot rung. */
288
+ function planDir(node, byPath) {
289
+ const children = node.childDirectories.map((child) => planDir(child, byPath));
290
+ const leaves = [];
291
+ let hiddenHere = 0;
292
+ for (const member of node.childDocuments) {
293
+ const doc = byPath.get(member.document.path);
294
+ if (doc === undefined)
295
+ continue;
296
+ if (isVisible(doc.bootRung))
297
+ leaves.push({ name: member.name, order: member.order, doc });
298
+ else
299
+ hiddenHere += 1;
300
+ }
301
+ const ownDoc = node.document === null ? null : byPath.get(node.document.path) ?? null;
302
+ let own = null;
303
+ if (ownDoc !== null) {
304
+ if (isVisible(ownDoc.bootRung))
305
+ own = ownDoc;
306
+ else
307
+ hiddenHere += 1;
308
+ }
309
+ return {
310
+ name: node.name,
311
+ segment: leafSegment(node.name),
312
+ order: node.order,
313
+ own,
314
+ children,
315
+ leaves,
316
+ hiddenHere,
317
+ renders: false,
318
+ ownCount: 0,
319
+ };
320
+ }
301
321
  /** Bottom-up pass: decide which dirs render and where hidden counts land. A dir
302
- * renders when it has ≥1 visible leaf, ≥1 rendering child dir, or it is
303
- * top-level (a direct child of the root always renders so its subtree count
304
- * is visible — the root itself always renders). Bootless docs in a
305
- * NON-rendering dir bubble up to the nearest rendering ancestor's `[+N more]`;
306
- * a rendering dir keeps its own subtree count. Returns the count to bubble up. */
322
+ * renders when it discloses something visible (its own document or ≥1 visible
323
+ * leaf), when ≥1 child dir renders, or when it is top-level (a direct child of
324
+ * the root always renders so its subtree count is visible — the root itself
325
+ * always renders). Bootless docs in a NON-rendering dir bubble up to the
326
+ * nearest rendering ancestor's `[+N more]`; a rendering dir keeps its own
327
+ * subtree count. Returns the count to bubble up. */
307
328
  function resolveDir(dir, isTopLevel) {
308
329
  let totalHidden = dir.hiddenHere;
309
- for (const child of dir.children.values()) {
310
- totalHidden += resolveDir(child, dir.path === '');
330
+ for (const child of dir.children) {
331
+ totalHidden += resolveDir(child, dir.name === '');
311
332
  }
312
- const hasVisibleLeaf = dir.leaves.length > 0;
313
- const anyChildRenders = [...dir.children.values()].some((c) => c.renders);
314
- dir.renders = hasVisibleLeaf || anyChildRenders || isTopLevel || dir.path === '';
333
+ const hasVisibleLeaf = dir.own !== null || dir.leaves.length > 0;
334
+ const anyChildRenders = dir.children.some((c) => c.renders);
335
+ dir.renders = hasVisibleLeaf || anyChildRenders || isTopLevel || dir.name === '';
315
336
  dir.ownCount = dir.renders ? totalHidden : 0;
316
337
  return dir.renders ? 0 : totalHidden;
317
338
  }
318
- function itemSortKey(item) {
339
+ function itemOrder(item) {
319
340
  if (item.kind === 'dir')
320
- return item.node.segment;
321
- if (item.kind === 'leaf')
322
- return leafSegment(item.doc.name);
323
- return '';
341
+ return item.node.order;
342
+ return item.kind === 'leaf' ? item.entry.order : '';
324
343
  }
325
- /** DISPLAY-ORDER PIN ONLY: a leaf whose last name segment is `INDEX` sorts
326
- * first among its directory's items. `INDEX` carries no mechanism anywhere in
327
- * the substrate anymore — these are ordinary docs — but the ex-router docs
328
- * keeping that name are each their dir's orientation entry, and localeCompare
329
- * would drop them mid-list. */
330
- function isIndexPinned(item) {
331
- return item.kind === 'leaf' && leafSegment(item.doc.name) === 'INDEX';
344
+ function itemName(item) {
345
+ if (item.kind === 'dir')
346
+ return item.node.name;
347
+ return item.kind === 'leaf' ? item.entry.name : '';
332
348
  }
333
- /** Render one canonical logical doc entry at its boot rung. */
334
- function renderDocEntry(doc, label, entryPrefix, childPrefix, lines) {
335
- switch (doc.bootRung) {
336
- case 'preview': {
337
- const pl = previewLine(doc);
338
- lines.push(pl === '' ? `${entryPrefix}${label}` : `${entryPrefix}${label} # read when: ${pl}`);
339
- break;
340
- }
341
- case 'content': {
342
- lines.push(`${entryPrefix}${label}`);
343
- const body = doc.body.trim();
344
- if (body !== '') {
345
- for (const raw of body.split('\n')) {
346
- lines.push(`${childPrefix} ${raw}`.replace(/\s+$/, ''));
347
- }
348
- }
349
- break;
350
- }
351
- default: // 'name'
352
- lines.push(`${entryPrefix}${label}`);
353
- }
349
+ /** Render one row's label at its document's boot rung: `preview` appends the
350
+ * read-when routing line, `name` leaves the bare label. A `content` winner
351
+ * never reaches the tree — renderGrouped renders its body as prose instead. */
352
+ function renderDocEntry(doc, label, entryPrefix, lines) {
353
+ const preview = doc.bootRung === 'preview' ? previewLine(doc) : '';
354
+ lines.push(preview === '' ? `${entryPrefix}${label}` : `${entryPrefix}${label} # read when: ${preview}`);
354
355
  }
355
356
  /** Render the children of a rendering dir, with `childPrefix` carrying the tree
356
- * guides for this depth. Order: an `INDEX`-named leaf first (display pin),
357
- * then child dirs and leaves intermixed alphabetically, then the `[+N more]`
358
- * count last. */
357
+ * guides for this depth. Order: child dirs and leaves intermixed by stored
358
+ * physical position, then the `[+N more]` count last. A dir that carries its
359
+ * own document renders that document's entry on the dir's own line. */
359
360
  function renderChildren(dir, childPrefix, lines) {
360
361
  const ordered = [];
361
- for (const child of dir.children.values()) {
362
+ for (const child of dir.children) {
362
363
  if (child.renders)
363
364
  ordered.push({ kind: 'dir', node: child });
364
365
  }
365
- for (const leaf of dir.leaves)
366
- ordered.push({ kind: 'leaf', doc: leaf });
367
- ordered.sort((a, b) => Number(isIndexPinned(b)) - Number(isIndexPinned(a)) ||
368
- itemSortKey(a).localeCompare(itemSortKey(b)));
366
+ for (const entry of dir.leaves)
367
+ ordered.push({ kind: 'leaf', entry });
368
+ ordered.sort((a, b) => itemOrder(a).localeCompare(itemOrder(b)) || itemName(a).localeCompare(itemName(b)));
369
369
  if (dir.ownCount > 0)
370
370
  ordered.push({ kind: 'more', count: dir.ownCount });
371
371
  ordered.forEach((item, i) => {
@@ -376,12 +376,17 @@ function renderChildren(dir, childPrefix, lines) {
376
376
  case 'more':
377
377
  lines.push(`${entryPrefix}[+${item.count} more]`);
378
378
  break;
379
- case 'dir':
380
- lines.push(`${entryPrefix}${item.node.segment}/`);
379
+ case 'dir': {
380
+ const label = `${item.node.segment}/`;
381
+ if (item.node.own === null)
382
+ lines.push(`${entryPrefix}${label}`);
383
+ else
384
+ renderDocEntry(item.node.own, label, entryPrefix, lines);
381
385
  renderChildren(item.node, nextPrefix, lines);
382
386
  break;
387
+ }
383
388
  case 'leaf':
384
- renderDocEntry(item.doc, leafSegment(item.doc.name), entryPrefix, nextPrefix, lines);
389
+ renderDocEntry(item.entry.doc, leafSegment(item.entry.name), entryPrefix, lines);
385
390
  break;
386
391
  }
387
392
  });
@@ -390,26 +395,18 @@ function renderChildren(dir, childPrefix, lines) {
390
395
  * headed by `rootLabel`. Returns '' when there are no docs at all (the
391
396
  * empty-tree contract).
392
397
  *
393
- * Callers (renderGrouped) pass docs that are ALREADY first-wins deduped by
394
- * Step 1 (selectWinners); the `seen` set below is a defensive no-op guard
395
- * against a residual duplicate name (e.g. the resolver-winners ∪ node-local
396
- * merge), never the primary dedup mechanism — sorting into display order
397
- * happens upstream of this call, never before dedup. */
398
+ * Callers (renderGrouped) pass docs already first-wins deduped by step 1
399
+ * (selectWinners); merging them into the canonical tree keeps that first-wins
400
+ * outcome structurally, because a node's `document` is the first candidate at
401
+ * its identity in source order. */
398
402
  function buildTree(docs, rootLabel) {
399
403
  if (docs.length === 0)
400
404
  return '';
401
- const root = newDir('');
402
- const seen = new Set();
403
- for (const d of docs) {
404
- if (seen.has(d.name))
405
- continue; // first-wins cross-scope dedup
406
- seen.add(d.name);
407
- const parent = ensureDir(root, parentDirOf(d.name));
408
- if (isVisible(d.bootRung))
409
- parent.leaves.push(d);
410
- else
411
- parent.hiddenHere += 1;
412
- }
405
+ const byPath = new Map();
406
+ for (const d of docs)
407
+ if (!byPath.has(d.path))
408
+ byPath.set(d.path, d);
409
+ const root = planDir(buildMemoryTree(docs.map((d) => d.source)).root, byPath);
413
410
  resolveDir(root, false);
414
411
  const lines = [rootLabel];
415
412
  renderChildren(root, '', lines);
@@ -476,7 +473,7 @@ function subKindDocCitation(subKind) {
476
473
  * Returns '' when the kind has zero available sub-kinds (no dangling empty
477
474
  * header). Emitted for BOTH base and orchestrator modes per the CTO's ruling —
478
475
  * mode is irrelevant to which sub-kinds are spawnable. */
479
- function buildSubPersonaMenu(kind) {
476
+ export function buildSubPersonaMenu(kind) {
480
477
  const subKinds = subKindsAvailableTo(kind);
481
478
  if (subKinds.length === 0)
482
479
  return '';
@@ -512,14 +509,14 @@ function buildSubPersonaMenu(kind) {
512
509
  * across nodes is preserved. ALWAYS returns a non-empty splice — the guidance
513
510
  * frame is unconditional, so it is never empty even when no preference is
514
511
  * eligible. */
515
- function buildPreferencesBlock(subject, seen) {
512
+ function buildPreferencesBlock(subject, target) {
516
513
  let contentDocs = [];
517
514
  let tree = '';
518
515
  let protocol = '';
519
516
  let memBody = MEMORY_USAGE_GUIDANCE;
520
517
  if (subject !== null) {
521
518
  const winners = selectWinners(subject, 'preference');
522
- seedBootDelivery(seen, winners);
519
+ recordRendered(target, winners);
523
520
  const grouped = renderGrouped(winners, 'preferences');
524
521
  contentDocs = grouped.contentDocs;
525
522
  tree = grouped.tree;
@@ -548,12 +545,22 @@ function buildPreferencesBlock(subject, seen) {
548
545
  const memoryBlock = `<memory kind="preference">\n${memBody}\n</memory>`;
549
546
  return { block: protocol === '' ? memoryBlock : `${protocol}\n\n${memoryBlock}`, contentDocs, tree };
550
547
  }
551
- export function renderPreferencesForSubject(subject, seen) {
552
- return buildPreferencesBlock(subject, seen);
548
+ export function renderPreferencesForSubject(subject, target) {
549
+ return buildPreferencesBlock(subject, target);
553
550
  }
554
551
  // ---------------------------------------------------------------------------
555
552
  // 2. Knowledge — `<memory kind="knowledge">` (the first-message bearings).
556
553
  // ---------------------------------------------------------------------------
554
+ /** STEP 1 for the knowledge block, shared by the boot render and the
555
+ * config-change delta: the resolver's knowledge winners ∪ the node's own store
556
+ * (any kind), first-wins deduped resolver-first so a same-named node doc
557
+ * loses. Node docs carry the one rung floor — a node doc with NO boot entry is
558
+ * bumped to `name` so it surfaces as a bare name instead of collapsing into a
559
+ * `[+N more]` count; a node doc whose boot entries all fail their gates stays
560
+ * absent. */
561
+ function knowledgeWinners(subject, nodeId) {
562
+ return dedupeFirstWins([...selectWinners(subject, 'knowledge'), ...nodeStoreDocs(nodeId, subject)]);
563
+ }
557
564
  /** The `<memory kind="knowledge">` block embedded in the session_start bearings
558
565
  * message (bearings.ts pushes it as a SIBLING of `<crtr-bearings>`, or drops it
559
566
  * when ''). The consultable catalog: every eligible `kind: knowledge` resolver
@@ -567,17 +574,10 @@ export function renderPreferencesForSubject(subject, seen) {
567
574
  * docs never reach the `content` rung). Procedural guidance and factual
568
575
  * references both live here as `knowledge`. Returns '' when nothing is
569
576
  * eligible. */
570
- export function renderKnowledgeForSubject(subject, nodeId, seen) {
571
- const nodeLocal = nodeLocalDocs(nodeId, subject).map((d) => {
572
- const r = bootRung(d, subject);
573
- return { ...d, bootRung: isVisible(r) || d.surfaces.some((e) => e.on === 'boot') ? r : 'name' };
574
- });
575
- const nodeLocalSet = new Set(nodeLocal);
576
- // Resolver winners first: first-wins dedup keeps the resolver's copy over a
577
- // same-named node-local doc, matching the original precedence.
578
- const winners = dedupeFirstWins([...selectWinners(subject, 'knowledge'), ...nodeLocal]);
579
- seedBootDelivery(seen, winners);
580
- const { contentProse, tree } = renderGrouped(winners, 'knowledge', nodeLocalSet);
577
+ export function renderKnowledgeForSubject(subject, nodeId, target) {
578
+ const winners = knowledgeWinners(subject, nodeId);
579
+ recordRendered(target, winners);
580
+ const { contentProse, tree } = renderGrouped(winners, 'knowledge');
581
581
  if (contentProse === '' && tree === '')
582
582
  return '';
583
583
  let out = KNOWLEDGE_INTRO;
@@ -589,3 +589,71 @@ export function renderKnowledgeForSubject(subject, nodeId, seen) {
589
589
  // Surfaced INTO this node: swap literal `$CRTR_CONTEXT_DIR` for the real path.
590
590
  return interpolateNodePaths(`<memory kind="knowledge">\n${out}\n</memory>`, nodeId);
591
591
  }
592
+ // ---------------------------------------------------------------------------
593
+ // 3. The config-change delta — the memory a node's NEW configuration exposes.
594
+ // ---------------------------------------------------------------------------
595
+ const DELTA_PROTOCOL_INTRO = 'Your node configuration changed. This operating protocol is in force for you now:';
596
+ const DELTA_PREFERENCES_INTRO = 'Your new configuration puts these preferences in force — standing directives and corrections ' +
597
+ 'from the user. To read one, run `crtr memory read <name>`. ' +
598
+ READ_LEGEND.replace('%s', 'preference');
599
+ const DELTA_KNOWLEDGE_INTRO = 'Your new configuration exposes these knowledge documents — consult them exactly like the rest ' +
600
+ 'of your catalog (`crtr memory read <name>`). ' +
601
+ READ_LEGEND.replace('%s', 'document');
602
+ /** Winners not yet loaded at their boot rung. Bootless winners remain absent. */
603
+ function undelivered(target, docs) {
604
+ return docs.filter((d) => rendersAtBoot(d) && !documentExposedAtOrAbove(target.state, d.path, d.body, d.bootRung));
605
+ }
606
+ /** What a node's configuration change newly exposes, rendered as up to three
607
+ * blocks in one string: the shipped protocol prose, a `<memory kind="preference">`
608
+ * block, and a `<memory kind="knowledge">` block — each dropped when empty, ''
609
+ * when the change exposes nothing.
610
+ *
611
+ * Both kinds use the current subject's ordinary winner assembly, then compare
612
+ * against the active exposure maximum. A lower rung or removal yields nothing
613
+ * because content already loaded into this context cannot be withdrawn.
614
+ * Rendered deltas register as transcript exposure through `target`. */
615
+ export function renderConfigChangeDelta(subject, nodeId, target) {
616
+ const prefs = undelivered(target, selectWinners(subject, 'preference'));
617
+ const knowledgeDelta = undelivered(target, knowledgeWinners(subject, nodeId));
618
+ recordRendered(target, [...prefs, ...knowledgeDelta]);
619
+ const { contentDocs, tree } = renderGrouped(prefs, 'preferences');
620
+ // Same split as the boot preference render: shipped bodies are protocol prose
621
+ // (the rules of the node's world), agent-editable bodies are memory the node
622
+ // can revise, and every non-content preference winner rides the one catalog.
623
+ const shipped = contentDocs
624
+ .filter((d) => !isAgentEditable(d))
625
+ .map((d) => d.body.trim())
626
+ .filter((b) => b !== '');
627
+ const editable = contentDocs
628
+ .filter((d) => isAgentEditable(d))
629
+ .map((d) => {
630
+ const b = d.body.trim();
631
+ return b === '' ? '' : `<preference name="${d.name}">\n${b}\n</preference>`;
632
+ })
633
+ .filter((b) => b !== '');
634
+ const blocks = [];
635
+ if (shipped.length > 0)
636
+ blocks.push(`${DELTA_PROTOCOL_INTRO}\n\n${shipped.join('\n\n')}`);
637
+ if (editable.length > 0 || tree !== '') {
638
+ let body = DELTA_PREFERENCES_INTRO;
639
+ if (editable.length > 0)
640
+ body += `\n\n${editable.join('\n\n')}`;
641
+ if (tree !== '')
642
+ body += `\n\n${tree}`;
643
+ body += `\n\n${PREFERENCES_OUTRO}`;
644
+ blocks.push(`<memory kind="preference">\n${body}\n</memory>`);
645
+ }
646
+ const knowledge = renderGrouped(knowledgeDelta, 'knowledge');
647
+ if (knowledge.contentProse !== '' || knowledge.tree !== '') {
648
+ let body = DELTA_KNOWLEDGE_INTRO;
649
+ if (knowledge.contentProse !== '')
650
+ body += `\n\n${knowledge.contentProse}`;
651
+ if (knowledge.tree !== '')
652
+ body += `\n\n${knowledge.tree}`;
653
+ body += `\n\n${KNOWLEDGE_OUTRO}`;
654
+ blocks.push(`<memory kind="knowledge">\n${body}\n</memory>`);
655
+ }
656
+ if (blocks.length === 0)
657
+ return '';
658
+ return interpolateNodePaths(blocks.join('\n\n'), nodeId);
659
+ }