@mnstry/atelier 0.2.0-alpha.5 → 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 (383) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +86 -25
  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/docs/assurance-controls.md +12 -2
  31. package/docs/atelier-runtime.md +13 -2
  32. package/docs/atelier-sync.md +172 -0
  33. package/docs/blocks/claims.md +28 -18
  34. package/docs/blocks/will-not-do.md +12 -3
  35. package/docs/coauthor-session.md +57 -0
  36. package/docs/connected-composition.md +75 -0
  37. package/docs/decisions.md +167 -0
  38. package/docs/exact-upgrades.md +159 -0
  39. package/docs/extension-pack-lifecycle.md +33 -0
  40. package/docs/guided-upgrades.md +99 -0
  41. package/docs/install.md +65 -9
  42. package/docs/intake-and-guides.md +65 -0
  43. package/docs/integration-and-review-plan.md +409 -0
  44. package/docs/integration-contract-decisions.md +22 -0
  45. package/docs/local-review.md +84 -0
  46. package/docs/local-services.md +227 -0
  47. package/docs/local-state.md +107 -0
  48. package/docs/obsidian-contract.md +687 -0
  49. package/docs/obsidian.md +387 -0
  50. package/docs/portable-presentation.md +328 -0
  51. package/docs/presentation-consumer-boundaries.md +75 -0
  52. package/docs/presentation-review-disposition.md +47 -0
  53. package/docs/project-command-inventory.md +38 -0
  54. package/docs/project-options.md +53 -0
  55. package/docs/release-engineering.md +99 -2
  56. package/docs/review-portability.md +55 -0
  57. package/docs/skill-steward.md +156 -0
  58. package/docs/upgrade.md +70 -2
  59. package/docs/vault-service.md +292 -0
  60. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  61. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  62. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  63. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  64. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  65. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  66. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  67. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  68. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  69. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  70. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  71. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  72. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  73. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  74. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  75. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  76. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  77. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  78. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  79. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  80. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  81. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  82. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  83. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  84. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  85. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  86. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  87. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  88. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  89. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  90. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  91. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  92. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  93. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  94. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  95. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  96. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  97. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  98. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  99. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  100. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  101. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  102. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  103. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  104. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  105. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  106. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  107. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  108. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  109. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  110. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  111. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  112. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  113. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  114. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  115. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  116. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  117. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  118. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  119. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  120. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  121. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  122. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  123. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  125. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  127. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  128. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  129. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  130. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  131. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  132. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  133. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  134. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  135. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  136. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  137. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  138. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  139. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  140. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  141. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  142. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  143. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  144. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  145. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  146. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  147. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  148. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  149. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  150. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  151. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  152. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  153. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  154. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  159. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  160. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  161. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  162. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  163. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  164. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  165. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  166. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  167. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  168. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  169. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  170. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  171. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  172. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  173. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  174. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  175. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  176. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  177. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  178. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  179. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  181. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  182. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  183. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  186. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  187. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  188. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  189. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  190. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  191. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  192. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  193. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  194. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  195. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  196. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  197. package/fixtures/obsidian/edits/workspace.json +65 -0
  198. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  199. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  200. package/fixtures/obsidian/materialization/workspace.json +54 -0
  201. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  202. package/fixtures/ui/presentation/pane.v1.json +7 -0
  203. package/fixtures/ui/presentation/reference.v1.json +293 -0
  204. package/package.json +61 -6
  205. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  206. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  207. package/skills/claude/atelier-open/SKILL.md +5 -0
  208. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  209. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  210. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  211. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  212. package/skills/codex/atelier-open/SKILL.md +5 -0
  213. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  214. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  215. package/src/access/preview-enforcer.mjs +84 -0
  216. package/src/boundary/content-rules.mjs +9 -4
  217. package/src/boundary/policy.mjs +74 -42
  218. package/src/cli/project-options.mjs +58 -0
  219. package/src/cli/run.mjs +61 -3
  220. package/src/coauthor/session.mjs +161 -0
  221. package/src/coauthor/store.mjs +173 -0
  222. package/src/collaboration/event-ledger.mjs +16 -33
  223. package/src/collaboration/inspection-bundle.mjs +223 -0
  224. package/src/collaboration/review-contracts.mjs +25 -0
  225. package/src/collaboration/review-store.mjs +305 -0
  226. package/src/commands/coauthor.mjs +26 -0
  227. package/src/commands/config.mjs +6 -1
  228. package/src/commands/extension-pack.mjs +15 -8
  229. package/src/commands/init.mjs +44 -5
  230. package/src/commands/obsidian.mjs +368 -0
  231. package/src/commands/review.mjs +147 -0
  232. package/src/commands/setup.mjs +51 -14
  233. package/src/commands/skills.mjs +199 -0
  234. package/src/commands/sync.mjs +107 -0
  235. package/src/composition/registry.mjs +27 -0
  236. package/src/composition/wire.mjs +122 -0
  237. package/src/contracts/corpus.mjs +35 -0
  238. package/src/decisions/contracts.d.mts +96 -0
  239. package/src/decisions/contracts.mjs +321 -0
  240. package/src/disclosure/content-scan.mjs +13 -0
  241. package/src/export/atelier-export-contract.mjs +16 -1
  242. package/src/extension-packs/lifecycle.mjs +154 -0
  243. package/src/graph/graph.mjs +64 -2
  244. package/src/graph/knowledge-graph.mjs +548 -24
  245. package/src/guides/contracts.mjs +54 -0
  246. package/src/index.mjs +29 -0
  247. package/src/intake/store.mjs +119 -0
  248. package/src/preview/channel.mjs +89 -0
  249. package/src/project/config.mjs +50 -49
  250. package/src/project/durable-state.mjs +161 -0
  251. package/src/project/file-class.mjs +166 -0
  252. package/src/project/private-state.mjs +11 -1
  253. package/src/projection/obsidian/contracts.mjs +571 -0
  254. package/src/projection/obsidian/edits/align.mjs +169 -0
  255. package/src/projection/obsidian/edits/apply.mjs +889 -0
  256. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  257. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  258. package/src/projection/obsidian/edits/index.mjs +19 -0
  259. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  260. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  261. package/src/projection/obsidian/edits/observe.mjs +123 -0
  262. package/src/projection/obsidian/edits/policy.mjs +123 -0
  263. package/src/projection/obsidian/edits/regions.mjs +648 -0
  264. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  265. package/src/projection/obsidian/materialize/index.mjs +7 -0
  266. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  267. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  268. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  269. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  270. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  271. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  272. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  273. package/src/projection/obsidian/proposals/index.mjs +23 -0
  274. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  275. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  276. package/src/projection/obsidian/proposals/router.mjs +168 -0
  277. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  278. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  279. package/src/projection/obsidian/publication/index.mjs +5 -0
  280. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  281. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  282. package/src/projection/obsidian/publication/transport.mjs +139 -0
  283. package/src/projection/obsidian/recovery/index.mjs +6 -0
  284. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  285. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  286. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  287. package/src/projection/obsidian/recovery/store.mjs +234 -0
  288. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  289. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  290. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  291. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  292. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  293. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  294. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  295. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  296. package/src/projection/project.mjs +28 -19
  297. package/src/readiness-protocols/evidence.mjs +282 -0
  298. package/src/readiness-protocols/runtime.mjs +4 -3
  299. package/src/readiness-protocols/source-read.mjs +39 -0
  300. package/src/runtime/git-adapter.mjs +189 -0
  301. package/src/runtime/local-state.mjs +439 -0
  302. package/src/runtime/obsidian/app-capability.mjs +110 -0
  303. package/src/runtime/obsidian/app-production-seams.mjs +96 -0
  304. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  305. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  306. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  307. package/src/runtime/obsidian/contributions.mjs +27 -0
  308. package/src/runtime/obsidian/documents.mjs +37 -0
  309. package/src/runtime/obsidian/enablement.mjs +53 -0
  310. package/src/runtime/obsidian/engine.mjs +532 -0
  311. package/src/runtime/obsidian/errors.mjs +14 -0
  312. package/src/runtime/obsidian/extension-points.mjs +103 -0
  313. package/src/runtime/obsidian/index.mjs +34 -0
  314. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  315. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  316. package/src/runtime/obsidian/observation.mjs +158 -0
  317. package/src/runtime/obsidian/opening.mjs +243 -0
  318. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  319. package/src/runtime/obsidian/pipeline.mjs +122 -0
  320. package/src/runtime/obsidian/private-lock.mjs +156 -0
  321. package/src/runtime/obsidian/process-identity.mjs +58 -0
  322. package/src/runtime/obsidian/service-client.mjs +66 -0
  323. package/src/runtime/obsidian/service-main.mjs +89 -0
  324. package/src/runtime/obsidian/service-record.mjs +169 -0
  325. package/src/runtime/obsidian/service-server.mjs +121 -0
  326. package/src/runtime/obsidian/service.mjs +216 -0
  327. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  328. package/src/runtime/obsidian/state-store.mjs +180 -0
  329. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  330. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  331. package/src/runtime/obsidian/watchers.mjs +36 -0
  332. package/src/runtime/repository-observation.mjs +491 -0
  333. package/src/runtime/supervisor.mjs +803 -0
  334. package/src/server/local-sidecar.mjs +44 -0
  335. package/src/server/server.mjs +1 -0
  336. package/src/skills/steward.mjs +780 -0
  337. package/src/ui/presentation/browser.mjs +278 -0
  338. package/src/ui/presentation/contract.mjs +116 -0
  339. package/src/ui/presentation/index.mjs +7 -0
  340. package/src/ui/presentation/native.mjs +153 -0
  341. package/src/ui/presentation/proof.mjs +57 -0
  342. package/src/ui/presentation/schema-check.mjs +30 -0
  343. package/src/ui/presentation/schema.generated.mjs +1371 -0
  344. package/src/ui/presentation/state.mjs +43 -0
  345. package/src/ui/presentation/styles.mjs +67 -0
  346. package/src/ui/presentation/tokens.mjs +74 -0
  347. package/src/ui/presentation/web.mjs +85 -0
  348. package/src/ui/review-page.mjs +32 -0
  349. package/src/upgrade/explanation.mjs +33 -0
  350. package/src/upgrade/provenance.mjs +338 -0
  351. package/src/upgrade/transaction-files.mjs +103 -0
  352. package/src/upgrade/transaction.mjs +475 -0
  353. package/src/upgrade/upgrade.mjs +63 -28
  354. package/src/vault/hosts.mjs +21 -0
  355. package/src/vault/identity.mjs +19 -0
  356. package/src/vault/index.mjs +7 -0
  357. package/src/vault/interface.mjs +11 -0
  358. package/src/vault/metadata.mjs +31 -0
  359. package/src/vault/privacy.mjs +148 -0
  360. package/src/vault/probe.mjs +89 -0
  361. package/src/vault/service.mjs +131 -0
  362. package/src/vault/source.mjs +45 -0
  363. package/src/vault/storage.mjs +23 -0
  364. package/templates/distribution-workspace/.claude/launch.json +6 -2
  365. package/templates/distribution-workspace/gitignore +1 -0
  366. package/templates/distribution-workspace/project/README.md +2 -2
  367. package/templates/external-project-workspace/README.md +45 -0
  368. package/templates/external-project-workspace/answers.example.json +4 -0
  369. package/templates/external-project-workspace/atelier.project.json +40 -0
  370. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  371. package/templates/external-project-workspace/gitignore +8 -0
  372. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  373. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  374. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  375. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  376. package/templates/external-project-workspace/source/README.md +11 -0
  377. package/templates/launch.json +6 -2
  378. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  379. package/templates/private-domain-workspace/domain/README.md +2 -2
  380. package/templates/private-domain-workspace/gitignore +1 -0
  381. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  382. package/templates/shared-project-workspace/gitignore +1 -0
  383. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,687 @@
1
+ # Obsidian projection contract
2
+
3
+ This document freezes what later work receives from the contract and
4
+ feasibility track. It states what is proven, on what, and what is not. Nothing
5
+ here enables a feature: a publisher exists under
6
+ `src/projection/obsidian/publication/`, and no command or service calls it yet.
7
+
8
+ ## Registered shapes
9
+
10
+ Eleven closed v1 schemas, `contracts/atelier-obsidian-<shape>.v1.schema.json`,
11
+ are registered in `src/contracts/corpus.mjs` with valid and invalid fixtures
12
+ under `fixtures/obsidian/contracts/<shape>/`. `src/projection/obsidian/contracts.mjs`
13
+ validates them and adds the refusals a schema cannot express.
14
+
15
+ | Shape | Portable | Purpose |
16
+ | --- | --- | --- |
17
+ | `corpus-profile` | yes | Workspace and repository identities, enrollment, audience |
18
+ | `scope` | yes | Mode and selector set-AST; an empty selection is valid and empty |
19
+ | `source-snapshot` | yes | Raw byte digests, graph pin, dirty state, single consistent read |
20
+ | `generation-manifest` | yes | Path map, byte regions, link inversion map, completeness |
21
+ | `publication-journal` | no | Conditional operations, partial transition, restart; protocol ID required |
22
+ | `service-state` | no | Literal loopback, port, runtime ID, PID, executable, consent |
23
+ | `edit-operation` | yes | Object identity, origin generation, base digest, idempotency key |
24
+ | `apply-policy` | no | `manual` or `automatic`; only `body-replacement` is an accepted edit class |
25
+ | `proposal-receipt` | yes | Repository-scoped store, adapter operation identity, dedupe outcome |
26
+ | `acceptance-receipt` | yes | Candidate identity, environment versions, evidence hashes, outcome |
27
+ | `ext-settings` | yes | The object under `ext["mnstry.atelier.obsidian"]` |
28
+
29
+ Portable shapes refuse absolute paths. `atelier-project-config.v1` is
30
+ unchanged: extension settings are validated by their own schema, and an
31
+ unknown extension key refuses in the adapter, not in the project validator.
32
+
33
+ ## Selection
34
+
35
+ `selectScope({ canonicalSnapshot, profile, selector, expansion })` is the only
36
+ runtime entry point. Visibility fails closed: a node is selectable only when it
37
+ is explicitly eligible, its repository is enrolled and its audience is allowed.
38
+ Edges to a withheld endpoint are dropped. Absent and withheld identities are
39
+ reported together. Expansion requires an explicit depth and node budget,
40
+ proceeds in canonical-identity order and reports truncation. Relation types are
41
+ `related`, `supports`, `supersedes`, `implements`, `depends_on`, `evidences`,
42
+ `contradicts`, `belongs_to` and the derived `links_to`. Literal oracles and
43
+ refusals live in `fixtures/obsidian/contracts/oracles/scope-cases.json`.
44
+
45
+ ### What redaction covers
46
+
47
+ Emission is byte-faithful: a visible author's bytes are emitted unchanged. A
48
+ visible author's own references to withheld or out-of-selection documents
49
+ (canonical identities in front matter, repository-relative paths, link text)
50
+ therefore appear in the vault exactly as written. Only generated bytes are
51
+ redacted: generated regions, note paths, manifest entries and link inversions
52
+ never carry a withheld node's identity, title, summary, tags or path. A scoped
53
+ vault is not a confidentiality boundary against what visible authors wrote. A
54
+ redaction boundary over authored bytes cannot coexist with byte-faithful
55
+ emission; it would be a separate mode with its own evidence.
56
+
57
+ Each note may carry the generated line `Relationships leading outside this
58
+ view: N`. N counts that note's relationships to notes that are visible but not
59
+ selected, so it reveals that count, and two views of one workspace can be
60
+ compared. It never counts a withheld node: edges to a withheld endpoint are
61
+ dropped before the count is taken.
62
+
63
+ ## Note paths
64
+
65
+ `notes/<readable title>--<identity suffix>.md`. The suffix derives from the
66
+ stable repository identity plus node identity, starts at 12 hexadecimal
67
+ characters and lengthens on collision. Titles are Unicode-normalized, platform
68
+ reserved names are avoided, and case-folding collisions are detected. Duplicate
69
+ titles are allowed; duplicate canonical identities refuse; a title-only link
70
+ that matches more than one visible note refuses. Paths are allocated once per
71
+ workspace and reused by every view.
72
+
73
+ ## Graph behaviour changes since the shared link resolver
74
+
75
+ The resolver that produces `links_to` edges changed in these ways, each pinned by a
76
+ test in `test/graph-knowledge-graph.test.mjs`. Repository artifacts committed by a
77
+ consumer may differ after upgrading in exactly these classes and no others:
78
+
79
+ 1. Links inside fenced code (backtick or tilde, any info string, up to three
80
+ spaces of indent, CRLF, CommonMark fence-length rules), inside inline code
81
+ (including two stray backticks that happen to pair across a link) and inside
82
+ front matter no longer produce edges. Links after an unbalanced fence that
83
+ runs to the end of the file are inside code.
84
+ 2. A link to a directory resolves to that directory's `README.md`, then
85
+ `index.md`, testing eligibility per candidate; a link to a parent directory
86
+ now resolves where the earlier reader missed it.
87
+ 3. A link that climbs above its own repository root and re-enters through the
88
+ checkout's directory name is reported as leaving the enrolled roots, as it
89
+ always was; it is never turned into a repository-local edge.
90
+ 4. Malformed percent-encoding in a link is a `link-href-malformed` finding;
91
+ it no longer throws out of the graph build.
92
+ 5. The workspace graph (not repository artifacts) additionally carries
93
+ wikilink edges and cross-repository Markdown-link edges, de-duplicated.
94
+
95
+ ## Embedded assets
96
+
97
+ An embed (`![](file)` or `![[file]]`) whose target is not a document of the
98
+ census resolves in the canonical graph, never in the emitter. The resolver
99
+ reports `embeds` beside `links`: one `embeds_asset` record per occurrence, with
100
+ the same UTF-16 and byte offsets, naming an asset
101
+ `{ id, repo, path, extension }` whose id is `<repository>:asset:<path>`.
102
+ `buildCanonicalGraph` returns `embeds` and the de-duplicated `assets`. Neither
103
+ is a node or an edge: committed graph artifacts and `markdownLinkEdges` are
104
+ byte-identical with and without assets.
105
+
106
+ An asset is a regular file inside an enrolled repository root. A link on disk,
107
+ a file reached through one, a git-ignored path, anything inside `.git`, a
108
+ Markdown file and a census node are never assets. Markdown embeds resolve by
109
+ relative path like links. A wikilink embed whose target contains `/` resolves
110
+ by repository-relative path, in the source's repository first and then as
111
+ `<repository>/<path>`; a bare file name resolves by basename across enrolled
112
+ repositories and refuses as `link-target-ambiguous` when more than one file
113
+ matches. `isAssetEligible({ repo, path })` fails closed and is asked per
114
+ candidate before choosing: a refused asset is reported exactly as an absent
115
+ one (`link-target-unresolved`), so a finding never confirms that a withheld
116
+ file exists.
117
+
118
+ Behaviour change: an embed that resolves to an asset no longer produces a
119
+ `link-target-unresolved` finding. No other finding changes.
120
+
121
+ In a view, an asset is copied only when the embedding note is in the vault
122
+ set, the asset record carries `eligible: true` (`withEligibility` takes a
123
+ second predicate; without it every asset is withheld) and the profile enrols
124
+ its repository under the same audience rule as a node without an audience of
125
+ its own. It is copied once, byte for byte, to
126
+ `attachments/<readable stem>--<identity suffix>.<ext>`, read through the pinned
127
+ snapshot (`source-not-in-snapshot`, `mixed-read`). The embed target is
128
+ rewritten through the link inversion machinery: a Markdown embed to the
129
+ percent-encoded vault-root path, a wikilink embed to the plain path with its
130
+ size or fragment left as authored and no alias added. The manifest lists the
131
+ copy in `attachments[]` with
132
+ `ext["mnstry.atelier.obsidian"] = { kind: "embedded-asset", repoId, assetPath }`
133
+ and records the inversions on the embedding note at
134
+ `notes[].ext["mnstry.atelier.obsidian"].assetEmbeds = [{ attachment, inversions }]`,
135
+ in the shape of link inversions. A withheld or out-of-selection asset leaves
136
+ the authored embed untouched and appears in no output, manifest entry or
137
+ diagnostic.
138
+
139
+ ## Unclosed code fence at the end of a source
140
+
141
+ An authored Markdown body that ends inside a fenced code block would turn any
142
+ generated section after it into code. When, and only when, a generated section
143
+ follows such a body, the emitter writes a closing fence as the first bytes of
144
+ the first generated region. The fence is generated, not authored: it lies
145
+ inside that region's range, so authored ranges and inversion stay exact. It
146
+ repeats the opener's indentation (0 to 3 spaces), character and length, starts
147
+ on its own line (a line break is added first when the body has none) and uses
148
+ the source's line ending, CRLF or LF. The region records it as
149
+ `ext["mnstry.atelier.obsidian"].fenceClosure = { fence, byteLength }`, where
150
+ `fence` is the emitted fence line without its line ending and `byteLength`
151
+ counts every closure byte, line breaks included. The view is not refused; its
152
+ diagnostics carry `unclosed-code-fence-closed-in-generated-region`. With no
153
+ generated section, nothing is emitted and nothing is reported.
154
+
155
+ Fence detection is the canonical graph scanner's: `unclosedFenceAtEnd` in
156
+ `src/graph/knowledge-graph.mjs` shares the rules that decide which text is
157
+ scanned for links. Front matter is never read for fences.
158
+
159
+ ## Incremental preparation
160
+
161
+ A maintenance tick rebuilds the canonical graph and prepares the whole view.
162
+ Both stages accept a cache that makes repeated runs proportional to what
163
+ changed, without changing a single emitted byte. Each cache is derived,
164
+ in-memory, droppable state: it is never written anywhere, a dropped cache
165
+ costs a full build or preparation, and the result is by construction the one
166
+ a run with no cache produces. `test/obsidian-incremental.test.mjs` proves the
167
+ equality over the materialization fixture under both scopes and over random
168
+ change sequences (edits, retitles, relations, links, added and removed notes,
169
+ eligibility and scope changes), and shows with mutation controls that the
170
+ oracle sees a wrong cached byte, entry, inversion, node or scan.
171
+
172
+ Graph stage: `buildKnowledgeGraph` / `buildCanonicalGraph` take `fileCache`
173
+ (`createGraphFileCache`) and, optionally, `observedDigest`. A census node and
174
+ link scan are reused only under an equal sha256 of the bytes and equal
175
+ per-file inputs outside the bytes (coverage and the repository's read
176
+ boundary), and the cache is rebuilt to hold exactly the current census.
177
+ Without `observedDigest` every Markdown source is read and hashed on every
178
+ build. The maintenance engine passes `observedDigest` from its observation
179
+ index, and a source whose observed digest equals the cached one is then not
180
+ opened: the bound is observation's own, a stat hint between full
181
+ reconciliations, so bytes that change under an unchanged stat hint are not
182
+ seen by the graph, the snapshot or the view until observation hashes the file
183
+ again. The result reports `fileCensus: { reused, derived, read }`.
184
+
185
+ Preparation stage: `prepareView` takes `cache` (`createPreparationCache`).
186
+ Each note's bytes and manifest entry are a function of its pinned source
187
+ digest, node record, allocated path, generated rows, outside-selection count
188
+ and rewritten occurrences with their emitted targets; those inputs form a
189
+ dependency key, and a note whose key equals the cached one reuses the cached
190
+ bytes, manifest entry, attachment record and inversions. Everything else
191
+ (asset copies, links, settings, collision checks, the manifest and the
192
+ redaction guard over the whole result) runs as before. The result reports
193
+ `preparation: { emitted, reused }`, and a preparation that emits exactly the
194
+ notes whose output changed is what the tests pin.
195
+
196
+ One bound is stated rather than hidden: a reused note's source is not read
197
+ again, so bytes that drift under an unchanged pinned digest are not seen by
198
+ `mixed-read` on that call. The emitted note is exactly the one the pin
199
+ describes, and the next observation by digest sees the drift. A note whose pin
200
+ changed is read and verified against the pin as always.
201
+
202
+ The engine holds one graph file cache and one preparation cache per scope for
203
+ its lifetime and hands them through the `createGraphCache` and
204
+ `createPreparationCache` production seams; a test replaces either with
205
+ `() => null` to build or prepare in full.
206
+
207
+ ## Publication protocol `obsidian-cli-critical-section/v1`
208
+
209
+ The journal's `protocolId` names this protocol. A publisher may use it only
210
+ within the proven boundary below.
211
+
212
+ 1. Stage the candidate on the vault's volume and bind it by SHA-256. A
213
+ candidate that replaces an existing file is staged in that file's unit
214
+ recovery directory (see "Where candidates are staged").
215
+ 2. Inside the app, in one synchronous step: refuse if any editor of the note in
216
+ any window is unsaved or differs from the expected base; refuse if the bytes
217
+ on disk differ from the expected base; refuse a staged file whose digest
218
+ differs; atomically exchange the staged candidate with the note, so
219
+ whatever occupied the path becomes the recovery file; update every open
220
+ editor in one transaction and record the view as saved with that content.
221
+ The app must not write the note as a result of publication.
222
+ 3. Reply immediately. Record the outcome in the app. A caller whose reply is
223
+ lost re-reads the outcome and never resends.
224
+ 4. Re-check the recovery file after a quiet period: a program that held the
225
+ note open before the exchange writes into it.
226
+ 5. Commit the trusted manifest last. Until then the transition is reported as
227
+ updating. A view converges to one verified generation; atomic visibility
228
+ across notes is not claimed.
229
+
230
+ A refusal is always an acceptable outcome. A note being edited stays one
231
+ generation behind until its editor is clean.
232
+
233
+ ### Where candidates are staged
234
+
235
+ The exchange in step 2 leaves the displaced bytes at the candidate's path until
236
+ they are moved to their recovery name. Those bytes may be a person's only copy,
237
+ and `staging/` is classed discardable, so that path is never in `staging/`.
238
+ This is a layout inside the private store; the protocol is unchanged.
239
+
240
+ | Bytes | Path |
241
+ | --- | --- |
242
+ | Any candidate while it is written and fsynced (generated bytes only) | `staging/<journalId>/NNNNNN.candidate`, or `NNNNNN.late.candidate` for one that could not be known when the run began |
243
+ | Candidate of a file that is created (exclusive link, no exchange) | stays at that staging path until it is linked |
244
+ | Candidate of a file that is replaced (note, attachment, policy settings) | moved, complete, to `recovery/<journalId>/<unit>/exchange.candidate`, or `exchange.late.candidate` |
245
+ | Displaced bytes | the vault path, then that same candidate path, then `recovery/<journalId>/<unit>/displaced.bin` |
246
+
247
+ The journal names every exchange candidate, by path and SHA-256, before the
248
+ file is at that path: in the header for candidates known when the run began, in
249
+ a write-ahead entry for a late one. A file at a candidate path whose digest is
250
+ the recorded one is a generated candidate and may be deleted; any other file
251
+ there is displaced bytes and is moved to its recovery name with a receipt,
252
+ never deleted. Restart recovery decides only this way. The candidate names end
253
+ in `.candidate`; every other name in a unit directory ends in `.bin` or
254
+ `.json`. The same-volume check covers staging and recovery, and the exchange
255
+ self-test runs inside the recovery area, in `recovery/.exchange-probe/`.
256
+
257
+ ### One publisher per vault
258
+
259
+ A publication holds two locks: the view's, in its private state, and the
260
+ vault's. Two views, or two sets of workspace state, that point at one vault
261
+ share nothing but the vault, so the vault lock lives under the vault's real
262
+ path in `.atelier-publication/`, a dot-directory that no note path can name
263
+ and that the app does not show. The second publisher refuses with
264
+ `publication-in-progress` and writes nothing. It is the only thing the
265
+ publisher writes in a vault besides notes, attachments and the policy
266
+ settings file.
267
+
268
+ Release and recovery follow the view lock exactly. A finished publication
269
+ writes a release marker. A publisher that dies leaves a ticket naming its
270
+ process; the next publisher on the same host sees that the process is gone and
271
+ takes the lock over, so a crash does not wedge the vault. Superseded tickets
272
+ are removed while the lock is held. Two cases refuse until a person acts: a
273
+ ticket written on another host (a vault reached through a shared or
274
+ synchronized folder), and an unrelated live process that reuses the dead
275
+ publisher's process ID, which clears when that process exits. When no
276
+ publisher is running anywhere, deleting `.atelier-publication/` is safe.
277
+
278
+ ### The path with no app
279
+
280
+ When the process table shows, positively, that no Obsidian runs, the same
281
+ critical section runs in the publisher's own process with no editor to
282
+ coordinate with. The table is read at path selection, again immediately before
283
+ the first note whatever time has passed, and again whenever two seconds have
284
+ passed since the last reading. Once a reading is anything other than absent,
285
+ every remaining note that would be written refuses.
286
+
287
+ Residual window, stated plainly: an app that starts after a reading and before
288
+ the next is not seen, for at most two seconds plus one note's publication. In
289
+ that window a note is exchanged with no editor check. Bytes saved to disk are
290
+ still protected by the on-disk comparison and the exchange. An unsaved buffer
291
+ in the newly started app is not: the app then takes its own
292
+ external-modification merge, which can drop overlapping edits. The probe also
293
+ cannot see an app on another machine that reaches the vault through a shared
294
+ or synchronized folder, or an app packaged under another executable name.
295
+
296
+ ### Proven boundary
297
+
298
+ macOS (Darwin 25, arm64) with Obsidian 1.13.7 (installer 1.12.7), CLI enabled,
299
+ no community plugin. Sixteen interleavings, 25 rounds for each racing case, in
300
+ four complete clean runs of the prototype in `experiments/obsidian-publication/`
301
+ at commits `34f5fab` and `c5142e7`. The receipts record those same trees under their
302
+ pre-sign-off identities `5930f85` and `93303ff`; the branch was rewritten only to add
303
+ sign-off trailers, with identical content. Receipt SHA-256 digests:
304
+
305
+ - `379992a6cc0e6c95581459412e8cab68d9b260a631fa891089613f120b40a67a`
306
+ - `63a5b69b203125b6099f2398d1ebddd0e4f3265a0401becbddff5e6d72fee16e`
307
+ - `95995051859e02042d648d1a0b513d86fa9f1d72d82359030e21858cdba5d959`
308
+ - `0984c35087ff91c0cc0d0f37f533eea1fc6c6380705491362eec98368f64fe01`
309
+
310
+ A fifth run aborted because the harness could not open a note after the
311
+ second-window case; no bytes were involved. The receipts are maintainer-held
312
+ and are not part of this repository.
313
+
314
+ ### Receiving obligations for the production publisher
315
+
316
+ The publisher may not claim the protocol outside the proven boundary until
317
+ each open item is discharged with its own evidence.
318
+
319
+ | Obligation | State |
320
+ | --- | --- |
321
+ | Atomic exchange without the system Python | Done. The system Python is gone. The exchange is a raw syscall (`renameatx_np` with `RENAME_SWAP` on macOS, `renameat2` with `RENAME_EXCHANGE` on Linux) reached through the system perl, with no perl module, compiler or package dependency. |
322
+ | Interpreter trust | Done. The perl binary is used only when uid 0 owns it and neither group nor others can write it, in the publisher and in the script the app runs; otherwise `exchange-interpreter-untrusted`. An interpreter is still a dependency, and the exchange still costs a process start inside the critical section. |
323
+ | Exit status of the exchange helper | Done. The critical section reads the staged path after the call and decides from the bytes whether the exchange took place; a helper killed after the call returned no longer reads as a failure. |
324
+ | Linux | Partly open. The exchange primitive is proven in a container on aarch64. The app suite (the interleavings above) has not been run on Linux. |
325
+ | x86_64 | Open. Never run, on macOS or Linux; the syscall numbers are present and untested. |
326
+ | Windows | Refuses. No direct equivalent is known; the publisher refuses with `exchange-unsupported-platform` and publishes nothing. |
327
+ | App capability floor | Partly open. The step that prevents an app write sets the view's undocumented `lastSavedData`; open notes refuse when it is absent. The floor is pinned: `MINIMUM_APP_VERSION` in `src/runtime/obsidian/app-capability.mjs` is 1.13.7, the only version the protocol was proven on, and an older, unreadable or unknown version is refused as `app-version-unsupported`. There is no ceiling: a newer app is admitted, and the cases must be re-run on each app release. |
328
+ | Late-writer re-check | Partly open. The publisher re-checks displaced files twice per publication (on the blocking path, and after the quiet period). A holder can write later still, so the check must be repeated by the future maintenance service; carried to the maintenance track. |
329
+ | Displaced bytes between exchange and recovery move | Done, by the maintenance track's staging layout change. The exchange still leaves the displaced bytes at the candidate's path until the next step moves them to their recovery name, and that path is now in the unit recovery directory: candidates for replaced files are staged there, and staging holds only generated bytes. A crash in between is settled by restart recovery from the journal, which names that path and the candidate's digest. A staging sweeper is still not provided; any future one may discard `staging/` only and must never touch `recovery/`. |
330
+ | Transport | CLI replies are occasionally lost while the app stays responsive. Calls are serialized; every call is idempotent or outcome-recorded, and a publish is never resent. |
331
+ | Timer throttling | A hidden app window delays the app's own autosave. Do not read that as a fault. |
332
+ | Unreproduced anomaly | One early run ended with typed text on disk but absent from the editor buffer. It did not recur in any later run. Keep the typing-race case in every qualification run and treat a recurrence as a failed gate. |
333
+ | Link scanner cost | Open, carried. The Markdown link scanner is roughly quadratic in skipped regions times link occurrences on pathological inputs; fine for ordinary documents, minutes for several megabytes of adversarial Markdown. |
334
+ | Link-then-rename | Rejected. It leaves a window in which a concurrent replacement is destroyed. |
335
+ | App-driven save after replacement | Rejected. Observed losing an outside writer's bytes; kept as a negative control. |
336
+
337
+ Source application back into canonical files carries the same conditional-write
338
+ obligation against other source writers; its protocol follows.
339
+
340
+ ## Source apply protocol `source-apply-exchange/v1`
341
+
342
+ Source apply writes one edit made in a vault back to the one source file the
343
+ note was generated from. It is the only operation of this integration that
344
+ writes a source file. A person's explicit Apply (`atelier obsidian apply run
345
+ EDIT [--actor ID]`, or the same call through the API by an agent acting for them)
346
+ and an automatic policy reach the same function; they differ only in who
347
+ authorises. It never stages, commits or otherwise asks git to change anything:
348
+ its two git calls ask whether the path is ignored and where the git directory
349
+ is.
350
+
351
+ ### Who authorises
352
+
353
+ | Mode | Authority | Actor recorded |
354
+ | --- | --- | --- |
355
+ | `manual` | an explicit request that names the edit; the integration must be enabled | the request's actor, else `manual-request` |
356
+ | `automatic` | the installed policy, read from private state at the decision and again immediately before the write: machine mode `automatic`, status `active`, the policy's recomputed digest equal to the digest it carries, the object inside the policy's `selector`, `body-replacement` in `allowedEditClasses`, retry budget not spent | the policy's actor, with the policy identifier and digest |
357
+
358
+ In both modes the object must be visible in the canonical graph as it is now
359
+ (eligible, enrolled, audience allowed). An absent object and a withheld one get
360
+ the same answer, `object-not-visible`, and they get it first: before the
361
+ manifest, the record of the object or the source path is looked at, with
362
+ nothing recorded, so a moved or deleted source, an earlier apply or a missing
363
+ manifest never tells the two apart. A source deleted from the corpus is
364
+ therefore answered `object-not-visible` as well. There is no ambient agent mode: an automatic request with no
365
+ matching active policy is refused.
366
+
367
+ The digest of a policy is `sha256:` and the hex SHA-256 of its canonical form:
368
+ the policy document without its `digest` member, keys sorted at every depth,
369
+ two-space indentation, one final newline, UTF-8. `atelier obsidian policy
370
+ digest FILE` prints it and writes nothing; `policy install FILE` recomputes it
371
+ and refuses `policy-digest-mismatch`, naming the digest the file has to carry.
372
+ The file is never rewritten for the person. `maxBatchSize` bounds an
373
+ automatic dispatch (the engine's, and `applyBatch` in automatic mode); a batch
374
+ a person names through `applyBatch` is bounded too, by 1000, the largest bound
375
+ a policy can carry, and the rest of either batch refuses `batch-bound-reached`. `retryBudget` allows one attempt and
376
+ that many retries per operation under one revision of the policy; it is counted
377
+ over the refusals recorded in the events of the object, so a restart does not
378
+ refill it, and a spent budget refuses before anything is recorded.
379
+ `conflictDisposition` is `hold`: a conflict stays queued with its bytes, and
380
+ nothing a policy says overrides a stale source.
381
+
382
+ ### Order of one apply
383
+
384
+ 1. Resolve the workspace and the pending edit. Build the canonical graph now
385
+ and ask whether this machine may see the object. Then resolve the view and
386
+ the immutable manifest of the generation the edit was observed under.
387
+ 2. The identity must still name the path the
388
+ manifest recorded, in an enrolled repository. That path must be a regular
389
+ file with one name, reached through no symbolic link, inside the repository,
390
+ outside every managed root and every git directory, and not git-ignored.
391
+ A git directory is any path with a segment spelled `.git` in any case (a
392
+ nested repository, a submodule), anything under `<root>/.git`, and anything
393
+ under the directory git itself names for the repository, which a `gitdir:`
394
+ file can place elsewhere; a git that cannot name it refuses
395
+ `source-ignore-state-unknown`. The only git calls ask whether the path is
396
+ ignored and where the git directory is.
397
+ 3. Read the source. Run the lens from the preserved edit bytes, never from the
398
+ note as it is now. Record the observation of this edit and of every other
399
+ open edit of the same object, so a divergent edit in another view makes the
400
+ object conflicted before anything is written.
401
+ 4. Take the object lease. An earlier apply whose outcome is unknown is settled
402
+ first, from digests on disk.
403
+ 5. Refuse, writing nothing: a repeated request (answered from the record), a
404
+ stale source, a conflicted object, a lens refusal, a change outside the
405
+ authored body, a result equal to the source, the policy, a volume that is
406
+ not the source's, a missing exchange.
407
+ 6. Write the apply record, then the candidate (the new source bytes, with the
408
+ source's mode, fsynced) in `recovery/<applyId>/000000/` of the private
409
+ workspace state.
410
+ 7. Record `apply-intent` in the object store.
411
+ 8. Read the policy again. Exchange the candidate with the source atomically.
412
+ 9. Read what the exchange displaced. Equal to the base: it stays as the
413
+ retained backup under its recovery name, with a receipt binding its digest;
414
+ the source is verified; `applied` is recorded with old and new digests, the
415
+ actor and the policy. Not equal: another program saved the source between
416
+ the read and the exchange. Its bytes are retained as an immutable object and
417
+ the files are exchanged back, so its bytes return to the source path; what
418
+ that displaces must be the candidate, and anything else is kept with a
419
+ receipt. `apply-refused`, `concurrent-source-writer`, with every reference.
420
+ Nothing is retried inside one call. An exchange that reports a failure is
421
+ not believed either way: only the candidate still at its path beside the
422
+ source as it was read means that nothing was exchanged
423
+ (`exchange-unavailable`); anything else is decided as restart recovery
424
+ decides it, so an exchange that did take place is recorded as applied
425
+ (`applied-after-restart`) with its backup.
426
+ 10. After a quiet period the backup is read again. A program that opened the
427
+ source before the exchange still holds the old file and can write into it
428
+ at any later time: `source-changed-after-apply`, both byte sets retained.
429
+ An applied source leaves a closed journal of this protocol beside the
430
+ publication journals of the view, so the maintenance engine's late-writer
431
+ re-check covers the backup on the tick of the apply and on later ticks,
432
+ for as long as it covers a publication.
433
+
434
+ The candidate is never written inside a repository working tree, where a stray
435
+ file could be committed by somebody. An exchange cannot cross a volume, so the
436
+ private workspace state and the source must share one; otherwise the apply
437
+ refuses `apply-volume-mismatch`. Where no atomic exchange exists (Windows
438
+ today) it refuses `exchange-unavailable`. Both write nothing.
439
+
440
+ ### Refusals
441
+
442
+ | Code | When |
443
+ | --- | --- |
444
+ | `integration-disabled`, `workspace-not-prepared`, `unknown-edit`, `foreign-workspace`, `unknown-scope`, `edit-not-open` | the request cannot be resolved |
445
+ | `corpus-unreadable` | the canonical graph cannot be built on this machine: an enrolled file may not be read; no file is named |
446
+ | `manifest-unavailable`, `published-note-unavailable` | the generation's manifest, or the note as it was published, cannot be established |
447
+ | `repository-not-enrolled`, `source-not-in-graph`, `source-moved` | the identity of a visible object no longer names that path: a renamed or moved source (a deleted one is `object-not-visible`) |
448
+ | `source-missing`, `source-symlink`, `source-not-regular-file`, `source-unreadable`, `source-hard-linked`, `source-outside-repository`, `source-inside-managed-root`, `source-inside-git-directory`, `source-git-ignored`, `source-ignore-state-unknown` | the path is not one this operation writes |
449
+ | `invalid-apply-request`, `object-not-visible`, `edit-class-not-allowed`, `conflict-disposition-unsupported`, `maintenance-mode-manual`, `no-apply-policy-installed`, `apply-policy-revoked`, `apply-policy-paused`, `apply-policy-invalid`, `apply-policy-reference-mismatch`, `policy-digest-mismatch`, `policy-changed-since-dispatch`, `policy-selector-invalid`, `outside-policy-selection`, `retry-budget-exhausted`, `batch-bound-reached` | the decision |
450
+ | `stale-source`, `object-conflicted`, `sibling-edit-unobservable`, `lease-held` | arbitration; the operation stays conflicted or pending with its bytes |
451
+ | `edit-not-applicable`, `change-outside-authored-body`, `no-source-change` | the lens result is not an applicable body replacement |
452
+ | `exchange-unavailable`, `apply-volume-mismatch` | this machine cannot write conditionally here |
453
+ | `concurrent-source-writer`, `source-changed-during-apply` | another program wrote the source during the apply; every byte is retained |
454
+ | `interrupted-before-exchange`, `apply-interrupted-needs-person` | what restart recovery decided for an interrupted apply |
455
+ | `recovery-state-unreadable` | a candidate, backup or source path of an interrupted apply may not be looked at or read; nothing is settled from a read this process was denied, that record is reported with its intent still open, and every other one is still settled |
456
+ | `apply-outcome-unknown` | the source was exchanged and the settlement from digests could not be carried out; the source may have been changed, the intent stays open and `apply recover` decides |
457
+
458
+ A path that another program removes or replaces between two steps, before the
459
+ intent is recorded, answers one of these refusals (`source-missing`,
460
+ `source-not-regular-file`, `workspace-not-prepared`), never an exception; one
461
+ that this process may not look at or read answers `corpus-unreadable` while the
462
+ graph is built and `source-unreadable` from then on. The
463
+ mode of the source is read from the descriptor its bytes were read from.
464
+
465
+ ### Restart recovery
466
+
467
+ An `apply-intent` with no outcome is never guessed. `atelier obsidian apply
468
+ recover`, and the next apply of the same object, decide from digests on disk.
469
+ Only bytes with the recorded candidate digest are ours to delete. Every
470
+ interrupted apply is settled on its own: an object whose record cannot be read
471
+ is reported with its code, left exactly as it is, and delays no other. `apply
472
+ list`, `show`, `run` and `recover` answer such a record as a typed refusal.
473
+
474
+ | At the candidate path | Source | Decision |
475
+ | --- | --- | --- |
476
+ | the candidate | anything | nothing was exchanged, or it was exchanged back: the candidate is retired, `interrupted-before-exchange` |
477
+ | the base | the candidate | applied: the base becomes the backup, `applied` is recorded |
478
+ | anything else | anything | everything is kept with receipts, `apply-interrupted-needs-person` |
479
+ | nothing, backup recorded as the base | the candidate | applied |
480
+ | nothing, no backup | the base | `interrupted-before-exchange` |
481
+
482
+ ### Limits
483
+
484
+ - The applied source is a new file: a hard link is refused up front, extended
485
+ attributes and ownership are those of the candidate, and only the permission
486
+ bits are carried over.
487
+ - A hard link is refused when the path is checked. One that another program
488
+ creates on the source after that check and before the exchange keeps the old
489
+ bytes under its other name; the source path itself ends as the applied file,
490
+ and nothing detects the second name.
491
+ - Every component of the source path is checked for a symbolic link, and the
492
+ file itself is opened without following one. A directory component that
493
+ another program replaces with a symbolic link after that check and before the
494
+ exchange is not detected; what contains it is the commit rule: unless the
495
+ file the exchange displaced holds exactly the bytes that were read, the files
496
+ are exchanged back and everything is retained.
497
+ - When the exchange back reports a failure, or the displaced file cannot be
498
+ retained first, nothing is guessed and the report is not believed either way:
499
+ the apply is settled from the digests on disk by the restart table above.
500
+ If the exchange back did not take place, the source path keeps the candidate,
501
+ whole, the other program's bytes stay where recovery references name them,
502
+ and the answer is `apply-interrupted-needs-person`. If it did take place
503
+ before it reported the failure, the source path holds the other program's
504
+ bytes, the candidate is retired, and the answer is
505
+ `interrupted-before-exchange`. If the digests show that the first exchange
506
+ displaced the base after all, the apply happened and is answered as applied.
507
+ - A refusal says that nothing was written. From the moment the first exchange
508
+ may have taken place (a clean exchange, or one that reported a failure while
509
+ the digests no longer show the untouched state), a failure is therefore never
510
+ returned as a plain refusal with the intent open: it is settled from the
511
+ digests on disk, and only when that settlement itself cannot be carried out
512
+ is the answer `apply-outcome-unknown`, which says that the source may have
513
+ been changed and that `apply recover` decides. Once the outcome is durable
514
+ that answer is no longer given.
515
+ - A read that this process is denied (a permission or I/O error) proves
516
+ nothing about a file, unlike a path that is no longer a regular file. It is
517
+ never taken as evidence that the source changed or that another program
518
+ wrote: the apply is settled again from digests when the path can be read.
519
+ A typed failure of the late-writer check after a durable apply leaves the
520
+ answer applied; the engine repeats that check.
521
+ - An enrolled file this process may not read while the canonical graph is built
522
+ refuses `corpus-unreadable`, naming no file. A source that cannot be read at
523
+ the moment of the apply refuses `source-unreadable`. Both carry the system's
524
+ error code as the cause. Only bytes with the recorded candidate digest are
525
+ ours to delete, so a file that cannot be read is never retired as a generated
526
+ candidate.
527
+ - Where no atomic exchange exists the whole apply half of the test suite is
528
+ skipped: on such a platform the only executed evidence is that apply refuses
529
+ `exchange-unavailable` and writes nothing.
530
+ - Lifting the hold after an apply relies on the publisher: it reads the note
531
+ again under its own lock and settles it as already current only while the
532
+ note still holds the prepared bytes. The engine's own read of the held note
533
+ narrows the window; the publisher's expected-bytes check closes it.
534
+ - Between the exchange and the exchange back, readers of the source path see
535
+ the candidate for a moment. A third write in that moment is kept: the source
536
+ ends as one whole version and the other is in recovery with a receipt.
537
+ - A program that holds the old file open and writes later has its bytes
538
+ retained and surfaced, not merged: deciding what the source should be is a
539
+ person's work.
540
+ - Generated regions are never applied. The edited note must still end with the
541
+ exact generated bytes, or have lost them whole while the end of its authored
542
+ text is provably the published end: the last authored line and its line
543
+ endings byte for byte, followed by nothing but what is left of the separator
544
+ or fence closure. A region cut in the middle of a line from either side, a
545
+ fragment of one left in the body, and text typed at the very end of the body
546
+ together with a removed region all refuse `generated-region-edited`. Some of
547
+ those refusals a person could have been spared; a generated byte in a source
548
+ is never accepted in exchange.
549
+ - After an apply the next tick prepares the view again. When the prepared note
550
+ is byte for byte the note the person edited (read from the vault at the
551
+ moment of that decision, never taken from the engine's observation index; a
552
+ note that cannot be read stays held), it is published over nothing,
553
+ the hold lifts and the pending record closes as `withdrawn`; the `applied`
554
+ record of the object store is the authority. When it is not (the person
555
+ removed a generated region, for example) the view stays held.
556
+ - `--actor` is an option of `apply run`. The command has one option table for
557
+ every operation; every other operation, of `apply` or not, refuses it as a
558
+ usage error and does nothing.
559
+ - Proven on macOS arm64 on APFS with a real second process. Linux, x86_64 and
560
+ other filesystems carry the open obligations of the exchange listed above.
561
+
562
+ ## Proposal adapter for structural edits
563
+
564
+ An edit the byte lens cannot turn into source bytes (a new or changed link to
565
+ another note of the vault, an edited front matter) is recorded as an edit
566
+ operation of kind `semantic-proposal`, state `proposed`. It is never applied.
567
+ The proposal adapter (`src/projection/obsidian/proposals/`) turns each such
568
+ operation into exactly one copy-only proposal in the existing proposal store of
569
+ the repository that owns the source, so that a reviewer of that repository sees
570
+ one durable request. It writes no source file and no vault, it never accepts or
571
+ applies a proposal, and no status of a proposal is an instruction to it.
572
+
573
+ The adapter is a contribution (`src/runtime/obsidian/contributions/`). On a
574
+ tick the engine first lets it observe the open pending edits (below), then,
575
+ after the automatic dispatch, hands it a copy of the pending edits once.
576
+
577
+ ### Observation on a tick
578
+
579
+ An operation exists once the edit has been observed: the source is read now,
580
+ the lens runs over the PRESERVED edit bytes (never the live note) against it,
581
+ and the resulting edit operation is recorded in the object store. Source apply
582
+ does this before it takes its lease. The adapter does the same on every tick
583
+ (`src/projection/obsidian/proposals/observation.mjs`), through the same
584
+ observer and the same store, for every open pending edit the object store does
585
+ not know yet, in manual and in automatic mode alike, so that a structural edit
586
+ becomes `proposed`, and is routed in the same tick, without anybody running
587
+ `atelier obsidian apply run EDIT`. What it records is what an apply would have
588
+ recorded: a body replacement `pending`, which stays queued and is written only
589
+ by apply; a structural edit `proposed`; a lens refusal `refused`; a base source
590
+ that moved on `conflicted`. It writes no source file and no vault, and takes no
591
+ lease.
592
+
593
+ Ticks stay quiet. The object store answers the same for an origin it already
594
+ holds and appends nothing, and an edit whose operation is recorded is not
595
+ offered again: the record of the object is the authority, and an adapter that
596
+ has just started finds it there. A refusal that comes before anything can be
597
+ recorded (`object-not-visible`, `source-not-in-graph`, `source-moved`, the
598
+ `source-*` codes of locating the file, `manifest-unavailable`,
599
+ `published-note-unavailable`, `stale-source`) is remembered in memory with its
600
+ code and offered again only at the full reconciliation cadence of the engine,
601
+ never on every tick. At most 16 edits are observed per tick, in the order they
602
+ were observed, starting where the last tick stopped. The tick reports
603
+ `observed`: for each edit looked at, its identifiers, `observed` or `refused`,
604
+ a code, and for a recorded one the kind and state of the operation.
605
+
606
+ ### Routing
607
+
608
+ The routing key is the whole identity `(workspaceId, repoId, nodeId)`. The store
609
+ is the one every other writer of proposals uses for a root:
610
+ `<repository root>/.atelier-proposals`. Two repositories that hold the same
611
+ relative path and the same local node id have different stores and different
612
+ operation identities. A route that cannot be resolved refuses, writes nothing,
613
+ and leaves the preserved bytes and the record of the object as they were:
614
+
615
+ | Code | Meaning |
616
+ | --- | --- |
617
+ | `invalid-operation`, `foreign-workspace` | the identity is malformed or belongs to another workspace |
618
+ | `repository-not-enrolled`, `repository-external`, `repository-root-unreadable` | the project does not enrol the repository on this machine |
619
+ | `route-withheld` | this machine may not see the object now (the rule source apply asks); `route-visibility-unknown` waits instead |
620
+ | `source-path-invalid`, `source-path-not-preservable` | the repository-relative path is unusable, or the store would trim or cut it (500 characters) |
621
+ | `proposal-store-unsafe`, `proposal-store-inside-managed-root` | the store directory is a link or a file, or overlaps private state or a vault |
622
+ | `proposal-store-not-ignored`, `proposal-store-ignore-unknown` | the store does not exist yet and git would report it; nothing is created that changes `git status` |
623
+
624
+ ### Operation identity and deduplication
625
+
626
+ The adapter operation identity is `pa-` and the SHA-256 of the identity and the
627
+ idempotency key of the edit operation, joined by a character no identifier can
628
+ hold: 67 lower-case characters. It is carried in `payload.adapter.operationId`
629
+ of the proposal, a member the store persists as JSON without normalising it.
630
+ The identifier the store gives a proposal is seeded with the time and decides
631
+ nothing here.
632
+
633
+ Per operation, under a private lock per repository (the generation lock of the
634
+ maintenance engine; taken over only with proof that its holder is gone, never
635
+ because time passed):
636
+
637
+ 1. the operation is recorded in the adapter queue,
638
+ `<state>/state/proposals/<repo>/operations/<operation>--NNNNNN.json`:
639
+ immutable, owner-only, canonical files, each naming the digest of the one
640
+ before it;
641
+ 2. the ledger of the store is read and its room judged;
642
+ 3. the persisted store is searched for a proposal that carries the operation
643
+ identity, and one is created only when there is none;
644
+ 4. `submitted` is recorded, the proposal is read back, and `acknowledged` is
645
+ recorded with an `atelier-obsidian-proposal-receipt/v1` that binds the edit
646
+ to the proposal (`dedupe`: `new`, `recovered` after a lost acknowledgement,
647
+ `duplicate` when an acknowledged operation is offered again).
648
+
649
+ A crash before the append leaves a queued record and no proposal; after it, a
650
+ proposal that step 3 finds. An unchanged tick reads the queue and nothing else:
651
+ no store is opened and nothing is written. An operation whose edit was withdrawn
652
+ or superseded while it waited is refused `edit-withdrawn` or `edit-superseded`.
653
+
654
+ ### What a proposal holds
655
+
656
+ Identifiers, the repository-relative source path, the lens code and reason,
657
+ byte offsets into the edited note, digests and recovery references of the
658
+ preserved bytes, and sentences made from those codes. It holds no text of any
659
+ note: what a person typed can carry the title of another document, and a
660
+ reviewer of one repository must not learn a title of another repository, or of
661
+ a withheld document, from a proposal. The preserved bytes stay in private state.
662
+
663
+ ### Ledger limits and backpressure
664
+
665
+ The limits are those of the existing ledger and are not widened: 256 KiB a
666
+ line, 16 MiB, 10,000 events, and an unreadable line refuses every append.
667
+
668
+ | Code | Outcome |
669
+ | --- | --- |
670
+ | `proposal-too-large` | refused before anything is appended; nothing is cut to fit |
671
+ | `ledger-full` | fewer than 1 + 32 events or less than the line + 512 KiB of room (the reserve is the reviewers'); the operation waits as `backpressure` with its edit retained |
672
+ | `ledger-corrupt` | refused, for that repository only; other repositories progress in the same tick |
673
+ | `store-unavailable` | the store cannot be read or is locked now; waits like a full one |
674
+
675
+ A waiting operation is tried at most 8 times, 60 seconds after the first attempt
676
+ and twice as long after each, up to an hour. At most 8 operations per repository
677
+ and 64 unexamined edits are looked at per tick. A repository holds at most 4096
678
+ operations, 256 of them open, 64 records of 16 KiB each; a full queue refuses
679
+ `queue-full` and records nothing. Nothing is compacted, rotated or deleted, in
680
+ the ledger or in the queue. A refused or exhausted operation is final until the
681
+ adapter's `requeue` is called for it, which is a person's decision after making
682
+ room or fixing the route; it is an exported function, and no command binds it
683
+ yet.
684
+
685
+ `atelier obsidian proposals list` and `show OPERATION` are read-only: per
686
+ repository the counts by state, the codes of the waiting and the refused, and
687
+ the events and bytes the ledger has left; never note or source text.