@north-light/crouter 0.3.226 → 0.3.228

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/dist/api/client.d.ts +5 -5
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/dto/broker-ops.d.ts +8 -0
  4. package/dist/api/dto/memory.d.ts +8 -7
  5. package/dist/api/dto/memory.js +2 -2
  6. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
  7. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
  8. package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
  9. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
  10. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
  11. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
  12. package/dist/builtin-memory/design/guide.md +38 -16
  13. package/dist/builtin-memory/design/roadmap.md +4 -4
  14. package/dist/builtin-memory/insights/init.md +5 -5
  15. package/dist/builtin-memory/internal/INDEX.md +1 -1
  16. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  17. package/dist/builtin-memory/internal/memory-loading.md +6 -6
  18. package/dist/builtin-memory/internal/plugins.md +1 -1
  19. package/dist/builtin-memory/plan/guide.md +53 -0
  20. package/dist/builtin-memory/plan/roadmap.md +10 -8
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
  22. package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
  23. package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
  24. package/dist/clients/attach/chrome/inbox-strip.js +11 -7
  25. package/dist/clients/attach/session/pane-tag.d.ts +28 -2
  26. package/dist/clients/attach/session/pane-tag.js +48 -22
  27. package/dist/clients/attach/session/profile-files.d.ts +6 -0
  28. package/dist/clients/attach/session/profile-files.js +44 -11
  29. package/dist/clients/attach/slash/dispatch.js +4 -7
  30. package/dist/clients/attach/viewer.js +588 -596
  31. package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
  32. package/dist/commands/api-client.d.ts +3 -7
  33. package/dist/commands/api-client.js +7 -12
  34. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
  35. package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
  36. package/dist/commands/memory/delete.js +51 -24
  37. package/dist/commands/memory/edit.js +50 -10
  38. package/dist/commands/memory/find.js +105 -56
  39. package/dist/commands/memory/history.js +51 -31
  40. package/dist/commands/memory/lint.d.ts +0 -9
  41. package/dist/commands/memory/lint.js +78 -322
  42. package/dist/commands/memory/list.d.ts +12 -4
  43. package/dist/commands/memory/list.js +64 -25
  44. package/dist/commands/memory/move.js +103 -73
  45. package/dist/commands/memory/origin.js +35 -5
  46. package/dist/commands/memory/read.d.ts +4 -0
  47. package/dist/commands/memory/read.js +132 -80
  48. package/dist/commands/memory/shared.d.ts +115 -25
  49. package/dist/commands/memory/shared.js +331 -74
  50. package/dist/commands/memory/write.js +72 -21
  51. package/dist/commands/memory.js +2 -2
  52. package/dist/commands/node/lifecycle.js +2 -2
  53. package/dist/commands/pkg/market-manage.js +25 -24
  54. package/dist/commands/pkg/plugin-manage.d.ts +22 -6
  55. package/dist/commands/pkg/plugin-manage.js +118 -25
  56. package/dist/commands/pkg/shared.d.ts +8 -0
  57. package/dist/commands/pkg/shared.js +23 -20
  58. package/dist/commands/surface-reload.d.ts +5 -0
  59. package/dist/commands/surface-reload.js +9 -1
  60. package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
  61. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
  62. package/dist/commands/sys/migrate.js +126 -140
  63. package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
  64. package/dist/commands/sys/panels/profiles-panel.js +599 -0
  65. package/dist/commands/sys/settings-shell.d.ts +4 -2
  66. package/dist/commands/sys/settings-shell.js +35 -4
  67. package/dist/commands/sys/settings.js +21 -5
  68. package/dist/commands/sys/sync-deps.d.ts +2 -0
  69. package/dist/commands/sys/sync-deps.js +26 -15
  70. package/dist/commands/sys/sync-project-guidance.js +228 -145
  71. package/dist/commands/sys/sync-skills.js +28 -17
  72. package/dist/commands/sys/update.js +11 -3
  73. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
  74. package/dist/core/__tests__/config-change-delta.test.js +106 -0
  75. package/dist/core/__tests__/context-intro.test.js +13 -9
  76. package/dist/core/__tests__/daemon-boot.test.js +22 -16
  77. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
  78. package/dist/core/__tests__/human-deliver.test.js +19 -0
  79. package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
  80. package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
  81. package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
  82. package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
  83. package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
  84. package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
  85. package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
  86. package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
  87. package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
  88. package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
  89. package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
  90. package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
  91. package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
  92. package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
  93. package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
  94. package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
  95. package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
  96. package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
  97. package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
  98. package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
  99. package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
  100. package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
  101. package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
  102. package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
  103. package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
  104. package/dist/core/__tests__/kickoff.test.js +16 -5
  105. package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
  106. package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
  107. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  108. package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
  109. package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
  110. package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
  111. package/dist/core/__tests__/repository-association.test.d.ts +1 -0
  112. package/dist/core/__tests__/repository-association.test.js +153 -0
  113. package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
  114. package/dist/core/__tests__/repository-root-identity.test.js +219 -0
  115. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
  116. package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
  117. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
  118. package/dist/core/canvas/canvas.d.ts +3 -0
  119. package/dist/core/canvas/canvas.js +30 -10
  120. package/dist/core/canvas/db.js +28 -1
  121. package/dist/core/canvas/paths.d.ts +4 -6
  122. package/dist/core/canvas/paths.js +8 -6
  123. package/dist/core/canvas/render-source.js +2 -2
  124. package/dist/core/canvas/types.d.ts +9 -13
  125. package/dist/core/exclusive-lock.d.ts +2 -0
  126. package/dist/core/exclusive-lock.js +21 -0
  127. package/dist/core/git.d.ts +0 -1
  128. package/dist/core/git.js +0 -3
  129. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
  130. package/dist/core/human/feedback-companion.js +3 -0
  131. package/dist/core/human/scan.js +8 -1
  132. package/dist/core/keybindings/catalog.d.ts +2 -2
  133. package/dist/core/keybindings/catalog.js +2 -1
  134. package/dist/core/memory/doc-link-grammar.js +2 -2
  135. package/dist/core/memory/history.d.ts +24 -0
  136. package/dist/core/memory/history.js +66 -1
  137. package/dist/core/memory/identity.d.ts +65 -0
  138. package/dist/core/memory/identity.js +185 -0
  139. package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
  140. package/dist/core/memory/inline-ref-guidance.js +1 -1
  141. package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
  142. package/dist/core/memory/inline-ref-inventory.js +30 -62
  143. package/dist/core/memory/lint.d.ts +129 -0
  144. package/dist/core/memory/lint.js +517 -0
  145. package/dist/core/memory/project-namespace.d.ts +31 -0
  146. package/dist/core/memory/project-namespace.js +85 -0
  147. package/dist/core/memory/repository-association.d.ts +31 -0
  148. package/dist/core/memory/repository-association.js +129 -0
  149. package/dist/core/memory/tree.d.ts +39 -0
  150. package/dist/core/memory/tree.js +93 -0
  151. package/dist/core/memory-resolver.d.ts +193 -90
  152. package/dist/core/memory-resolver.js +461 -375
  153. package/dist/core/nested-stores.js +6 -13
  154. package/dist/core/profiles/select.d.ts +4 -1
  155. package/dist/core/profiles/select.js +108 -48
  156. package/dist/core/review/__tests__/capture-origin.test.js +2 -2
  157. package/dist/core/review/__tests__/stage-identity.test.js +2 -2
  158. package/dist/core/review/companion.js +11 -2
  159. package/dist/core/runtime/bearings.d.ts +2 -2
  160. package/dist/core/runtime/bearings.js +3 -3
  161. package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
  162. package/dist/core/runtime/broker/rebind.js +5 -0
  163. package/dist/core/runtime/broker-extension-render.d.ts +7 -3
  164. package/dist/core/runtime/broker-extension-render.js +10 -5
  165. package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
  166. package/dist/core/runtime/broker-persona-guidance.js +122 -27
  167. package/dist/core/runtime/deliver-live.d.ts +16 -4
  168. package/dist/core/runtime/deliver-live.js +29 -15
  169. package/dist/core/runtime/kickoff.d.ts +3 -3
  170. package/dist/core/runtime/kickoff.js +8 -16
  171. package/dist/core/runtime/lifecycle.js +2 -3
  172. package/dist/core/runtime/nodes.d.ts +3 -4
  173. package/dist/core/runtime/nodes.js +3 -4
  174. package/dist/core/runtime/persona.d.ts +8 -12
  175. package/dist/core/runtime/persona.js +24 -96
  176. package/dist/core/runtime/promote.d.ts +2 -2
  177. package/dist/core/runtime/promote.js +8 -21
  178. package/dist/core/runtime/revive.js +20 -17
  179. package/dist/core/runtime/spawn.js +18 -7
  180. package/dist/core/runtime/tmux-bindings.js +2 -3
  181. package/dist/core/runtime/warm-pool.js +3 -4
  182. package/dist/core/scope.js +2 -0
  183. package/dist/core/self-update.d.ts +0 -2
  184. package/dist/core/self-update.js +2 -35
  185. package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
  186. package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
  187. package/dist/core/substrate/index.d.ts +2 -2
  188. package/dist/core/substrate/index.js +1 -1
  189. package/dist/core/substrate/injected-store.d.ts +43 -27
  190. package/dist/core/substrate/injected-store.js +208 -104
  191. package/dist/core/substrate/listings.d.ts +19 -12
  192. package/dist/core/substrate/listings.js +75 -52
  193. package/dist/core/substrate/on-read-node.d.ts +4 -7
  194. package/dist/core/substrate/on-read-node.js +6 -8
  195. package/dist/core/substrate/on-read.d.ts +22 -25
  196. package/dist/core/substrate/on-read.js +103 -147
  197. package/dist/core/substrate/render-node.d.ts +4 -7
  198. package/dist/core/substrate/render-node.js +5 -7
  199. package/dist/core/substrate/render.d.ts +21 -3
  200. package/dist/core/substrate/render.js +291 -223
  201. package/dist/core/substrate/schema.d.ts +1 -13
  202. package/dist/core/substrate/schema.js +5 -40
  203. package/dist/core/substrate/session-cache.d.ts +14 -4
  204. package/dist/core/substrate/session-cache.js +40 -22
  205. package/dist/core/substrate/surface-match.d.ts +9 -7
  206. package/dist/core/substrate/surface-match.js +26 -25
  207. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
  208. package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
  209. package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
  210. package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
  211. package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +37 -9
  212. package/dist/daemon/api/bridge.d.ts +35 -4
  213. package/dist/daemon/api/bridge.js +50 -8
  214. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  215. package/dist/daemon/api/handlers/memory.js +2 -0
  216. package/dist/daemon/crtrd.js +2 -0
  217. package/dist/daemon/human/finish.js +8 -1
  218. package/dist/daemon/manage.d.ts +0 -1
  219. package/dist/daemon/manage.js +7 -16
  220. package/dist/daemon/startup-policy.d.ts +1 -0
  221. package/dist/daemon/startup-policy.js +1 -0
  222. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  223. package/dist/migrations/002-profile-project-memory.js +1 -0
  224. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  225. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  226. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  227. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  228. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  229. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  230. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  231. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  232. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  233. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  234. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  235. package/dist/migrations/__tests__/activation.test.js +149 -0
  236. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  237. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  238. package/dist/migrations/activation.d.ts +16 -0
  239. package/dist/migrations/activation.js +78 -0
  240. package/dist/migrations/convergent.d.ts +14 -3
  241. package/dist/migrations/convergent.js +21 -10
  242. package/dist/migrations/corpus.d.ts +78 -0
  243. package/dist/migrations/corpus.js +497 -0
  244. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  245. package/dist/migrations/frontmatter-splice.js +176 -0
  246. package/dist/migrations/registry.d.ts +6 -1
  247. package/dist/migrations/registry.js +7 -2
  248. package/dist/migrations/runner.d.ts +41 -0
  249. package/dist/migrations/runner.js +81 -0
  250. package/dist/migrations/types.d.ts +148 -9
  251. package/dist/migrations/types.js +9 -2
  252. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  253. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  254. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  255. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  256. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  257. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  258. package/dist/pi-extensions/canvas-stophook.js +6 -13
  259. package/dist/shared/generated-context.d.ts +0 -3
  260. package/dist/shared/generated-context.js +0 -57
  261. package/dist/shared/tool-groups.js +2 -3
  262. package/package.json +5 -4
  263. package/runtime.lock.json +2 -2
  264. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  265. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  266. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  267. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  268. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  269. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  270. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  271. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  272. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  273. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  274. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  275. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  276. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  277. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  278. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  279. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  280. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  281. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  282. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  283. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  284. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  285. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  286. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  287. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  288. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -1,23 +1,23 @@
1
- import { join } from 'node:path';
2
1
  import { defineLeaf } from '../../core/command.js';
3
2
  import { CrtrError, notFound, usage } from '../../core/errors.js';
4
- import { nativeMemoryStoresInPrecedence, resolveHistoryLogPath, resolveMemoryDoc } from '../../core/memory-resolver.js';
3
+ import { nativeMemoryStoresInPrecedence, resolveMemoryDoc, resolveMemoryDocInStore, } from '../../core/memory-resolver.js';
5
4
  import { renderResult } from '../../core/render.js';
6
- import { parseSkillQualifier } from '../../core/resolver.js';
7
- import { HISTORY_DIR, historyLogPathFor, readHistoryRecords, summarizeChange, unifiedDiff, } from '../../core/memory/history.js';
8
- import { MEMORY_SCOPES } from './shared.js';
9
- /** Locate a document's revision log. A live doc resolves through the normal
10
- * document precedence walk; a DELETED one has no file to resolve, so its log
11
- * is resolved by name against each native store's `.history` tree in the same
12
- * order, under the same normalization the doc had — history outlives the
13
- * document it describes, answering to the name that document answered to. */
14
- function resolveLog(nameRaw, scopeArg) {
5
+ import { historyLogPathForDoc, readHistoryRecords, resolveDeletedHistoryLogPath, summarizeChange, unifiedDiff, } from '../../core/memory/history.js';
6
+ import { requireCanonicalName, requireLocalNameInStore, requireMountedStore, resolveReadSelector, selectorParam, } from './shared.js';
7
+ import { stripNamespace } from '../../core/memory/identity.js';
8
+ /** Locate a document's revision log. A live doc is resolved to its physical
9
+ * descriptor first; a deleted doc is looked up in the selected store(s) by the
10
+ * exact canonical name, stripping only that store's declared project namespace.
11
+ * There is no scope-prefix or leaf fallback. */
12
+ function resolveLog(nameRaw, selectorInput) {
13
+ const selector = resolveReadSelector(selectorInput);
14
+ if (selector.store !== null)
15
+ requireMountedStore(selector.store);
15
16
  let doc;
16
17
  try {
17
- doc = resolveMemoryDoc(nameRaw, {
18
- includeDescendants: true,
19
- ...(scopeArg !== undefined ? { scope: scopeArg } : {}),
20
- });
18
+ doc = selector.store !== null
19
+ ? resolveMemoryDocInStore(nameRaw, selector.store)
20
+ : resolveMemoryDoc(nameRaw, { includeDescendants: true, ...(selector.scope ? { scope: selector.scope } : {}) });
21
21
  }
22
22
  catch (e) {
23
23
  if (!(e instanceof CrtrError && e.code === 'not_found'))
@@ -36,17 +36,20 @@ function resolveLog(nameRaw, scopeArg) {
36
36
  scope: 'builtin',
37
37
  });
38
38
  }
39
- return { name: doc.name, scope: doc.scope, logPath: historyLogPathFor(doc.root, doc.path) };
39
+ return { name: doc.name, scope: doc.scope, logPath: historyLogPathForDoc(doc) };
40
40
  }
41
- const parsed = parseSkillQualifier(nameRaw);
42
- const name = parsed.segments.join('/');
43
- if (name === '')
44
- throw usage('memory document name required');
45
- const scope = scopeArg ?? parsed.scope;
46
- for (const store of nativeMemoryStoresInPrecedence(scope, true)) {
47
- const logPath = resolveHistoryLogPath(join(store.memoryDir, HISTORY_DIR), parsed.segments);
48
- if (logPath !== null)
49
- return { name, scope: store.scope, logPath };
41
+ const canonicalName = requireCanonicalName(nameRaw);
42
+ const stores = selector.store !== null
43
+ ? [{ scope: selector.store.scope, memoryDir: selector.store.storeRoot, store: selector.store }]
44
+ : nativeMemoryStoresInPrecedence(selector.scope, true);
45
+ for (const store of stores) {
46
+ if (selector.store === null && store.store.namespace !== '' && stripNamespace(canonicalName, store.store.namespace) === null)
47
+ continue;
48
+ const localName = requireLocalNameInStore(store.store, canonicalName);
49
+ const deleted = resolveDeletedHistoryLogPath(store.memoryDir, localName);
50
+ if (deleted !== null) {
51
+ return { name: canonicalName, scope: store.scope, logPath: deleted.logPath, deleted };
52
+ }
50
53
  }
51
54
  throw notFound(`no memory document or revision history found: ${nameRaw}`, {
52
55
  memory: nameRaw,
@@ -64,20 +67,25 @@ export const historyLeaf = defineLeaf({
64
67
  'A record whose `before` differs from the previous record\u2019s `after` is the evidence of an out-of-band direct file edit; nothing prevents those, and nothing detects them beyond this.\n\n' +
65
68
  'The log is a plain JSONL file at `log_path`, one JSON object per line \u2014 an external consumer reads it directly rather than through this command.',
66
69
  params: [
67
- { kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`), resolved as `read` resolves it. When no such document exists, the same resolution runs against each writable store\u2019s history tree in precedence order \u2014 which is how a deleted document\u2019s history still answers to the name that document answered to. Builtin and installed-plugin docs are read-only corpora and carry no history.' },
70
+ { kind: 'positional', name: 'name', required: true, constraint: 'Full canonical memory name (a project document includes its namespace), resolved exactly. Deleted history uses the same canonical name and the selected store\u2019s physical sidecars; there is no leaf or scope-prefix fallback. Builtin and installed-plugin docs are read-only corpora and carry no history.' },
71
+ selectorParam('scope-read'),
72
+ selectorParam('dir'),
73
+ selectorParam('profile'),
68
74
  { kind: 'flag', name: 'limit', type: 'int', required: false, default: 10, constraint: 'How many revisions to return, newest first. Default 10.' },
69
75
  { kind: 'flag', name: 'diff', type: 'bool', required: false, constraint: 'Include a unified diff per listed revision, computed from that record\u2019s own before/after texts.' },
70
76
  { kind: 'flag', name: 'revision', type: 'int', required: false, constraint: 'Show ONE revision in full \u2014 its metadata plus the complete before and after document texts. 1-based position in the log: the number `crtr memory edit` reports. Overrides --limit and --diff.' },
71
- { kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Restrict resolution to this scope. A disambiguation filter for a name present at several scopes.' },
77
+ // Scope, --dir, and --profile are shared selector contracts above.
72
78
  ],
73
79
  output: [
74
80
  { name: 'name', type: 'string', required: true, constraint: 'Resolved document name.' },
75
81
  { name: 'scope', type: 'string', required: true, constraint: 'Scope whose store holds the log: node, project, profile, or user.' },
76
82
  { name: 'log_path', type: 'string', required: true, constraint: 'Absolute path to the append-only JSONL log \u2014 the direct-consumption pointer, one self-contained record per line.' },
83
+ { name: 'history_candidates', type: 'object[]', required: false, constraint: 'Deleted-history physical candidates: {path, representation, winner}. The directory-index form is listed first and wins deterministically.' },
84
+ { name: 'history_collision', type: 'object', required: false, constraint: 'Present when deleted history has both physical forms: {paths, winner}.' },
77
85
  { name: 'total', type: 'number', required: true, constraint: 'Total revisions in the log.' },
78
86
  { name: 'revisions', type: 'object[]', required: false, constraint: 'Newest-first, capped by --limit. Each: {revision, op, at, node, rationale, body_changed, frontmatter_changed, diff}. `node` and `rationale` are present only when recorded; `diff` only with --diff. Absent when --revision selects one record.' },
79
87
  { name: 'record', type: 'object', required: false, constraint: 'Present only with --revision: {revision, op, at, node, cwd, rationale, verbatim, before, after} \u2014 the full record including both complete document texts.' },
80
- { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands \u2014 the flag variants and the doc read.' },
88
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands \u2014 the flag variants and the doc read, or, for a deleted document, the revision carrying its retained final text.' },
81
89
  ],
82
90
  outputKind: 'object',
83
91
  effects: ['None. Read-only.'],
@@ -123,10 +131,16 @@ export const historyLeaf = defineLeaf({
123
131
  run: async (input) => {
124
132
  const nameRaw = input['name'];
125
133
  const scopeArg = input['scope'];
134
+ const dirArg = input['dir'];
135
+ const profileArg = input['profile'];
126
136
  const limit = input['limit'] ?? 10;
127
137
  const wantDiff = input['diff'] === true;
128
138
  const revisionArg = input['revision'];
129
- const { name, scope, logPath } = resolveLog(nameRaw, scopeArg);
139
+ const { name, scope, logPath, deleted } = resolveLog(nameRaw, { scope: scopeArg, dir: dirArg, profile: profileArg });
140
+ const historyMeta = deleted === undefined ? {} : {
141
+ history_candidates: deleted.candidates,
142
+ ...(deleted.collision === undefined ? {} : { history_collision: deleted.collision }),
143
+ };
130
144
  const records = readHistoryRecords(logPath);
131
145
  if (records.length === 0) {
132
146
  throw notFound(`no revision history recorded for ${name}`, {
@@ -146,9 +160,12 @@ export const historyLeaf = defineLeaf({
146
160
  name,
147
161
  scope,
148
162
  log_path: logPath,
163
+ ...historyMeta,
149
164
  total: records.length,
150
165
  record: { revision: revisionArg, ...r },
151
- follow_up: `Read the document as it stands with \`crtr memory read ${name}\`, or list the revisions with \`crtr memory history ${name} --diff\`.`,
166
+ follow_up: deleted === undefined
167
+ ? `Read the document as it stands with \`crtr memory read ${name}\`, or list the revisions with \`crtr memory history ${name} --diff\`.`
168
+ : `${name} is deleted — no read answers to it. This record's \`before\` is its text at that revision; the last revision (${records.length}) carries the text it was deleted with. List the revisions with \`crtr memory history ${name} --diff\`.`,
152
169
  };
153
170
  }
154
171
  const revisions = records
@@ -171,9 +188,12 @@ export const historyLeaf = defineLeaf({
171
188
  name,
172
189
  scope,
173
190
  log_path: logPath,
191
+ ...historyMeta,
174
192
  total: records.length,
175
193
  revisions,
176
- follow_up: `Add --diff for per-revision unified diffs, --revision N for one record's full before/after text, or read the document as it stands with \`crtr memory read ${name}\`.`,
194
+ follow_up: deleted === undefined
195
+ ? `Add --diff for per-revision unified diffs, --revision N for one record's full before/after text, or read the document as it stands with \`crtr memory read ${name}\`.`
196
+ : `${name} is deleted and only this log survives — no read answers to it. Add --diff for per-revision unified diffs, or recover its final text with \`crtr memory history ${name} --revision ${records.length}\`, whose \`before\` is the document as it stood when deleted.`,
177
197
  };
178
198
  },
179
199
  });
@@ -1,10 +1 @@
1
- export { lintSubstrateFrontmatter as lintSubstrateSchema } from '../../core/substrate/frontmatter-validation.js';
2
- /** Warn when a body short enough to inline still pays a routing line. */
3
- export declare function lintShortPreviewBody(fm: Record<string, unknown>, body: string): string | null;
4
- /** The length rule. Caps are computed from the parsed entries — an invalid
5
- * entry already fails the schema check, so it simply matches no cap here.
6
- * `docName` is the doc's canonical resolver identity (explicit frontmatter
7
- * `name`, else the normalized path-derived name) — persona layers under
8
- * `kinds/` are recognized by it and never capped. */
9
- export declare function lintBodyLength(fm: Record<string, unknown>, body: string, docName: string): string | null;
10
1
  export declare const lintLeaf: import("../../core/command.js").LeafDef;
@@ -1,341 +1,97 @@
1
- // `crtr memory lint` — the permanent valid-YAML gate over the bounded corpus
2
- // (the CTO green-checkpoint: zero frontmatter parse errors at authoring time,
3
- // so an invalid doc fails HERE instead of being silently isolated at runtime).
4
- // Bounded corpus = the substrate memory dirs (ancestor projects/user/builtin).
5
- // Never a filesystem-wide scan.
6
- import { basename, join, relative, sep } from 'node:path';
1
+ // `crtr memory lint` — the selector/help wrapper over the memory lint engine
2
+ // (`core/memory/lint.ts`). Every rule lives in the engine, which migration
3
+ // verification, the plugin candidate gate, and the build's builtin gate consume
4
+ // too; this leaf only picks WHAT to lint (a target view, or one exact store
5
+ // with --dir) and shapes the findings for the CLI.
7
6
  import { defineLeaf } from '../../core/command.js';
8
- import { general } from '../../core/errors.js';
9
7
  import { warn } from '../../core/output.js';
10
- import { pathExists, readText, walkFiles } from '../../core/fs-utils.js';
11
- import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
12
- import { listInstalledPlugins, listInstalledPluginsInRoot } from '../../core/resolver.js';
13
- import { pluginMemoryDir, projectScopeRoots, scopeMemoryDir } from '../../core/scope.js';
14
- import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
15
- import { getDefaultProfileId } from '../../core/profiles/default-binding.js';
16
- import { normalizeDocName, parseSubstrateFrontmatter, resolveDocName, } from '../../core/substrate/schema.js';
17
- import { lintSubstrateFrontmatter } from '../../core/substrate/frontmatter-validation.js';
18
- export { lintSubstrateFrontmatter as lintSubstrateSchema } from '../../core/substrate/frontmatter-validation.js';
19
- import { memoryExtensionValidationCatalog, validateMemoryExtensionValues } from '../../core/memory/extensions.js';
20
- import { docLinkNames } from '../../core/memory/doc-link-grammar.js';
21
- import { listAllMemoryDocs } from '../../core/memory-resolver.js';
22
- import { descendantStoreRoots } from '../../core/nested-stores.js';
23
- /** Rung-scaled body-length caps, measured in WORDS (frontmatter excluded).
24
- * Words, not lines: house style writes each paragraph as ONE logical line
25
- * and lets the editor soft-wrap, so a line count measures wrapping style
26
- * rather than context cost — words track what the reader actually pays.
27
- *
28
- * A `boot` entry at `content` inlines the whole body into every agent's
29
- * system prompt; a `content` entry on any other event (workspace-open, read,
30
- * memory-read, command) surfaces the whole body whenever the entry fires —
31
- * both cap at 1000 words. A `boot` entry at `preview` routes a deliberate
32
- * reader into the whole body, so its cap is the longest doc worth reading
33
- * end-to-end (calibrated to the humanizer doc, 2936 words). `name` entries
34
- * and surface-less docs never cap: such a doc is only reached deliberately
35
- * (a listing, browse, or a [[link]]), so its length is the reader's choice.
36
- * The strictest applicable cap wins.
37
- *
38
- * Persona layers — canonical names under `kinds/`, the docs the prompt
39
- * render composes into an agent's persona — are structurally exempt: an
40
- * agent reads its whole persona by construction, so persona length is a
41
- * persona-design choice, not an authoring smell. Suppression elsewhere is
42
- * deliberate and per-doc: `lint-ignore: length` in the frontmatter,
43
- * surfaced ONLY by the finding itself — never advertised in authoring
44
- * help. */
45
- const BOOT_CONTENT_MAX_WORDS = 1000;
46
- const EVENT_CONTENT_MAX_WORDS = 1000;
47
- const BOOT_PREVIEW_MAX_WORDS = 3000;
48
- /** A routing line costs more than it saves for a short rule or fact. */
49
- const PREVIEW_TO_CONTENT_MAX_WORDS = 30;
50
- function ignoresRule(fm, rule) {
51
- const v = fm['lint-ignore'];
52
- return v === rule || (Array.isArray(v) && v.includes(rule));
8
+ import { ExitCode } from '../../types.js';
9
+ import { lintMemoryStore, lintMemoryTarget, resolvableCanonicalNames, } from '../../core/memory/lint.js';
10
+ import { loadMemoryTargetView } from '../../core/memory-resolver.js';
11
+ import { resolveReadSelector, selectorParam } from './shared.js';
12
+ function findingRow(finding) {
13
+ return {
14
+ rule: finding.rule,
15
+ path: finding.path,
16
+ message: finding.message,
17
+ ...(finding.name === undefined ? {} : { name: finding.name }),
18
+ ...(finding.scope === undefined ? {} : { scope: finding.scope }),
19
+ ...(finding.storeRoot === undefined ? {} : { store_root: finding.storeRoot }),
20
+ ...(finding.paths === undefined ? {} : { paths: [...finding.paths] }),
21
+ };
53
22
  }
54
- function countBodyWords(body) {
55
- const trimmed = body.trim();
56
- return trimmed === '' ? 0 : trimmed.split(/\s+/).length;
23
+ /** The canonical names a `--dir` store's own links and routes must resolve in:
24
+ * the target view an agent standing in that project sees. Never the store in
25
+ * isolation — a project doc legitimately links a user or builtin doc. */
26
+ function resolvableNamesForStore(store) {
27
+ const view = loadMemoryTargetView({
28
+ cwd: store.ownerDir ?? store.storeRoot,
29
+ profileId: process.env['CRTR_PROFILE_ID'] || null,
30
+ nodeId: null,
31
+ }, { quiet: true, includeDescendants: true });
32
+ return resolvableCanonicalNames(view.docs);
57
33
  }
58
- /** The doc's surfaces entries as the runtime will read them. Used by the cap
59
- * checks AFTER the strict schema check has run — for a doc that passes it,
60
- * the tolerant parse and the strict contract agree. */
61
- function parsedSurfaces(fm) {
62
- return parseSubstrateFrontmatter(fm)?.surfaces ?? [];
63
- }
64
- /** Warn when a body short enough to inline still pays a routing line. */
65
- export function lintShortPreviewBody(fm, body) {
66
- const words = countBodyWords(body);
67
- if (words >= PREVIEW_TO_CONTENT_MAX_WORDS)
68
- return null;
69
- const previewEvents = [...new Set(parsedSurfaces(fm).filter((e) => e.at === 'preview').map((e) => e.on))];
70
- if (previewEvents.length === 0)
71
- return null;
72
- const noun = previewEvents.length === 1 ? 'entry delivers' : 'entries deliver';
73
- return `body is ${words} words but the ${previewEvents.join('/')} ${noun} \`preview\`; use \`at: content\` so agents receive the whole rule without a separate memory read`;
74
- }
75
- /** The length rule. Caps are computed from the parsed entries — an invalid
76
- * entry already fails the schema check, so it simply matches no cap here.
77
- * `docName` is the doc's canonical resolver identity (explicit frontmatter
78
- * `name`, else the normalized path-derived name) — persona layers under
79
- * `kinds/` are recognized by it and never capped. */
80
- export function lintBodyLength(fm, body, docName) {
81
- if (ignoresRule(fm, 'length'))
82
- return null;
83
- // Persona exemption: render composes an agent's persona from the docs
84
- // resolving under `kinds/...`, and an agent reads its whole persona by
85
- // construction — persona length is persona design, never an authoring smell.
86
- if (docName === 'kinds' || docName.startsWith('kinds/'))
87
- return null;
88
- const entries = parsedSurfaces(fm);
89
- const words = countBodyWords(body);
90
- const remedy = 'Keep the load-bearing core here and split the depth into [[linked]] reference docs carrying no surfaces at all (the listing and the link are how they are found, so they cost nothing until followed). Split by subject: each leaf covers a different subject a task might need on its own; never split off “further evidence”, examples, or references — a references leaf is never followed, so supporting material stays next to the point it supports or gets cut. Keep it whole — `lint-ignore: length` in the frontmatter — only when every reader who surfaces this doc genuinely benefits from reading 100% of it, or it is one indivisible body of knowledge; then splitting just adds hops.';
91
- if (entries.some((e) => e.on === 'boot' && e.at === 'content') && words > BOOT_CONTENT_MAX_WORDS) {
92
- return `body is ${words} words but a boot entry at \`content\` inlines every word into every agent's system prompt — capped at ${BOOT_CONTENT_MAX_WORDS} words (boot preview gets ${BOOT_PREVIEW_MAX_WORDS}; name entries are never capped). ${remedy}`;
93
- }
94
- const contentEvents = [...new Set(entries.filter((e) => e.on !== 'boot' && e.at === 'content').map((e) => e.on))];
95
- if (contentEvents.length > 0 && words > EVENT_CONTENT_MAX_WORDS) {
96
- return `body is ${words} words, over the ${EVENT_CONTENT_MAX_WORDS}-word cap for a \`content\` entry on ${contentEvents.join('/')} (the whole body delivers every time the entry fires; name entries are never capped). ${remedy}`;
97
- }
98
- if (entries.some((e) => e.on === 'boot' && e.at === 'preview') && words > BOOT_PREVIEW_MAX_WORDS) {
99
- return `body is ${words} words, over the ${BOOT_PREVIEW_MAX_WORDS}-word cap for boot preview (the routing line invites every reader into the whole body, so the cap is the longest doc worth reading end-to-end; boot content is capped at ${BOOT_CONTENT_MAX_WORDS} words; name entries are never capped). ${remedy}`;
100
- }
101
- return null;
102
- }
103
- /** Strict-parse one file; push a finding on a YAML error, then run the
104
- * schema check when the file lives in a substrate store, then validate every
105
- * `[[canonical/name]]` doc link in the body against the exact resolvable
106
- * corpus. A dangling link is an authoring error caught HERE, never silently
107
- * carried; leaf-name fallback is deliberately excluded so links stay stable
108
- * as the graph grows and another document acquires the same leaf name. */
109
- function lintFile(file, substrateStore, findings, warnings, corpusNames, fallbackName, scope) {
110
- let fm;
111
- let body;
112
- try {
113
- ({ data: fm, body } = parseFrontmatterGeneric(readText(file)));
114
- }
115
- catch (e) {
116
- const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
117
- findings.push({ path: file, error: `invalid YAML frontmatter: ${msg}` });
118
- return;
119
- }
120
- if (!substrateStore)
121
- return;
122
- const schemaError = lintSubstrateFrontmatter(fm);
123
- if (schemaError !== null)
124
- findings.push({ path: file, error: schemaError });
125
- if (fm !== null) {
126
- for (const issue of validateMemoryExtensionValues(fm['extensions'], memoryExtensionValidationCatalog({ scope, path: file }))) {
127
- findings.push({ path: file, error: `${issue.path}: ${issue.message}` });
128
- }
129
- const lengthError = lintBodyLength(fm, body, resolveDocName(fm, fallbackName));
130
- if (lengthError !== null)
131
- findings.push({ path: file, error: lengthError });
132
- const shortPreviewWarning = lintShortPreviewBody(fm, body);
133
- if (shortPreviewWarning !== null)
134
- warnings.push(`${file}: ${shortPreviewWarning}`);
135
- // An over-broad memory-read glob fires on EVERY memory read — almost
136
- // always an authoring accident, so flag it without failing the corpus.
137
- for (const entry of parsedSurfaces(fm)) {
138
- if (entry.on !== 'memory-read')
139
- continue;
140
- for (const glob of entry.match ?? []) {
141
- if ((glob === '**' || glob === '*') && !ignoresRule(fm, 'broad-memory-read')) {
142
- warnings.push(`${file}: memory-read glob \`${glob}\` fires on every memory read — scope it to a name subtree`);
143
- }
144
- }
145
- }
146
- }
147
- for (const name of docLinkNames(body)) {
148
- if (!corpusNames.has(name)) {
149
- findings.push({
150
- path: file,
151
- error: `dangling doc link [[${name}]]: no memory document or directory has that exact canonical name — retarget the link (\`crtr memory find ${name.split('/').pop()}\`) or drop it`,
152
- });
153
- }
154
- }
155
- }
156
- /** The files in a project store carrying a workspace front-door entry
157
- * ({on: workspace-open, at: content}). Raw fm scan; a missing store or an
158
- * unparseable doc contributes zero (the YAML failure is its own finding). */
159
- function frontDoorDocs(storeDir) {
160
- const hits = [];
161
- if (!pathExists(storeDir))
162
- return hits;
163
- for (const file of walkFiles(storeDir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
164
- if (basename(file) === 'MEMORY.md')
165
- continue;
166
- let fm;
167
- try {
168
- fm = parseFrontmatterGeneric(readText(file)).data;
169
- }
170
- catch {
171
- continue;
172
- }
173
- if (fm === null)
174
- continue;
175
- const entries = parsedSurfaces(fm);
176
- if (entries.some((e) => e.on === 'workspace-open' && e.at === 'content'))
177
- hits.push(file);
178
- }
179
- return hits;
180
- }
181
- const FRONT_DOOR_REMEDY = 'author the project\'s operating guide as an ordinary doc with `surfaces: [{on: workspace-open, at: content}, {on: read, match: "./**", at: content}]`; run `crtr memory write -h` first';
182
34
  export const lintLeaf = defineLeaf({
183
35
  name: 'lint',
184
- description: 'validate frontmatter and body length across the whole bounded document corpus',
185
- whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (the substrate memory stores) and fail loudly on any invalid YAML, substrate schema violation (including retired visibility fields and malformed `surfaces` entries), body longer than its delivery entries earn, or dangling `[[canonical/name]]` doc link; also validates that every project managed by the selected profile has exactly one workspace front door and warns when an unprofiled working directory has none. Run it before shipping doc changes; CI-friendly (non-zero exit on findings, warnings remain non-fatal).',
36
+ description: 'validate memory identity, frontmatter, and references across a target view or one exact store',
37
+ whenToUse: 'you authored, moved, or migrated documents and want the authoring-time gate before shipping them. It strict-parses every doc the selection covers and fails on: invalid YAML or substrate schema, a repository whose root store declares no valid `namespace:` (so the runtime refuses to mount it), a `namespace:` outside the repository-root store\u2019s root INDEX.md, two directories in one repository whose paths normalize to the same prefix, a `name:` on a root INDEX.md or a non-root `name:` that is not a valid store-LOCAL name, two files in one store owning one canonical identity, a `[[link]]` or literal `memory-read` route naming no document or directory in the view (a directory\u2019s own document answers at the directory name, never at `<dir>/INDEX`), a body longer than its delivery entries earn, and a repository whose root store’s workspace front door is missing or sits anywhere but its root INDEX.md. One canonical identity held by SEVERAL stores is legitimate (a linked worktree sharing its main checkout\u2019s namespace), so it is reported as a notice with the view\u2019s winner and every candidate, never failed. CI-friendly: non-zero exit on any error, warnings and notices stay non-fatal.',
186
38
  help: {
187
39
  name: 'memory lint',
188
- summary: 'strict-parse the bounded memory corpus and validate project workspace front doors',
189
- params: [],
40
+ summary: 'validate canonical identity, frontmatter, and references across the memory corpus',
41
+ params: [
42
+ selectorParam('scope-read', {}, 'Lints only that source class. Cross-store candidate notices and project front-door coverage need the unfiltered view, so a scope filter suppresses them.'),
43
+ selectorParam('dir', {}, 'Lints that one store even when the runtime refuses to mount it — the only way to check a project store before its repository namespace is declared. Its links and routes are resolved against the target view that project\u2019s own directory sees, and an unmounted store defers those reference checks (its identities are undetermined until the namespace exists).'),
44
+ ],
190
45
  output: [
191
- { name: 'checked', type: 'number', required: true, constraint: 'Files linted across all corpora.' },
192
- { name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus counts: {memory_stores (files), profile_projects (managed dirs checked for a workspace front door)}.' },
193
- { name: 'findings', type: 'object[]', required: true, constraint: 'One row per failure: {path, error}. Empty when green.' },
194
- { name: 'warnings', type: 'string[]', required: true, constraint: 'Non-fatal authoring gaps detected for the directory where lint ran.' },
46
+ { name: 'checked', type: 'number', required: true, constraint: 'Markdown files examined.' },
47
+ { name: 'stores', type: 'number', required: true, constraint: 'Physical memory stores linted.' },
48
+ { name: 'profile_projects', type: 'number', required: false, constraint: 'Projects the selected profile manages that were checked for a workspace front door — zero under a scope filter excluding project stores. Absent under --dir.' },
49
+ { name: 'findings', type: 'object[]', required: true, constraint: 'One row per ERROR: {rule, path, message, name?, scope?, store_root?, paths?}. `rule` names the violated rule (yaml, schema, extension, length, store-namespace, namespace-placement, root-name, root-index-unaddressable, local-name, canonical-collision, dangling-link, memory-read-route, front-door, owner-path-collision). `paths` carries every file sharing a collided identity, read winner first. Empty when green.' },
50
+ { name: 'warnings', type: 'object[]', required: true, constraint: 'Same row shape, non-fatal authoring smells: short-preview, broad-memory-read, nested-store-boot, an advisory missing front door, a wildcard memory-read glob matching nothing.' },
51
+ { name: 'notices', type: 'object[]', required: true, constraint: 'Same row shape, inspection data rather than faults: cross-store-candidates (one canonical name held by several stores, winner first) and reference-check-deferred.' },
52
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next command.' },
195
53
  ],
196
54
  outputKind: 'object',
197
- effects: ['None. Read-only. Exits non-zero when any finding exists.'],
55
+ effects: ['None. Read-only. Exits non-zero when any error finding exists.'],
198
56
  },
199
- run: async () => {
200
- const findings = [];
201
- const warnings = [];
202
- let memoryCount = 0;
203
- const workingDirectory = process.cwd();
204
- // Substrate memory stores (ancestor projects/user/builtin), schema-aware.
205
- // A source's corpus is its NATIVE memory dir plus each enabled plugin's memory dir —
206
- // plugin docs are substrate docs and lint through the same schema gate.
207
- // MEMORY.md index files are not substrate docs — YAML-parse only.
208
- // Hidden dirs are skipped to mirror doc enumeration (a dot-segment
209
- // path never yields a doc name, so e.g. the maintainer store shipped
210
- // at builtin-memory/.crouter can never register) — lint must not
211
- // flag files the substrate can never load.
212
- // The resolvable link targets: exact doc names, plus every proper name
213
- // prefix — a bare-dir [[ref]] is a legal listing link (`crtr memory read
214
- // <dir>` answers with the directory listing).
215
- const corpusNames = new Set();
216
- for (const doc of listAllMemoryDocs(undefined, true, true)) {
217
- if (doc.name === '')
218
- continue;
219
- corpusNames.add(doc.name);
220
- const segs = doc.name.split('/');
221
- for (let i = 1; i < segs.length; i++)
222
- corpusNames.add(segs.slice(0, i).join('/'));
223
- }
224
- const lintDir = (dir, scope) => {
225
- if (!dir || !pathExists(dir))
226
- return;
227
- for (const file of walkFiles(dir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
228
- const relPath = relative(dir, file).split(sep).join('/');
229
- if (!relPath)
230
- continue;
231
- memoryCount += 1;
232
- const fallbackName = normalizeDocName(relPath.replace(/\.md$/, ''));
233
- lintFile(file, basename(file) !== 'MEMORY.md', findings, warnings, corpusNames, fallbackName, scope);
234
- }
57
+ run: async (input) => {
58
+ const selector = resolveReadSelector({
59
+ scope: input['scope'],
60
+ dir: input['dir'],
61
+ });
62
+ const result = selector.store !== null
63
+ ? lintMemoryStore(selector.store, {
64
+ resolvable: resolvableNamesForStore(selector.store),
65
+ // Asked about directly, not as a profile-managed project: a missing
66
+ // front door is a gap to report, not a failed gate.
67
+ frontDoor: 'advisory',
68
+ })
69
+ : lintMemoryTarget({ ...(selector.scope === undefined ? {} : { scope: selector.scope }) });
70
+ const errors = result.errors.map(findingRow);
71
+ const warnings = result.warnings.map(findingRow);
72
+ const notices = result.notices.map(findingRow);
73
+ const counts = {
74
+ checked: result.checked,
75
+ stores: result.storesLinted,
76
+ ...('profileProjects' in result ? { profile_projects: result.profileProjects } : {}),
235
77
  };
236
- for (const root of projectScopeRoots()) {
237
- lintDir(join(root, 'memory'), 'project');
238
- for (const plugin of listInstalledPluginsInRoot('project', root)) {
239
- if (plugin.enabled)
240
- lintDir(pluginMemoryDir(plugin), 'project');
241
- }
242
- }
243
- // Nested descendant stores: same schema gate, plus a boot-entry warning —
244
- // the boot catalog deliberately never includes nested stores, so a boot
245
- // surfaces entry there is inert.
246
- for (const root of descendantStoreRoots(projectScopeRoots())) {
247
- const dir = join(root, 'memory');
248
- lintDir(dir, 'project');
249
- if (!pathExists(dir))
250
- continue;
251
- for (const file of walkFiles(dir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
252
- if (basename(file) === 'MEMORY.md')
253
- continue;
254
- let fm;
255
- try {
256
- fm = parseFrontmatterGeneric(readText(file)).data;
257
- }
258
- catch {
259
- continue; // invalid YAML is already a finding from lintDir
260
- }
261
- if (fm !== null && parsedSurfaces(fm).some((e) => e.on === 'boot')) {
262
- warnings.push(`${file}: nested-store docs never ride the boot catalog, so its boot surfaces entries are inert — drop them`);
263
- }
264
- }
265
- }
266
- for (const scope of ['user', 'builtin']) {
267
- lintDir(scopeMemoryDir(scope), scope);
268
- for (const plugin of listInstalledPlugins(scope)) {
269
- if (plugin.enabled)
270
- lintDir(pluginMemoryDir(plugin), scope);
271
- }
272
- }
273
- // Profile coverage: each managed project has exactly ONE workspace front
274
- // door — a doc carrying {on: workspace-open, at: content}, the guide that
275
- // enters first-message context when cwd/profile mounts the store. Zero
276
- // means agents open the workspace blind; two means both deliver whole at
277
- // every open, and the store needs consolidating.
278
- let profileProjects = 0;
279
- let profileResolved = false;
280
- const profileIdOrName = process.env['CRTR_PROFILE_ID'] || getDefaultProfileId(process.cwd());
281
- if (profileIdOrName) {
282
- try {
283
- const { profileId, manifest } = loadProfileManifest(profileIdOrName);
284
- profileResolved = true;
285
- lintDir(profileMemoryDir(profileId), 'user');
286
- for (const project of manifest.projects) {
287
- profileProjects += 1;
288
- const dir = project.path;
289
- const doors = frontDoorDocs(join(dir, '.crouter', 'memory'));
290
- if (doors.length === 0) {
291
- findings.push({
292
- path: dir,
293
- error: `profile "${manifest.name}" manages this dir but no doc in its .crouter/memory carries a {on: workspace-open, at: content} surfaces entry — ${FRONT_DOOR_REMEDY}`,
294
- });
295
- }
296
- else if (doors.length > 1) {
297
- findings.push({
298
- path: dir,
299
- error: `multiple workspace front doors (${doors.length}): ${doors.join(', ')} — exactly one doc per project store may carry {on: workspace-open, at: content}; consolidate the others into it or lower their entries`,
300
- });
301
- }
302
- }
303
- }
304
- catch {
305
- // Unresolvable profile: profile coverage does not apply. Treat cwd as
306
- // unprofiled for the advisory front-door warning below.
307
- }
308
- }
309
- // Without a selected, resolvable profile there is no managed-project
310
- // finding to carry this signal, so keep a non-fatal cwd adoption warning.
311
- if (!profileResolved) {
312
- if (frontDoorDocs(join(workingDirectory, '.crouter', 'memory')).length === 0) {
313
- warnings.push(`${workingDirectory}: no workspace front door — ${FRONT_DOOR_REMEDY}`);
314
- }
315
- }
316
- const checked = memoryCount;
317
- // Warnings are surfaced on stderr and returned structurally, but never
318
- // convert a green corpus into a failed authoring gate.
319
- for (const warning of warnings)
320
- warn(`memory lint: warning: ${warning}`);
321
- if (findings.length > 0) {
322
- // Human/agent path renders only the message — surface every offender as
323
- // a scoped stderr notice (the --json path carries them in details too).
324
- for (const f of findings)
325
- warn(`memory lint: ${f.path}: ${f.error}`);
326
- throw general(`memory lint: ${findings.length} finding(s) across ${checked} files`, {
327
- checked,
328
- findings: findings.map((f) => ({ path: f.path, error: f.error })),
329
- warnings,
330
- next: 'Fix each doc (quote YAML values containing `: `; use a valid kind; run `crtr sys migrate` for retired visibility fields; give each surfaces entry a valid on/at and the match its event requires; retarget or drop dangling [[links]]; split an over-length body into [[linked]] surface-less reference docs); give every profile-managed project exactly one workspace front door; then re-run `crtr memory lint`.',
331
- });
78
+ for (const finding of [...result.warnings, ...result.errors]) {
79
+ warn(`memory lint: ${finding.rule}: ${finding.path}: ${finding.message}`);
332
80
  }
81
+ // A failing lint still owes its declared rows — the findings ARE the
82
+ // result. It fails by exit code, not by throwing them away.
83
+ if (errors.length > 0)
84
+ process.exitCode = ExitCode.GENERAL;
333
85
  return {
334
- checked,
335
- corpora: { memory_stores: memoryCount, profile_projects: profileProjects },
336
- findings: [],
86
+ ...counts,
87
+ findings: errors,
337
88
  warnings,
338
- follow_up: warnings.length > 0 ? 'Corpus green — review the warnings above for non-fatal authoring improvements.' : 'Corpus green — zero invalid frontmatter docs.',
89
+ notices,
90
+ follow_up: errors.length > 0
91
+ ? `${errors.length} error(s) across ${result.checked} files in ${result.storesLinted} store(s) — fix each, then re-run. A repository store that does not mount is repaired with \`crtr sys migrate --dir <repository-root>\`; an owner-path collision is fixed by renaming one of the directories named in the finding; a canonical collision is recovered with \`crtr memory move <name> --to <new-free-name>\` (it selects the first physical path in stable lexical order); a dangling link or route is retargeted with the name \`crtr memory find <leaf>\` reports, or dropped; an over-length body splits into [[linked]] surface-less reference docs.`
92
+ : warnings.length > 0
93
+ ? 'No errors — review the warnings above, then revise with `crtr memory edit <name> --rationale "<why>"`.'
94
+ : 'No errors. Notices are inspection data, not faults: a canonical name held by several stores is reachable per-store with `crtr memory read <name> --dir <project>`.',
339
95
  };
340
96
  },
341
97
  });
@@ -1,15 +1,23 @@
1
1
  import type { MemoryDoc, MemoryScope } from '../../core/memory-resolver.js';
2
- /** A fully-resolved memory inventory before command pagination/output shaping.
3
- * Shared in-process consumers retain `memory list`'s scope ordering and
4
- * identity behavior without rescanning the corpus once per output page. */
2
+ /** One inventory row before pagination/output shaping. */
5
3
  export interface ListedMemoryDoc {
6
4
  name: string;
7
5
  kind: string;
8
6
  scope: MemoryScope;
9
7
  path: string;
8
+ storeRoot: string;
9
+ physicalRelativePath: string;
10
10
  shortForm: string;
11
11
  slash: boolean;
12
12
  extensions: Record<string, Record<string, string | boolean | number>>;
13
+ /** Every physical document sharing this canonical name in the same corpus,
14
+ * in source order — the winner first. */
15
+ candidates: readonly MemoryDoc[];
13
16
  }
14
- export declare function listMemoryDocs(kindFilter?: string, scopeFilter?: MemoryScope, quiet?: boolean, docs?: readonly MemoryDoc[]): ListedMemoryDoc[];
17
+ /** The inventory over one corpus. A target view is winner-only: one row per
18
+ * canonical identity, first in source order, because that is the document a
19
+ * read of the name returns. An exact store (`--dir`) is not deduped — the
20
+ * point of naming a store is to see what it physically holds, including a pair
21
+ * that shares one identity. */
22
+ export declare function listMemoryDocs(kindFilter: string | undefined, docs: readonly MemoryDoc[], dedup: boolean): ListedMemoryDoc[];
15
23
  export declare const listLeaf: import("../../core/command.js").LeafDef;