@mnstry/atelier 0.2.0-alpha.6 → 0.2.0-alpha.8

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 (374) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/README.md +59 -15
  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-review.v1.schema.json +1601 -0
  25. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  26. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  27. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  28. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  29. package/docs/assurance-controls.md +9 -1
  30. package/docs/atelier-sync.md +5 -4
  31. package/docs/blocks/claims.md +15 -10
  32. package/docs/blocks/will-not-do.md +4 -1
  33. package/docs/coauthor-session.md +57 -0
  34. package/docs/connected-composition.md +75 -0
  35. package/docs/decisions.md +167 -0
  36. package/docs/exact-upgrades.md +159 -0
  37. package/docs/extension-pack-lifecycle.md +33 -0
  38. package/docs/guided-upgrades.md +99 -0
  39. package/docs/install.md +65 -9
  40. package/docs/intake-and-guides.md +65 -0
  41. package/docs/integration-and-review-plan.md +409 -0
  42. package/docs/integration-contract-decisions.md +22 -0
  43. package/docs/local-review.md +84 -0
  44. package/docs/local-services.md +227 -0
  45. package/docs/local-state.md +107 -0
  46. package/docs/obsidian-contract.md +687 -0
  47. package/docs/obsidian.md +387 -0
  48. package/docs/portable-presentation.md +328 -0
  49. package/docs/presentation-consumer-boundaries.md +75 -0
  50. package/docs/presentation-review-disposition.md +47 -0
  51. package/docs/project-command-inventory.md +38 -0
  52. package/docs/project-options.md +53 -0
  53. package/docs/release-engineering.md +88 -0
  54. package/docs/review-portability.md +55 -0
  55. package/docs/skill-steward.md +156 -0
  56. package/docs/upgrade.md +55 -2
  57. package/docs/vault-service.md +292 -0
  58. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  59. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  60. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  61. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  62. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  63. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  64. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  65. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  66. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  67. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  68. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  69. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  70. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  71. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  72. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  73. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  74. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  75. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  76. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  77. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  78. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  79. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  80. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  81. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  82. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  83. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  84. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  85. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  86. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  87. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  88. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  89. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  90. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  91. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  92. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  93. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  94. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  95. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  96. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  97. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  98. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  99. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  100. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  101. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  102. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  103. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  104. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  105. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  106. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  107. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  108. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  109. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  110. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  111. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  112. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  113. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  114. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  115. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  116. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  117. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  118. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  119. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  120. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  121. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  122. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  123. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  124. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  125. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  126. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  127. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  128. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  129. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  130. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  131. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  132. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  133. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  134. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  135. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  136. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  137. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  138. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  139. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  140. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  141. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  142. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  143. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  144. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  145. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  146. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  147. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  148. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  149. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  150. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  151. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  152. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  153. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  154. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  155. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  156. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  157. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  158. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  159. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  160. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  161. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  162. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  163. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  164. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  165. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  166. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  167. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  168. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  169. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  170. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  171. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  172. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  173. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  174. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  175. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  176. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  177. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  178. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  179. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  180. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  181. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  182. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  183. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  184. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  185. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  186. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  187. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  188. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  189. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  190. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  191. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  192. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  193. package/fixtures/obsidian/edits/workspace.json +65 -0
  194. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  195. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  196. package/fixtures/obsidian/materialization/workspace.json +54 -0
  197. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  198. package/fixtures/ui/presentation/pane.v1.json +7 -0
  199. package/fixtures/ui/presentation/reference.v1.json +293 -0
  200. package/package.json +57 -6
  201. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  202. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  203. package/skills/claude/atelier-open/SKILL.md +5 -0
  204. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  205. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  206. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  207. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  208. package/skills/codex/atelier-open/SKILL.md +5 -0
  209. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  210. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  211. package/src/access/preview-enforcer.mjs +84 -0
  212. package/src/boundary/policy.mjs +57 -25
  213. package/src/cli/project-options.mjs +58 -0
  214. package/src/cli/run.mjs +43 -3
  215. package/src/coauthor/session.mjs +161 -0
  216. package/src/coauthor/store.mjs +173 -0
  217. package/src/collaboration/event-ledger.mjs +16 -33
  218. package/src/collaboration/inspection-bundle.mjs +223 -0
  219. package/src/collaboration/review-contracts.mjs +25 -0
  220. package/src/collaboration/review-store.mjs +305 -0
  221. package/src/commands/coauthor.mjs +26 -0
  222. package/src/commands/config.mjs +6 -1
  223. package/src/commands/extension-pack.mjs +15 -8
  224. package/src/commands/init.mjs +44 -5
  225. package/src/commands/obsidian.mjs +368 -0
  226. package/src/commands/review.mjs +147 -0
  227. package/src/commands/setup.mjs +51 -14
  228. package/src/commands/skills.mjs +199 -0
  229. package/src/commands/sync.mjs +8 -1
  230. package/src/composition/registry.mjs +27 -0
  231. package/src/composition/wire.mjs +122 -0
  232. package/src/contracts/corpus.mjs +29 -0
  233. package/src/decisions/contracts.d.mts +96 -0
  234. package/src/decisions/contracts.mjs +321 -0
  235. package/src/disclosure/content-scan.mjs +13 -0
  236. package/src/export/atelier-export-contract.mjs +16 -1
  237. package/src/extension-packs/lifecycle.mjs +154 -0
  238. package/src/graph/graph.mjs +64 -2
  239. package/src/graph/knowledge-graph.mjs +548 -24
  240. package/src/guides/contracts.mjs +54 -0
  241. package/src/intake/store.mjs +119 -0
  242. package/src/preview/channel.mjs +89 -0
  243. package/src/project/config.mjs +26 -27
  244. package/src/project/durable-state.mjs +161 -0
  245. package/src/project/file-class.mjs +166 -0
  246. package/src/project/private-state.mjs +11 -1
  247. package/src/projection/obsidian/contracts.mjs +571 -0
  248. package/src/projection/obsidian/edits/align.mjs +169 -0
  249. package/src/projection/obsidian/edits/apply.mjs +889 -0
  250. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  251. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  252. package/src/projection/obsidian/edits/index.mjs +19 -0
  253. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  254. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  255. package/src/projection/obsidian/edits/observe.mjs +123 -0
  256. package/src/projection/obsidian/edits/policy.mjs +123 -0
  257. package/src/projection/obsidian/edits/regions.mjs +648 -0
  258. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  259. package/src/projection/obsidian/materialize/index.mjs +7 -0
  260. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  261. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  262. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  263. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  264. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  265. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  266. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  267. package/src/projection/obsidian/proposals/index.mjs +23 -0
  268. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  269. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  270. package/src/projection/obsidian/proposals/router.mjs +168 -0
  271. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  272. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  273. package/src/projection/obsidian/publication/index.mjs +5 -0
  274. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  275. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  276. package/src/projection/obsidian/publication/transport.mjs +139 -0
  277. package/src/projection/obsidian/recovery/index.mjs +6 -0
  278. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  279. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  280. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  281. package/src/projection/obsidian/recovery/store.mjs +234 -0
  282. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  283. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  284. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  285. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  286. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  287. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  288. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  289. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  290. package/src/projection/project.mjs +28 -19
  291. package/src/readiness-protocols/evidence.mjs +282 -0
  292. package/src/readiness-protocols/runtime.mjs +4 -3
  293. package/src/readiness-protocols/source-read.mjs +39 -0
  294. package/src/runtime/obsidian/app-capability.mjs +110 -0
  295. package/src/runtime/obsidian/app-production-seams.mjs +96 -0
  296. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  297. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  298. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  299. package/src/runtime/obsidian/contributions.mjs +27 -0
  300. package/src/runtime/obsidian/documents.mjs +37 -0
  301. package/src/runtime/obsidian/enablement.mjs +53 -0
  302. package/src/runtime/obsidian/engine.mjs +532 -0
  303. package/src/runtime/obsidian/errors.mjs +14 -0
  304. package/src/runtime/obsidian/extension-points.mjs +103 -0
  305. package/src/runtime/obsidian/index.mjs +34 -0
  306. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  307. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  308. package/src/runtime/obsidian/observation.mjs +158 -0
  309. package/src/runtime/obsidian/opening.mjs +243 -0
  310. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  311. package/src/runtime/obsidian/pipeline.mjs +122 -0
  312. package/src/runtime/obsidian/private-lock.mjs +156 -0
  313. package/src/runtime/obsidian/process-identity.mjs +58 -0
  314. package/src/runtime/obsidian/service-client.mjs +66 -0
  315. package/src/runtime/obsidian/service-main.mjs +89 -0
  316. package/src/runtime/obsidian/service-record.mjs +169 -0
  317. package/src/runtime/obsidian/service-server.mjs +121 -0
  318. package/src/runtime/obsidian/service.mjs +216 -0
  319. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  320. package/src/runtime/obsidian/state-store.mjs +180 -0
  321. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  322. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  323. package/src/runtime/obsidian/watchers.mjs +36 -0
  324. package/src/runtime/supervisor.mjs +16 -1
  325. package/src/server/local-sidecar.mjs +44 -0
  326. package/src/server/server.mjs +1 -0
  327. package/src/skills/steward.mjs +780 -0
  328. package/src/ui/presentation/browser.mjs +278 -0
  329. package/src/ui/presentation/contract.mjs +116 -0
  330. package/src/ui/presentation/index.mjs +7 -0
  331. package/src/ui/presentation/native.mjs +153 -0
  332. package/src/ui/presentation/proof.mjs +57 -0
  333. package/src/ui/presentation/schema-check.mjs +30 -0
  334. package/src/ui/presentation/schema.generated.mjs +1371 -0
  335. package/src/ui/presentation/state.mjs +43 -0
  336. package/src/ui/presentation/styles.mjs +67 -0
  337. package/src/ui/presentation/tokens.mjs +74 -0
  338. package/src/ui/presentation/web.mjs +85 -0
  339. package/src/ui/review-page.mjs +32 -0
  340. package/src/upgrade/explanation.mjs +33 -0
  341. package/src/upgrade/provenance.mjs +338 -0
  342. package/src/upgrade/transaction-files.mjs +103 -0
  343. package/src/upgrade/transaction.mjs +475 -0
  344. package/src/upgrade/upgrade.mjs +63 -28
  345. package/src/vault/hosts.mjs +21 -0
  346. package/src/vault/identity.mjs +19 -0
  347. package/src/vault/index.mjs +7 -0
  348. package/src/vault/interface.mjs +11 -0
  349. package/src/vault/metadata.mjs +31 -0
  350. package/src/vault/privacy.mjs +148 -0
  351. package/src/vault/probe.mjs +89 -0
  352. package/src/vault/service.mjs +131 -0
  353. package/src/vault/source.mjs +45 -0
  354. package/src/vault/storage.mjs +23 -0
  355. package/templates/distribution-workspace/.claude/launch.json +6 -2
  356. package/templates/distribution-workspace/gitignore +1 -0
  357. package/templates/distribution-workspace/project/README.md +2 -2
  358. package/templates/external-project-workspace/README.md +45 -0
  359. package/templates/external-project-workspace/answers.example.json +4 -0
  360. package/templates/external-project-workspace/atelier.project.json +40 -0
  361. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  362. package/templates/external-project-workspace/gitignore +8 -0
  363. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  364. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  365. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  366. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  367. package/templates/external-project-workspace/source/README.md +11 -0
  368. package/templates/launch.json +6 -2
  369. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  370. package/templates/private-domain-workspace/domain/README.md +2 -2
  371. package/templates/private-domain-workspace/gitignore +1 -0
  372. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  373. package/templates/shared-project-workspace/gitignore +1 -0
  374. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,113 @@
1
+ import { COLLABORATION_LEDGER_LIMITS } from '../../../collaboration/event-ledger.mjs'
2
+
3
+ // The limits of the existing proposal ledger are contracts of that ledger:
4
+ // 256 KiB a line, 16 MiB in all, 10,000 events, and a line that cannot be read
5
+ // refuses every append. This module asks about them BEFORE the adapter appends
6
+ // anything, and decides what becomes of an operation the ledger cannot take.
7
+ //
8
+ // proposal-too-large the event would be longer than a line may be. Refused; a proposal refers to the preserved
9
+ // bytes by digest instead of holding them, and nothing is ever cut to fit.
10
+ // ledger-full the ledger has no room (or too little: see the reserve). The operation waits in the adapter
11
+ // queue as `backpressure`, its preserved edit untouched, and is tried again later.
12
+ // ledger-corrupt a line of the ledger cannot be read. Refused, for that repository only.
13
+ // store-unavailable the store could not be read or is locked by another writer right now. Waits, like a full one.
14
+ //
15
+ // Nothing here, and nothing in the adapter, compacts, rotates or deletes a
16
+ // ledger or a proposal. Making room is a decision of a person.
17
+
18
+ export const PROPOSAL_LEDGER_LIMITS = Object.freeze({
19
+ maxLineBytes: COLLABORATION_LEDGER_LIMITS.maxLineBytes,
20
+ maxBytes: COLLABORATION_LEDGER_LIMITS.maxBytes,
21
+ maxEvents: COLLABORATION_LEDGER_LIMITS.maxEvents,
22
+ })
23
+
24
+ export const PROPOSAL_BACKPRESSURE = Object.freeze({
25
+ // A proposal the adapter creates is reviewed by people, and a review is two more events. The adapter never takes
26
+ // the last of the room they need to settle what is already there.
27
+ reserveEvents: 32,
28
+ reserveBytes: 512 * 1024,
29
+ // What the store adds around a proposal when it writes the event (identifiers, timestamps) is a few hundred bytes
30
+ // of fixed length; the estimate is that and this much more.
31
+ lineMarginBytes: 1024,
32
+ // Per tick and per repository.
33
+ maxBatchPerRepository: 8,
34
+ // Pending edits looked at per tick to find new structural edits.
35
+ maxExaminedPerTick: 64,
36
+ // Pending edits the object store does not know yet that one tick observes and records, never applies.
37
+ maxObservedPerTick: 16,
38
+ // An operation under backpressure is tried at most this many times, each time later than the last.
39
+ maxAttempts: 8,
40
+ retryIntervalMs: 60 * 1000,
41
+ maxRetryIntervalMs: 60 * 60 * 1000,
42
+ })
43
+
44
+ export const BACKPRESSURE_CODES = Object.freeze(['ledger-full', 'store-unavailable', 'route-visibility-unknown'])
45
+ export const STORE_REFUSAL_CODES = Object.freeze(['proposal-too-large', 'ledger-corrupt', 'store-refused', 'identity-not-preserved'])
46
+
47
+ // What a read of the whole ledger says about it. `read` is the answer of
48
+ // `eventLedger.readAll()` (or of `listProposals()`, which carries the same
49
+ // status, diagnostics and stats).
50
+ export function classifyLedgerRead(read, limits = PROPOSAL_LEDGER_LIMITS) {
51
+ const events = read?.stats?.eventCount ?? 0
52
+ const bytes = read?.stats?.bytes ?? 0
53
+ const headroom = { events: Math.max(0, limits.maxEvents - events), bytes: Math.max(0, limits.maxBytes - bytes) }
54
+ if (read?.ok) return { state: 'readable', code: null, events, bytes, headroom }
55
+ const diagnostics = Array.isArray(read?.diagnostics) ? read.diagnostics : []
56
+ // Larger than a ledger may be, or more events than one may hold: full, and then some. Not a line that is wrong.
57
+ if (read?.status === 413 || (diagnostics.length > 0 && diagnostics.every((item) => item.code === 'ledger-event-limit'))) {
58
+ return { state: 'full', code: 'ledger-full', events, bytes, headroom: { events: 0, bytes: read?.status === 413 ? 0 : headroom.bytes } }
59
+ }
60
+ if (read?.status === 422) return { state: 'corrupt', code: 'ledger-corrupt', events, bytes, headroom: { events: 0, bytes: 0 }, lines: diagnostics.length }
61
+ return { state: 'unavailable', code: 'store-unavailable', events, bytes, headroom: { events: 0, bytes: 0 } }
62
+ }
63
+
64
+ // The length of the line the store would write for `body`, to within the margin, and never less than it. It repeats
65
+ // what createProposal makes of a body: the members it copies, and the payload, which it keeps whole. The identifiers
66
+ // and timestamps the store adds have a fixed length, so placeholders of that length stand for them.
67
+ export function estimateEventLineBytes({ body, workspaceId, marginBytes = PROPOSAL_BACKPRESSURE.lineMarginBytes }) {
68
+ const at = '2000-01-01T00:00:00.000Z'
69
+ const id = `proposal-${'0'.repeat(32)}`
70
+ const record = {
71
+ schema: 'atelier-proposal@v1', workspaceId,
72
+ proposal: {
73
+ id, status: 'proposed', createdAt: at, updatedAt: at, sessionId: String(body.sessionId ?? ''), viewId: String(body.viewId ?? ''), path: String(body.path ?? ''), action: String(body.action ?? ''),
74
+ intent: String(body.intent ?? ''), reason: String(body.reason ?? ''), storage: { kind: 'local', ignored: true },
75
+ authority: { action: String(body.action ?? ''), capability: 'proposal.copy-only', copyOnly: true, directWrite: false, applyEndpoint: null }, eventVersion: 1,
76
+ },
77
+ diff: typeof body.diff === 'string' ? body.diff : '',
78
+ payload: body.proposal ?? {},
79
+ }
80
+ const event = { schema: 'atelier-collaboration-event@v1', aggregateId: id, version: 1, type: 'proposal-created', actor: String(body.actor ?? ''), at, payload: { record }, id: `event-${'0'.repeat(32)}` }
81
+ return Buffer.byteLength(`${JSON.stringify(event)}\n`) + marginBytes
82
+ }
83
+
84
+ // { ok: true } or { ok: false, code, detail }. `ledger` is a classifyLedgerRead answer that is `readable`.
85
+ export function preflightAppend({ ledger, lineBytes, limits = PROPOSAL_LEDGER_LIMITS, bounds = PROPOSAL_BACKPRESSURE }) {
86
+ if (lineBytes > limits.maxLineBytes) return { ok: false, code: 'proposal-too-large', detail: { lineBytes, limit: limits.maxLineBytes } }
87
+ if (ledger.headroom.events < 1 + bounds.reserveEvents) return { ok: false, code: 'ledger-full', detail: { member: 'events', headroom: ledger.headroom.events, reserve: bounds.reserveEvents } }
88
+ if (ledger.headroom.bytes < lineBytes + bounds.reserveBytes) return { ok: false, code: 'ledger-full', detail: { member: 'bytes', headroom: ledger.headroom.bytes, reserve: bounds.reserveBytes, lineBytes } }
89
+ return { ok: true }
90
+ }
91
+
92
+ // What a refused createProposal means, from its status.
93
+ export function classifyStoreRefusal(status) {
94
+ if (status === 413) return 'ledger-full'
95
+ if (status === 422) return 'ledger-corrupt'
96
+ if (status === 423 || status === 500 || status === 409) return 'store-unavailable'
97
+ return 'store-refused'
98
+ }
99
+
100
+ // When an operation under backpressure is tried next: twice as long after each attempt, up to the maximum.
101
+ export function nextAttemptAt({ attempts, nowMs, bounds = PROPOSAL_BACKPRESSURE }) {
102
+ const wait = Math.min(bounds.maxRetryIntervalMs, bounds.retryIntervalMs * 2 ** Math.max(0, Math.min(attempts, 30) - 1))
103
+ return new Date(nowMs + wait).toISOString()
104
+ }
105
+
106
+ export const isExhausted = (record, bounds = PROPOSAL_BACKPRESSURE) => record.state === 'backpressure' && record.attempts >= bounds.maxAttempts
107
+
108
+ // Whether the adapter looks at this operation on this tick. Never for one that is settled, refused or exhausted, and
109
+ // never before its time: an unchanged tick reads the queue and nothing else.
110
+ export function isDue(record, { nowMs, bounds = PROPOSAL_BACKPRESSURE }) {
111
+ if (record.state === 'acknowledged' || record.state === 'refused' || isExhausted(record, bounds)) return false
112
+ return record.nextAttemptAt === null || Date.parse(record.nextAttemptAt) <= nowMs
113
+ }
@@ -0,0 +1,55 @@
1
+ import { refuse } from '../../../runtime/obsidian/errors.mjs'
2
+ import { protectedRoots } from '../../../runtime/obsidian/machine-settings.mjs'
3
+ import { createProposalAdapter } from './adapter.mjs'
4
+ import { ProposalQueueRefusal } from './queue.mjs'
5
+ import { isAdapterOperationId } from './router.mjs'
6
+
7
+ // Binds the proposal adapter to the two places that reach it: the maintenance
8
+ // engine, which hands it the pending edits on a tick, and `atelier obsidian
9
+ // proposals`, which is how a person or an agent reads what became of them.
10
+ // The command reads; it creates, accepts and applies nothing.
11
+
12
+ export const PROPOSAL_ADAPTER_CONTRIBUTION_ID = 'atelier.proposal-adapter'
13
+ const EXIT = Object.freeze({ ok: 0 })
14
+ const SUMMARY = 'list | show OPERATION Structural edits routed as copy-only proposals: per repository what is queued, submitted, acknowledged, waiting or refused, and the room its ledger has. Read-only.'
15
+
16
+ export function createProposalsCommandOperation({ create = createProposalAdapter } = {}) {
17
+ return {
18
+ name: 'proposals',
19
+ summary: SUMMARY,
20
+ async run({ args, env, clock, readable }) {
21
+ const [sub = 'list', operationId, ...extra] = args
22
+ const { project, workspace, workspaceId } = readable()
23
+ if (workspace === null) refuse('workspace-not-prepared', 'this workspace has no private state yet; nothing was ever routed')
24
+ const context = { project, workspaceRoot: workspace.workspaceRoot, workspaceId, repositoryRoots: protectedRoots(project) }
25
+ const adapter = create({ env, clock })
26
+ // An adapter queue that cannot be read is an answer with a code, never an internal error.
27
+ const typed = (operation) => { try { return operation() } catch (error) { if (error instanceof ProposalQueueRefusal) refuse(error.code, 'the record of the proposal adapter cannot be read; nothing was changed'); throw error } }
28
+ if (sub === 'list' && operationId === undefined) {
29
+ const status = typed(() => adapter.status(context))
30
+ const operations = typed(() => adapter.list(context))
31
+ return {
32
+ exit: EXIT.ok, document: { status, operations },
33
+ human: status.repositories.length === 0 ? ['no enrolled repository'] : status.repositories.map((item) => `${item.repoId}\tqueued ${item.queued}\tsubmitted ${item.submitted}\tacknowledged ${item.acknowledged}\twaiting ${item.backpressure}\trefused ${item.refused}\tledger ${item.ledger.state}${item.ledger.headroom ? ` (${item.ledger.headroom.events} events, ${item.ledger.headroom.bytes} bytes free)` : ''}`),
34
+ }
35
+ }
36
+ if (sub === 'show' && extra.length === 0) {
37
+ if (!isAdapterOperationId(operationId)) refuse('usage', 'name the operation: an identifier that `obsidian proposals list` printed')
38
+ const shown = typed(() => adapter.show(context, { adapterOperationId: operationId }))
39
+ if (shown === null) refuse('unknown-operation', 'the proposal adapter holds no operation of that name')
40
+ return { exit: EXIT.ok, document: shown, human: [JSON.stringify(shown, null, 2)] }
41
+ }
42
+ return refuse('usage', 'proposals takes list or show OPERATION')
43
+ },
44
+ }
45
+ }
46
+
47
+ export function createProposalAdapterContribution(options = {}) {
48
+ return {
49
+ id: PROPOSAL_ADAPTER_CONTRIBUTION_ID,
50
+ register({ extensions, operations }) {
51
+ extensions.register('proposal-adapter', (options.create ?? createProposalAdapter)(options.adapter ?? {}))
52
+ operations.register(createProposalsCommandOperation(options))
53
+ },
54
+ }
55
+ }
@@ -0,0 +1,23 @@
1
+ // Structural edits made in a vault, routed as copy-only proposals: which
2
+ // existing proposal store of which enrolled repository an edit belongs to
3
+ // (router.mjs), the private record the adapter keeps of every operation before
4
+ // it asks a store for anything (queue.mjs), what it does when the ledger of a
5
+ // store has no room or cannot be read (backpressure.mjs), and the adapter that
6
+ // creates exactly one proposal per operation (adapter.mjs). Nothing here
7
+ // writes a source file or a vault, and no proposal holds the text of a note.
8
+ export {
9
+ MAX_CHANGE_OCCURRENCES, PROPOSAL_ADAPTER_ACTOR, PROPOSAL_ADAPTER_CRASH_STEPS, PROPOSAL_ADAPTER_ID, PROPOSAL_ADAPTER_PRIMITIVES, PROPOSAL_LOCK_PURPOSE, PROPOSAL_PAYLOAD_KIND, PROPOSAL_PAYLOAD_SCHEMA,
10
+ buildProposalContent, createProposalAdapter, createProposalAdapterForOracleTests, describeChange,
11
+ } from './adapter.mjs'
12
+ export {
13
+ BACKPRESSURE_CODES, PROPOSAL_BACKPRESSURE, PROPOSAL_LEDGER_LIMITS, STORE_REFUSAL_CODES, classifyLedgerRead, classifyStoreRefusal, estimateEventLineBytes, isDue, isExhausted, nextAttemptAt, preflightAppend,
14
+ } from './backpressure.mjs'
15
+ export { PROPOSAL_ADAPTER_CONTRIBUTION_ID, createProposalAdapterContribution, createProposalsCommandOperation } from './contribution.mjs'
16
+ export { createTickObservation, manifestOf, recordedOperationOf, retained } from './observation.mjs'
17
+ export {
18
+ OPEN_QUEUE_STATES, PROPOSAL_OPERATION_SCHEMA, PROPOSAL_QUEUE_CRASH_STEPS, PROPOSAL_QUEUE_DIRECTORY, PROPOSAL_QUEUE_LIMITS, PROPOSAL_QUEUE_STATES, ProposalQueueRefusal, isProposalOperationRecord, openProposalQueue,
19
+ } from './queue.mjs'
20
+ export {
21
+ MAX_ROUTED_SOURCE_PATH, PROPOSAL_ROUTER_PRIMITIVES, PROPOSAL_ROUTE_REFUSALS, PROPOSAL_STORE_DIRECTORY, PROPOSAL_STORE_LEDGER, TRANSIENT_ROUTE_REFUSALS, adapterOperationId, createProposalRouterForOracleTests, isAdapterOperationId, isRoutableSourcePath, proposalStoreId, resolveProposalRoute,
22
+ survivesStoreNormalisation,
23
+ } from './router.mjs'
@@ -0,0 +1,216 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { AtelierDiagnosticError } from '../../../project/config.mjs'
4
+ import { compareText, isoTime } from '../../../runtime/obsidian/documents.mjs'
5
+ import { readObsidianEnablement } from '../../../runtime/obsidian/enablement.mjs'
6
+ import { ObsidianMaintenanceRefusal } from '../../../runtime/obsidian/errors.mjs'
7
+ import { OPEN_EDIT_STATES, createMaintenanceStateStore } from '../../../runtime/obsidian/state-store.mjs'
8
+ import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal } from '../contracts.mjs'
9
+ import { SOURCE_APPLY_PRIMITIVES, SourceApplyRefusal, locateSource } from '../edits/apply.mjs'
10
+ import { EditArbitrationRefusal } from '../edits/arbitrate.mjs'
11
+ import { createEditObserverForOracleTests, editIdempotencyKey, observeEdit } from '../edits/observe.mjs'
12
+ import { decideApply } from '../edits/policy.mjs'
13
+ import { PublicationRefusal, readFileBytes, sha256Digest } from '../recovery/store.mjs'
14
+ import { PROPOSAL_BACKPRESSURE } from './backpressure.mjs'
15
+
16
+ // Observation on a tick: the part of a source apply that comes before its
17
+ // lease, for every open pending edit the object store does not know yet.
18
+ //
19
+ // In manual mode nothing on a tick applies, so until here a structural edit
20
+ // was recorded as `proposed` only after a person asked apply to look at it.
21
+ // Here the same observation runs on the tick itself, in manual and in
22
+ // automatic mode alike: the source is read now, the lens runs over the
23
+ // PRESERVED edit bytes (never the live note) against it, and the resulting
24
+ // operation is recorded in the object store exactly as an apply records it,
25
+ // through the same observer and the same store. A body replacement is recorded
26
+ // `pending` and left for apply, which is the only thing that writes a source;
27
+ // a structural edit is recorded `proposed`, and the adapter routes it in the
28
+ // same tick; a lens refusal is recorded `refused`; a base that moved on is
29
+ // recorded `conflicted`. Nothing here writes a source file or a vault, and
30
+ // nothing takes a lease.
31
+ //
32
+ // Quiet: the object store answers the same for an origin it already holds and
33
+ // appends nothing, and an edit it holds is not offered again. A refusal that
34
+ // comes before anything can be recorded (a source that is missing, a manifest
35
+ // that cannot be read, an object this machine may not see, a note that cannot
36
+ // be prepared again) is remembered in memory with its code and offered again
37
+ // only when the caller says so, which the engine does at its full
38
+ // reconciliation cadence, never on every tick. A bounded number of edits is
39
+ // looked at per tick, in the order they were observed, starting where the last
40
+ // tick stopped.
41
+
42
+ const EXT = OBSIDIAN_EXT_KEY
43
+ const GONE = new Set(['ENOENT', 'ENOTDIR', 'ELOOP'])
44
+ const segment = (identifier) => identifier.replaceAll(':', '_')
45
+ const refuse = (code, detail) => { throw new SourceApplyRefusal(code, detail) }
46
+ const isTyped = (error) => error instanceof SourceApplyRefusal || error instanceof EditArbitrationRefusal || error instanceof PublicationRefusal || error instanceof ObsidianMaintenanceRefusal
47
+ || error instanceof ObsidianContractRefusal || error instanceof AtelierDiagnosticError
48
+
49
+ // Observation of an edit whose base source is no longer the source: a conflict, decided without the lens. The same
50
+ // observer source apply uses for that case.
51
+ const STALE_OBSERVER = createEditObserverForOracleTests({
52
+ preserve({ store, edit, baseSourceBytes }) {
53
+ let edited = null
54
+ try { edited = store.readObject(edit.observedDigest) } catch (error) { if (error.code !== 'ENOENT' && error.code !== 'recovery-object-corrupt') throw error }
55
+ return edited === null ? { edited: null } : { edited, publishedRef: undefined, baseRef: store.retainObject(baseSourceBytes).ref }
56
+ },
57
+ classify: () => ({ kind: 'refusal', code: 'stale-base', detail: {} }),
58
+ })
59
+
60
+ // One open, never through a symbolic link, and only of a regular file.
61
+ function readNoFollow(file) {
62
+ const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0))
63
+ try {
64
+ if (!fs.fstatSync(descriptor).isFile()) throw Object.assign(new Error('not a regular file'), { code: 'EISDIR' })
65
+ return fs.readFileSync(descriptor)
66
+ } finally { fs.closeSync(descriptor) }
67
+ }
68
+ const digestOrNull = (file) => { try { return sha256Digest(readNoFollow(file)) } catch { return null } }
69
+
70
+ // The immutable manifest of one generation of one view: the current one when it is that generation, else the stored
71
+ // file, whose name carries the generation and the head of its digest. `workspace` answers `recoveryOf(scopeId)`.
72
+ export function manifestOf(workspace, scopeId, generationId) {
73
+ const current = workspace.recoveryOf(scopeId).readCurrentManifest()
74
+ if (current?.generationId === generationId) return current
75
+ const directory = path.join(workspace.workspaceRoot, 'state', 'manifests', segment(scopeId))
76
+ let names = []
77
+ try { names = fs.readdirSync(directory) } catch (error) { if (error.code !== 'ENOENT') throw error }
78
+ for (const name of names.filter((item) => item.startsWith(`${segment(generationId)}--`) && item.endsWith('.json')).sort()) {
79
+ let bytes
80
+ try { bytes = readFileBytes(path.join(directory, name)) } catch (error) { if (error.code === 'ENOENT') continue; throw error }
81
+ if (name !== `${segment(generationId)}--${sha256Digest(bytes).slice(7, 19)}.json`) continue
82
+ const manifest = JSON.parse(bytes.toString('utf8'))
83
+ if (manifest.generationId === generationId && manifest.scopeId === scopeId) return manifest
84
+ }
85
+ return null
86
+ }
87
+
88
+ // The retained bytes of that digest, or null when the store does not hold them intact.
89
+ export function retained(store, digest) {
90
+ if (typeof digest !== 'string') return null
91
+ try { const bytes = store.readObject(digest); return sha256Digest(bytes) === digest ? bytes : null } catch (error) {
92
+ if (error?.code === 'ENOENT' || error?.code === 'recovery-object-corrupt') return null
93
+ throw error
94
+ }
95
+ }
96
+
97
+ // The operation the object store recorded for this edit, or null.
98
+ export function recordedOperationOf(objects, edit) {
99
+ return objects.stateOf({ repoId: edit.identity.repoId, nodeId: edit.identity.nodeId }).operations.find((entry) => entry.origins.some((origin) => origin.editId === edit.editId && origin.scopeId === edit.scopeId)) ?? null
100
+ }
101
+
102
+ // `seams` prepare a view again when the published note is not retained; `isGitIgnored` and `gitDirectory` are what
103
+ // git is asked through; `bounds.maxObservedPerTick` is how many edits one tick looks at.
104
+ export function createTickObservation(options = {}) {
105
+ const { seams, isGitIgnored = SOURCE_APPLY_PRIMITIVES.isGitIgnored, gitDirectory = SOURCE_APPLY_PRIMITIVES.gitDirectory, bounds = PROPOSAL_BACKPRESSURE } = options
106
+ if (!seams || typeof seams.captureSnapshot !== 'function' || typeof seams.prepareView !== 'function') throw new TypeError('tick observation needs the seams that prepare a view')
107
+ // Edits whose operation the object store holds: a cache, losing it costs one read of an object. And edits whose
108
+ // observation refused before anything could be recorded, with the code.
109
+ const recorded = new Set()
110
+ const unobservable = new Map()
111
+ let cursor = 0
112
+
113
+ // Everything about one pending edit that the manifest of its view says.
114
+ function resolveEdit(workspace, edit) {
115
+ const { repoId, nodeId } = edit.identity
116
+ const scope = readObsidianEnablement(workspace.project).scopes.find((item) => item.scopeId === edit.scopeId)
117
+ if (!scope) refuse('unknown-scope')
118
+ const manifest = manifestOf(workspace, edit.scopeId, edit.generationId)
119
+ const noteEntry = manifest?.notes.find((note) => note.repoId === repoId && note.nodeId === nodeId && note.path === edit.path)
120
+ if (!noteEntry) refuse('manifest-unavailable')
121
+ const source = noteEntry.ext?.[EXT]?.source
122
+ if (typeof source?.rawDigest !== 'string' || typeof source?.path !== 'string') refuse('manifest-unavailable')
123
+ return { scope, manifest, noteEntry, recorded: source, baseSourceDigest: source.rawDigest, idempotencyKey: editIdempotencyKey({ workspaceId: workspace.workspaceId, repoId, nodeId, baseSourceDigest: source.rawDigest, observedDigest: edit.observedDigest }) }
124
+ }
125
+
126
+ // The note exactly as it was published, which the lens needs to tell generated bytes from authored ones. The
127
+ // publisher keeps no copy of a note it created, so the note is prepared again from the sources as they are now and
128
+ // used only when it has the digest the manifest recorded; a retained object of that digest serves as well.
129
+ function publishedNoteOf(workspace, { scope, manifest, noteEntry }, now) {
130
+ const store = workspace.recoveryOf(scope.scopeId)
131
+ const kept = retained(store, noteEntry.noteDigest)
132
+ if (kept) return kept
133
+ workspace.prepared ??= new Map()
134
+ if (!workspace.prepared.has(scope.scopeId)) {
135
+ let files = []
136
+ try {
137
+ const { graph, profile } = workspace.corpus()
138
+ const snapshot = seams.captureSnapshot({ project: workspace.project, graph, workspaceId: workspace.workspaceId, index: new Map(), configDigest: manifest.ext?.[EXT]?.configDigest ?? `sha256:${'0'.repeat(64)}`, capturedAt: now })
139
+ const persistentPathRegistry = createMaintenanceStateStore({ workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId }).readPathRegistry()
140
+ files = seams.prepareView({ snapshot, profile, scope, persistentPathRegistry, priorManifest: manifest, existingSettings: null, clock: workspace.clock, vaultRootBytes: Buffer.byteLength(store.vaultRoot, 'utf8') }).files
141
+ } catch (error) { if (!isTyped(error) && !GONE.has(error?.code)) throw error }
142
+ workspace.prepared.set(scope.scopeId, files)
143
+ }
144
+ const file = workspace.prepared.get(scope.scopeId).find((item) => item.path === noteEntry.path && item.digest === noteEntry.noteDigest)
145
+ return file ? file.bytes : null
146
+ }
147
+
148
+ // One edit, in the order of an apply up to its lease. Returns what was recorded; throws a typed refusal.
149
+ function observeOne(workspace, edit, now) {
150
+ if (edit.identity.workspaceId !== workspace.workspaceId) refuse('foreign-workspace')
151
+ const identity = { repoId: edit.identity.repoId, nodeId: edit.identity.nodeId }
152
+ const known = recordedOperationOf(workspace.objects(), edit)
153
+ if (known) return { code: 'already-observed', kind: known.kind, operationState: known.state, idempotencyKey: known.idempotencyKey }
154
+
155
+ // Whether this machine may see the object is asked first, of the canonical graph as it is now, before the
156
+ // manifest, the record of the object or the source path is looked at: an object that is withheld and one that
157
+ // is absent get this one answer, with nothing read and nothing recorded.
158
+ const { graph, profile } = workspace.corpus()
159
+ const visibility = decideApply({ request: { editId: edit.editId, mode: 'manual' }, workspace: { workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId }, graph, profile, object: identity, editClass: 'body-replacement', attempts: [] })
160
+ if (!visibility.allowed && visibility.code === 'object-not-visible') refuse('object-not-visible')
161
+ const resolved = resolveEdit(workspace, edit)
162
+ const node = graph.nodes.find((item) => item.repo === identity.repoId && item.id === identity.nodeId)
163
+ if (!node) refuse('source-not-in-graph')
164
+ if (node.path !== resolved.recorded.path) refuse('source-moved')
165
+
166
+ // The source, now: the one file of an enrolled repository the manifest names, reached through no link.
167
+ const vaultRoots = readObsidianEnablement(workspace.project).scopes.map((scope) => workspace.recoveryOf(scope.scopeId).vaultRoot)
168
+ const located = locateSource({ project: workspace.project, repoId: identity.repoId, relative: node.path, managedRoots: [workspace.workspaceRoot, ...vaultRoots], isGitIgnored, gitDirectory, env: workspace.env ?? process.env })
169
+ let sourceBytes
170
+ try { sourceBytes = readNoFollow(located.absolute) } catch (error) { refuse(error.code === 'ENOENT' ? 'source-missing' : error.code === 'ELOOP' ? 'source-symlink' : 'source-not-regular-file') }
171
+ const sourceDigest = sha256Digest(sourceBytes)
172
+
173
+ // The lens over the preserved bytes, or the conflict when the source moved on; then the record, through the store.
174
+ const store = workspace.recoveryOf(edit.scopeId)
175
+ let observed
176
+ if (sourceDigest !== resolved.baseSourceDigest) {
177
+ observed = STALE_OBSERVER({ edit, manifest: resolved.manifest, publishedNoteBytes: Buffer.alloc(0), baseSourceBytes: sourceBytes, store })
178
+ delete observed.operation.ext[EXT].publishedNoteRef
179
+ } else {
180
+ const publishedNoteBytes = publishedNoteOf(workspace, resolved, now)
181
+ // Preparing the note reads the source again: a source that changed since it was read here is a stale source.
182
+ if (publishedNoteBytes === null && digestOrNull(located.absolute) !== sourceDigest) refuse('stale-source', { cause: 'changed-while-reading' })
183
+ if (publishedNoteBytes === null) refuse('published-note-unavailable')
184
+ observed = observeEdit({ edit, manifest: resolved.manifest, publishedNoteBytes, baseSourceBytes: sourceBytes, store })
185
+ }
186
+ const result = workspace.objects().observe(observed.operation, { presentSourceDigest: sourceDigest })
187
+ return { code: result.appended ? 'observed' : 'already-observed', kind: observed.operation.kind, operationState: result.operation?.state ?? observed.operation.state, idempotencyKey: observed.operation.idempotencyKey }
188
+ }
189
+
190
+ // `workspace` is the opened workspace of the adapter; `edits` the pending edit records, open and closed;
191
+ // `retryRefused` offers the edits whose observation refused earlier once more. Returns one entry per edit looked
192
+ // at: { editId, repoId, nodeId, status: 'observed' | 'refused', code, kind?, operationState?, idempotencyKey? }.
193
+ return function observePendingEdits(workspace, { edits, retryRefused = false }) {
194
+ if (retryRefused) unobservable.clear()
195
+ const now = isoTime(workspace.clock)
196
+ const candidates = edits.filter((edit) => edit.closedAt === null && OPEN_EDIT_STATES.includes(edit.state) && !recorded.has(edit.editId) && !unobservable.has(edit.editId))
197
+ .sort((left, right) => compareText(left.observedAt, right.observedAt) || compareText(left.editId, right.editId))
198
+ const start = candidates.length === 0 ? 0 : cursor % candidates.length
199
+ const looked = [...candidates.slice(start), ...candidates.slice(0, start)].slice(0, bounds.maxObservedPerTick)
200
+ cursor = candidates.length > bounds.maxObservedPerTick ? start + looked.length : 0
201
+ const observed = []
202
+ for (const edit of looked) {
203
+ const entry = { editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId }
204
+ try {
205
+ const outcome = observeOne(workspace, edit, now)
206
+ recorded.add(edit.editId)
207
+ observed.push({ ...entry, status: 'observed', ...outcome })
208
+ } catch (error) {
209
+ if (!isTyped(error)) throw error
210
+ unobservable.set(edit.editId, error.code)
211
+ observed.push({ ...entry, status: 'refused', code: error.code })
212
+ }
213
+ }
214
+ return observed
215
+ }
216
+ }