@north-light/crouter 0.3.226 → 0.3.228

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/dist/api/client.d.ts +5 -5
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/dto/broker-ops.d.ts +8 -0
  4. package/dist/api/dto/memory.d.ts +8 -7
  5. package/dist/api/dto/memory.js +2 -2
  6. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
  7. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
  8. package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
  9. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
  10. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
  11. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
  12. package/dist/builtin-memory/design/guide.md +38 -16
  13. package/dist/builtin-memory/design/roadmap.md +4 -4
  14. package/dist/builtin-memory/insights/init.md +5 -5
  15. package/dist/builtin-memory/internal/INDEX.md +1 -1
  16. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  17. package/dist/builtin-memory/internal/memory-loading.md +6 -6
  18. package/dist/builtin-memory/internal/plugins.md +1 -1
  19. package/dist/builtin-memory/plan/guide.md +53 -0
  20. package/dist/builtin-memory/plan/roadmap.md +10 -8
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
  22. package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
  23. package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
  24. package/dist/clients/attach/chrome/inbox-strip.js +11 -7
  25. package/dist/clients/attach/session/pane-tag.d.ts +28 -2
  26. package/dist/clients/attach/session/pane-tag.js +48 -22
  27. package/dist/clients/attach/session/profile-files.d.ts +6 -0
  28. package/dist/clients/attach/session/profile-files.js +44 -11
  29. package/dist/clients/attach/slash/dispatch.js +4 -7
  30. package/dist/clients/attach/viewer.js +588 -596
  31. package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
  32. package/dist/commands/api-client.d.ts +3 -7
  33. package/dist/commands/api-client.js +7 -12
  34. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
  35. package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
  36. package/dist/commands/memory/delete.js +51 -24
  37. package/dist/commands/memory/edit.js +50 -10
  38. package/dist/commands/memory/find.js +105 -56
  39. package/dist/commands/memory/history.js +51 -31
  40. package/dist/commands/memory/lint.d.ts +0 -9
  41. package/dist/commands/memory/lint.js +78 -322
  42. package/dist/commands/memory/list.d.ts +12 -4
  43. package/dist/commands/memory/list.js +64 -25
  44. package/dist/commands/memory/move.js +103 -73
  45. package/dist/commands/memory/origin.js +35 -5
  46. package/dist/commands/memory/read.d.ts +4 -0
  47. package/dist/commands/memory/read.js +132 -80
  48. package/dist/commands/memory/shared.d.ts +115 -25
  49. package/dist/commands/memory/shared.js +331 -74
  50. package/dist/commands/memory/write.js +72 -21
  51. package/dist/commands/memory.js +2 -2
  52. package/dist/commands/node/lifecycle.js +2 -2
  53. package/dist/commands/pkg/market-manage.js +25 -24
  54. package/dist/commands/pkg/plugin-manage.d.ts +22 -6
  55. package/dist/commands/pkg/plugin-manage.js +118 -25
  56. package/dist/commands/pkg/shared.d.ts +8 -0
  57. package/dist/commands/pkg/shared.js +23 -20
  58. package/dist/commands/surface-reload.d.ts +5 -0
  59. package/dist/commands/surface-reload.js +9 -1
  60. package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
  61. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
  62. package/dist/commands/sys/migrate.js +126 -140
  63. package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
  64. package/dist/commands/sys/panels/profiles-panel.js +599 -0
  65. package/dist/commands/sys/settings-shell.d.ts +4 -2
  66. package/dist/commands/sys/settings-shell.js +35 -4
  67. package/dist/commands/sys/settings.js +21 -5
  68. package/dist/commands/sys/sync-deps.d.ts +2 -0
  69. package/dist/commands/sys/sync-deps.js +26 -15
  70. package/dist/commands/sys/sync-project-guidance.js +228 -145
  71. package/dist/commands/sys/sync-skills.js +28 -17
  72. package/dist/commands/sys/update.js +11 -3
  73. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
  74. package/dist/core/__tests__/config-change-delta.test.js +106 -0
  75. package/dist/core/__tests__/context-intro.test.js +13 -9
  76. package/dist/core/__tests__/daemon-boot.test.js +22 -16
  77. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
  78. package/dist/core/__tests__/human-deliver.test.js +19 -0
  79. package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
  80. package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
  81. package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
  82. package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
  83. package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
  84. package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
  85. package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
  86. package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
  87. package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
  88. package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
  89. package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
  90. package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
  91. package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
  92. package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
  93. package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
  94. package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
  95. package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
  96. package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
  97. package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
  98. package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
  99. package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
  100. package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
  101. package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
  102. package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
  103. package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
  104. package/dist/core/__tests__/kickoff.test.js +16 -5
  105. package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
  106. package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
  107. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  108. package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
  109. package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
  110. package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
  111. package/dist/core/__tests__/repository-association.test.d.ts +1 -0
  112. package/dist/core/__tests__/repository-association.test.js +153 -0
  113. package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
  114. package/dist/core/__tests__/repository-root-identity.test.js +219 -0
  115. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
  116. package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
  117. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
  118. package/dist/core/canvas/canvas.d.ts +3 -0
  119. package/dist/core/canvas/canvas.js +30 -10
  120. package/dist/core/canvas/db.js +28 -1
  121. package/dist/core/canvas/paths.d.ts +4 -6
  122. package/dist/core/canvas/paths.js +8 -6
  123. package/dist/core/canvas/render-source.js +2 -2
  124. package/dist/core/canvas/types.d.ts +9 -13
  125. package/dist/core/exclusive-lock.d.ts +2 -0
  126. package/dist/core/exclusive-lock.js +21 -0
  127. package/dist/core/git.d.ts +0 -1
  128. package/dist/core/git.js +0 -3
  129. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
  130. package/dist/core/human/feedback-companion.js +3 -0
  131. package/dist/core/human/scan.js +8 -1
  132. package/dist/core/keybindings/catalog.d.ts +2 -2
  133. package/dist/core/keybindings/catalog.js +2 -1
  134. package/dist/core/memory/doc-link-grammar.js +2 -2
  135. package/dist/core/memory/history.d.ts +24 -0
  136. package/dist/core/memory/history.js +66 -1
  137. package/dist/core/memory/identity.d.ts +65 -0
  138. package/dist/core/memory/identity.js +185 -0
  139. package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
  140. package/dist/core/memory/inline-ref-guidance.js +1 -1
  141. package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
  142. package/dist/core/memory/inline-ref-inventory.js +30 -62
  143. package/dist/core/memory/lint.d.ts +129 -0
  144. package/dist/core/memory/lint.js +517 -0
  145. package/dist/core/memory/project-namespace.d.ts +31 -0
  146. package/dist/core/memory/project-namespace.js +85 -0
  147. package/dist/core/memory/repository-association.d.ts +31 -0
  148. package/dist/core/memory/repository-association.js +129 -0
  149. package/dist/core/memory/tree.d.ts +39 -0
  150. package/dist/core/memory/tree.js +93 -0
  151. package/dist/core/memory-resolver.d.ts +193 -90
  152. package/dist/core/memory-resolver.js +461 -375
  153. package/dist/core/nested-stores.js +6 -13
  154. package/dist/core/profiles/select.d.ts +4 -1
  155. package/dist/core/profiles/select.js +108 -48
  156. package/dist/core/review/__tests__/capture-origin.test.js +2 -2
  157. package/dist/core/review/__tests__/stage-identity.test.js +2 -2
  158. package/dist/core/review/companion.js +11 -2
  159. package/dist/core/runtime/bearings.d.ts +2 -2
  160. package/dist/core/runtime/bearings.js +3 -3
  161. package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
  162. package/dist/core/runtime/broker/rebind.js +5 -0
  163. package/dist/core/runtime/broker-extension-render.d.ts +7 -3
  164. package/dist/core/runtime/broker-extension-render.js +10 -5
  165. package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
  166. package/dist/core/runtime/broker-persona-guidance.js +122 -27
  167. package/dist/core/runtime/deliver-live.d.ts +16 -4
  168. package/dist/core/runtime/deliver-live.js +29 -15
  169. package/dist/core/runtime/kickoff.d.ts +3 -3
  170. package/dist/core/runtime/kickoff.js +8 -16
  171. package/dist/core/runtime/lifecycle.js +2 -3
  172. package/dist/core/runtime/nodes.d.ts +3 -4
  173. package/dist/core/runtime/nodes.js +3 -4
  174. package/dist/core/runtime/persona.d.ts +8 -12
  175. package/dist/core/runtime/persona.js +24 -96
  176. package/dist/core/runtime/promote.d.ts +2 -2
  177. package/dist/core/runtime/promote.js +8 -21
  178. package/dist/core/runtime/revive.js +20 -17
  179. package/dist/core/runtime/spawn.js +18 -7
  180. package/dist/core/runtime/tmux-bindings.js +2 -3
  181. package/dist/core/runtime/warm-pool.js +3 -4
  182. package/dist/core/scope.js +2 -0
  183. package/dist/core/self-update.d.ts +0 -2
  184. package/dist/core/self-update.js +2 -35
  185. package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
  186. package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
  187. package/dist/core/substrate/index.d.ts +2 -2
  188. package/dist/core/substrate/index.js +1 -1
  189. package/dist/core/substrate/injected-store.d.ts +43 -27
  190. package/dist/core/substrate/injected-store.js +208 -104
  191. package/dist/core/substrate/listings.d.ts +19 -12
  192. package/dist/core/substrate/listings.js +75 -52
  193. package/dist/core/substrate/on-read-node.d.ts +4 -7
  194. package/dist/core/substrate/on-read-node.js +6 -8
  195. package/dist/core/substrate/on-read.d.ts +22 -25
  196. package/dist/core/substrate/on-read.js +103 -147
  197. package/dist/core/substrate/render-node.d.ts +4 -7
  198. package/dist/core/substrate/render-node.js +5 -7
  199. package/dist/core/substrate/render.d.ts +21 -3
  200. package/dist/core/substrate/render.js +291 -223
  201. package/dist/core/substrate/schema.d.ts +1 -13
  202. package/dist/core/substrate/schema.js +5 -40
  203. package/dist/core/substrate/session-cache.d.ts +14 -4
  204. package/dist/core/substrate/session-cache.js +40 -22
  205. package/dist/core/substrate/surface-match.d.ts +9 -7
  206. package/dist/core/substrate/surface-match.js +26 -25
  207. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
  208. package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
  209. package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
  210. package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
  211. package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +37 -9
  212. package/dist/daemon/api/bridge.d.ts +35 -4
  213. package/dist/daemon/api/bridge.js +50 -8
  214. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  215. package/dist/daemon/api/handlers/memory.js +2 -0
  216. package/dist/daemon/crtrd.js +2 -0
  217. package/dist/daemon/human/finish.js +8 -1
  218. package/dist/daemon/manage.d.ts +0 -1
  219. package/dist/daemon/manage.js +7 -16
  220. package/dist/daemon/startup-policy.d.ts +1 -0
  221. package/dist/daemon/startup-policy.js +1 -0
  222. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  223. package/dist/migrations/002-profile-project-memory.js +1 -0
  224. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  225. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  226. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  227. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  228. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  229. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  230. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  231. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  232. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  233. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  234. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  235. package/dist/migrations/__tests__/activation.test.js +149 -0
  236. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  237. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  238. package/dist/migrations/activation.d.ts +16 -0
  239. package/dist/migrations/activation.js +78 -0
  240. package/dist/migrations/convergent.d.ts +14 -3
  241. package/dist/migrations/convergent.js +21 -10
  242. package/dist/migrations/corpus.d.ts +78 -0
  243. package/dist/migrations/corpus.js +497 -0
  244. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  245. package/dist/migrations/frontmatter-splice.js +176 -0
  246. package/dist/migrations/registry.d.ts +6 -1
  247. package/dist/migrations/registry.js +7 -2
  248. package/dist/migrations/runner.d.ts +41 -0
  249. package/dist/migrations/runner.js +81 -0
  250. package/dist/migrations/types.d.ts +148 -9
  251. package/dist/migrations/types.js +9 -2
  252. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  253. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  254. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  255. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  256. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  257. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  258. package/dist/pi-extensions/canvas-stophook.js +6 -13
  259. package/dist/shared/generated-context.d.ts +0 -3
  260. package/dist/shared/generated-context.js +0 -57
  261. package/dist/shared/tool-groups.js +2 -3
  262. package/package.json +5 -4
  263. package/runtime.lock.json +2 -2
  264. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  265. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  266. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  267. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  268. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  269. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  270. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  271. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  272. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  273. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  274. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  275. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  276. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  277. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  278. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  279. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  280. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  281. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  282. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  283. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  284. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  285. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  286. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  287. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  288. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -5,9 +5,10 @@
5
5
  //
6
6
  // One real fixture drives the real scope resolver, the real boot renderer, the
7
7
  // real workspace-open renderer, and the real `crtr memory read` leaf: a profile
8
- // whose four projects sit at none/name/preview/content in manifest order, with
9
- // the `preview` project as cwd and canonical-name collisions across projects and
10
- // against the wider user scope.
8
+ // whose first four projects sit at none/name/preview/content in manifest order,
9
+ // with the `preview` project as cwd, plus two more projects declaring ONE shared
10
+ // namespace so a single canonical identity has two physical candidates split
11
+ // across a `none` and a `content` relationship.
11
12
  //
12
13
  // Migration convergence is NOT here — it lives in migration.test.ts.
13
14
  //
@@ -25,8 +26,13 @@ import { memoryDir } from '../runtime/memory.js';
25
26
  import { clearSessionCache } from '../substrate/session-cache.js';
26
27
  import { renderKnowledgeBlock } from '../substrate/render-node.js';
27
28
  import { renderWorkspaceOpenDocs } from '../substrate/on-read-node.js';
28
- import { deliveredAtOrAbove, loadInjectedDocs, saveInjectedDocs, } from '../substrate/injected-store.js';
29
+ import { documentExposedAtOrAbove, emptyContextExposureState, exposedAtOrAbove, exposureTarget, loadContextExposureState, saveContextExposureState, } from '../substrate/injected-store.js';
29
30
  import { readLeaf } from '../../commands/memory/read.js';
31
+ import { listLeaf } from '../../commands/memory/list.js';
32
+ import { findLeaf } from '../../commands/memory/find.js';
33
+ function freshTranscriptTarget() {
34
+ return exposureTarget(emptyContextExposureState(), 'transcript');
35
+ }
30
36
  const B = {
31
37
  pnone: 'BODY-T9-PNONE',
32
38
  pname: 'BODY-T9-PNAME',
@@ -38,6 +44,8 @@ const B = {
38
44
  nameFromPcontent: 'BODY-T9-NAMEDUP-FROM-PCONTENT',
39
45
  userDupFromPnone: 'BODY-T9-USERDUP-FROM-PNONE',
40
46
  userDupFromUser: 'BODY-T9-USERDUP-FROM-USER',
47
+ dupNear: 'BODY-T9-DUP-FROM-NEAR',
48
+ dupFar: 'BODY-T9-DUP-FROM-FAR',
41
49
  kindSplit: 'BODY-T9-KIND-SPLIT',
42
50
  documentGateWins: 'BODY-T9-DOCUMENT-GATE-WINS',
43
51
  };
@@ -91,31 +99,50 @@ before(() => {
91
99
  canvasHome = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-ppm-canvas-')));
92
100
  process.env['HOME'] = home;
93
101
  process.env['CRTR_HOME'] = canvasHome;
94
- const store = (slug) => {
102
+ // A project store answers only under the namespace its root INDEX.md
103
+ // declares; without that declaration the store does not mount at all.
104
+ const store = (slug, namespace) => {
95
105
  const dir = join(home, slug, '.crouter', 'memory');
96
106
  mkdirSync(dir, { recursive: true });
107
+ writeFileSync(join(dir, 'INDEX.md'), [
108
+ '---',
109
+ 'kind: knowledge',
110
+ `namespace: ${namespace}`,
111
+ `when-and-why-to-read: "When the ${slug} store is addressed"`,
112
+ '---',
113
+ '',
114
+ `STORE-ROOT-${slug.toUpperCase()}`,
115
+ '',
116
+ ].join('\n'));
97
117
  return dir;
98
118
  };
99
- const pnone = store('pnone');
100
- const pname = store('pname');
101
- const ppreview = store('ppreview');
102
- const pcontent = store('pcontent');
119
+ const pnone = store('pnone', 'pnone');
120
+ const pname = store('pname', 'pname');
121
+ const ppreview = store('ppreview', 'ppreview');
122
+ const pcontent = store('pcontent', 'pcontent');
123
+ // Two stores declaring ONE namespace — the only way two projects can hold the
124
+ // same canonical identity.
125
+ const pdupnear = store('pdupnear', 'shared');
126
+ const pdupfar = store('pdupfar', 'shared');
103
127
  const user = join(home, '.crouter', 'memory');
104
128
  mkdirSync(user, { recursive: true });
105
129
  writeDoc(pnone, 't9-pnone-doc', autoDoc('When the none project delivers', B.pnone));
106
130
  writeDoc(pname, 't9-pname-doc', autoDoc('When the name project delivers', B.pname));
107
131
  ppreviewDocPath = writeDoc(ppreview, 't9-ppreview-doc', autoDoc('When the cwd project delivers', B.ppreview));
108
132
  writeDoc(pcontent, 't9-pcontent-doc', autoDoc('When the content project delivers', B.pcontent));
109
- // Collision one: `none` (earlier in manifest order) vs `content`. The cap must
110
- // apply before name dedupe, so the dropped entry cannot shadow the winner.
133
+ // Equal-canonical collision: `shared/dup-doc` in both stores that declare
134
+ // `shared`, the `none`-capped one nearer in source order. The cap must apply
135
+ // BEFORE dedupe, so the capped nearer candidate yields to the farther one
136
+ // instead of suppressing it.
137
+ writeDoc(pdupnear, 'dup-doc', autoDoc('When the shared identity resolves', B.dupNear));
138
+ writeDoc(pdupfar, 'dup-doc', autoDoc('When the shared identity resolves', B.dupFar));
139
+ // Distinct identities that only LOOK like collisions: each project composes
140
+ // its own namespace, so a capped project cannot suppress another store's
141
+ // document that merely shares a local name.
111
142
  writeDoc(pnone, 't9-shared-doc', autoDoc('When the shared name resolves', B.sharedFromPnone));
112
143
  writeDoc(pcontent, 't9-shared-doc', autoDoc('When the shared name resolves', B.sharedFromPcontent));
113
- // Collision two: `name` vs `content`. A non-none cap keeps its project's
114
- // precedence and discloses at the capped rung — it does not step aside.
115
144
  writeDoc(pname, 't9-namedup-doc', autoDoc('When the name-capped duplicate resolves', B.nameFromPname));
116
145
  writeDoc(pcontent, 't9-namedup-doc', autoDoc('When the name-capped duplicate resolves', B.nameFromPcontent));
117
- // Collision three: `none` vs the wider user scope, which project precedence
118
- // would otherwise beat.
119
146
  writeDoc(pnone, 't9-userdup-doc', autoDoc('When the user duplicate resolves', B.userDupFromPnone));
120
147
  writeDoc(user, 't9-userdup-doc', autoDoc('When the user duplicate resolves', B.userDupFromUser));
121
148
  writeDoc(user, 't9-kind-split', [
@@ -151,6 +178,8 @@ before(() => {
151
178
  { path: join(home, 'pname'), memory: 'name' },
152
179
  { path: join(home, 'ppreview'), memory: 'preview' },
153
180
  { path: join(home, 'pcontent'), memory: 'content' },
181
+ { path: join(home, 'pdupnear'), memory: 'none' },
182
+ { path: join(home, 'pdupfar'), memory: 'content' },
154
183
  ]).profileId;
155
184
  process.env['CRTR_PROFILE_ID'] = profileId;
156
185
  // The node works INSIDE a capped project: the reported failure.
@@ -183,7 +212,7 @@ function newNode(kind = 'general') {
183
212
  return spawnNode({ kind, cwd: cwdProject, parent: null, profile_id: profileId }).node_id;
184
213
  }
185
214
  test('boot delivery caps every profile project at its relationship, including the one used as cwd', () => {
186
- const boot = renderKnowledgeBlock(newNode(), new Map());
215
+ const boot = renderKnowledgeBlock(newNode(), freshTranscriptTarget());
187
216
  // The control: an uncapped relationship still renders its authored content.
188
217
  assert.ok(boot.includes(B.pcontent), `the content-capped project renders its body; got:\n${boot}`);
189
218
  // The reported failure. cwd reaches this root before the manifest pointer
@@ -197,20 +226,20 @@ test('boot delivery caps every profile project at its relationship, including th
197
226
  });
198
227
  test('surface-entry gates select the rung by node kind while the document gate remains absolute', () => {
199
228
  for (const kind of ['design', 'spec', 'plan', 'developer']) {
200
- const boot = renderKnowledgeBlock(newNode(kind), new Map());
229
+ const boot = renderKnowledgeBlock(newNode(kind), freshTranscriptTarget());
201
230
  assert.ok(boot.includes(B.kindSplit), `${kind} receives the content rung; got:\n${boot}`);
202
231
  }
203
232
  for (const kind of ['general', 'review', 'advisor']) {
204
- const boot = renderKnowledgeBlock(newNode(kind), new Map());
233
+ const boot = renderKnowledgeBlock(newNode(kind), freshTranscriptTarget());
205
234
  assert.match(boot, /t9-kind-split {2}# read when: When the node needs this kind-specific guide\./, `${kind} receives the preview rung; got:\n${boot}`);
206
235
  assert.ok(!boot.includes(B.kindSplit), `${kind} does not receive the body`);
207
236
  }
208
237
  for (const kind of ['explore', 'personal-assistant']) {
209
- const boot = renderKnowledgeBlock(newNode(kind), new Map());
238
+ const boot = renderKnowledgeBlock(newNode(kind), freshTranscriptTarget());
210
239
  assert.ok(!boot.includes('t9-kind-split'), `${kind} receives no kind-split entry; got:\n${boot}`);
211
240
  assert.ok(!boot.includes(B.kindSplit), `${kind} receives no kind-split body`);
212
241
  }
213
- const developer = renderKnowledgeBlock(newNode('developer'), new Map());
242
+ const developer = renderKnowledgeBlock(newNode('developer'), freshTranscriptTarget());
214
243
  assert.ok(!developer.includes('t9-document-gate-wins'), `a failing document gate excludes every entry; got:\n${developer}`);
215
244
  assert.ok(!developer.includes(B.documentGateWins), 'a failing document gate excludes its content');
216
245
  });
@@ -229,56 +258,121 @@ test('a node-local doc floors to name only without boot entries', () => {
229
258
  'BODY-T9-NODE-LOCAL-ENTRY-GATED',
230
259
  '',
231
260
  ].join('\n'));
232
- const boot = renderKnowledgeBlock(node, new Map());
261
+ const boot = renderKnowledgeBlock(node, freshTranscriptTarget());
233
262
  assert.match(boot, /[├└]─ t9-node-local-bootless$/m, `a bootless node-local doc falls to name; got:\n${boot}`);
234
263
  assert.ok(!boot.includes('BODY-T9-NODE-LOCAL-BOOTLESS'), 'the fallback discloses no body');
235
264
  assert.ok(!boot.includes('t9-node-local-entry-gated'), `ineligible boot entries keep a node-local doc absent; got:\n${boot}`);
236
265
  assert.ok(!boot.includes('BODY-T9-NODE-LOCAL-ENTRY-GATED'), 'ineligible boot entries disclose no body');
237
266
  });
238
267
  test('a `none` project contributes nothing to boot — no body, no name, no hidden count, no shadow', () => {
239
- const boot = renderKnowledgeBlock(newNode(), new Map());
268
+ const boot = renderKnowledgeBlock(newNode(), freshTranscriptTarget());
240
269
  assert.ok(!boot.includes(B.pnone), 'a none-capped body must not render');
241
- assert.ok(!boot.includes('t9-pnone-doc'), 'a none-capped doc must not appear as a name either');
242
- // Hidden counts are disclosure too: the control dir proves a bootless doc
243
- // does surface as a `[+N more]` count, so the none project's absence is the cap.
244
- assert.match(boot, /t9-hidden-control\/\n.*\[\+1 more\]/, `the control dir shows its hidden count; got:\n${boot}`);
270
+ assert.ok(!boot.includes('pnone'), 'a none-capped store must not appear as a name either — not even its namespace row');
271
+ // Hidden counts are disclosure too: the control project's namespace row proves
272
+ // a bootless doc does surface as a `[+N more]` count, so the none project's
273
+ // total absence is the cap and not a rendering quirk.
274
+ assert.match(boot, /pcontent\/\n[^\n]*\[\+\d+ more\]/, `the control project's row shows its hidden count; got:\n${boot}`);
245
275
  assert.ok(!boot.includes('t9-none-hidden'), 'a none-capped project contributes no `[+N more]` count');
246
276
  // Shadowing: `none` wins name dedupe on precedence, so a cap applied after
247
277
  // dedupe would silently delete the winner instead of deferring to it.
248
- assert.ok(boot.includes(B.sharedFromPcontent) && !boot.includes(B.sharedFromPnone), `the content project wins the collision the none project would have taken; got:\n${boot}`);
249
- assert.ok(boot.includes(B.userDupFromUser) && !boot.includes(B.userDupFromPnone), `a none project does not shadow the wider user-scope winner; got:\n${boot}`);
278
+ // A capped project cannot suppress a document it never shared an identity
279
+ // with: each store composes its own namespace, so these are distinct names.
280
+ assert.ok(boot.includes(B.sharedFromPcontent) && !boot.includes(B.sharedFromPnone), `pcontent/t9-shared-doc delivers while the none project contributes nothing; got:\n${boot}`);
281
+ assert.ok(boot.includes(B.userDupFromUser) && !boot.includes(B.userDupFromPnone), `the unqualified user identity delivers while the none project contributes nothing; got:\n${boot}`);
282
+ });
283
+ test('a capped nearer candidate yields the shared identity instead of suppressing it', async () => {
284
+ // Two project stores declare the SAME namespace, so `shared/dup-doc` has two
285
+ // physical candidates. Automatic delivery filters candidates by the profile
286
+ // cap BEFORE dedupe, so the `none`-capped nearer one drops out and the farther
287
+ // one delivers; a deliberate read stays winner-only on source order.
288
+ const boot = renderKnowledgeBlock(newNode(), freshTranscriptTarget());
289
+ assert.ok(boot.includes(B.dupFar), `the farther uncapped candidate delivers the shared identity; got:\n${boot}`);
290
+ assert.ok(!boot.includes(B.dupNear), 'the none-capped nearer candidate delivers nothing');
291
+ const read = (await readLeaf.run({
292
+ name: 'shared/dup-doc',
293
+ kind: undefined,
294
+ frontmatter: false,
295
+ scope: undefined,
296
+ dir: undefined,
297
+ }));
298
+ assert.ok(read.content.includes(B.dupNear) && !read.content.includes(B.dupFar), `a deliberate read resolves the source-order winner; got:\n${read.content}`);
250
299
  });
251
300
  test('a non-none cap keeps its project precedence and discloses the duplicate at the capped rung', () => {
252
- const boot = renderKnowledgeBlock(newNode(), new Map());
253
- assert.match(boot, /[├└]─ t9-namedup-doc$/m, `the name-capped duplicate wins dedupe; got:\n${boot}`);
254
- assert.ok(!boot.includes(B.nameFromPname), 'the winner discloses at `name`, never its body');
255
- assert.ok(!boot.includes(B.nameFromPcontent), 'the content project does not take a collision the name project already won');
301
+ const boot = renderKnowledgeBlock(newNode(), freshTranscriptTarget());
302
+ assert.match(boot, /[├└]─ t9-namedup-doc$/m, `the name-capped project discloses its doc as a bare name; got:\n${boot}`);
303
+ assert.ok(!boot.includes(B.nameFromPname), 'a name-capped doc discloses at `name`, never its body');
304
+ assert.ok(boot.includes(B.nameFromPcontent), 'the content project delivers its own distinct identity in full');
256
305
  });
257
306
  test('workspace-open clamps each project store the same way boot does', () => {
258
- const opened = renderWorkspaceOpenDocs(newNode(), new Map());
307
+ const opened = renderWorkspaceOpenDocs(newNode(), freshTranscriptTarget());
259
308
  assert.ok(opened.includes(B.pcontent), `the content-capped project delivers its body; got:\n${opened}`);
260
309
  assert.ok(!opened.includes(B.ppreview), `the cwd project must not deliver its body; got:\n${opened}`);
261
- assert.match(opened, /name="t9-ppreview-doc" readWhen="When the cwd project delivers\./, `the cwd project delivers its preview line; got:\n${opened}`);
262
- assert.match(opened, /name="t9-pname-doc"[^>]*\/>/, `the name-capped doc delivers name-only; got:\n${opened}`);
310
+ assert.match(opened, /name="ppreview\/t9-ppreview-doc" readWhen="When the cwd project delivers\./, `the cwd project delivers its preview line under its canonical name; got:\n${opened}`);
311
+ assert.match(opened, /name="pname\/t9-pname-doc"[^>]*\/>/, `the name-capped doc delivers name-only; got:\n${opened}`);
263
312
  assert.ok(!opened.includes('t9-pnone-doc'), 'the none-capped project delivers nothing at workspace-open');
264
313
  assert.ok(!opened.includes(B.sharedFromPnone), 'nor its side of a name collision');
265
314
  });
315
+ test('memory list and find record synthetic pages without crediting matched documents', async () => {
316
+ const node = newNode();
317
+ process.env['CRTR_NODE_ID'] = node;
318
+ try {
319
+ const listed = await listLeaf.run({
320
+ kind: undefined,
321
+ scope: undefined,
322
+ dir: undefined,
323
+ paths: false,
324
+ limit: 50,
325
+ cursor: undefined,
326
+ });
327
+ const found = await findLeaf.run({
328
+ query: 'ppreview/t9-ppreview-doc',
329
+ kind: undefined,
330
+ scope: undefined,
331
+ dir: undefined,
332
+ grep: false,
333
+ body: false,
334
+ limit: 10,
335
+ cursor: undefined,
336
+ minScore: undefined,
337
+ paths: false,
338
+ });
339
+ assert.ok(listed.items.some((item) => item.name === 'ppreview/t9-ppreview-doc'), 'the inventory page listed the fixture document under its canonical name');
340
+ assert.ok(found.hits.some((hit) => hit.name === 'ppreview/t9-ppreview-doc'), 'the search page matched the fixture document under its canonical name');
341
+ const state = loadContextExposureState(node);
342
+ const listingKeys = [...state.exposures.keys()].filter((identity) => identity.startsWith('listing:memory-'));
343
+ assert.equal(listingKeys.length, 2, 'list and find each register their delivered page');
344
+ assert.ok(listingKeys.some((identity) => identity.startsWith('listing:memory-list:')));
345
+ assert.ok(listingKeys.some((identity) => identity.startsWith('listing:memory-find:')));
346
+ assert.ok(listingKeys.every((identity) => exposedAtOrAbove(state, identity, 'content')), 'each synthetic page is registered at its complete delivered representation');
347
+ assert.equal(documentExposedAtOrAbove(state, ppreviewDocPath, B.ppreview, 'name'), false, 'inventory metadata and matching lines never claim disclosure of the document itself');
348
+ }
349
+ finally {
350
+ delete process.env['CRTR_NODE_ID'];
351
+ }
352
+ });
266
353
  test('an explicit `crtr memory read` returns and records the full body a cap withheld', async () => {
267
354
  const node = newNode();
268
355
  process.env['CRTR_NODE_ID'] = node;
269
- const seen = loadInjectedDocs(node);
270
- const opened = renderWorkspaceOpenDocs(node, seen);
356
+ const state = loadContextExposureState(node);
357
+ const transcript = exposureTarget(state, 'transcript');
358
+ const opened = renderWorkspaceOpenDocs(node, transcript);
271
359
  assert.ok(!opened.includes(B.ppreview), 'workspace-open disclosed the capped doc at preview only');
272
- saveInjectedDocs(node, seen);
273
- const result = (await readLeaf.run({ name: 't9-ppreview-doc', frontmatter: false }));
360
+ saveContextExposureState(node, state);
361
+ const result = (await readLeaf.run({
362
+ name: 'ppreview/t9-ppreview-doc',
363
+ kind: undefined,
364
+ frontmatter: false,
365
+ scope: undefined,
366
+ dir: undefined,
367
+ }));
274
368
  assert.equal(result.path, ppreviewDocPath, 'the read resolved the cwd project doc');
275
369
  assert.ok(result.content.includes(B.ppreview), `an explicit read is uncapped and returns the full body; got:\n${result.content}`);
276
- const afterRead = loadInjectedDocs(node);
277
- assert.ok(deliveredAtOrAbove(afterRead, ppreviewDocPath, 'content'), 'the explicit read records a content delivery, upgrading the preview disclosure');
370
+ const afterRead = loadContextExposureState(node);
371
+ assert.ok(documentExposedAtOrAbove(afterRead, ppreviewDocPath, B.ppreview, 'content'), 'the explicit read records a content delivery, upgrading the preview disclosure');
278
372
  // A later automatic event runs at the capped rung, which is now below what the
279
373
  // transcript holds, so it re-delivers nothing.
280
- assert.equal(renderWorkspaceOpenDocs(node, afterRead), '', 'a later lower automatic event re-delivers nothing');
281
- // ...and the emptiness is the dedup set, not a dead render.
282
- assert.ok(renderWorkspaceOpenDocs(node, new Map()).includes('t9-ppreview-doc'), 'the same call against a fresh transcript still delivers');
374
+ assert.equal(renderWorkspaceOpenDocs(node, exposureTarget(afterRead, 'transcript')), '', 'a later lower automatic event re-delivers nothing');
375
+ // A fresh ledger proves the renderer itself is still live.
376
+ assert.ok(renderWorkspaceOpenDocs(node, freshTranscriptTarget()).includes('ppreview/t9-ppreview-doc'), 'the same call against a fresh transcript still delivers');
283
377
  delete process.env['CRTR_NODE_ID'];
284
378
  });
@@ -0,0 +1,153 @@
1
+ import { test, before, beforeEach, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { gitSync } from '../git.js';
7
+ import { realpathOrSelf } from '../fs-utils.js';
8
+ import { associateRepository, normalizeOwnerRelativePath, resetRepositoryAssociationCache, } from '../memory/repository-association.js';
9
+ import { slugCanonicalPath, slugCanonicalSegment } from '../memory/identity.js';
10
+ const cleanups = [];
11
+ let previousHome;
12
+ before(() => {
13
+ previousHome = process.env['HOME'];
14
+ });
15
+ beforeEach(() => {
16
+ process.env['HOME'] = realpathOrSelf(mkdtempSync(join('/tmp', 'crtr-association-home-')));
17
+ cleanups.push(process.env['HOME']);
18
+ resetRepositoryAssociationCache();
19
+ });
20
+ after(() => {
21
+ resetRepositoryAssociationCache();
22
+ for (const path of cleanups)
23
+ rmSync(path, { recursive: true, force: true });
24
+ if (previousHome === undefined)
25
+ delete process.env['HOME'];
26
+ else
27
+ process.env['HOME'] = previousHome;
28
+ });
29
+ function temp(prefix) {
30
+ const path = realpathOrSelf(mkdtempSync(join('/tmp', prefix)));
31
+ cleanups.push(path);
32
+ return path;
33
+ }
34
+ function initGit(path) {
35
+ mkdirSync(path, { recursive: true });
36
+ assert.equal(gitSync(['-C', path, 'init', '-q'], undefined).status, 0);
37
+ }
38
+ function makeStore(owner) {
39
+ mkdirSync(join(owner, '.crouter', 'memory'), { recursive: true });
40
+ writeFileSync(join(owner, '.crouter', 'memory', 'INDEX.md'), '# store\n');
41
+ }
42
+ test('associates git roots, nested owners, and embedded repositories', () => {
43
+ const root = temp('crtr-association-git-');
44
+ initGit(root);
45
+ const nested = join(root, 'packages', 'api');
46
+ mkdirSync(nested, { recursive: true });
47
+ const atRoot = associateRepository(root);
48
+ assert.equal(atRoot.ok, true);
49
+ if (!atRoot.ok)
50
+ return;
51
+ assert.equal(atRoot.association.kind, 'git');
52
+ assert.equal(atRoot.association.repositoryRoot, root);
53
+ assert.equal(atRoot.association.ownerRelativePath, '');
54
+ assert.match(atRoot.association.repositoryKey, /^git:/);
55
+ assert.equal(atRoot.association.declarationPath, join(root, '.crouter', 'memory', 'INDEX.md'));
56
+ const belowRoot = associateRepository(nested);
57
+ assert.equal(belowRoot.ok, true);
58
+ if (!belowRoot.ok)
59
+ return;
60
+ assert.equal(belowRoot.association.repositoryRoot, root);
61
+ assert.equal(belowRoot.association.ownerRelativePath, 'packages/api');
62
+ const embedded = join(root, 'embedded');
63
+ initGit(embedded);
64
+ const embeddedOwner = join(embedded, 'pkg');
65
+ mkdirSync(embeddedOwner, { recursive: true });
66
+ const embeddedResult = associateRepository(embeddedOwner);
67
+ assert.equal(embeddedResult.ok, true);
68
+ if (!embeddedResult.ok)
69
+ return;
70
+ assert.equal(embeddedResult.association.repositoryRoot, embedded);
71
+ assert.equal(embeddedResult.association.ownerRelativePath, 'pkg');
72
+ });
73
+ test('linked worktrees share repository key and owner-relative path', () => {
74
+ const main = temp('crtr-association-main-');
75
+ initGit(main);
76
+ writeFileSync(join(main, 'README'), 'repo\n');
77
+ assert.equal(gitSync(['-C', main, 'add', 'README'], undefined).status, 0);
78
+ assert.equal(gitSync(['-C', main, '-c', 'user.name=Test', '-c', 'user.email=test@example.com', 'commit', '-qm', 'initial'], undefined).status, 0);
79
+ const linked = join(temp('crtr-association-linked-parent-'), 'checkout');
80
+ assert.equal(gitSync(['-C', main, 'worktree', 'add', '-q', linked, 'HEAD'], undefined).status, 0);
81
+ const mainOwner = join(main, 'packages', 'api');
82
+ const linkedOwner = join(linked, 'packages', 'api');
83
+ mkdirSync(mainOwner, { recursive: true });
84
+ mkdirSync(linkedOwner, { recursive: true });
85
+ const first = associateRepository(mainOwner);
86
+ const second = associateRepository(linkedOwner);
87
+ assert.equal(first.ok, true);
88
+ assert.equal(second.ok, true);
89
+ if (!first.ok || !second.ok)
90
+ return;
91
+ assert.equal(first.association.repositoryKey, second.association.repositoryKey);
92
+ assert.equal(first.association.ownerRelativePath, second.association.ownerRelativePath);
93
+ assert.notEqual(first.association.repositoryRoot, second.association.repositoryRoot);
94
+ });
95
+ test('non-git association chooses the outermost ancestor store and respects the home fence', () => {
96
+ const outer = join(homedir(), 'workspace');
97
+ const inner = join(outer, 'packages', 'api');
98
+ mkdirSync(inner, { recursive: true });
99
+ makeStore(outer);
100
+ makeStore(inner);
101
+ const result = associateRepository(inner);
102
+ assert.equal(result.ok, true);
103
+ if (!result.ok)
104
+ return;
105
+ assert.equal(result.association.kind, 'non-git');
106
+ assert.equal(result.association.repositoryRoot, outer);
107
+ assert.equal(result.association.repositoryKey, `path:${outer}`);
108
+ assert.equal(result.association.ownerRelativePath, 'packages/api');
109
+ const homeStoreOwner = join(homedir(), 'not-a-project');
110
+ mkdirSync(homeStoreOwner, { recursive: true });
111
+ makeStore(homedir());
112
+ const fenced = associateRepository(homeStoreOwner);
113
+ assert.equal(fenced.ok, false);
114
+ if (!fenced.ok)
115
+ assert.equal(fenced.code, 'no-project-root');
116
+ });
117
+ test('non-git failures are not cached and symlinked owners realpath before deriving paths', () => {
118
+ const root = temp('crtr-association-heal-');
119
+ const owner = join(root, 'docs');
120
+ mkdirSync(owner, { recursive: true });
121
+ const before = associateRepository(owner);
122
+ assert.equal(before.ok, false);
123
+ if (!before.ok)
124
+ assert.equal(before.code, 'no-project-root');
125
+ makeStore(root);
126
+ const healed = associateRepository(owner);
127
+ assert.equal(healed.ok, true);
128
+ if (!healed.ok)
129
+ return;
130
+ assert.equal(healed.association.repositoryRoot, root);
131
+ assert.equal(healed.association.ownerRelativePath, 'docs');
132
+ const link = join(root, 'alias');
133
+ symlinkSync(owner, link, 'dir');
134
+ const linked = associateRepository(link);
135
+ assert.equal(linked.ok, true);
136
+ if (!linked.ok)
137
+ return;
138
+ assert.equal(linked.association.ownerRelativePath, 'docs');
139
+ assert.equal(linked.association.repositoryRoot, root);
140
+ });
141
+ test('slugging and owner-path validation are explicit and lossless', () => {
142
+ assert.equal(slugCanonicalSegment(' My Notes!!! '), 'my-notes');
143
+ assert.equal(slugCanonicalSegment('notes.md'), 'notes.md');
144
+ assert.equal(slugCanonicalPath('01-My Notes!!!/notes.md'), 'my-notes/notes');
145
+ assert.deepEqual(normalizeOwnerRelativePath('My Notes!!!\\notes.md'), { ok: true, path: 'my-notes/notes.md' });
146
+ assert.deepEqual(normalizeOwnerRelativePath('01-api/00-Notes'), { ok: true, path: 'api/notes' });
147
+ assert.deepEqual(normalizeOwnerRelativePath(''), { ok: true, path: '' });
148
+ assert.deepEqual(normalizeOwnerRelativePath('.'), { ok: true, path: '' });
149
+ for (const path of ['!!!', 'INDEX']) {
150
+ const result = normalizeOwnerRelativePath(path);
151
+ assert.equal(result.ok, false, `${path} must be rejected`);
152
+ }
153
+ });