@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,34 +1,18 @@
1
- import { basename, join, relative, sep } from 'node:path';
1
+ import { dirname, join, relative, sep } from 'node:path';
2
2
  import { readdirSync } from 'node:fs';
3
3
  import { CRTR_DIR_NAME } from '../types.js';
4
- import { pathExists, readText, walkFiles } from './fs-utils.js';
4
+ import { pathExists, readText, realpathOrSelf, walkFiles } from './fs-utils.js';
5
5
  import { parseFrontmatterGeneric } from './frontmatter.js';
6
- import { listInstalledPlugins, listInstalledPluginsInRoot, parseSkillQualifier } from './resolver.js';
7
- import { ambiguous, notFound, usage } from './errors.js';
6
+ import { listInstalledPlugins, listInstalledPluginsInRoot } from './resolver.js';
7
+ import { notFound, usage } from './errors.js';
8
8
  import { warn } from './output.js';
9
9
  import { NEUTRAL_PROJECT_MEMORY, pluginMemoryDir, projectScopeRoots, projectScopeRootsWithMemory, scopeMemoryDir } from './scope.js';
10
- import { effectiveDocKind, normalizeDocName, normalizeNameSegment, resolveDocName } from './substrate/schema.js';
10
+ import { effectiveDocKind } from './substrate/schema.js';
11
+ import { INDEX_SEGMENT, canonicalSegments, composeCanonicalName, localNameFromPath, normalizeNameSegment, representationForPath, resolveLocalName, routingAnchorFor, validateNamespace, } from './memory/identity.js';
11
12
  import { loadProfileManifest, profileMemoryDir } from './profiles/manifest.js';
12
13
  import { memoryDir as nodeMemoryDir } from './runtime/memory.js';
13
14
  import { descendantStoreRoots } from './nested-stores.js';
14
- /** The native (non-plugin) store dirs in resolution precedence — node >
15
- * project stack > profile > user, builtin excluded as a read-only corpus.
16
- * Addresses a store directly when the document itself may be absent, which is
17
- * how a DELETED doc's revision log is still found: history outlives the doc,
18
- * so its lookup cannot go through document resolution. */
19
- export function nativeMemoryStoresInPrecedence(scope, includeDescendants = false) {
20
- const out = [];
21
- for (const source of memorySourcesInPrecedence(ambientTarget(), scope, includeDescendants)) {
22
- if (source.scope === 'builtin' || source.memoryDir === null)
23
- continue;
24
- out.push({ scope: source.scope, memoryDir: source.memoryDir });
25
- }
26
- return out;
27
- }
28
- /** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
29
- export function memoryDocId(doc) {
30
- return `${doc.scope}/${doc.name}`;
31
- }
15
+ import { associateRepository } from './memory/repository-association.js';
32
16
  /** The target the ambient process describes: cwd + the two env seams every
33
17
  * profile/node-aware memory path reads through (`CRTR_PROFILE_ID`,
34
18
  * `CRTR_NODE_ID`). */
@@ -43,6 +27,10 @@ function ambientTarget() {
43
27
  function selectedProfileId() {
44
28
  return ambientTarget().profileId ?? '';
45
29
  }
30
+ // ---------------------------------------------------------------------------
31
+ // Store discovery: which physical stores a target sees, in what order, and
32
+ // whether each one's namespace lets it mount.
33
+ // ---------------------------------------------------------------------------
46
34
  /** The memory scopes in resolution precedence: node > project stack > profile >
47
35
  * user > builtin. Node is included only when the target names one and ranks
48
36
  * NEAREST — a node doc overrides any wider scope. Project is included only when
@@ -63,38 +51,168 @@ function scopesInPrecedence(target, scope) {
63
51
  out.push('builtin');
64
52
  return out;
65
53
  }
66
- /** Memory loads project scopes as a stack, not a single nearest root: every
67
- * ancestor `.crouter/` from cwd upward (widened by a selected profile's
68
- * `projects`) contributes, nearest first. Profile is a singleton store
69
- * resolved through the centralized `loadProfileManifest` — never a raw path
70
- * join — and drops out silently (never throws the resolver) when the
71
- * selected profile id no longer resolves to a manifest. User and builtin
72
- * remain singleton scopes after that. */
73
- function memorySourcesInPrecedence(target, scope, includeDescendants = false) {
54
+ /** Resolve a project store through its repository-root declaration. */
55
+ function projectStoreDescriptor(ownerDir, storeRoot, projectMemory) {
56
+ const base = { scope: 'project', storeRoot, ownerDir, projectMemory };
57
+ if (!pathExists(storeRoot))
58
+ return { ...base, namespace: '', mountStatus: 'absent' };
59
+ const association = associateRepository(ownerDir);
60
+ if (!association.ok) {
61
+ const remedy = `run \`crtr sys migrate --dir ${ownerDir}\``;
62
+ if (association.code === 'no-project-root') {
63
+ return {
64
+ ...base,
65
+ namespace: '',
66
+ mountStatus: 'undeclared',
67
+ mountFailure: 'association',
68
+ diagnostic: `project memory store at ${storeRoot} has no repository root declaring \`namespace:\` — ${remedy}`,
69
+ };
70
+ }
71
+ return {
72
+ ...base,
73
+ namespace: '',
74
+ mountStatus: 'invalid',
75
+ mountFailure: 'association',
76
+ diagnostic: `project memory store at ${storeRoot} cannot be associated: ${association.reason} — ${remedy}`,
77
+ };
78
+ }
79
+ const { association: repository } = association;
80
+ const repositoryFields = {
81
+ namespacePath: repository.declarationPath,
82
+ repositoryRoot: repository.repositoryRoot,
83
+ repositoryKey: repository.repositoryKey,
84
+ ownerRelativePath: repository.ownerRelativePath,
85
+ };
86
+ const rootIndex = repository.declarationPath;
87
+ const nested = repository.ownerRelativePath !== '';
88
+ const remedy = `run \`crtr sys migrate --dir ${repository.repositoryRoot}\``;
89
+ const missingDiagnostic = nested
90
+ ? `nested project memory store at ${storeRoot} takes its identity from ${rootIndex}, which is missing root ${INDEX_SEGMENT}.md declaring \`namespace:\` — ${remedy}`
91
+ : `project memory store has no root ${INDEX_SEGMENT}.md declaring \`namespace:\` — ${remedy}`;
92
+ if (!pathExists(rootIndex)) {
93
+ return {
94
+ ...base,
95
+ ...repositoryFields,
96
+ namespace: '',
97
+ mountStatus: 'undeclared',
98
+ mountFailure: 'undeclared-root',
99
+ diagnostic: missingDiagnostic,
100
+ };
101
+ }
102
+ let declared;
103
+ try {
104
+ declared = parseFrontmatterGeneric(readText(rootIndex)).data?.['namespace'];
105
+ }
106
+ catch (e) {
107
+ return {
108
+ ...base,
109
+ ...repositoryFields,
110
+ namespace: '',
111
+ mountStatus: 'invalid',
112
+ mountFailure: 'invalid-root',
113
+ diagnostic: `project memory store's ${rootIndex} does not parse (${firstLine(e)}) — ${remedy}`,
114
+ };
115
+ }
116
+ if (declared === undefined || declared === null || declared === '') {
117
+ const diagnostic = nested
118
+ ? `nested project memory store at ${storeRoot} takes its identity from ${rootIndex}, which declares no \`namespace:\` — ${remedy}`
119
+ : `project memory store's ${rootIndex} declares no \`namespace:\` — ${remedy}`;
120
+ return {
121
+ ...base,
122
+ ...repositoryFields,
123
+ namespace: '',
124
+ mountStatus: 'undeclared',
125
+ mountFailure: 'undeclared-root',
126
+ diagnostic,
127
+ };
128
+ }
129
+ // No coercion: a numeric or otherwise non-string declaration is a malformed
130
+ // store, not a namespace. Coercing it would mount a store whose namespace no
131
+ // memory command can then edit.
132
+ if (typeof declared !== 'string') {
133
+ return {
134
+ ...base,
135
+ ...repositoryFields,
136
+ namespace: '',
137
+ mountStatus: 'invalid',
138
+ mountFailure: 'invalid-root',
139
+ diagnostic: `project memory store declares a non-string \`namespace:\` (${JSON.stringify(declared)}) — quote it in ${rootIndex}, or ${remedy}`,
140
+ };
141
+ }
142
+ const valid = validateNamespace(declared);
143
+ if (!valid.ok) {
144
+ return {
145
+ ...base,
146
+ ...repositoryFields,
147
+ namespace: '',
148
+ mountStatus: 'invalid',
149
+ mountFailure: 'invalid-root',
150
+ diagnostic: `project memory store declares an invalid namespace \`${declared}\` (${valid.reason}) — ${remedy}`,
151
+ };
152
+ }
153
+ return {
154
+ ...base,
155
+ ...repositoryFields,
156
+ repositoryNamespace: declared,
157
+ namespace: composeCanonicalName(declared, repository.ownerRelativePath),
158
+ mountStatus: 'ready',
159
+ };
160
+ }
161
+ /** A store with no namespace of its own: user, profile, node, builtin. */
162
+ function plainStoreDescriptor(scope, storeRoot) {
163
+ if (storeRoot === null) {
164
+ return { scope, storeRoot: '', namespace: '', mountStatus: 'absent', projectMemory: NEUTRAL_PROJECT_MEMORY };
165
+ }
166
+ return {
167
+ scope,
168
+ storeRoot,
169
+ namespace: '',
170
+ mountStatus: pathExists(storeRoot) ? 'ready' : 'absent',
171
+ projectMemory: NEUTRAL_PROJECT_MEMORY,
172
+ };
173
+ }
174
+ /** An installed plugin's store, mounted under the plugin's own name. */
175
+ function pluginStoreDescriptor(plugin, scope, projectMemory) {
176
+ const storeRoot = pluginMemoryDir(plugin);
177
+ return {
178
+ scope,
179
+ storeRoot,
180
+ namespace: plugin.name,
181
+ plugin: plugin.name,
182
+ mountStatus: pathExists(storeRoot) ? 'ready' : 'absent',
183
+ projectMemory,
184
+ };
185
+ }
186
+ /** The exact project store owned by `ownerDir` — the `--dir` selector's store,
187
+ * loaded whatever its mount status so lint and migration can inspect a store
188
+ * the runtime refuses to mount. */
189
+ export function openProjectMemoryStore(ownerDir) {
190
+ const owner = realpathOrSelf(ownerDir);
191
+ return projectStoreDescriptor(owner, join(owner, CRTR_DIR_NAME, 'memory'), NEUTRAL_PROJECT_MEMORY);
192
+ }
193
+ /** Every store a target sees, in precedence order: node, each project store
194
+ * nearest-first (its enabled plugins after it), then profile, user, builtin.
195
+ * One real store reached twice contributes once, at its first position. */
196
+ export function memoryStoresInPrecedence(target, scope, includeDescendants = false) {
74
197
  const out = [];
198
+ const projectStores = (crtrRoot, projectMemory) => {
199
+ out.push(projectStoreDescriptor(dirname(crtrRoot), join(crtrRoot, 'memory'), projectMemory));
200
+ for (const plugin of listInstalledPluginsInRoot('project', crtrRoot).filter((p) => p.enabled)) {
201
+ out.push(pluginStoreDescriptor(plugin, 'project', projectMemory));
202
+ }
203
+ };
75
204
  for (const s of scopesInPrecedence(target, scope)) {
76
205
  if (s === 'project') {
77
206
  const annotated = projectScopeRootsWithMemory(target.cwd, target.profileId);
78
- for (const { root, memory } of annotated) {
79
- out.push({
80
- scope: 'project',
81
- memoryDir: join(root, 'memory'),
82
- plugins: listInstalledPluginsInRoot('project', root).filter((p) => p.enabled),
83
- projectMemory: memory,
84
- });
85
- }
207
+ for (const { root, memory } of annotated)
208
+ projectStores(root, memory);
86
209
  // Descendant stores rank after every ancestor source (and before
87
- // profile): an ancestor doc always beats a nested doc on a name tie.
88
- // They carry the neutral ceiling — no profile entry names them, and
89
- // descendant discovery is off for every automatic delivery path.
210
+ // profile): an ancestor doc always wins an equal-canonical tie against a
211
+ // nested one. They carry the neutral ceiling — no profile entry names
212
+ // them, and descendant discovery is off for every automatic delivery path.
90
213
  if (includeDescendants) {
91
214
  for (const root of descendantStoreRoots(annotated.map(({ root: r }) => r))) {
92
- out.push({
93
- scope: 'project',
94
- memoryDir: join(root, 'memory'),
95
- plugins: listInstalledPluginsInRoot('project', root).filter((p) => p.enabled),
96
- projectMemory: NEUTRAL_PROJECT_MEMORY,
97
- });
215
+ projectStores(root, NEUTRAL_PROJECT_MEMORY);
98
216
  }
99
217
  }
100
218
  }
@@ -103,173 +221,330 @@ function memorySourcesInPrecedence(target, scope, includeDescendants = false) {
103
221
  if (profileId !== '') {
104
222
  try {
105
223
  const { profileId: resolvedId } = loadProfileManifest(profileId);
106
- out.push({ scope: 'profile', memoryDir: profileMemoryDir(resolvedId), plugins: [], projectMemory: NEUTRAL_PROJECT_MEMORY });
224
+ out.push(plainStoreDescriptor('profile', profileMemoryDir(resolvedId)));
107
225
  }
108
226
  catch {
109
- // Missing/deleted/invalid selected profile: no profile memory source.
227
+ // Missing/deleted/invalid selected profile: no profile memory store.
110
228
  }
111
229
  }
112
230
  }
113
231
  else if (s === 'node') {
114
- // The this-node store — a singleton dir in the node's context, no plugins.
115
- // Silently absent when the target names no node.
116
232
  const nodeId = target.nodeId ?? '';
117
233
  if (nodeId !== '')
118
- out.push({ scope: 'node', memoryDir: nodeMemoryDir(nodeId), plugins: [], projectMemory: NEUTRAL_PROJECT_MEMORY });
234
+ out.push(plainStoreDescriptor('node', nodeMemoryDir(nodeId)));
119
235
  }
120
236
  else {
121
- out.push({
122
- scope: s,
123
- memoryDir: scopeMemoryDir(s),
124
- plugins: listInstalledPlugins(s).filter((p) => p.enabled),
125
- projectMemory: NEUTRAL_PROJECT_MEMORY,
126
- });
237
+ out.push(plainStoreDescriptor(s, scopeMemoryDir(s)));
238
+ for (const plugin of listInstalledPlugins(s).filter((p) => p.enabled)) {
239
+ out.push(pluginStoreDescriptor(plugin, s, NEUTRAL_PROJECT_MEMORY));
240
+ }
127
241
  }
128
242
  }
129
- return out;
243
+ const seen = new Set();
244
+ const deduped = out.filter((store) => {
245
+ if (store.storeRoot === '')
246
+ return false;
247
+ const key = realpathOrSelf(store.storeRoot);
248
+ if (seen.has(key))
249
+ return false;
250
+ seen.add(key);
251
+ return true;
252
+ });
253
+ const collisions = new Map();
254
+ for (const store of deduped) {
255
+ if (store.scope !== 'project' ||
256
+ store.mountStatus !== 'ready' ||
257
+ store.repositoryRoot === undefined ||
258
+ store.ownerRelativePath === undefined)
259
+ continue;
260
+ const key = `${store.repositoryRoot}\u0000${store.ownerRelativePath}`;
261
+ const group = collisions.get(key);
262
+ if (group)
263
+ group.push(store);
264
+ else
265
+ collisions.set(key, [store]);
266
+ }
267
+ return deduped.map((store) => {
268
+ if (store.scope !== 'project' ||
269
+ store.mountStatus !== 'ready' ||
270
+ store.repositoryRoot === undefined ||
271
+ store.ownerRelativePath === undefined)
272
+ return store;
273
+ const group = collisions.get(`${store.repositoryRoot}\u0000${store.ownerRelativePath}`);
274
+ if (group === undefined || group.length < 2)
275
+ return store;
276
+ const owners = group.map((member) => member.ownerDir ?? member.storeRoot).join(', ');
277
+ return {
278
+ ...store,
279
+ mountStatus: 'invalid',
280
+ mountFailure: 'collision',
281
+ diagnostic: `project memory stores at ${owners} normalize to the same effective prefix \`${store.namespace}\` — rename one owner directory`,
282
+ };
283
+ });
284
+ }
285
+ /** The native (non-plugin) MOUNTED store dirs in resolution precedence.
286
+ * Addresses a store directly when the document itself may be absent, which is
287
+ * how a DELETED doc's revision log is still found: history outlives the doc,
288
+ * so its lookup cannot go through document resolution. */
289
+ export function nativeMemoryStoresInPrecedence(scope, includeDescendants = false) {
290
+ return memoryStoresInPrecedence(ambientTarget(), scope, includeDescendants)
291
+ .filter((store) => store.mountStatus === 'ready' && store.scope !== 'builtin' && store.plugin === undefined)
292
+ .map((store) => ({ scope: store.scope, memoryDir: store.storeRoot, store }));
293
+ }
294
+ // ---------------------------------------------------------------------------
295
+ // Document loading: one store's markdown files become canonically-identified
296
+ // documents.
297
+ // ---------------------------------------------------------------------------
298
+ function firstLine(e) {
299
+ return (e instanceof Error ? e.message : String(e)).split('\n')[0] ?? '';
130
300
  }
131
- function loadMemoryDoc(scope, root, path, fallbackName, projectMemory = NEUTRAL_PROJECT_MEMORY) {
301
+ /** Load one markdown file as a document of `store`, or null when it has no
302
+ * canonical identity — a root `INDEX.md` in a store without a namespace, which
303
+ * would compose to the empty name. */
304
+ function loadMemoryDoc(store, path, physicalRelativePath) {
132
305
  const { data, body } = parseFrontmatterGeneric(readText(path));
133
- return { name: resolveDocName(data, fallbackName), scope, path, root, frontmatter: data, body, projectMemory };
306
+ const pathLocal = localNameFromPath(physicalRelativePath);
307
+ const representation = representationForPath(physicalRelativePath);
308
+ // A store's root INDEX.md is the store's own document: its identity is the
309
+ // namespace itself, never an explicit `name` (lint rejects one there).
310
+ const localName = pathLocal === '' ? '' : resolveLocalName(data, pathLocal);
311
+ const name = composeCanonicalName(store.namespace, localName);
312
+ if (name === '')
313
+ return null;
314
+ return {
315
+ name,
316
+ localName,
317
+ scope: store.scope,
318
+ path,
319
+ root: store.storeRoot,
320
+ physicalRelativePath,
321
+ representation,
322
+ routingAnchor: routingAnchorFor(name, representation),
323
+ store,
324
+ frontmatter: data,
325
+ body,
326
+ projectMemory: store.projectMemory,
327
+ ...(store.plugin === undefined ? {} : { plugin: store.plugin }),
328
+ };
329
+ }
330
+ /** Within one store, `foo/INDEX.md` deterministically precedes `foo.md` for the
331
+ * same canonical identity: the directory form physically fronts the directory.
332
+ * Lint errors on the pair and every mutation refuses it, but a read still has
333
+ * one answer. */
334
+ function compareDocs(a, b) {
335
+ const byName = a.name.localeCompare(b.name);
336
+ if (byName !== 0)
337
+ return byName;
338
+ if (a.representation !== b.representation)
339
+ return a.representation === 'directory-index' ? -1 : 1;
340
+ return a.physicalRelativePath.localeCompare(b.physicalRelativePath);
134
341
  }
135
- /** All memory docs in one memory/ dir, scanned recursively for *.md (topical
136
- * subdirs supported), sorted by resolver identity (explicit frontmatter name
137
- * when present, else path-derived). SKILL.md bundles are legacy Agent Skills
138
- * and are ignored; crouter memory docs are plain .md files under memory/. */
139
- function listMemoryDocsInDir(scope, dir, quiet = false, projectMemory = NEUTRAL_PROJECT_MEMORY) {
140
- if (!dir || !pathExists(dir))
342
+ /** Every document in one store, sorted by canonical identity. SKILL.md bundles
343
+ * are Agent Skills, not memory docs. Enumeration stops at a nested `.crouter/`:
344
+ * that is another store's mount point, never this store's content. */
345
+ export function loadStoreMemoryDocs(store, quiet = false) {
346
+ if (store.storeRoot === '' || !pathExists(store.storeRoot))
141
347
  return [];
142
348
  const docs = [];
143
- const found = [];
144
- const stack = [dir];
145
- while (stack.length) {
146
- const d = stack.pop();
147
- let entries;
148
- try {
149
- entries = readdirSync(d, { withFileTypes: true });
150
- }
151
- catch {
152
- continue;
153
- }
154
- for (const e of entries) {
155
- // Enumeration stops at store boundaries: a nested `.crouter/` is another
156
- // scope's mount point (e.g. a dir-scoped store inside a dir that is itself
157
- // a store, like builtin-memory in the crouter repo), never this store's
158
- // own content — walking into it would register a different tier's docs.
159
- if (e.isDirectory()) {
160
- if (e.name !== CRTR_DIR_NAME)
161
- stack.push(join(d, e.name));
162
- }
163
- else if (e.isFile() && e.name.endsWith('.md') && e.name !== 'SKILL.md') {
164
- const file = join(d, e.name);
165
- const raw = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
166
- const name = normalizeDocName(raw);
167
- if (name)
168
- found.push({ path: file, name });
169
- }
170
- }
171
- }
172
- for (const { path, name } of found) {
349
+ for (const path of walkFiles(store.storeRoot, (n) => n.endsWith('.md') && n !== 'SKILL.md', (d) => d === CRTR_DIR_NAME)) {
350
+ const physicalRelativePath = relative(store.storeRoot, path).split(sep).join('/');
173
351
  // COLLECTION layer: the strict frontmatter parser throws on invalid YAML.
174
- // Isolate one malformed doc with a clear scoped notice + skip, so a single
175
- // bad file can't brick `memory list`/`find` or the substrate boot render.
176
- // `quiet` suppresses the notice for a targeted resolve (a leaf-name read),
177
- // where another doc's health is irrelevant noise before the result.
352
+ // Isolate one malformed doc with a scoped notice + skip, so a single bad
353
+ // file can't brick `memory list`/`find` or the substrate boot render.
354
+ // `quiet` suppresses the notice for a targeted resolve, where another doc's
355
+ // health is noise before the result.
178
356
  try {
179
- docs.push(loadMemoryDoc(scope, dir, path, name, projectMemory));
357
+ const doc = loadMemoryDoc(store, path, physicalRelativePath);
358
+ if (doc === null) {
359
+ if (!quiet)
360
+ warn(`${path}: a root ${INDEX_SEGMENT}.md has no canonical name in a store without a namespace`);
361
+ continue;
362
+ }
363
+ docs.push(doc);
180
364
  }
181
365
  catch (e) {
182
- const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
183
366
  if (!quiet)
184
- warn(`invalid frontmatter in ${path}: ${msg}`);
367
+ warn(`invalid frontmatter in ${path}: ${firstLine(e)}`);
185
368
  }
186
369
  }
187
- return docs.sort((a, b) => a.name.localeCompare(b.name));
370
+ return docs.sort(compareDocs);
188
371
  }
189
- /** All native memory docs for a scope. Project scope is a nearest-first stack of
190
- * every ancestor `.crouter/memory/` (widened by a selected profile's project
191
- * stack); profile is the selected profile's own singleton store, resolved
192
- * through `loadProfileManifest`; user and builtin are singleton stores. */
193
- export function listMemoryDocs(scope, quiet = false) {
194
- const target = ambientTarget();
195
- if (scope === 'project') {
196
- return listProjectMemoryDocs(target.cwd, target.profileId, quiet);
197
- }
198
- if (scope === 'profile') {
199
- const profileId = target.profileId ?? '';
200
- if (profileId === '')
201
- return [];
202
- try {
203
- const { profileId: resolvedId } = loadProfileManifest(profileId);
204
- return listMemoryDocsInDir('profile', profileMemoryDir(resolvedId), quiet);
205
- }
206
- catch {
207
- return [];
208
- }
209
- }
210
- if (scope === 'node') {
211
- const nodeId = target.nodeId ?? '';
212
- if (nodeId === '')
213
- return [];
214
- return listMemoryDocsInDir('node', nodeMemoryDir(nodeId), quiet);
215
- }
216
- return listMemoryDocsInDir(scope, scopeMemoryDir(scope), quiet);
217
- }
218
- /** All of one plugin's substrate docs, mounted under the virtual `<pluginName>/`
219
- * namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
220
- * doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
221
- * slash-separated) then prefixing the plugin name. Builtin has no plugins. */
372
+ /** All of one plugin's substrate docs, mounted under the plugin's own name:
373
+ * each doc's store-local name (explicit or path-derived) composes under that
374
+ * mount prefix. */
222
375
  export function listPluginMemoryDocs(plugin, scope, quiet = false, projectMemory = NEUTRAL_PROJECT_MEMORY) {
223
- const dir = pluginMemoryDir(plugin);
224
- if (!pathExists(dir))
225
- return [];
376
+ return loadStoreMemoryDocs(pluginStoreDescriptor(plugin, scope, projectMemory), quiet);
377
+ }
378
+ // ---------------------------------------------------------------------------
379
+ // Views: exact canonical resolution over a loaded corpus.
380
+ // ---------------------------------------------------------------------------
381
+ /** The canonical form of a query: empty segments dropped. An `NN-` ordering
382
+ * prefix is physical-path-only and is NOT stripped here — a canonical name
383
+ * never carries one, so `04-base` is simply no document's address. */
384
+ function normalizeQuery(rawName) {
385
+ return canonicalSegments(rawName.trim()).join('/');
386
+ }
387
+ function notFoundError(rawName, name, scope) {
388
+ const segments = canonicalSegments(name);
389
+ const collapsedIndex = segments[segments.length - 1] === INDEX_SEGMENT;
390
+ if (collapsedIndex)
391
+ segments.pop();
392
+ const leaf = segments[segments.length - 1] ?? name;
393
+ const next = collapsedIndex
394
+ ? `A directory's own document answers at the directory name — drop the trailing /${INDEX_SEGMENT}. Run \`crtr memory find ${leaf}\` to find the canonical name.`
395
+ : `Resolution takes the full canonical name (a project doc is <namespace>/<local name>). Run \`crtr memory find ${leaf}\` to find it.`;
396
+ return notFound(`memory document not found: ${rawName}`, { memory: name, ...(scope ? { scope } : {}), next });
397
+ }
398
+ function makeView(stores, docs, scope) {
399
+ const candidatesFor = (rawName, opts) => {
400
+ const name = normalizeQuery(rawName);
401
+ if (name === '')
402
+ return null;
403
+ const kind = opts?.kind;
404
+ const candidates = docs.filter((d) => d.name === name && (kind === undefined || effectiveDocKind(d) === kind));
405
+ if (candidates.length === 0)
406
+ return null;
407
+ return { name, winner: candidates[0], candidates };
408
+ };
409
+ return {
410
+ stores,
411
+ docs,
412
+ candidates: candidatesFor,
413
+ candidateSets(opts) {
414
+ const kind = opts?.kind;
415
+ const grouped = new Map();
416
+ for (const doc of docs) {
417
+ if (kind !== undefined && effectiveDocKind(doc) !== kind)
418
+ continue;
419
+ const bucket = grouped.get(doc.name);
420
+ if (bucket)
421
+ bucket.push(doc);
422
+ else
423
+ grouped.set(doc.name, [doc]);
424
+ }
425
+ return [...grouped.entries()]
426
+ .sort(([a], [b]) => a.localeCompare(b))
427
+ .map(([name, candidates]) => ({ name, winner: candidates[0], candidates }));
428
+ },
429
+ resolve(rawName, opts) {
430
+ const name = normalizeQuery(rawName);
431
+ if (name === '')
432
+ throw usage('memory document name required');
433
+ const set = candidatesFor(name, opts);
434
+ if (set === null)
435
+ throw notFoundError(rawName, name, scope);
436
+ return set.winner;
437
+ },
438
+ };
439
+ }
440
+ /** Load one target's whole corpus: every mounted store's documents in source
441
+ * order. A project store that does not mount is reported once (unless quiet)
442
+ * with its migration remedy and contributes nothing. */
443
+ export function loadMemoryTargetView(target, opts = {}) {
444
+ const quiet = opts.quiet ?? false;
445
+ const stores = memoryStoresInPrecedence(target, opts.scope, opts.includeDescendants ?? false);
226
446
  const docs = [];
227
- for (const file of walkFiles(dir, (n) => n.endsWith('.md') && n !== 'SKILL.md', (d) => d === CRTR_DIR_NAME)) {
228
- const derived = normalizeDocName(relative(dir, file).replace(/\.md$/i, '').split(sep).join('/'));
229
- if (!derived)
447
+ for (const store of stores) {
448
+ if (store.mountStatus === 'absent')
449
+ continue;
450
+ if (store.mountStatus !== 'ready') {
451
+ if (!quiet && store.diagnostic !== undefined)
452
+ warn(`memory store not mounted: ${store.diagnostic}`);
230
453
  continue;
231
- const name = `${plugin.name}/${derived}`;
232
- try {
233
- docs.push({ ...loadMemoryDoc(scope, dir, file, name, projectMemory), plugin: plugin.name });
234
- }
235
- catch (e) {
236
- const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
237
- if (!quiet)
238
- warn(`invalid frontmatter in ${file}: ${msg}`);
239
454
  }
455
+ docs.push(...loadStoreMemoryDocs(store, quiet));
240
456
  }
241
- return docs.sort((a, b) => a.name.localeCompare(b.name));
457
+ return { target, ...makeView(stores, docs, opts.scope) };
242
458
  }
243
- /** All docs belonging to a single resolved source: native docs first, then
244
- * enabled-plugin docs — native wins on a first-wins dedup within the source.
245
- * Shared by `listAllMemoryDocs` (flattens every source) and `resolveMemoryDoc`
246
- * (consults one source's docs at a time, nearest source first). */
247
- function sourceMemoryDocs(source, quiet = false) {
248
- return [
249
- ...listMemoryDocsInDir(source.scope, source.memoryDir, quiet, source.projectMemory),
250
- ...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope, quiet, source.projectMemory)),
251
- ];
459
+ /** Load ONE exact physical store. An undeclared/invalid project store loads
460
+ * under the empty namespace so lint and migration can inspect what it holds;
461
+ * no target view ever sees those documents. */
462
+ export function loadMemoryStoreView(store, quiet = false) {
463
+ return { store, ...makeView([store], loadStoreMemoryDocs(store, quiet)) };
252
464
  }
465
+ // ---------------------------------------------------------------------------
466
+ // Corpus listing.
467
+ // ---------------------------------------------------------------------------
253
468
  /** All project-scoped docs visible from an explicit node workspace/profile — the
254
469
  * project-only slice of a `MemoryTarget` view, used where only workspace docs
255
470
  * are wanted (a workspace-open render). For a full-precedence target-addressed
256
471
  * lookup, use `resolveMemoryDocForTarget`. */
257
472
  export function listProjectMemoryDocs(startDir = process.cwd(), profileId = selectedProfileId() || null, quiet = false) {
258
- return projectScopeRootsWithMemory(startDir, profileId).flatMap(({ root, memory }) => sourceMemoryDocs({
259
- scope: 'project',
260
- memoryDir: join(root, 'memory'),
261
- plugins: listInstalledPluginsInRoot('project', root).filter((plugin) => plugin.enabled),
262
- projectMemory: memory,
263
- }, quiet));
473
+ return [
474
+ ...loadMemoryTargetView({ cwd: startDir, profileId, nodeId: null }, { scope: 'project', quiet }).docs,
475
+ ];
264
476
  }
265
477
  /** All memory docs across the resolved sources, in precedence order: each
266
478
  * ancestor project `.crouter/` from nearest to farthest, then the selected
267
479
  * profile's memory (if any), then user, then builtin. Within each source,
268
- * native docs are emitted before enabled-plugin docs, so native wins on the
269
- * caller's first-wins dedup. */
480
+ * native docs are emitted before enabled-plugin docs, and equal-canonical
481
+ * candidates stay in source order, so a caller's first-wins dedup yields the
482
+ * address winners. */
270
483
  export function listAllMemoryDocs(scope, quiet = false, includeDescendants = false) {
271
- return memorySourcesInPrecedence(ambientTarget(), scope, includeDescendants).flatMap((source) => sourceMemoryDocs(source, quiet));
484
+ return [...loadMemoryTargetView(ambientTarget(), { ...(scope ? { scope } : {}), quiet, includeDescendants }).docs];
485
+ }
486
+ export function createMemoryDocSnapshot(target = ambientTarget()) {
487
+ // Quiet: a snapshot feeds command registration and migration planning, where
488
+ // another doc's frontmatter health is noise — corpus health is `lint`'s job.
489
+ const view = loadMemoryTargetView(target, { quiet: true });
490
+ return {
491
+ docs: view.docs,
492
+ resolve: (names) => {
493
+ const resolved = new Map();
494
+ for (const rawName of names) {
495
+ const set = view.candidates(rawName);
496
+ // A command registration reports its own body-read failure; one
497
+ // malformed/stale identity must not suppress every other slash command.
498
+ if (set !== null)
499
+ resolved.set(rawName, set.winner);
500
+ }
501
+ return resolved;
502
+ },
503
+ };
504
+ }
505
+ /** Resolve several canonical names over one memory-source snapshot. Failed
506
+ * names are omitted so callers can preserve their per-document error behavior. */
507
+ export function resolveMemoryDocs(names) {
508
+ return createMemoryDocSnapshot().resolve(names);
272
509
  }
510
+ /** The address winner for an exact canonical name in the ambient target. */
511
+ export function resolveMemoryDoc(rawName, opts = {}) {
512
+ return resolveMemoryDocForTarget(rawName, ambientTarget(), opts);
513
+ }
514
+ /** Resolve a memory document as ANOTHER node would see it — the same precedence
515
+ * chain (node-local > project stack > profile > user > builtin), read from the
516
+ * target's cwd/profile/node rather than the host process's. This is what crtrd
517
+ * resolves a `[[name]]` link through: the daemon's own cwd and env name no
518
+ * node, and the same name can be a different document for two nodes. */
519
+ export function resolveMemoryDocForTarget(rawName, target, opts = {}) {
520
+ return resolveMemoryCandidatesForTarget(rawName, target, opts).winner;
521
+ }
522
+ /** Every physical candidate for an exact canonical name in the ambient target,
523
+ * in source order — what path-aware inventory, diagnostics, and automatic
524
+ * event eligibility consume instead of the winner alone. */
525
+ export function resolveMemoryCandidates(rawName, opts = {}) {
526
+ return resolveMemoryCandidatesForTarget(rawName, ambientTarget(), opts);
527
+ }
528
+ export function resolveMemoryCandidatesForTarget(rawName, target, opts = {}) {
529
+ const view = loadMemoryTargetView(target, { ...opts, quiet: true });
530
+ const name = normalizeQuery(rawName);
531
+ if (name === '')
532
+ throw usage('memory document name required');
533
+ const set = view.candidates(name, opts);
534
+ if (set === null)
535
+ throw notFoundError(rawName, name, opts.scope);
536
+ return set;
537
+ }
538
+ /** The address winner for an exact canonical name inside ONE project store —
539
+ * the `--dir` selector's resolution. The query is still the full canonical
540
+ * name, including that store's namespace. */
541
+ export function resolveMemoryDocInStore(rawName, store, opts = {}) {
542
+ return loadMemoryStoreView(store, true).resolve(rawName, opts);
543
+ }
544
+ // ---------------------------------------------------------------------------
545
+ // Physical sidecar lookup: the revision log mirrors the doc tree, so it is
546
+ // addressed by PHYSICAL path segments, not by canonical identity.
547
+ // ---------------------------------------------------------------------------
273
548
  /** Find the direct child of `dir` — an `ext` file (matched on name minus
274
549
  * extension) or a directory — whose NORMALIZED display name equals
275
550
  * `segment`. An exact literal match (no prefix to strip) wins over a
@@ -309,215 +584,26 @@ function matchNormalizedChild(dir, segment, want, ext = '.md') {
309
584
  return normalizedHit;
310
585
  }
311
586
  /** Resolve a NORMALIZED (numeric-prefix-stripped) segment path against a
312
- * physical memory dir: walk intermediate segments matching each as a
313
- * directory by normalized name, then resolve the final segment as either a
314
- * `.md` file or a directory. Either half is null when
315
- * the segment path does not resolve that way. This is what makes
316
- * `00-runtime-base/00-authoring.md` findable as `runtime-base/authoring` and `01-spine/00-has-manager`
317
- * findable as `spine/has-manager` — the physical path keeps its pins, only
318
- * lookup is prefix-blind. */
319
- function resolveNormalizedPath(baseDir, segments, ext = '.md') {
587
+ * physical dir: walk intermediate segments matching each as a directory by
588
+ * normalized name, then resolve the final segment as an `ext` file. Null when
589
+ * the segment path does not resolve. This is what keeps a sidecar findable
590
+ * when its physical path carries ordering pins — `00-topic.md` logged at
591
+ * `.history/00-topic.jsonl` still answers to `topic`. */
592
+ function resolveNormalizedFile(baseDir, segments, ext) {
320
593
  let curDir = baseDir;
321
594
  for (let i = 0; i < segments.length - 1; i++) {
322
595
  const next = matchNormalizedChild(curDir, segments[i], 'dir', ext);
323
596
  if (!next)
324
- return { filePath: null, dirPath: null };
597
+ return null;
325
598
  curDir = next;
326
599
  }
327
- const last = segments[segments.length - 1];
328
- return {
329
- filePath: matchNormalizedChild(curDir, last, 'file', ext),
330
- dirPath: matchNormalizedChild(curDir, last, 'dir', ext),
331
- };
600
+ return matchNormalizedChild(curDir, segments[segments.length - 1], 'file', ext);
332
601
  }
333
- /** Resolve a document NAME against a `.history` tree, which mirrors the doc
334
- * tree segment for segment with `.jsonl` in place of `.md`. Reuses the doc
335
- * resolution rules so a log outlives its doc under the same name the doc had:
336
- * numeric prefixes stay prefix-blind (`00-topic.md` logged at
337
- * `.history/00-topic.jsonl` still answers to `topic`). Returns null when
338
- * nothing resolves. */
602
+ /** Resolve PHYSICAL path segments against a `.history` tree, which mirrors the
603
+ * doc tree segment for segment with `.jsonl` in place of `.md`. A log outlives
604
+ * its document, so this lookup cannot go through document resolution. */
339
605
  export function resolveHistoryLogPath(historyDir, segments) {
340
- return resolveNormalizedPath(historyDir, segments, '.jsonl').filePath;
341
- }
342
- /** Direct full-path lookup of memory/<name>.md within ONE source. Returns that
343
- * source's single hit, or undefined — a source can produce at most one direct
344
- * match (native wins over plugin within the source, and at most one plugin's
345
- * name can match). A directory name matches NO doc here: the read leaf
346
- * answers a directory with its listing, never a stand-in document.
347
- *
348
- * `name`'s segments are already normalized (a caller-supplied identifier is
349
- * never authored with a numeric prefix); resolution against the physical tree
350
- * is prefix-blind via `resolveNormalizedPath` so a normalized name finds an
351
- * `NN-`-pinned physical file/dir. */
352
- function findMemoryMatchInSource(name, segments, source, legacyDirectoryIndex = false) {
353
- const isLegacySkillDoc = segments.at(-1) === 'SKILL';
354
- // Native memory dir first inside this source (native-before-plugin
355
- // precedence), then its enabled plugins.
356
- const dir = source.memoryDir;
357
- if (dir) {
358
- const { filePath, dirPath } = resolveNormalizedPath(dir, segments);
359
- if (!isLegacySkillDoc && filePath !== null)
360
- return loadMemoryDoc(source.scope, dir, filePath, name, source.projectMemory);
361
- if (legacyDirectoryIndex && dirPath !== null) {
362
- const indexPath = join(dirPath, 'INDEX.md');
363
- if (pathExists(indexPath))
364
- return loadMemoryDoc(source.scope, dir, indexPath, name, source.projectMemory);
365
- }
366
- }
367
- // Plugin memory dir: a `<plugin>/<rest>` name resolves against that enabled
368
- // plugin's memory/ tree (the `<pluginName>/` mount that listAllMemoryDocs
369
- // enumerates — `read` must resolve what `list` shows).
370
- const slash = name.indexOf('/');
371
- if (slash <= 0 && !legacyDirectoryIndex)
372
- return undefined;
373
- const pluginName = slash > 0 ? name.slice(0, slash) : name;
374
- const rest = slash > 0 ? name.slice(slash + 1) : '';
375
- for (const p of source.plugins) {
376
- if (p.name !== pluginName)
377
- continue;
378
- const pdir = pluginMemoryDir(p);
379
- const restSegments = rest === '' ? [] : rest.split('/');
380
- if (restSegments.length > 0) {
381
- const { filePath } = resolveNormalizedPath(pdir, restSegments);
382
- if (restSegments.at(-1) !== 'SKILL' && filePath !== null)
383
- return loadMemoryDoc(source.scope, pdir, filePath, name, source.projectMemory);
384
- }
385
- if (legacyDirectoryIndex) {
386
- const indexDir = rest === '' ? pdir : resolveNormalizedPath(pdir, restSegments).dirPath;
387
- if (indexDir !== null) {
388
- const indexPath = join(indexDir, 'INDEX.md');
389
- if (pathExists(indexPath))
390
- return loadMemoryDoc(source.scope, pdir, indexPath, name, source.projectMemory);
391
- }
392
- }
393
- }
394
- return undefined;
395
- }
396
- function formatLeafAmbiguous(leaf, matches) {
397
- const ids = matches.map(memoryDocId).join(', ');
398
- return `ambiguous memory document: ${leaf} matches multiple documents: ${ids}`;
399
- }
400
- function loadMemorySources(target, scope, includeDescendants = false) {
401
- // Quiet: a targeted read must not spew other docs' frontmatter warnings
402
- // before its own result (esp. a not_found) — corpus health is `lint`'s job.
403
- return memorySourcesInPrecedence(target, scope, includeDescendants).map((source) => ({
404
- source,
405
- docs: sourceMemoryDocs(source, true),
406
- }));
407
- }
408
- export function createMemoryDocSnapshot(target = ambientTarget()) {
409
- const defaultSources = loadMemorySources(target);
410
- const sourcesByScope = new Map([[undefined, defaultSources]]);
411
- const sourceFor = (scope) => {
412
- let sources = sourcesByScope.get(scope);
413
- if (sources === undefined) {
414
- sources = loadMemorySources(target, scope);
415
- sourcesByScope.set(scope, sources);
416
- }
417
- return sources;
418
- };
419
- return {
420
- docs: defaultSources.flatMap(({ docs }) => docs),
421
- resolve: (names) => {
422
- const resolved = new Map();
423
- for (const rawName of names) {
424
- try {
425
- const parsed = parseSkillQualifier(rawName);
426
- resolved.set(rawName, resolveMemoryDocFromSources(rawName, {}, sourceFor(parsed.scope)));
427
- }
428
- catch {
429
- // A command registration reports its own body-read failure below; one
430
- // malformed/stale identity must not suppress every other slash command.
431
- }
432
- }
433
- return resolved;
434
- },
435
- legacyDirectoryIndex: (name) => {
436
- if (name === 'INDEX' || name.endsWith('/INDEX'))
437
- return null;
438
- try {
439
- const parsed = parseSkillQualifier(name);
440
- const sources = sourceFor(parsed.scope);
441
- const legacy = resolveMemoryDocFromSources(name, {}, sources, true);
442
- if (basename(legacy.path) !== 'INDEX.md')
443
- return null;
444
- const explicitName = `${name}/INDEX`;
445
- const explicit = resolveMemoryDocFromSources(explicitName, {}, sources);
446
- return explicit.path === legacy.path ? explicitName : null;
447
- }
448
- catch {
449
- return null;
450
- }
451
- },
452
- };
453
- }
454
- function resolveMemoryDocFromSources(rawName, opts, sources, legacyDirectoryIndex = false) {
455
- const parsed = parseSkillQualifier(rawName);
456
- if (parsed.scope && opts.scope && parsed.scope !== opts.scope) {
457
- throw usage(`scope conflict: identifier "${rawName}" uses scope "${parsed.scope}" but --scope is "${opts.scope}"`);
458
- }
459
- const name = parsed.segments.join('/');
460
- if (name === '')
461
- throw usage(`memory document name required`);
462
- const segments = name.split('/');
463
- const isLeafQuery = !name.includes('/');
464
- const kindOk = (d) => opts.kind === undefined || effectiveDocKind(d) === opts.kind;
465
- // Strong matches across the full scope stack come first. This preserves
466
- // nearest-scope shadowing for canonical/direct collisions without allowing a
467
- // nested plugin template's leaf to hide a user doc whose full identity is the
468
- // query.
469
- for (const { source, docs } of sources) {
470
- const identity = docs.find((d) => d.name === name);
471
- if (identity && kindOk(identity))
472
- return identity;
473
- const direct = findMemoryMatchInSource(name, segments, source, legacyDirectoryIndex);
474
- if (direct && kindOk(direct))
475
- return direct;
476
- }
477
- if (isLeafQuery) {
478
- for (const { docs } of sources) {
479
- const leafMatches = docs.filter((d) => (d.name.split('/').pop() ?? d.name) === name && kindOk(d));
480
- if (leafMatches.length === 0)
481
- continue;
482
- // Same path-derived name within this source → precedence wins (return
483
- // first); genuinely different docs sharing a leaf → ambiguous.
484
- const distinctNames = new Set(leafMatches.map((d) => d.name));
485
- if (distinctNames.size === 1)
486
- return leafMatches[0];
487
- throw ambiguous(formatLeafAmbiguous(name, leafMatches), {
488
- memory: name,
489
- candidates: leafMatches.map((d) => ({
490
- id: memoryDocId(d),
491
- scope: d.scope,
492
- path: d.path,
493
- })),
494
- next: 'Multiple documents share this leaf name. Re-run with one of the full names in candidates.',
495
- });
496
- }
497
- }
498
- throw notFound(`memory document not found: ${rawName}`, {
499
- memory: name,
500
- scope: parsed.scope,
501
- });
502
- }
503
- /** Resolve several unqualified names over one memory-source snapshot. Failed
504
- * names are omitted so callers can preserve their per-document error behavior. */
505
- export function resolveMemoryDocs(names) {
506
- return createMemoryDocSnapshot().resolve(names);
507
- }
508
- export function resolveMemoryDoc(rawName, opts = {}) {
509
- return resolveMemoryDocForTarget(rawName, ambientTarget(), opts);
510
- }
511
- /** Resolve a memory document as ANOTHER node would see it — the same precedence
512
- * chain (node-local > project stack > profile > user > builtin), read from the
513
- * target's cwd/profile/node rather than the host process's. This is what crtrd
514
- * resolves a `[[name]]` link through: the daemon's own cwd and env name no
515
- * node, and the same name can be a different document for two nodes. */
516
- export function resolveMemoryDocForTarget(rawName, target, opts = {}) {
517
- const parsed = parseSkillQualifier(rawName);
518
- if (parsed.scope && opts.scope && parsed.scope !== opts.scope) {
519
- throw usage(`scope conflict: identifier "${rawName}" uses scope "${parsed.scope}" but --scope is "${opts.scope}"`);
520
- }
521
- const effectiveScope = opts.scope ?? parsed.scope;
522
- return resolveMemoryDocFromSources(rawName, opts, loadMemorySources(target, effectiveScope, opts.includeDescendants ?? false));
606
+ if (segments.length === 0)
607
+ return null;
608
+ return resolveNormalizedFile(historyDir, segments, '.jsonl');
523
609
  }