@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18

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 (357) hide show
  1. package/README.md +134 -15
  2. package/assets/integrations/skills/pappra/SKILL.md +28 -7
  3. package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
  4. package/assets/integrations/skills/pappra/references/environment.md +360 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +20 -0
  6. package/assets/integrations/skills/pappra/references/operations.md +36 -5
  7. package/dist/adapters/pi/recovery-extension.d.ts +65 -0
  8. package/dist/adapters/pi/recovery-extension.js +238 -0
  9. package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
  10. package/dist/adapters/pi/session-transition-extension.js +265 -0
  11. package/dist/app/automatic-handoff-events.d.ts +26 -0
  12. package/dist/app/automatic-handoff-events.js +134 -0
  13. package/dist/app/bounded-file.d.ts +2 -0
  14. package/dist/app/bounded-file.js +2 -0
  15. package/dist/app/capability-evidence/claude.d.ts +37 -0
  16. package/dist/app/capability-evidence/claude.js +206 -0
  17. package/dist/app/capability-evidence/codex.d.ts +35 -0
  18. package/dist/app/capability-evidence/codex.js +193 -0
  19. package/dist/app/capability-evidence/index.d.ts +33 -0
  20. package/dist/app/capability-evidence/index.js +117 -0
  21. package/dist/app/capability-evidence/pi.d.ts +73 -0
  22. package/dist/app/capability-evidence/pi.js +669 -0
  23. package/dist/app/capability-evidence/shared.d.ts +69 -0
  24. package/dist/app/capability-evidence/shared.js +195 -0
  25. package/dist/app/collab-auto-handoff.d.ts +55 -0
  26. package/dist/app/collab-auto-handoff.js +384 -0
  27. package/dist/app/collab-bindings.d.ts +57 -0
  28. package/dist/app/collab-bindings.js +1006 -0
  29. package/dist/app/collab-bootstrap-transport.d.ts +163 -0
  30. package/dist/app/collab-bootstrap-transport.js +769 -0
  31. package/dist/app/collab-bootstrap.d.ts +137 -0
  32. package/dist/app/collab-bootstrap.js +3455 -0
  33. package/dist/app/collab-handoff.d.ts +12 -0
  34. package/dist/app/collab-handoff.js +511 -0
  35. package/dist/app/collab-messages.d.ts +2 -0
  36. package/dist/app/collab-messages.js +2 -0
  37. package/dist/app/collab-notify.d.ts +156 -0
  38. package/dist/app/collab-notify.js +1208 -0
  39. package/dist/app/collab-preflight.d.ts +34 -0
  40. package/dist/app/collab-preflight.js +96 -0
  41. package/dist/app/collab-recovery-executor.d.ts +161 -0
  42. package/dist/app/collab-recovery-executor.js +1153 -0
  43. package/dist/app/collab-recovery-projection.d.ts +103 -0
  44. package/dist/app/collab-recovery-projection.js +659 -0
  45. package/dist/app/collab-recovery.d.ts +25 -0
  46. package/dist/app/collab-recovery.js +429 -0
  47. package/dist/app/collab-resources.d.ts +16 -0
  48. package/dist/app/collab-resources.js +278 -0
  49. package/dist/app/collab-role-events.d.ts +22 -0
  50. package/dist/app/collab-role-events.js +329 -0
  51. package/dist/app/collab-role-orchestration.d.ts +53 -0
  52. package/dist/app/collab-role-orchestration.js +601 -0
  53. package/dist/app/collab-role-plan.d.ts +81 -0
  54. package/dist/app/collab-role-plan.js +504 -0
  55. package/dist/app/collab-role-repair.d.ts +109 -0
  56. package/dist/app/collab-role-repair.js +821 -0
  57. package/dist/app/collab-rollover.d.ts +263 -0
  58. package/dist/app/collab-rollover.js +1112 -0
  59. package/dist/app/collab-route.d.ts +64 -0
  60. package/dist/app/collab-route.js +240 -0
  61. package/dist/app/collab-session-transition.d.ts +221 -0
  62. package/dist/app/collab-session-transition.js +3334 -0
  63. package/dist/app/collab-state.d.ts +7 -0
  64. package/dist/app/collab-state.js +162 -0
  65. package/dist/app/collab-task-store.d.ts +22 -0
  66. package/dist/app/collab-task-store.js +204 -0
  67. package/dist/app/collab-work-execution.d.ts +127 -0
  68. package/dist/app/collab-work-execution.js +1027 -0
  69. package/dist/app/collab-work-plan.d.ts +87 -0
  70. package/dist/app/collab-work-plan.js +437 -0
  71. package/dist/app/collaboration.d.ts +8 -2
  72. package/dist/app/collaboration.js +101 -12
  73. package/dist/app/doctor.js +108 -5
  74. package/dist/app/host-mcp.js +34 -8
  75. package/dist/app/host-probe.js +4 -4
  76. package/dist/app/init.d.ts +58 -4
  77. package/dist/app/init.js +768 -155
  78. package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
  79. package/dist/app/managed-pi-bootstrap-admission.js +104 -0
  80. package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
  81. package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
  82. package/dist/app/managed-pi-delivery.d.ts +8 -0
  83. package/dist/app/managed-pi-delivery.js +110 -0
  84. package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
  85. package/dist/app/managed-pi-handoff-receiver.js +263 -0
  86. package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
  87. package/dist/app/managed-pi-handoff-reference.js +60 -0
  88. package/dist/app/managed-pi-herdr.d.ts +21 -0
  89. package/dist/app/managed-pi-herdr.js +185 -0
  90. package/dist/app/managed-pi-initial-admission.d.ts +28 -0
  91. package/dist/app/managed-pi-initial-admission.js +416 -0
  92. package/dist/app/managed-pi-initial-launch.d.ts +5 -0
  93. package/dist/app/managed-pi-initial-launch.js +61 -0
  94. package/dist/app/managed-pi-native-observation.d.ts +36 -0
  95. package/dist/app/managed-pi-native-observation.js +191 -0
  96. package/dist/app/managed-pi-readiness.d.ts +29 -0
  97. package/dist/app/managed-pi-readiness.js +288 -0
  98. package/dist/app/managed-pi-repair-stop.d.ts +21 -0
  99. package/dist/app/managed-pi-repair-stop.js +371 -0
  100. package/dist/app/managed-pi-resume-admission.d.ts +32 -0
  101. package/dist/app/managed-pi-resume-admission.js +618 -0
  102. package/dist/app/managed-pi-retirement.d.ts +28 -0
  103. package/dist/app/managed-pi-retirement.js +112 -0
  104. package/dist/app/managed-pi-sdk-control.d.ts +7 -0
  105. package/dist/app/managed-pi-sdk-control.js +169 -0
  106. package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
  107. package/dist/app/managed-pi-sdk-runtime.js +483 -0
  108. package/dist/app/managed-pi-source-admission.d.ts +14 -0
  109. package/dist/app/managed-pi-source-admission.js +323 -0
  110. package/dist/app/managed-pi-transition-host.d.ts +36 -0
  111. package/dist/app/managed-pi-transition-host.js +292 -0
  112. package/dist/app/managed-pi.d.ts +54 -0
  113. package/dist/app/managed-pi.js +248 -0
  114. package/dist/app/shell-readiness.d.ts +66 -0
  115. package/dist/app/shell-readiness.js +368 -0
  116. package/dist/app/task-pin.d.ts +2 -23
  117. package/dist/app/task-pin.js +2 -113
  118. package/dist/cli/commands/collaboration-binding.d.ts +13 -0
  119. package/dist/cli/commands/collaboration-binding.js +118 -0
  120. package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
  121. package/dist/cli/commands/collaboration-bootstrap.js +84 -0
  122. package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
  123. package/dist/cli/commands/collaboration-capabilities.js +127 -0
  124. package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
  125. package/dist/cli/commands/collaboration-handoff.js +102 -0
  126. package/dist/cli/commands/collaboration-message.d.ts +17 -0
  127. package/dist/cli/commands/collaboration-message.js +177 -0
  128. package/dist/cli/commands/collaboration-plan.d.ts +8 -0
  129. package/dist/cli/commands/collaboration-plan.js +108 -0
  130. package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
  131. package/dist/cli/commands/collaboration-preflight.js +167 -0
  132. package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
  133. package/dist/cli/commands/collaboration-recovery-run.js +224 -0
  134. package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
  135. package/dist/cli/commands/collaboration-recovery.js +104 -0
  136. package/dist/cli/commands/collaboration-resources.d.ts +11 -0
  137. package/dist/cli/commands/collaboration-resources.js +96 -0
  138. package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
  139. package/dist/cli/commands/collaboration-role-plan.js +69 -0
  140. package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
  141. package/dist/cli/commands/collaboration-role-run.js +128 -0
  142. package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
  143. package/dist/cli/commands/collaboration-rollover.js +103 -0
  144. package/dist/cli/commands/collaboration-state.d.ts +10 -0
  145. package/dist/cli/commands/collaboration-state.js +96 -0
  146. package/dist/cli/commands/collaboration-transition.d.ts +10 -0
  147. package/dist/cli/commands/collaboration-transition.js +75 -0
  148. package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
  149. package/dist/cli/commands/collaboration-work-run.js +178 -0
  150. package/dist/cli/commands/collaboration.d.ts +4 -0
  151. package/dist/cli/commands/collaboration.js +39 -4
  152. package/dist/cli/commands/continuity.js +2 -6
  153. package/dist/cli/commands/docs.d.ts +2 -0
  154. package/dist/cli/commands/docs.js +89 -15
  155. package/dist/cli/commands/host.d.ts +7 -1
  156. package/dist/cli/commands/host.js +122 -7
  157. package/dist/cli/commands/init.d.ts +20 -3
  158. package/dist/cli/commands/init.js +897 -232
  159. package/dist/cli/commands/mcp.d.ts +1 -0
  160. package/dist/cli/commands/mcp.js +11 -5
  161. package/dist/cli/commands/repair.d.ts +12 -0
  162. package/dist/cli/commands/repair.js +41 -0
  163. package/dist/cli/commands/runtime.d.ts +1 -1
  164. package/dist/cli/commands/runtime.js +139 -38
  165. package/dist/cli/commands/session.d.ts +7 -0
  166. package/dist/cli/commands/session.js +82 -0
  167. package/dist/cli/commands/task.js +19 -5
  168. package/dist/cli/run.js +866 -24
  169. package/dist/code/identity.d.ts +7 -0
  170. package/dist/code/identity.js +7 -3
  171. package/dist/code/registration.js +7 -0
  172. package/dist/code/service.d.ts +9 -0
  173. package/dist/code/service.js +16 -3
  174. package/dist/code/text.js +19 -7
  175. package/dist/collab/admission.d.ts +91 -66
  176. package/dist/collab/admission.js +21 -6
  177. package/dist/collab/canonical.d.ts +1 -1
  178. package/dist/collab/canonical.js +44 -3
  179. package/dist/collab/envelope.d.ts +2 -2
  180. package/dist/collab/envelope.js +12 -12
  181. package/dist/collab/first-work.d.ts +3 -0
  182. package/dist/collab/first-work.js +15 -0
  183. package/dist/collab/handoff.d.ts +16 -0
  184. package/dist/collab/handoff.js +39 -5
  185. package/dist/collab/index.d.ts +97 -1
  186. package/dist/collab/index.js +9 -1
  187. package/dist/collab/messages.js +126 -3
  188. package/dist/collab/persisted-messages.d.ts +123 -0
  189. package/dist/collab/persisted-messages.js +496 -0
  190. package/dist/collab/preflight.d.ts +1 -1
  191. package/dist/collab/resources.d.ts +13 -3
  192. package/dist/collab/resources.js +37 -6
  193. package/dist/collab/rollover.d.ts +311 -0
  194. package/dist/collab/rollover.js +1024 -0
  195. package/dist/collab/store.d.ts +1 -0
  196. package/dist/collab/store.js +1 -4
  197. package/dist/collab/types.d.ts +2 -354
  198. package/dist/collab/validation.d.ts +2 -10
  199. package/dist/collab/validation.js +2 -303
  200. package/dist/collab/work-plan.d.ts +105 -0
  201. package/dist/collab/work-plan.js +843 -0
  202. package/dist/continuity/context.d.ts +11 -2
  203. package/dist/continuity/context.js +100 -13
  204. package/dist/continuity/handoff-bundle.d.ts +296 -0
  205. package/dist/continuity/handoff-bundle.js +477 -0
  206. package/dist/continuity/index.d.ts +2 -0
  207. package/dist/continuity/index.js +1 -0
  208. package/dist/continuity/paths.js +10 -2
  209. package/dist/continuity/service.js +91 -15
  210. package/dist/continuity/task-pin.d.ts +24 -0
  211. package/dist/continuity/task-pin.js +590 -0
  212. package/dist/doctor.d.ts +1 -0
  213. package/dist/doctor.js +6 -0
  214. package/dist/experimental/index.d.ts +13 -0
  215. package/dist/experimental/index.js +25 -0
  216. package/dist/experimental/pi.d.ts +4 -0
  217. package/dist/experimental/pi.js +12 -0
  218. package/dist/hosts/claude/native.js +1 -1
  219. package/dist/hosts/codex/native.js +1 -1
  220. package/dist/hosts/codex/protocol.js +1 -1
  221. package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
  222. package/dist/hosts/integration/bootstrap-config.js +254 -0
  223. package/dist/hosts/integration/bundle-export.d.ts +44 -0
  224. package/dist/hosts/integration/bundle-export.js +366 -3
  225. package/dist/hosts/integration/bundle.js +3 -0
  226. package/dist/hosts/integration/hook.js +36 -9
  227. package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
  228. package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
  229. package/dist/hosts/integration/pappra-hook.mjs +61 -24749
  230. package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
  231. package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
  232. package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
  233. package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
  234. package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
  235. package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
  236. package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
  237. package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
  238. package/dist/hosts/integration/pi-native-events.d.ts +112 -0
  239. package/dist/hosts/integration/pi-native-events.js +348 -0
  240. package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
  241. package/dist/hosts/integration/pi-rollover-extension.js +431 -0
  242. package/dist/hosts/integration/recovery-config.d.ts +48 -0
  243. package/dist/hosts/integration/recovery-config.js +117 -0
  244. package/dist/hosts/integration/service.d.ts +6 -1
  245. package/dist/hosts/integration/service.js +485 -35
  246. package/dist/hosts/integration/support.d.ts +7 -0
  247. package/dist/hosts/integration/support.js +9 -1
  248. package/dist/hosts/integration/templates.d.ts +3 -0
  249. package/dist/hosts/integration/templates.js +26 -3
  250. package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
  251. package/dist/hosts/pi/managed-host-identity.js +40 -0
  252. package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
  253. package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
  254. package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
  255. package/dist/hosts/pi/managed-sdk-session.js +1639 -0
  256. package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
  257. package/dist/hosts/pi/managed-session-extension.js +84 -0
  258. package/dist/hosts/pi/managed-session.d.ts +177 -0
  259. package/dist/hosts/pi/managed-session.js +1557 -0
  260. package/dist/hosts/pi/protocol.js +1 -1
  261. package/dist/hosts/pi/query-result.js +1 -1
  262. package/dist/hosts/pi/resume-capability.d.ts +41 -0
  263. package/dist/hosts/pi/resume-capability.js +33 -0
  264. package/dist/hosts/pi/rpc.d.ts +182 -134
  265. package/dist/hosts/pi/rpc.js +230 -37
  266. package/dist/hosts/pilot.d.ts +97 -74
  267. package/dist/hosts/pilot.js +5 -1
  268. package/dist/hosts/version.d.ts +3 -0
  269. package/dist/hosts/version.js +18 -0
  270. package/dist/index.d.ts +0 -1
  271. package/dist/index.js +0 -1
  272. package/dist/infra/bounded-file.d.ts +19 -0
  273. package/dist/infra/bounded-file.js +66 -0
  274. package/dist/infra/fs.js +10 -2
  275. package/dist/infra/git.d.ts +29 -0
  276. package/dist/infra/git.js +45 -9
  277. package/dist/infra/json-property.d.ts +2 -0
  278. package/dist/infra/json-property.js +69 -0
  279. package/dist/infra/json.d.ts +4 -0
  280. package/dist/infra/json.js +24 -0
  281. package/dist/infra/owned-process-group.js +1 -1
  282. package/dist/infra/private-lock.d.ts +16 -0
  283. package/dist/infra/private-lock.js +148 -0
  284. package/dist/infra/private-state.d.ts +7 -0
  285. package/dist/infra/private-state.js +146 -0
  286. package/dist/infra/process-identity.d.ts +12 -0
  287. package/dist/infra/process-identity.js +142 -0
  288. package/dist/infra/validation.d.ts +5 -0
  289. package/dist/infra/validation.js +15 -0
  290. package/dist/init.d.ts +1 -0
  291. package/dist/init.js +6 -0
  292. package/dist/install/apply.d.ts +9 -1
  293. package/dist/install/apply.js +176 -12
  294. package/dist/install/dependencies.d.ts +38 -0
  295. package/dist/install/dependencies.js +282 -0
  296. package/dist/install/ownership.js +30 -1
  297. package/dist/install/references.d.ts +31 -0
  298. package/dist/install/references.js +208 -0
  299. package/dist/install/repair.d.ts +37 -0
  300. package/dist/install/repair.js +798 -0
  301. package/dist/install/reset-local.d.ts +16 -0
  302. package/dist/install/reset-local.js +304 -0
  303. package/dist/install/rollback.d.ts +3 -0
  304. package/dist/install/rollback.js +9 -3
  305. package/dist/install/runtime.d.ts +10 -6
  306. package/dist/install/runtime.js +299 -59
  307. package/dist/install/source-fingerprint.d.ts +2 -0
  308. package/dist/install/source-fingerprint.js +103 -0
  309. package/dist/knowledge/catalog.d.ts +1 -0
  310. package/dist/knowledge/catalog.js +143 -10
  311. package/dist/knowledge/journal.js +17 -7
  312. package/dist/knowledge/paths.js +2 -1
  313. package/dist/knowledge/refs.js +4 -0
  314. package/dist/knowledge/register-batch.d.ts +4 -0
  315. package/dist/knowledge/register-batch.js +225 -0
  316. package/dist/knowledge/relocate.d.ts +5 -0
  317. package/dist/knowledge/relocate.js +92 -0
  318. package/dist/knowledge/scan.js +103 -47
  319. package/dist/knowledge/service.d.ts +3 -0
  320. package/dist/knowledge/service.js +33 -1
  321. package/dist/knowledge/write.d.ts +4 -1
  322. package/dist/knowledge/write.js +1 -1
  323. package/dist/mcp/server.d.ts +4 -2
  324. package/dist/mcp/server.js +87 -136
  325. package/dist/schemas/collaboration-state-validation.d.ts +6 -0
  326. package/dist/schemas/collaboration-state-validation.js +342 -0
  327. package/dist/schemas/collaboration-state.d.ts +436 -0
  328. package/dist/schemas/collaboration-state.js +1 -0
  329. package/dist/schemas/collaboration.d.ts +335 -0
  330. package/dist/schemas/collaboration.js +272 -0
  331. package/dist/schemas/continuity.d.ts +8 -0
  332. package/dist/schemas/continuity.js +13 -3
  333. package/dist/schemas/doctor.d.ts +1 -0
  334. package/dist/schemas/doctor.js +1 -0
  335. package/dist/schemas/document.d.ts +61 -0
  336. package/dist/schemas/document.js +9 -0
  337. package/dist/schemas/errors.d.ts +1 -0
  338. package/dist/schemas/errors.js +1 -0
  339. package/dist/schemas/host.d.ts +24 -0
  340. package/dist/schemas/host.js +19 -0
  341. package/dist/schemas/install.d.ts +8 -2
  342. package/dist/schemas/install.js +11 -1
  343. package/dist/schemas/managed-pi-execution.d.ts +16 -0
  344. package/dist/schemas/managed-pi-execution.js +2 -0
  345. package/dist/schemas/outcome.d.ts +9 -5
  346. package/dist/workspace/config-file.js +1 -3
  347. package/dist/workspace/documents.js +12 -2
  348. package/dist/workspace/repositories.d.ts +7 -0
  349. package/dist/workspace/repositories.js +14 -5
  350. package/package.json +35 -35
  351. package/skills/pappra/SKILL.md +28 -7
  352. package/skills/pappra/references/collaboration.md +633 -0
  353. package/skills/pappra/references/environment.md +360 -0
  354. package/skills/pappra/references/hosts.md +20 -0
  355. package/skills/pappra/references/operations.md +36 -5
  356. package/dist/app/collab-from.d.ts +0 -35
  357. package/dist/app/collab-from.js +0 -84
@@ -1,9 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { mkdir, open, rename, rm } from 'node:fs/promises';
3
- import { dirname } from 'node:path';
2
+ import { mkdir, open, readdir, rename, rm, unlink } from 'node:fs/promises';
3
+ import { ensurePrivateState } from '../infra/private-state.js';
4
+ import { dirname, join } from 'node:path';
4
5
  import { createDiagnostic } from '../schemas/diagnostics.js';
5
6
  import { diagnosticCodes } from '../schemas/errors.js';
6
- import { DOCUMENT_CATALOG_SCHEMA, documentCatalogSchema, } from '../schemas/document.js';
7
+ import { DOCUMENT_CATALOG_SCHEMA, documentCatalogSchema, documentRecordFileSchema, } from '../schemas/document.js';
7
8
  import { failed, succeeded } from '../schemas/outcome.js';
8
9
  import { observePath, readFileBounded, readTextFileBounded, sha256Digest, } from '../infra/fs.js';
9
10
  import { ensureKnowledgePathBoundary, knowledgePaths, } from './paths.js';
@@ -34,6 +35,8 @@ function isUnknownRecord(value) {
34
35
  return typeof value === 'object' && value !== null;
35
36
  }
36
37
  async function readCatalogFile(context, scope, path) {
38
+ if (scope === 'shared')
39
+ return readSharedRecords(context, path);
37
40
  let observation;
38
41
  try {
39
42
  observation = await observePath(path);
@@ -156,6 +159,85 @@ async function readCatalogFile(context, scope, path) {
156
159
  valid: diagnostics.length === 0,
157
160
  };
158
161
  }
162
+ async function readSharedRecords(context, path) {
163
+ const diagnostics = [];
164
+ const invalid = (message) => ({
165
+ scope: 'shared',
166
+ path,
167
+ exists: true,
168
+ digest: null,
169
+ catalog: null,
170
+ diagnostics: [
171
+ catalogDiagnostic(diagnosticCodes.document_catalog_invalid, message, path, 'Inspect the per-document records; malformed state is preserved. Legacy catalog.json requires an explicit reset, not an automatic migration.'),
172
+ ],
173
+ valid: false,
174
+ });
175
+ try {
176
+ const legacy = await observePath(join(dirname(path), 'catalog.json'));
177
+ if (legacy.kind !== 'missing')
178
+ return invalid('Legacy shared catalog.json is unsupported; preserve or export it and explicitly reset the shared catalog before using per-document records.');
179
+ const observation = await observePath(path);
180
+ if (observation.kind === 'missing')
181
+ return {
182
+ scope: 'shared',
183
+ path,
184
+ exists: false,
185
+ digest: null,
186
+ catalog: null,
187
+ diagnostics: [],
188
+ valid: true,
189
+ };
190
+ if (observation.kind !== 'directory')
191
+ return invalid('Shared document records must be a real directory.');
192
+ const names = (await readdir(path))
193
+ .filter((name) => name !== '.DS_Store' && name !== 'Thumbs.db')
194
+ .sort();
195
+ if (names.length > 2048)
196
+ return invalid('Shared document record limit exceeded (2048).');
197
+ const records = [];
198
+ const digests = [];
199
+ const recordDigests = new Map();
200
+ let totalBytes = 0;
201
+ for (const name of names) {
202
+ if (/^[0-9a-f-]{36}\.json\.tmp-[0-9a-f-]{36}$/i.test(name))
203
+ continue;
204
+ if (!/^[0-9a-f-]{36}\.json$/i.test(name))
205
+ return invalid(`Unexpected file in shared records directory: ${name}`);
206
+ const file = join(path, name);
207
+ const read = await readTextFileBounded(file, DEFAULT_CATALOG_MAX_BYTES);
208
+ if (!read.ok)
209
+ return invalid(`Cannot safely read shared document record: ${name}`);
210
+ totalBytes += Buffer.byteLength(read.value.text);
211
+ if (totalBytes > DEFAULT_CATALOG_MAX_BYTES)
212
+ return invalid('Shared document records exceed the catalog byte budget.');
213
+ const parsed = documentRecordFileSchema.safeParse(JSON.parse(read.value.text));
214
+ if (!parsed.success ||
215
+ `${parsed.data.record.documentId}.json` !== name ||
216
+ parsed.data.workspaceId !== context.config.workspaceId ||
217
+ parsed.data.record.visibility !== 'shareable')
218
+ return invalid(`Invalid document identity, scope or schema in shared record: ${name}`);
219
+ records.push(parsed.data.record);
220
+ digests.push(`${name}:${read.value.digest}`);
221
+ recordDigests.set(parsed.data.record.documentId, read.value.digest);
222
+ }
223
+ return {
224
+ scope: 'shared',
225
+ path,
226
+ exists: true,
227
+ digest: sha256Digest(digests.join('\n')),
228
+ catalog: {
229
+ ...emptyCatalog(context, '1970-01-01T00:00:00.000Z'),
230
+ records,
231
+ },
232
+ diagnostics,
233
+ valid: true,
234
+ recordDigests,
235
+ };
236
+ }
237
+ catch (error) {
238
+ return invalid(`Cannot read shared document records: ${error instanceof Error ? error.message : String(error)}`);
239
+ }
240
+ }
159
241
  function combineDiagnostics(shared, privateState) {
160
242
  return [...shared.diagnostics, ...privateState.diagnostics];
161
243
  }
@@ -180,6 +262,8 @@ export async function loadKnowledgeCatalogs(context) {
180
262
  else {
181
263
  records.set(record.documentId, record);
182
264
  }
265
+ if (record.lifecycle === 'archived')
266
+ continue;
183
267
  const key = locationKey(record.location);
184
268
  const owner = locations.get(key);
185
269
  if (owner !== undefined && owner !== record.documentId) {
@@ -248,6 +332,58 @@ export async function writeCatalogFile(context, scope, catalog, expectedDigest)
248
332
  const path = scope === 'shared'
249
333
  ? knowledgePaths(context.root).sharedCatalog
250
334
  : knowledgePaths(context.root).privateCatalog;
335
+ const parsed = documentCatalogSchema.safeParse(catalog);
336
+ if (!parsed.success || catalog.workspaceId !== context.config.workspaceId)
337
+ return failed(diagnosticCodes.document_record_invalid, 'Refusing to write an invalid catalog or foreign workspace identity.');
338
+ if (scope === 'shared') {
339
+ try {
340
+ return await writeSharedRecords(context, path, parsed.data, expectedDigest);
341
+ }
342
+ catch (error) {
343
+ return failed(diagnosticCodes.fileUnreadable, `Cannot commit shared document records: ${error instanceof Error ? error.message : String(error)}`, {
344
+ nextAction: 'Inspect permissions and any individually committed records; rerun scan to reconcile without replacing document content.',
345
+ });
346
+ }
347
+ }
348
+ return writeCatalogBytes(context, path, Buffer.from(`${JSON.stringify(parsed.data, null, 2)}\n`), expectedDigest, scope);
349
+ }
350
+ async function writeSharedRecords(context, path, catalog, expectedDigest) {
351
+ const boundary = await ensureKnowledgePathBoundary(context.root, path);
352
+ if (!boundary.ok)
353
+ return boundary;
354
+ const before = await readSharedRecords(context, path);
355
+ if (!before.valid || before.digest !== expectedDigest)
356
+ return failed(diagnosticCodes.document_catalog_conflict, 'Shared document records changed before commit; reload before retrying.');
357
+ await mkdir(path, { recursive: true });
358
+ const previous = new Map(before.catalog?.records.map((record) => [record.documentId, record]) ?? []);
359
+ for (const record of catalog.records) {
360
+ const old = previous.get(record.documentId);
361
+ previous.delete(record.documentId);
362
+ if (old && JSON.stringify(old) === JSON.stringify(record))
363
+ continue;
364
+ const recordPath = join(path, `${record.documentId}.json`);
365
+ const bytes = Buffer.from(`${JSON.stringify({ schema: 'pappra/document-record-file/1', workspaceId: context.config.workspaceId, record }, null, 2)}\n`);
366
+ const written = await writeCatalogBytes(context, recordPath, bytes, before.recordDigests?.get(record.documentId) ?? null, 'shared');
367
+ if (!written.ok)
368
+ return written;
369
+ }
370
+ for (const record of previous.values()) {
371
+ const file = join(path, `${record.documentId}.json`);
372
+ const boundary = await ensureKnowledgePathBoundary(context.root, file);
373
+ if (!boundary.ok)
374
+ return boundary;
375
+ const current = await readTextFileBounded(file, DEFAULT_CATALOG_MAX_BYTES);
376
+ if (!current.ok ||
377
+ JSON.stringify(JSON.parse(current.value.text).record) !== JSON.stringify(record))
378
+ return failed(diagnosticCodes.document_catalog_conflict, 'Shared document record changed before removal.');
379
+ await unlink(file);
380
+ }
381
+ const after = await readSharedRecords(context, path);
382
+ return after.valid && after.digest !== null
383
+ ? succeeded({ path, digest: after.digest })
384
+ : failed(diagnosticCodes.document_catalog_conflict, 'Shared catalog commit is incomplete; inspect the per-document files and retry.');
385
+ }
386
+ async function writeCatalogBytes(context, path, bytes, expectedDigest, scope) {
251
387
  const boundary = await ensureKnowledgePathBoundary(context.root, path);
252
388
  if (!boundary.ok)
253
389
  return boundary;
@@ -255,14 +391,11 @@ export async function writeCatalogFile(context, scope, catalog, expectedDigest)
255
391
  if (current.kind !== 'missing' && current.kind !== 'file') {
256
392
  return failed(diagnosticCodes.document_path_unsafe, `Catalog path is not a regular file: ${path}`, { nextAction: 'Inspect the catalog path; it was not replaced.' });
257
393
  }
258
- if (catalog.workspaceId !== context.config.workspaceId) {
259
- return failed(diagnosticCodes.document_catalog_conflict, 'Refusing to write a catalog for a different workspace id.', { nextAction: 'Inspect the workspace and catalog identity.' });
260
- }
261
- const parsed = documentCatalogSchema.safeParse(catalog);
262
- if (!parsed.success) {
263
- return failed(diagnosticCodes.document_record_invalid, `Refusing to write an invalid catalog: ${parsed.error.issues[0]?.message ?? 'unknown issue'}`, { nextAction: 'Fix the record; no catalog bytes were written.' });
394
+ if (scope === 'private') {
395
+ const prepared = await ensurePrivateState(context.root);
396
+ if (!prepared.ok)
397
+ return prepared;
264
398
  }
265
- const bytes = Buffer.from(`${JSON.stringify(parsed.data, null, 2)}\n`);
266
399
  const temporary = `${path}.tmp-${randomUUID()}`;
267
400
  let handle;
268
401
  try {
@@ -5,6 +5,9 @@ import { diagnosticCodes } from '../schemas/errors.js';
5
5
  import { errorMessage, failed, succeeded } from '../schemas/outcome.js';
6
6
  import { knowledgeJournalSchema, knowledgeLockSchema, } from '../schemas/document.js';
7
7
  import { readTextFileBounded } from '../infra/fs.js';
8
+ import { ensurePrivateState } from '../infra/private-state.js';
9
+ import { publishPrivateLock } from '../infra/private-lock.js';
10
+ import { captureProcessIdentity } from '../infra/process-identity.js';
8
11
  import { ensureKnowledgePathBoundary, knowledgePaths } from './paths.js';
9
12
  function lockFailure(path, status, message) {
10
13
  return { ok: false, status, lock: null, path, message };
@@ -15,6 +18,9 @@ export async function acquireKnowledgeLock(context, operation) {
15
18
  if (!boundary.ok) {
16
19
  return lockFailure(path, 'failed', boundary.message);
17
20
  }
21
+ const prepared = await ensurePrivateState(context.root);
22
+ if (!prepared.ok)
23
+ return lockFailure(path, 'failed', prepared.message);
18
24
  try {
19
25
  await mkdir(dirname(path), { recursive: true });
20
26
  }
@@ -26,19 +32,20 @@ export async function acquireKnowledgeLock(context, operation) {
26
32
  token: randomUUID(),
27
33
  operation,
28
34
  pid: process.pid,
35
+ ...(captureProcessIdentity() === null
36
+ ? {}
37
+ : { process_identity: captureProcessIdentity() }),
29
38
  createdAt: new Date().toISOString(),
30
39
  };
31
- let handle;
32
40
  try {
33
- handle = await open(path, 'wx', 0o600);
34
- await handle.writeFile(Buffer.from(`${JSON.stringify(lock, null, 2)}\n`));
35
- await handle.sync();
36
- await handle.close();
37
- handle = undefined;
41
+ await publishPrivateLock(path, Buffer.from(`${JSON.stringify(lock, null, 2)}\n`), async () => {
42
+ const current = await ensureKnowledgePathBoundary(context.root, path);
43
+ if (!current.ok)
44
+ throw new Error(current.message);
45
+ });
38
46
  return { ok: true, status: 'acquired', lock, path, message: null };
39
47
  }
40
48
  catch (error) {
41
- await handle?.close().catch(() => undefined);
42
49
  const code = typeof error === 'object' &&
43
50
  error !== null &&
44
51
  'code' in error &&
@@ -88,6 +95,9 @@ export async function writeKnowledgeJournal(context, journal) {
88
95
  const boundary = await ensureKnowledgePathBoundary(context.root, path);
89
96
  if (!boundary.ok)
90
97
  return boundary;
98
+ const prepared = await ensurePrivateState(context.root);
99
+ if (!prepared.ok)
100
+ return prepared;
91
101
  const temporary = `${path}.tmp-${randomUUID()}`;
92
102
  const bytes = Buffer.from(`${JSON.stringify(parsed.data, null, 2)}\n`);
93
103
  let handle;
@@ -6,7 +6,7 @@ import { observePath, readFileBounded, resolveRealpath } from '../infra/fs.js';
6
6
  export function knowledgePaths(root) {
7
7
  const localDir = join(root, '.pappra', 'local', 'knowledge');
8
8
  return {
9
- sharedCatalog: join(root, '.pappra', 'knowledge', 'catalog.json'),
9
+ sharedCatalog: join(root, '.pappra', 'knowledge', 'records'),
10
10
  privateCatalog: join(localDir, 'catalog.json'),
11
11
  localDir,
12
12
  lockPath: join(localDir, 'apply.lock'),
@@ -17,6 +17,7 @@ const managedStatePrefixes = [
17
17
  '.pappra/local',
18
18
  '.pappra/generated',
19
19
  '.pappra/knowledge/catalog.json',
20
+ '.pappra/knowledge/records',
20
21
  ];
21
22
  /**
22
23
  * Rejects a path if its final component or any existing ancestor resolves
@@ -10,6 +10,8 @@ export function findRecord(catalogs, documentId) {
10
10
  }
11
11
  export async function findCanonicalAlias(context, catalogs, target, ignoreDocumentId = null) {
12
12
  for (const record of catalogs.records.values()) {
13
+ if (record.lifecycle === 'archived')
14
+ continue;
13
15
  if (record.documentId === ignoreDocumentId)
14
16
  continue;
15
17
  const resolved = await resolveKnowledgeLocation(context, record.location);
@@ -24,6 +26,8 @@ export function findRecordByLocation(catalogs, key) {
24
26
  if (catalogs.locationConflicts.has(key))
25
27
  return null;
26
28
  for (const record of catalogs.records.values()) {
29
+ if (record.lifecycle === 'archived')
30
+ continue;
27
31
  const recordKey = record.location.kind === 'workspace'
28
32
  ? `workspace:${record.location.path}`
29
33
  : `source:${record.location.sourceId}:${record.location.path}`;
@@ -0,0 +1,4 @@
1
+ import { type KnowledgeContext } from './context.js';
2
+ import type { RegisterDocumentInput, RegisterDocumentResult } from './types.js';
3
+ /** One context, catalog snapshot and lock for a bounded discovery batch. */
4
+ export declare function registerDocumentBatch(context: KnowledgeContext, inputs: readonly RegisterDocumentInput[]): Promise<readonly RegisterDocumentResult[]>;
@@ -0,0 +1,225 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createDiagnostic } from '../schemas/diagnostics.js';
3
+ import { diagnosticCodes } from '../schemas/errors.js';
4
+ import { samePath } from '../schemas/paths.js';
5
+ import { observePath } from '../infra/fs.js';
6
+ import { loadKnowledgeContext, sameRootIdentity, } from './context.js';
7
+ import { catalogDigestFor, catalogFor, catalogScopeFor, loadKnowledgeCatalogs, locationKey, writeCatalogFile, } from './catalog.js';
8
+ import { acquireKnowledgeLock, releaseKnowledgeLock, writeKnowledgeJournal, } from './journal.js';
9
+ import { readKnowledgeFile, resolveKnowledgeLocation } from './paths.js';
10
+ import { validateSources } from './refs.js';
11
+ import { validateAndBuildRecord } from './write.js';
12
+ /** One context, catalog snapshot and lock for a bounded discovery batch. */
13
+ export async function registerDocumentBatch(context, inputs) {
14
+ if (inputs.length === 0)
15
+ return [];
16
+ const fail = (message, diagnostics = []) => inputs.map(() => ({
17
+ ok: false,
18
+ status: 'failed',
19
+ record: null,
20
+ diagnostics: [
21
+ ...diagnostics,
22
+ createDiagnostic({
23
+ code: diagnosticCodes.document_write_conflict,
24
+ severity: 'error',
25
+ message,
26
+ source: context.configPath,
27
+ }),
28
+ ],
29
+ }));
30
+ const held = await acquireKnowledgeLock(context, 'write-document');
31
+ if (!held.ok || !held.lock)
32
+ return fail(held.message ?? 'Knowledge lock is unavailable.');
33
+ try {
34
+ const freshContext = await loadKnowledgeContext(context.root);
35
+ if (!freshContext.ok ||
36
+ !freshContext.context ||
37
+ !sameRootIdentity(freshContext.context, context.rootIdentity))
38
+ return fail('Workspace identity changed before batch registration.', freshContext.diagnostics);
39
+ context = freshContext.context;
40
+ const catalogs = await loadKnowledgeCatalogs(context);
41
+ if (!catalogs.ok)
42
+ return fail('Catalog conflicts prevent registration.', catalogs.diagnostics);
43
+ const records = new Map(catalogs.records);
44
+ const resolvedRecords = [];
45
+ for (const record of records.values()) {
46
+ if (record.lifecycle === 'archived')
47
+ continue;
48
+ const resolved = await resolveKnowledgeLocation(context, record.location);
49
+ const observation = resolved.ok
50
+ ? await observePath(resolved.value.absolutePath).catch(() => null)
51
+ : null;
52
+ resolvedRecords.push({
53
+ record,
54
+ canonical: resolved.ok && observation?.kind === 'file'
55
+ ? resolved.value.canonicalPath
56
+ : null,
57
+ missing: (!resolved.ok &&
58
+ resolved.code === diagnosticCodes.document_not_found) ||
59
+ observation?.kind === 'missing',
60
+ });
61
+ }
62
+ const results = [];
63
+ for (const input of inputs) {
64
+ const diagnostics = [];
65
+ const conflict = (message) => ({
66
+ ok: false,
67
+ status: 'conflict',
68
+ record: null,
69
+ diagnostics: [
70
+ createDiagnostic({
71
+ code: diagnosticCodes.document_location_conflict,
72
+ severity: 'error',
73
+ message,
74
+ source: input.location.path,
75
+ }),
76
+ ],
77
+ });
78
+ const resolved = await resolveKnowledgeLocation(context, input.location);
79
+ if (!resolved.ok) {
80
+ results.push(conflict(resolved.message));
81
+ continue;
82
+ }
83
+ const file = await readKnowledgeFile(resolved.value.absolutePath, 8 * 1024 * 1024);
84
+ if (!file.ok) {
85
+ results.push(conflict(file.message));
86
+ continue;
87
+ }
88
+ const existing = [...records.values()].find((record) => record.lifecycle !== 'archived' &&
89
+ locationKey(record.location) === locationKey(input.location));
90
+ if (existing) {
91
+ results.push(existing.digest === file.value.digest &&
92
+ (input.documentId === undefined ||
93
+ input.documentId === existing.documentId)
94
+ ? { ok: true, status: 'existing', record: existing, diagnostics }
95
+ : conflict('An active registration exists with different content; review and update it explicitly.'));
96
+ continue;
97
+ }
98
+ const aliases = resolvedRecords.filter((entry) => entry.canonical !== null &&
99
+ samePath(entry.canonical, resolved.value.canonicalPath));
100
+ const moved = aliases.length === 1 ? aliases[0] : undefined;
101
+ const byDigest = resolvedRecords.filter((entry) => entry.missing && entry.record.digest === file.value.digest);
102
+ const previous = moved ?? (byDigest.length === 1 ? byDigest[0] : undefined);
103
+ if (aliases.length > 1 || byDigest.length > 1) {
104
+ results.push(conflict('Several registrations could identify this path; use docs relocate with an explicit document id.'));
105
+ continue;
106
+ }
107
+ if (previous) {
108
+ if (previous.record.digest !== file.value.digest ||
109
+ (input.documentId !== undefined &&
110
+ input.documentId !== previous.record.documentId)) {
111
+ results.push(conflict('The canonical document changed; review it before relocation.'));
112
+ continue;
113
+ }
114
+ const record = {
115
+ ...previous.record,
116
+ location: resolved.value.location,
117
+ };
118
+ records.set(record.documentId, record);
119
+ previous.record = record;
120
+ previous.canonical = resolved.value.canonicalPath;
121
+ previous.missing = false;
122
+ results.push({ ok: true, status: 'existing', record, diagnostics });
123
+ continue;
124
+ }
125
+ const built = await validateAndBuildRecord(context, input, resolved.value);
126
+ if (!built.ok) {
127
+ results.push(conflict(built.message));
128
+ continue;
129
+ }
130
+ const record = { ...built.value, digest: file.value.digest };
131
+ if (records.has(record.documentId)) {
132
+ results.push(conflict('Document id is already registered; batch discovery never replaces another document identity.'));
133
+ continue;
134
+ }
135
+ diagnostics.push(...validateSources(context, catalogs, record.documentId, record.visibility, record.sources));
136
+ if (diagnostics.some((entry) => entry.severity === 'error')) {
137
+ results.push({
138
+ ok: false,
139
+ status: 'failed',
140
+ record: null,
141
+ diagnostics,
142
+ });
143
+ continue;
144
+ }
145
+ records.set(record.documentId, record);
146
+ resolvedRecords.push({
147
+ record,
148
+ canonical: resolved.value.canonicalPath,
149
+ missing: false,
150
+ });
151
+ results.push({ ok: true, status: 'registered', record, diagnostics });
152
+ }
153
+ const changedScopes = ['shared', 'private'].filter((scope) => {
154
+ const previous = catalogFor(catalogs, scope, context);
155
+ const next = [...records.values()]
156
+ .filter((record) => catalogScopeFor(record) === scope)
157
+ .sort((a, b) => a.documentId < b.documentId
158
+ ? -1
159
+ : a.documentId > b.documentId
160
+ ? 1
161
+ : 0);
162
+ return JSON.stringify(previous.records) !== JSON.stringify(next);
163
+ });
164
+ if (changedScopes.length === 0)
165
+ return results;
166
+ const timestamp = new Date().toISOString();
167
+ const journal = {
168
+ schema: 'pappra/knowledge-journal/1',
169
+ operationId: randomUUID(),
170
+ operation: 'write-document',
171
+ stage: 'prepared',
172
+ documentId: null,
173
+ targetPath: null,
174
+ catalogScope: null,
175
+ expectedDigest: null,
176
+ fileDigest: null,
177
+ catalogDigest: catalogs.catalogDigest,
178
+ createdAt: timestamp,
179
+ updatedAt: timestamp,
180
+ residualPaths: [],
181
+ detail: `Batch registration of ${inputs.length} discovered documents. Document content is preserved; rerun scan after interruption to reconcile committed metadata.`,
182
+ };
183
+ const prepared = await writeKnowledgeJournal(context, journal);
184
+ if (!prepared.ok)
185
+ return fail(`Cannot prepare batch registration journal: ${prepared.message}`);
186
+ for (const scope of changedScopes) {
187
+ const previous = catalogFor(catalogs, scope, context);
188
+ const next = [...records.values()]
189
+ .filter((record) => catalogScopeFor(record) === scope)
190
+ .sort((a, b) => a.documentId < b.documentId
191
+ ? -1
192
+ : a.documentId > b.documentId
193
+ ? 1
194
+ : 0);
195
+ if (JSON.stringify(previous.records) === JSON.stringify(next))
196
+ continue;
197
+ const written = await writeCatalogFile(context, scope, { ...previous, records: next }, catalogDigestFor(catalogs, scope));
198
+ if (!written.ok) {
199
+ await writeKnowledgeJournal(context, {
200
+ ...journal,
201
+ stage: 'failed',
202
+ updatedAt: new Date().toISOString(),
203
+ residualPaths: [
204
+ catalogs.paths.sharedCatalog,
205
+ catalogs.paths.privateCatalog,
206
+ ],
207
+ });
208
+ return fail(`Batch registration was interrupted: ${written.message}. Rerun scan to reconcile already committed records; document files were not changed.`);
209
+ }
210
+ }
211
+ const completed = await writeKnowledgeJournal(context, {
212
+ ...journal,
213
+ stage: 'complete',
214
+ updatedAt: new Date().toISOString(),
215
+ });
216
+ if (!completed.ok)
217
+ return fail(`Registrations committed but their completion journal could not be saved: ${completed.message}. Rerun scan before treating coverage as complete.`);
218
+ return results;
219
+ }
220
+ finally {
221
+ const released = await releaseKnowledgeLock(held.lock, held.path);
222
+ if (!released.ok)
223
+ return fail('Registration may have committed, but its lock could not be released; inspect the catalog before retrying.');
224
+ }
225
+ }
@@ -0,0 +1,5 @@
1
+ import type { DocumentLocation } from '../schemas/document.js';
2
+ import type { KnowledgeContext } from './context.js';
3
+ import { type CatalogScope } from './catalog.js';
4
+ import type { RegisterDocumentResult } from './types.js';
5
+ export declare function relocateOrForgetDocument(context: KnowledgeContext, documentId: string, location: DocumentLocation | null, scope?: CatalogScope): Promise<RegisterDocumentResult>;
@@ -0,0 +1,92 @@
1
+ import { createDiagnostic } from '../schemas/diagnostics.js';
2
+ import { diagnosticCodes } from '../schemas/errors.js';
3
+ import { loadKnowledgeContext, sameRootIdentity } from './context.js';
4
+ import { catalogDigestFor, catalogFor, loadKnowledgeCatalogs, recordScope, recordsForCatalog, setCatalogRecord, writeCatalogFile, } from './catalog.js';
5
+ import { acquireKnowledgeLock, releaseKnowledgeLock } from './journal.js';
6
+ import { findCanonicalAlias, findRecord, findRegisteredReferences, } from './refs.js';
7
+ import { readKnowledgeFile, resolveKnowledgeLocation } from './paths.js';
8
+ export async function relocateOrForgetDocument(context, documentId, location, scope) {
9
+ const failure = (message) => ({
10
+ ok: false,
11
+ status: 'conflict',
12
+ record: null,
13
+ diagnostics: [
14
+ createDiagnostic({
15
+ code: diagnosticCodes.document_write_conflict,
16
+ severity: 'error',
17
+ message,
18
+ source: context.configPath,
19
+ hint: 'No document content is changed by relocate or forget.',
20
+ }),
21
+ ],
22
+ });
23
+ if (scope !== undefined &&
24
+ (!['shared', 'private'].includes(scope) || location !== null))
25
+ return failure('An explicit shared/private scope is accepted only for forget.');
26
+ const held = await acquireKnowledgeLock(context, 'write-document');
27
+ if (!held.ok || !held.lock)
28
+ return failure(held.message ?? 'Knowledge state is locked.');
29
+ try {
30
+ const loaded = await loadKnowledgeContext(context.root);
31
+ if (!loaded.ok ||
32
+ !loaded.context ||
33
+ !sameRootIdentity(loaded.context, context.rootIdentity))
34
+ return failure('Workspace identity changed.');
35
+ context = loaded.context;
36
+ const catalogs = await loadKnowledgeCatalogs(context);
37
+ if (!catalogs.shared.valid ||
38
+ !catalogs.private.valid ||
39
+ (scope === undefined && catalogs.idConflicts.size > 0))
40
+ return {
41
+ ok: false,
42
+ status: 'conflict',
43
+ record: null,
44
+ diagnostics: [...catalogs.diagnostics],
45
+ };
46
+ const scoped = scope === undefined
47
+ ? null
48
+ : catalogFor(catalogs, scope, context).records.filter((entry) => entry.documentId === documentId);
49
+ const record = scoped === null
50
+ ? findRecord(catalogs, documentId)
51
+ : scoped.length === 1
52
+ ? scoped[0]
53
+ : null;
54
+ if (!record)
55
+ return failure('Document id is not registered or is ambiguous.');
56
+ if (record.taskId !== null)
57
+ return failure('Task-linked records require explicit task ownership review before relocating or forgetting.');
58
+ if (location === null) {
59
+ const retainedInOtherScope = scope === 'private' &&
60
+ catalogFor(catalogs, 'shared', context).records.some((entry) => entry.documentId === documentId);
61
+ if (!retainedInOtherScope &&
62
+ findRegisteredReferences(catalogs, documentId).length > 0)
63
+ return failure('Other documents reference this id; update their references before forgetting it.');
64
+ const removedScope = scope ?? recordScope(catalogs, record);
65
+ const catalog = catalogFor(catalogs, removedScope, context);
66
+ const written = await writeCatalogFile(context, removedScope, { ...catalog, records: recordsForCatalog(catalog, documentId, null) }, catalogDigestFor(catalogs, removedScope));
67
+ return written.ok
68
+ ? { ok: true, status: 'existing', record, diagnostics: [] }
69
+ : failure(written.message);
70
+ }
71
+ const resolved = await resolveKnowledgeLocation(context, location);
72
+ if (!resolved.ok)
73
+ return failure(resolved.message);
74
+ const file = await readKnowledgeFile(resolved.value.absolutePath, 8 * 1024 * 1024);
75
+ if (!file.ok || file.value.digest !== record.digest)
76
+ return failure('Relocation target does not match the registered content digest.');
77
+ const alias = await findCanonicalAlias(context, catalogs, resolved.value, record.documentId);
78
+ if (alias)
79
+ return failure('Relocation target is owned by another active document.');
80
+ const next = { ...record, location: resolved.value.location };
81
+ const update = setCatalogRecord(context, catalogs, next);
82
+ const written = await writeCatalogFile(context, update.scope, update.catalog, catalogDigestFor(catalogs, update.scope));
83
+ return written.ok
84
+ ? { ok: true, status: 'existing', record: next, diagnostics: [] }
85
+ : failure(written.message);
86
+ }
87
+ finally {
88
+ const released = await releaseKnowledgeLock(held.lock, held.path);
89
+ if (!released.ok)
90
+ return failure('Metadata may have committed, but its lock could not be released; inspect the catalog before retrying.');
91
+ }
92
+ }