@mnstry/atelier 0.2.0-alpha.1 → 0.2.0-alpha.10

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 (414) hide show
  1. package/CHANGELOG.md +372 -0
  2. package/README.md +402 -205
  3. package/SECURITY.md +6 -1
  4. package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
  5. package/contracts/atelier-coauthor.v1.schema.json +163 -0
  6. package/contracts/atelier-decision-request.v1.schema.json +476 -0
  7. package/contracts/atelier-decision-result.v1.schema.json +500 -0
  8. package/contracts/atelier-guide.v1.schema.json +227 -0
  9. package/contracts/atelier-intake.v1.schema.json +151 -0
  10. package/contracts/atelier-migration.v2.schema.json +61 -0
  11. package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
  12. package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
  13. package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
  14. package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
  15. package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
  16. package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
  17. package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
  18. package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
  19. package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
  20. package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
  21. package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
  22. package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
  23. package/contracts/atelier-presentation.v1.schema.json +1293 -0
  24. package/contracts/atelier-repository-observation.v1.schema.json +163 -0
  25. package/contracts/atelier-review.v1.schema.json +1601 -0
  26. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  27. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  28. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  29. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  30. package/contracts/public-api-baseline.json +57 -0
  31. package/docs/assurance-controls.md +49 -0
  32. package/docs/atelier-runtime.md +28 -2
  33. package/docs/atelier-sync.md +172 -0
  34. package/docs/blocks/audience-visibility.md +9 -0
  35. package/docs/blocks/claims.md +61 -0
  36. package/docs/blocks/conformance-admission.md +15 -0
  37. package/docs/blocks/will-not-do.md +18 -0
  38. package/docs/coauthor-session.md +57 -0
  39. package/docs/connected-composition.md +75 -0
  40. package/docs/continuity.md +3 -4
  41. package/docs/decisions.md +167 -0
  42. package/docs/design.md +167 -0
  43. package/docs/exact-upgrades.md +159 -0
  44. package/docs/extension-pack-lifecycle.md +33 -0
  45. package/docs/guided-upgrades.md +99 -0
  46. package/docs/install.md +99 -16
  47. package/docs/intake-and-guides.md +65 -0
  48. package/docs/integration-and-review-plan.md +409 -0
  49. package/docs/integration-contract-decisions.md +22 -0
  50. package/docs/knowledge-graph.md +8 -4
  51. package/docs/local-review.md +84 -0
  52. package/docs/local-services.md +328 -0
  53. package/docs/local-state.md +107 -0
  54. package/docs/obsidian-contract.md +690 -0
  55. package/docs/obsidian.md +423 -0
  56. package/docs/portable-presentation.md +328 -0
  57. package/docs/presentation-consumer-boundaries.md +75 -0
  58. package/docs/presentation-review-disposition.md +47 -0
  59. package/docs/project-command-inventory.md +38 -0
  60. package/docs/project-options.md +53 -0
  61. package/docs/release-engineering.md +174 -12
  62. package/docs/repo-boundary-guard.md +12 -2
  63. package/docs/review-portability.md +55 -0
  64. package/docs/skill-steward.md +156 -0
  65. package/docs/upgrade.md +146 -5
  66. package/docs/vault-service.md +292 -0
  67. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  68. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  69. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  70. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  71. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  72. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  73. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  74. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  75. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  76. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  77. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  78. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  79. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  80. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  81. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  82. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  83. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  84. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  85. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  86. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  87. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  88. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  89. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  90. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  91. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  92. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  93. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  94. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  95. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  96. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  97. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  98. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  99. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  100. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  101. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  102. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  103. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  104. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  105. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  106. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  107. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  108. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  109. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  110. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  111. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  112. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  113. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  114. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  115. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  116. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  117. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  118. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  119. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  120. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  121. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  122. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  123. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  125. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  127. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  128. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  129. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  130. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  131. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  132. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  133. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  134. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  135. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  136. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  137. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  138. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  139. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  140. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  141. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  142. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  143. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  144. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  145. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  146. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  147. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  148. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  149. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  150. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  151. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  152. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  153. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  154. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  159. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  160. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  161. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  162. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  163. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  164. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  165. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  166. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  167. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  168. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  169. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  170. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  171. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  172. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  173. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  174. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  175. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  176. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  177. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  178. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  179. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  181. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  182. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  183. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  186. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  187. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  188. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  189. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  190. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  191. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  192. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  193. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  194. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  195. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  196. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  197. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  198. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  199. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  200. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  201. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  202. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  203. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  204. package/fixtures/obsidian/edits/workspace.json +65 -0
  205. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  206. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  207. package/fixtures/obsidian/materialization/workspace.json +54 -0
  208. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  209. package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
  210. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
  211. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
  212. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
  213. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
  214. package/fixtures/ui/presentation/pane.v1.json +7 -0
  215. package/fixtures/ui/presentation/reference.v1.json +293 -0
  216. package/package.json +86 -10
  217. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  218. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  219. package/skills/claude/atelier-local-service/SKILL.md +47 -0
  220. package/skills/claude/atelier-open/SKILL.md +11 -5
  221. package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
  222. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  223. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  224. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  225. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  226. package/skills/codex/atelier-local-service/SKILL.md +47 -0
  227. package/skills/codex/atelier-open/SKILL.md +11 -5
  228. package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
  229. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  230. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  231. package/src/access/preview-enforcer.mjs +84 -0
  232. package/src/boundary/content-rules.mjs +283 -20
  233. package/src/boundary/policy.mjs +213 -91
  234. package/src/cli/execute-command.mjs +36 -0
  235. package/src/cli/project-options.mjs +58 -0
  236. package/src/cli/run.mjs +78 -10
  237. package/src/coauthor/session.mjs +161 -0
  238. package/src/coauthor/store.mjs +173 -0
  239. package/src/collaboration/event-ledger.mjs +348 -0
  240. package/src/collaboration/index.mjs +17 -0
  241. package/src/collaboration/inspection-bundle.mjs +223 -0
  242. package/src/collaboration/proposals.mjs +265 -65
  243. package/src/collaboration/review-contracts.mjs +25 -0
  244. package/src/collaboration/review-store.mjs +305 -0
  245. package/src/commands/attestation.mjs +20 -6
  246. package/src/commands/coauthor.mjs +26 -0
  247. package/src/commands/config.mjs +6 -1
  248. package/src/commands/disclosure.mjs +133 -0
  249. package/src/commands/distribution.mjs +2 -1
  250. package/src/commands/extension-pack.mjs +17 -9
  251. package/src/commands/init.mjs +46 -6
  252. package/src/commands/obsidian.mjs +371 -0
  253. package/src/commands/review.mjs +147 -0
  254. package/src/commands/server.mjs +1 -4
  255. package/src/commands/setup.mjs +51 -14
  256. package/src/commands/skills.mjs +199 -0
  257. package/src/commands/sync.mjs +107 -0
  258. package/src/composition/registry.mjs +27 -0
  259. package/src/composition/wire.mjs +122 -0
  260. package/src/contracts/corpus.mjs +35 -0
  261. package/src/decisions/contracts.d.mts +96 -0
  262. package/src/decisions/contracts.mjs +321 -0
  263. package/src/disclosure/content-scan.mjs +206 -0
  264. package/src/egress/check.mjs +7 -38
  265. package/src/egress/forbidden-egress.mjs +32 -18
  266. package/src/export/atelier-export-contract.mjs +16 -1
  267. package/src/extension-packs/lifecycle.mjs +154 -0
  268. package/src/graph/graph.mjs +176 -316
  269. package/src/graph/knowledge-graph.mjs +643 -43
  270. package/src/guides/contracts.mjs +54 -0
  271. package/src/harness/context-client.mjs +9 -1
  272. package/src/index.mjs +41 -0
  273. package/src/intake/store.mjs +119 -0
  274. package/src/preview/channel.mjs +89 -0
  275. package/src/project/config.mjs +112 -52
  276. package/src/project/durable-state.mjs +161 -0
  277. package/src/project/file-class.mjs +180 -0
  278. package/src/project/package-root.mjs +10 -0
  279. package/src/project/path-match.mjs +38 -15
  280. package/src/project/private-state.mjs +120 -0
  281. package/src/projection/obsidian/contracts.mjs +571 -0
  282. package/src/projection/obsidian/edits/align.mjs +169 -0
  283. package/src/projection/obsidian/edits/apply.mjs +889 -0
  284. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  285. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  286. package/src/projection/obsidian/edits/index.mjs +19 -0
  287. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  288. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  289. package/src/projection/obsidian/edits/observe.mjs +123 -0
  290. package/src/projection/obsidian/edits/policy.mjs +123 -0
  291. package/src/projection/obsidian/edits/regions.mjs +648 -0
  292. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  293. package/src/projection/obsidian/materialize/index.mjs +7 -0
  294. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  295. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  296. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  297. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  298. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  299. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  300. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  301. package/src/projection/obsidian/proposals/index.mjs +23 -0
  302. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  303. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  304. package/src/projection/obsidian/proposals/router.mjs +168 -0
  305. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  306. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  307. package/src/projection/obsidian/publication/index.mjs +5 -0
  308. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  309. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  310. package/src/projection/obsidian/publication/transport.mjs +160 -0
  311. package/src/projection/obsidian/recovery/index.mjs +6 -0
  312. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  313. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  314. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  315. package/src/projection/obsidian/recovery/store.mjs +234 -0
  316. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  317. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  318. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  319. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  320. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  321. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  322. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  323. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  324. package/src/projection/project.mjs +28 -19
  325. package/src/readiness-protocols/evidence.mjs +282 -0
  326. package/src/readiness-protocols/runtime.mjs +4 -3
  327. package/src/readiness-protocols/source-read.mjs +39 -0
  328. package/src/runtime/git-adapter.mjs +189 -0
  329. package/src/runtime/local-state.mjs +439 -0
  330. package/src/runtime/obsidian/app-capability.mjs +129 -0
  331. package/src/runtime/obsidian/app-production-seams.mjs +105 -0
  332. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  333. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  334. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  335. package/src/runtime/obsidian/contributions.mjs +27 -0
  336. package/src/runtime/obsidian/documents.mjs +37 -0
  337. package/src/runtime/obsidian/enablement.mjs +53 -0
  338. package/src/runtime/obsidian/engine.mjs +532 -0
  339. package/src/runtime/obsidian/errors.mjs +14 -0
  340. package/src/runtime/obsidian/extension-points.mjs +103 -0
  341. package/src/runtime/obsidian/index.mjs +34 -0
  342. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  343. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  344. package/src/runtime/obsidian/observation.mjs +158 -0
  345. package/src/runtime/obsidian/opening.mjs +263 -0
  346. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  347. package/src/runtime/obsidian/pipeline.mjs +122 -0
  348. package/src/runtime/obsidian/private-lock.mjs +156 -0
  349. package/src/runtime/obsidian/process-identity.mjs +58 -0
  350. package/src/runtime/obsidian/service-client.mjs +66 -0
  351. package/src/runtime/obsidian/service-entry-path.mjs +9 -0
  352. package/src/runtime/obsidian/service-main.mjs +91 -0
  353. package/src/runtime/obsidian/service-record.mjs +169 -0
  354. package/src/runtime/obsidian/service-server.mjs +121 -0
  355. package/src/runtime/obsidian/service.mjs +216 -0
  356. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  357. package/src/runtime/obsidian/state-store.mjs +180 -0
  358. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  359. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  360. package/src/runtime/obsidian/watchers.mjs +36 -0
  361. package/src/runtime/repository-observation.mjs +491 -0
  362. package/src/runtime/supervisor.mjs +803 -0
  363. package/src/server/local-sidecar.mjs +125 -59
  364. package/src/server/security.mjs +89 -4
  365. package/src/server/server.mjs +4 -2
  366. package/src/skills/steward.mjs +780 -0
  367. package/src/support/feedback-report.mjs +4 -3
  368. package/src/ui/presentation/browser.mjs +278 -0
  369. package/src/ui/presentation/contract.mjs +116 -0
  370. package/src/ui/presentation/index.mjs +7 -0
  371. package/src/ui/presentation/native.mjs +153 -0
  372. package/src/ui/presentation/proof.mjs +57 -0
  373. package/src/ui/presentation/schema-check.mjs +30 -0
  374. package/src/ui/presentation/schema.generated.mjs +1371 -0
  375. package/src/ui/presentation/state.mjs +43 -0
  376. package/src/ui/presentation/styles.mjs +67 -0
  377. package/src/ui/presentation/tokens.mjs +74 -0
  378. package/src/ui/presentation/web.mjs +85 -0
  379. package/src/ui/review-page.mjs +32 -0
  380. package/src/upgrade/explanation.mjs +33 -0
  381. package/src/upgrade/provenance.mjs +338 -0
  382. package/src/upgrade/transaction-files.mjs +103 -0
  383. package/src/upgrade/transaction.mjs +475 -0
  384. package/src/upgrade/upgrade.mjs +65 -29
  385. package/src/vault/hosts.mjs +21 -0
  386. package/src/vault/identity.mjs +19 -0
  387. package/src/vault/index.mjs +7 -0
  388. package/src/vault/interface.mjs +11 -0
  389. package/src/vault/metadata.mjs +31 -0
  390. package/src/vault/privacy.mjs +148 -0
  391. package/src/vault/probe.mjs +89 -0
  392. package/src/vault/service.mjs +131 -0
  393. package/src/vault/source.mjs +45 -0
  394. package/src/vault/storage.mjs +23 -0
  395. package/templates/distribution-workspace/.claude/launch.json +6 -2
  396. package/templates/distribution-workspace/gitignore +1 -0
  397. package/templates/distribution-workspace/project/README.md +2 -2
  398. package/templates/external-project-workspace/README.md +45 -0
  399. package/templates/external-project-workspace/answers.example.json +4 -0
  400. package/templates/external-project-workspace/atelier.project.json +40 -0
  401. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  402. package/templates/external-project-workspace/gitignore +8 -0
  403. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  404. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  405. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  406. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  407. package/templates/external-project-workspace/source/README.md +11 -0
  408. package/templates/launch.json +6 -2
  409. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  410. package/templates/private-domain-workspace/domain/README.md +2 -2
  411. package/templates/private-domain-workspace/gitignore +1 -0
  412. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  413. package/templates/shared-project-workspace/gitignore +1 -0
  414. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,160 @@
1
+ import { execFile, execFileSync } from 'node:child_process'
2
+ import { POLICY_SETTINGS_PATH, buildEvalCode, createInProcessHost, runInProcess, validatePayload } from './bridge-script.mjs'
3
+
4
+ // Editor coordination adapter.
5
+ //
6
+ // adapter.probe({ vaultRoot }) -> { state: 'coordinated' | 'absent' | 'uncoordinated', reason }
7
+ // adapter.inspect(payload) -> reply read-only, may retry
8
+ // adapter.collect(payload) -> reply read-only, may retry
9
+ // adapter.publish(payload) -> reply sent at most once
10
+ //
11
+ // A raw `call(payload)` sends one payload once and resolves with the parsed
12
+ // reply. It throws TransportTimeout when the reply did not arrive in time and
13
+ // any other error when the call could not be made. createEditorAdapter adds
14
+ // the rules every transport must follow: calls are serialized, read-only calls
15
+ // may retry, and a publish is never resent. When a publish reply is lost the
16
+ // recorded outcome is re-read from the app; when none is recorded the caller
17
+ // is told so and reconciles from the files themselves.
18
+
19
+ export class TransportTimeout extends Error {
20
+ constructor(message) {
21
+ super(message)
22
+ this.name = 'TransportTimeout'
23
+ this.code = 'transport-timeout'
24
+ }
25
+ }
26
+
27
+ const READ_RETRIES = 2
28
+
29
+ export function createEditorAdapter({ call, processProbe, kind = 'custom' }) {
30
+ if (typeof call !== 'function') throw new TypeError('createEditorAdapter needs a call function')
31
+ if (typeof processProbe !== 'function') throw new TypeError('createEditorAdapter needs a processProbe function')
32
+ const retries = []
33
+ let tail = Promise.resolve()
34
+ const serialized = (operation) => {
35
+ const next = tail.then(operation, operation)
36
+ tail = next.catch(() => {})
37
+ return next
38
+ }
39
+ const readOnly = async (payload) => {
40
+ for (let attempt = 0; ; attempt += 1) {
41
+ try { return await call(validatePayload(payload)) } catch (error) {
42
+ if (attempt >= READ_RETRIES || !(error instanceof TransportTimeout)) throw error
43
+ retries.push(payload.op)
44
+ }
45
+ }
46
+ }
47
+ return {
48
+ kind,
49
+ transportRetries: retries,
50
+ inspect: (payload) => serialized(() => readOnly({ ...payload, op: 'inspect' })),
51
+ collect: (payload) => serialized(() => readOnly({ ...payload, op: 'collect' })),
52
+ publish: (payload) => serialized(async () => {
53
+ validatePayload(payload)
54
+ if (payload.op !== 'publish') throw new TypeError('publish needs a publish payload')
55
+ try { return await call(payload) } catch (error) {
56
+ retries.push(error instanceof TransportTimeout ? 'publish-reply-lost' : 'publish-call-failed')
57
+ let record = null
58
+ try { record = await readOnly({ op: 'collect', vaultRoot: payload.vaultRoot, path: payload.path }) } catch { /* the app is not answering */ }
59
+ if (record && record.operationId === payload.operationId && typeof record.outcome === 'string') return { ...record, status: record.outcome, replyLost: true }
60
+ return { status: 'outcome-unknown', operationId: payload.operationId, wrote: null, replyLost: true, transportError: String(error.message || error).slice(0, 300) }
61
+ }
62
+ }),
63
+ // Path selection. `absent` is returned only when the process probe says,
64
+ // positively, that no Obsidian is running. A running or unknowable app
65
+ // that does not answer for this exact vault is `uncoordinated`.
66
+ probe: ({ vaultRoot }) => serialized(async () => {
67
+ let processes
68
+ try { processes = await processProbe() } catch { processes = 'unknown' }
69
+ if (processes === 'absent') return { state: 'absent', reason: 'no Obsidian process is running' }
70
+ try {
71
+ const reply = await readOnly({ op: 'inspect', vaultRoot, path: POLICY_SETTINGS_PATH })
72
+ // `path-unsafe` for the probe path still proves the app answered for this vault; the settings unit reports the path.
73
+ if (['inspected', 'path-unsafe'].includes(reply.status) && reply.vaultBasePath === vaultRoot) return { state: 'coordinated', reason: 'the app answered for this vault' }
74
+ return { state: 'uncoordinated', reason: reply.status === 'vault-mismatch' ? 'the app answered for another vault' : `the app answered ${reply.status}` }
75
+ } catch (error) {
76
+ return { state: 'uncoordinated', reason: `an Obsidian process may be running and the bridge did not answer: ${String(error.message || error).slice(0, 200)}` }
77
+ }
78
+ }),
79
+ }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // In-process transport: the same script, run here. Used for the path with no
84
+ // app, and by tests that supply a host modelling an app.
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export function createInProcessCall(host = createInProcessHost()) {
88
+ return async (payload) => runInProcess(payload, host)
89
+ }
90
+
91
+ export function createDirectAdapter({ crashSeam } = {}) {
92
+ return createEditorAdapter({ call: createInProcessCall(createInProcessHost({ crashSeam })), processProbe: () => 'absent', kind: 'direct' })
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Obsidian CLI transport
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export function defaultCliPath(platform = process.platform) {
100
+ if (platform === 'darwin') return '/Applications/Obsidian.app/Contents/MacOS/obsidian-cli'
101
+ return 'obsidian-cli'
102
+ }
103
+
104
+ // The CLI reaches the app through a socket under $HOME, so `env` selects the
105
+ // app instance. It is passed explicitly and never edited here. A call that
106
+ // outlives its timeout is killed with SIGKILL: the CLI ignores SIGTERM while
107
+ // it waits on the app.
108
+ export function createObsidianCliCall({ cliPath = defaultCliPath(), env = process.env, timeoutMs = 20000 } = {}) {
109
+ return (payload) => new Promise((resolve, reject) => {
110
+ execFile(cliPath, ['eval', `code=${buildEvalCode(payload)}`], { env, timeout: timeoutMs, killSignal: 'SIGKILL', maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
111
+ if (error) return reject(error.killed ? new TransportTimeout(`CLI call timed out: ${payload.op}`) : new Error(`CLI call failed: ${String(error.message).slice(0, 300)}`))
112
+ const out = stdout || stderr || ''
113
+ const start = out.indexOf('=> ')
114
+ if (start < 0) return reject(new Error(`bridge returned no value: ${JSON.stringify(out.slice(0, 300))}`))
115
+ try { return resolve(JSON.parse(out.slice(start + 3))) } catch { return reject(new Error('bridge returned a value that is not JSON')) }
116
+ })
117
+ })
118
+ }
119
+
120
+ // The app is recognised by the executable a process runs, never by its
121
+ // arguments: a path argument with an `obsidian` segment (this package's own
122
+ // maintenance service, its private data root) is not the app. `ps -o comm=`
123
+ // names the executable as it was started on macOS, usually its full path, and
124
+ // by its command name of at most 15 characters on Linux.
125
+ //
126
+ // macOS `Obsidian` and the bundle's `Obsidian Helper…` processes, in any
127
+ // directory: a moved or renamed bundle still counts, and so does a
128
+ // process whose path ps cannot read and names by its short name.
129
+ // Linux `obsidian`, the executable of the .deb, snap, AppImage and Flatpak
130
+ // builds. A distribution that runs the app under a system Electron
131
+ // shows only `electron`, and which app it hosts is not known.
132
+ //
133
+ // The command-line tool is a client of the app, not the app: counting it would
134
+ // make every call this package makes through it look like a running app.
135
+ const APP_EXECUTABLE = Object.freeze({ darwin: /^obsidian(?: helper\b.*)?$/i, linux: /^obsidian$/i })
136
+ const ELECTRON_HOST = /^\.?electron(?:\d+|-wrap.*)?$/i
137
+
138
+ // Default process probe: 'absent' only when the process table was read and
139
+ // holds no Obsidian. Limits: it sees this machine's processes as this user
140
+ // can list them; it cannot see an app on another machine that reaches the
141
+ // vault through a shared or synchronized folder, an app packaged under another
142
+ // executable name, or an app that starts after the probe. Anything it cannot
143
+ // establish is 'unknown', which the adapter treats as a running app: an empty
144
+ // table, a failed read, and on Linux a system Electron process.
145
+ export function defaultObsidianProcessProbe({ platform = process.platform, run = execFileSync } = {}) {
146
+ if (!Object.hasOwn(APP_EXECUTABLE, platform)) return 'unknown'
147
+ try {
148
+ const table = run('/bin/ps', ['-A', '-o', 'comm='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 16 * 1024 * 1024 })
149
+ if (typeof table !== 'string' || table.trim() === '') return 'unknown'
150
+ const names = table.split('\n').map((line) => line.trim()).filter((line) => line !== '').map((line) => line.slice(line.lastIndexOf('/') + 1))
151
+ if (names.some((name) => APP_EXECUTABLE[platform].test(name))) return 'running'
152
+ return platform === 'linux' && names.some((name) => ELECTRON_HOST.test(name)) ? 'unknown' : 'absent'
153
+ } catch {
154
+ return 'unknown'
155
+ }
156
+ }
157
+
158
+ export function createObsidianCliAdapter({ cliPath, env, timeoutMs, processProbe = () => defaultObsidianProcessProbe() } = {}) {
159
+ return createEditorAdapter({ call: createObsidianCliCall({ cliPath, env, timeoutMs }), processProbe, kind: 'obsidian-cli' })
160
+ }
@@ -0,0 +1,6 @@
1
+ // Recovery state for Obsidian publication: the immutable store, the durable
2
+ // journal, restart recovery and the late-writer re-check.
3
+ export { EXCHANGE_CANDIDATE_NAME, LATE_EXCHANGE_CANDIDATE_NAME, PublicationRefusal, VAULT_LOCK_DIRECTORY, acquireVaultLock, createRecoveryStore, readFileDigest, sha256Digest } from './store.mjs'
4
+ export { JOURNAL_SCHEMA, createJournal, journalDetail, listJournals, newJournalId } from './journal.mjs'
5
+ export { classifyCandidateFile, namedCandidates, publishedSinceCommit, reconcileUnit, recoverPublications, retireStagedFile } from './restart.mjs'
6
+ export { recheckDisplacedFiles } from './late-writer.mjs'
@@ -0,0 +1,134 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { isPendingPrivateWrite, publishPrivateFile } from '../../../project/durable-state.mjs'
5
+ import { readRegularTextNoFollow } from '../../../project/private-state.mjs'
6
+ import { OBSIDIAN_EXT_KEY, assertObsidianContract } from '../contracts.mjs'
7
+ import { refuse } from './store.mjs'
8
+
9
+ // Durable publication journal. The registered shape is one document; on disk
10
+ // it is a header plus one immutable file per entry, each published atomically
11
+ // under a name that is never overwritten, so an entry is either wholly present
12
+ // or absent and history cannot be rewritten in place. The document is
13
+ // assembled from those files and validated against the registered schema every
14
+ // time it is read or extended.
15
+ //
16
+ // Entry steps follow the schema: `capture` (the write-ahead record binding a
17
+ // note to its base, candidate, staged file and recovery path, written before
18
+ // anything is exchanged), `conditional-update` (the exchange, exclusive
19
+ // create or conditional removal and its outcome), `verify`, `manifest-commit`
20
+ // and `restart`. Each entry file also carries the journal state that holds
21
+ // after it.
22
+
23
+ export const JOURNAL_SCHEMA = 'atelier-obsidian-publication-journal/v1'
24
+ const ENTRY_FILE = /^\d{8}\.json$/
25
+ const CLOSED_FILE = 'closed.json'
26
+
27
+ const isoTime = (clock) => {
28
+ const value = clock()
29
+ const date = value instanceof Date ? value : new Date(value)
30
+ if (Number.isNaN(date.getTime())) throw new TypeError('the clock did not return a time')
31
+ return date.toISOString()
32
+ }
33
+
34
+ export function newJournalId(clock = () => new Date()) {
35
+ return `journal-${isoTime(clock).replace(/[-:.TZ]/g, '')}-${randomBytes(4).toString('hex')}`
36
+ }
37
+
38
+ function load(directory) {
39
+ const header = JSON.parse(readRegularTextNoFollow(path.join(directory, 'header.json')))
40
+ const entriesDir = path.join(directory, 'entries')
41
+ const names = fs.existsSync(entriesDir) ? fs.readdirSync(entriesDir).filter((name) => !isPendingPrivateWrite(name)).sort() : []
42
+ const records = names.map((name, index) => {
43
+ if (!ENTRY_FILE.test(name) || Number(name.slice(0, 8)) !== index) refuse('journal-corrupt', 'journal entries are not a contiguous sequence; preserve and inspect')
44
+ const record = JSON.parse(readRegularTextNoFollow(path.join(entriesDir, name)))
45
+ if (record.entry?.seq !== index) refuse('journal-corrupt', 'a journal entry does not carry its own sequence number')
46
+ return record
47
+ })
48
+ return { header, records }
49
+ }
50
+
51
+ function assemble({ header, records }) {
52
+ const restarts = records.filter((record) => record.entry.step === 'restart' && record.entry.ext?.[OBSIDIAN_EXT_KEY]?.phase === 'begin')
53
+ const document = {
54
+ ...header,
55
+ state: records.length > 0 ? records.at(-1).state : 'prepared',
56
+ entries: records.map((record) => record.entry),
57
+ restart: { count: restarts.length, ...(restarts.length > 0 ? { lastRecoveredAt: restarts.at(-1).entry.at } : {}) },
58
+ }
59
+ return assertObsidianContract('publication-journal', document)
60
+ }
61
+
62
+ function handle(store, directory, clock) {
63
+ // One writer at a time extends a journal (the publisher holds the view's
64
+ // lock), so the entries read once stay the entries on disk.
65
+ let cached = null
66
+ const current = () => (cached ??= load(directory))
67
+ const journal = {
68
+ directory,
69
+ document: () => assemble(current()),
70
+ // detail lands under the entry's ext container; the rest are schema fields.
71
+ append({ step, outcome, state, notePath, beforeDigest, afterDigest, recoveryRef, detail }) {
72
+ const loaded = current()
73
+ const entry = {
74
+ seq: loaded.records.length,
75
+ at: isoTime(clock),
76
+ step,
77
+ outcome,
78
+ ...(notePath === undefined ? {} : { notePath }),
79
+ ...(beforeDigest ? { beforeDigest } : {}),
80
+ ...(afterDigest ? { afterDigest } : {}),
81
+ ...(recoveryRef ? { recoveryRef } : {}),
82
+ ...(detail ? { ext: { [OBSIDIAN_EXT_KEY]: detail } } : {}),
83
+ }
84
+ const record = { entry, state: state ?? (loaded.records.at(-1)?.state ?? 'prepared') }
85
+ assemble({ header: loaded.header, records: [record] })
86
+ try {
87
+ publishPrivateFile(path.join(directory, 'entries', `${String(entry.seq).padStart(8, '0')}.json`), `${JSON.stringify(record, null, 2)}\n`)
88
+ } catch (error) {
89
+ cached = null
90
+ throw error
91
+ }
92
+ loaded.records.push(record)
93
+ return entry
94
+ },
95
+ // A journal that can no longer matter (committed, or superseded by a later
96
+ // commit) is closed so that later runs do not read it again. The flag file
97
+ // is advisory: a journal without one is simply read and judged again.
98
+ close() {
99
+ try { publishPrivateFile(path.join(directory, CLOSED_FILE), `${JSON.stringify({ state: assemble(current()).state })}\n`) } catch (error) { if (error.code !== 'EEXIST') throw error }
100
+ },
101
+ }
102
+ return journal
103
+ }
104
+
105
+ export function createJournal(store, { journalId, protocolId, expectedGeneration, targetGeneration, detail, clock = () => new Date() }) {
106
+ const directory = store.journalDir(journalId)
107
+ fs.mkdirSync(path.join(directory, 'entries'), { recursive: true, mode: 0o700 })
108
+ const header = {
109
+ schema: JOURNAL_SCHEMA,
110
+ contractVersion: '1.0.0',
111
+ journalId,
112
+ workspaceId: store.workspaceId,
113
+ scopeId: store.scopeId,
114
+ protocolId,
115
+ expectedGeneration,
116
+ targetGeneration,
117
+ ...(detail ? { ext: { [OBSIDIAN_EXT_KEY]: detail } } : {}),
118
+ }
119
+ assemble({ header, records: [] })
120
+ publishPrivateFile(path.join(directory, 'header.json'), `${JSON.stringify(header, null, 2)}\n`)
121
+ return handle(store, directory, clock)
122
+ }
123
+
124
+ // Journals of this view, oldest first. A directory without a header is a
125
+ // journal that never began: nothing can refer to it.
126
+ export function listJournals(store, { clock = () => new Date(), openOnly = false } = {}) {
127
+ if (!fs.existsSync(store.journalsRoot)) return []
128
+ return fs.readdirSync(store.journalsRoot).sort()
129
+ .filter((name) => fs.existsSync(path.join(store.journalsRoot, name, 'header.json')))
130
+ .filter((name) => !openOnly || !fs.existsSync(path.join(store.journalsRoot, name, CLOSED_FILE)))
131
+ .map((name) => handle(store, path.join(store.journalsRoot, name), clock))
132
+ }
133
+
134
+ export const journalDetail = (value) => value?.ext?.[OBSIDIAN_EXT_KEY] ?? {}
@@ -0,0 +1,46 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { listJournals } from './journal.mjs'
4
+ import { readFileBytes, sha256Digest } from './store.mjs'
5
+
6
+ // A program that opened a note before it was exchanged still holds the
7
+ // displaced file, and may write into it at any later time. Those bytes land in
8
+ // the recovery area, not in the vault, so nothing shows them to the person
9
+ // unless the displaced files are looked at again. This compares every
10
+ // displaced file with the digest recorded when it was moved. A difference is
11
+ // kept as an immutable object and reported as `late-writer-captured`.
12
+ //
13
+ // The check is meaningful only after a quiet period, and one pass does not end
14
+ // the obligation: a holder can write later still. Callers repeat it.
15
+ export function recheckDisplacedFiles({ store, journalIds, clock = () => new Date() } = {}) {
16
+ const ids = journalIds ?? listJournals(store).map((journal) => journal.document().journalId)
17
+ const findings = []
18
+ for (const journalId of ids) {
19
+ const receipts = store.listReceipts(journalId)
20
+ const known = new Set(receipts.filter((receipt) => receipt.role === 'late-writer').map((receipt) => `${receipt.unit}\u0000${receipt.observedDigest}`))
21
+ for (const receipt of receipts.filter((item) => item.role === 'displaced')) {
22
+ // A recovery name that is a hard link to the live note (an interrupted
23
+ // put-back) is the note itself: edits to it are not a late writer's.
24
+ const live = receipt.notePath ? fs.lstatSync(path.join(store.vaultRoot, receipt.notePath), { throwIfNoEntry: false }) : null
25
+ const held = fs.lstatSync(store.resolve(receipt.displacedRef), { throwIfNoEntry: false })
26
+ if (live && held && live.ino === held.ino && live.dev === held.dev) continue
27
+ let bytes
28
+ try { bytes = readFileBytes(store.resolve(receipt.displacedRef)) } catch (error) {
29
+ if (error.code !== 'ENOENT') throw error
30
+ findings.push({ code: 'displaced-file-missing', journalId, unit: receipt.unit, notePath: receipt.notePath, displacedRef: receipt.displacedRef })
31
+ continue
32
+ }
33
+ const observedDigest = sha256Digest(bytes)
34
+ if (observedDigest === receipt.digestAtMove) continue
35
+ const object = store.retainObject(bytes)
36
+ const finding = { code: 'late-writer-captured', journalId, unit: receipt.unit, notePath: receipt.notePath, displacedRef: receipt.displacedRef, digestAtMove: receipt.digestAtMove, observedDigest, objectRef: object.ref }
37
+ if (!known.has(`${receipt.unit}\u0000${observedDigest}`)) {
38
+ const value = clock()
39
+ store.writeReceipt(journalId, receipt.unit, { role: 'late-writer', notePath: receipt.notePath, displacedRef: receipt.displacedRef, digestAtMove: receipt.digestAtMove, observedDigest, objectRef: object.ref,
40
+ at: (value instanceof Date ? value : new Date(value)).toISOString() })
41
+ }
42
+ findings.push(finding)
43
+ }
44
+ }
45
+ return findings
46
+ }