@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,572 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { createProposalStore } from '../../../collaboration/proposals.mjs'
5
+ import { AtelierDiagnosticError } from '../../../project/config.mjs'
6
+ import { compareText, isoTime } from '../../../runtime/obsidian/documents.mjs'
7
+ import { ObsidianMaintenanceRefusal } from '../../../runtime/obsidian/errors.mjs'
8
+ import { readMachineSettings } from '../../../runtime/obsidian/machine-settings.mjs'
9
+ import { DEFAULT_ELIGIBILITY, createProductionSeams } from '../../../runtime/obsidian/pipeline.mjs'
10
+ import { acquirePrivateGenerationLock, createAbandonmentProof } from '../../../runtime/obsidian/private-lock.mjs'
11
+ import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, assertObsidianContract } from '../contracts.mjs'
12
+ import { SOURCE_APPLY_PRIMITIVES } from '../edits/apply.mjs'
13
+ import { EditArbitrationRefusal } from '../edits/arbitrate.mjs'
14
+ import { isIdentifier } from '../edits/object-identity.mjs'
15
+ import { openObjectStore } from '../edits/object-store.mjs'
16
+ import { decideApply } from '../edits/policy.mjs'
17
+ import { applyEditLens } from '../edits/regions.mjs'
18
+ import { PublicationRefusal } from '../recovery/store.mjs'
19
+ import {
20
+ PROPOSAL_BACKPRESSURE, PROPOSAL_LEDGER_LIMITS, classifyLedgerRead, classifyStoreRefusal, estimateEventLineBytes, isDue, isExhausted, nextAttemptAt, preflightAppend,
21
+ } from './backpressure.mjs'
22
+ import { createTickObservation, manifestOf, recordedOperationOf, retained } from './observation.mjs'
23
+ import { OPEN_QUEUE_STATES, ProposalQueueRefusal, openProposalQueue } from './queue.mjs'
24
+ import { PROPOSAL_STORE_DIRECTORY, adapterOperationId, isAdapterOperationId, isRoutableSourcePath, proposalStoreId, resolveProposalRoute } from './router.mjs'
25
+
26
+ // The proposal adapter: a structural edit somebody made in a vault becomes ONE
27
+ // durable copy-only proposal in the store of the repository that owns the
28
+ // source, and nothing else happens to it.
29
+ //
30
+ // An edit the byte lens cannot turn into source bytes (a new or changed link
31
+ // to another note, an edited front matter) is recorded by the object store as
32
+ // an operation of kind `semantic-proposal`, state `proposed`. On a tick the
33
+ // adapter first observes the open pending edits the object store does not know
34
+ // yet (observation.mjs: the pre-lease part of an apply, which records what
35
+ // each edit is and writes no source), then is handed the pending edits; for
36
+ // each such operation it has not settled, in this order and under the private
37
+ // lock of that repository:
38
+ //
39
+ // 1. records the operation in its queue (queue.mjs), before anything else;
40
+ // 2. reads the ledger of the store and refuses or waits when it has no room
41
+ // or cannot be read (backpressure.mjs);
42
+ // 3. LOOKS in the persisted store for a proposal that already carries this
43
+ // adapter operation identity, and creates one only when there is none;
44
+ // 4. records `submitted`, reads the proposal back, and records
45
+ // `acknowledged` with a receipt that binds the edit to the proposal.
46
+ //
47
+ // The identifier the store gives a proposal is seeded with the time, so it
48
+ // names a proposal and decides nothing here: what makes two proposals the same
49
+ // request is the adapter operation identity inside the payload. A crash at any
50
+ // point leaves either no proposal and a queued record, or a proposal that step
51
+ // 3 finds on the next tick. Two ticks or two processes cannot both create,
52
+ // because step 3 and the creation happen under one lock, which is taken from a
53
+ // holder only with proof that its process is gone and never because time
54
+ // passed.
55
+ //
56
+ // What a proposal says. Identifiers, the repository-relative path of the
57
+ // source, codes, byte offsets, digests and references to the preserved bytes,
58
+ // and sentences made from those codes. It holds NO text of any note: the bytes
59
+ // a person typed may name or quote another document (a link carries the title
60
+ // of its target), and a reviewer of one repository must not learn the title of
61
+ // a document of another, or of one withheld from them, from a proposal. The
62
+ // preserved bytes stay in private state, named by digest.
63
+ //
64
+ // The adapter writes no source file and no vault, and no status of a proposal
65
+ // is an instruction to it: a proposal a reviewer accepts is a note for a
66
+ // person or an agent with a checkout, exactly as the store says.
67
+
68
+ export const PROPOSAL_ADAPTER_ID = 'atelier.obsidian-proposal-adapter/v1'
69
+ export const PROPOSAL_ADAPTER_ACTOR = 'atelier obsidian proposal adapter'
70
+ export const PROPOSAL_PAYLOAD_SCHEMA = 'atelier-obsidian-structural-proposal/v1'
71
+ export const PROPOSAL_PAYLOAD_KIND = 'obsidian-structural-edit'
72
+ export const PROPOSAL_LOCK_PURPOSE = 'proposal-adapter'
73
+ export const PROPOSAL_ADAPTER_CRASH_STEPS = Object.freeze(['queued', 'before-append', 'appended', 'submitted', 'acknowledged'])
74
+ export const MAX_CHANGE_OCCURRENCES = 64
75
+
76
+ const EXT = OBSIDIAN_EXT_KEY
77
+ const STRUCTURAL_CODES = Object.freeze(['unsupported-structural-edit', 'unsupported-frontmatter-edit'])
78
+ const CODE = /^[a-z][a-z0-9-]{0,63}$/
79
+ const CHANGE_SENTENCES = Object.freeze({
80
+ 'unsupported-structural-edit': 'A link to another note of the vault was added, changed or removed.',
81
+ 'unsupported-frontmatter-edit': 'The front matter of the note was edited.',
82
+ unclassified: 'The edit changes the structure of the note in a way that was not classified further.',
83
+ })
84
+ const isTyped = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof AtelierDiagnosticError || error instanceof ObsidianContractRefusal || error instanceof PublicationRefusal
85
+ || error instanceof EditArbitrationRefusal || error instanceof ProposalQueueRefusal
86
+ const hashOf = (parts) => createHash('sha256').update(parts.join('\u0000')).digest('hex')
87
+ const isOffset = (value) => Number.isInteger(value) && value >= 0
88
+
89
+ // What of a lens refusal a proposal repeats: the code, a reason code and byte offsets. Anything else it holds
90
+ // (a digest of the note, a name) stays behind.
91
+ export function describeChange(outcome) {
92
+ if (outcome?.kind !== 'refusal' || !STRUCTURAL_CODES.includes(outcome.code)) return { code: 'unclassified', reason: typeof outcome?.code === 'string' && CODE.test(outcome.code) ? outcome.code : null }
93
+ const detail = outcome.detail ?? {}
94
+ const change = { code: outcome.code, reason: typeof detail.reason === 'string' && CODE.test(detail.reason) ? detail.reason : null }
95
+ for (const name of ['firstDifference', 'publishedStart', 'publishedEnd']) if (isOffset(detail[name])) change[name] = detail[name]
96
+ if (Array.isArray(detail.occurrences)) {
97
+ const occurrences = detail.occurrences.filter((item) => isOffset(item?.noteStart) && isOffset(item?.noteEnd)).map(({ noteStart, noteEnd }) => ({ noteStart, noteEnd }))
98
+ // Said, never silent: how many there were, and how many of them are listed.
99
+ change.occurrenceCount = occurrences.length
100
+ change.occurrences = occurrences.slice(0, MAX_CHANGE_OCCURRENCES)
101
+ }
102
+ return change
103
+ }
104
+
105
+ // The body handed to createProposal. `item` is the operation, `route` where it goes, `change` what describeChange said.
106
+ export function buildProposalContent({ item, route, change }) {
107
+ const sentence = CHANGE_SENTENCES[change.code] ?? CHANGE_SENTENCES.unclassified
108
+ const summary = [
109
+ 'Somebody edited the generated note of this document in an Obsidian view, and the edit cannot be written back as a replacement of the body.',
110
+ sentence,
111
+ `Code: ${change.code}${change.reason ? ` (${change.reason})` : ''}.${change.occurrenceCount === undefined ? '' : ` Places in the edited note: ${change.occurrenceCount}.`}`,
112
+ 'The edited bytes and the source they were judged against are kept in the private state of the workspace that observed the edit, under the digests below. Nothing was written to the source.',
113
+ ]
114
+ return {
115
+ path: route.sourcePath,
116
+ action: 'copy.agentPrompt',
117
+ actor: PROPOSAL_ADAPTER_ACTOR,
118
+ viewId: item.scopeId,
119
+ intent: `Review a structural edit of ${route.sourcePath} made in an Obsidian view (${change.code}).`.slice(0, 500),
120
+ reason: sentence,
121
+ diff: '',
122
+ proposal: {
123
+ schema: PROPOSAL_PAYLOAD_SCHEMA,
124
+ kind: PROPOSAL_PAYLOAD_KIND,
125
+ adapter: { id: PROPOSAL_ADAPTER_ID, operationId: item.adapterOperationId },
126
+ identity: { workspaceId: item.workspaceId, repoId: item.repoId, nodeId: item.nodeId },
127
+ sourcePath: route.sourcePath,
128
+ editId: item.editId,
129
+ idempotencyKey: item.idempotencyKey,
130
+ origin: { scopeId: item.scopeId, generationId: item.generationId },
131
+ change,
132
+ references: {
133
+ observed: { digest: item.observed.digest, byteLength: item.observed.byteLength, recoveryRef: item.observed.recoveryRef },
134
+ baseSourceDigest: item.baseSourceDigest,
135
+ ...(item.publishedNoteDigest ? { publishedNoteDigest: item.publishedNoteDigest } : {}),
136
+ },
137
+ summary,
138
+ },
139
+ }
140
+ }
141
+
142
+ // The decisions the oracles of test/obsidian-proposals.test.mjs are sensitive
143
+ // to. Production always uses these; the tests substitute deliberately broken
144
+ // ones through createProposalAdapterForOracleTests to prove each oracle can fail.
145
+ export const PROPOSAL_ADAPTER_PRIMITIVES = Object.freeze({
146
+ // The proposals of the persisted store that are this operation: the ones whose payload carries its identity.
147
+ // Never the identifier of a proposal, which is seeded with the time it was made.
148
+ existingFor: ({ proposals, item }) => proposals.filter((record) => record?.payload?.adapter?.id === PROPOSAL_ADAPTER_ID && record.payload.adapter.operationId === item.adapterOperationId),
149
+ // The store is asked before anything is created, on the first attempt and on every one after it.
150
+ lookBeforeCreate: true,
151
+ // An operation that is acknowledged or refused is not looked at again, and one that waits is looked at when it is due.
152
+ handOver: ({ head, nowMs, bounds }) => isDue(head, { nowMs, bounds }),
153
+ isSettled: ({ head }) => head.state === 'acknowledged' || head.state === 'refused',
154
+ content: buildProposalContent,
155
+ // The check-then-create of one repository is serialized between ticks and between processes by a private lock.
156
+ acquireLock: acquirePrivateGenerationLock,
157
+ // What the adapter does to a ledger that has no room: nothing.
158
+ relieve: () => {},
159
+ // What the adapter does when it sees how a reviewer settled a proposal: nothing. It is shown, and that is all.
160
+ onReviewStatus: () => {},
161
+ })
162
+
163
+ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTER_PRIMITIVES) {
164
+ const rules = { ...PROPOSAL_ADAPTER_PRIMITIVES, ...primitives }
165
+
166
+ // `clock` is injected. `crash(step, detail)` is the crash seam. `openStore` opens the existing proposal store of a
167
+ // repository root; `objectStore` the arbitration store; `isVisible(identity, context)` and `isGitIgnored` replace the
168
+ // two questions asked of the machine; `proveAbandoned` is the proof a held lock is passed over with.
169
+ return function createProposalAdapter(options = {}) {
170
+ const {
171
+ crash = () => {}, bounds: boundsInput = {}, limits = PROPOSAL_LEDGER_LIMITS, eligibility = DEFAULT_ELIGIBILITY,
172
+ openStore = createProposalStore, objectStore = openObjectStore, isGitIgnored = SOURCE_APPLY_PRIMITIVES.isGitIgnored, proveAbandoned = createAbandonmentProof(), queueCrash = () => {},
173
+ } = options
174
+ const bounds = { ...PROPOSAL_BACKPRESSURE, ...boundsInput }
175
+ // The environment git is asked under: the one given when the adapter was made, else the one of the engine that calls it.
176
+ const env = options.env
177
+ // The clock given when the adapter was made; else the one of the engine that calls it; else the time of day.
178
+ let callerClock = null
179
+ const clock = () => (options.clock ?? callerClock ?? (() => new Date()))()
180
+ const seams = { ...createProductionSeams(), ...(options.seams ?? {}) }
181
+ let cursor = 0
182
+ const queues = new Map()
183
+ // The observation of a tick keeps what it recorded and what it could not; one per workspace, like the queue.
184
+ let observation = { key: null, run: null }
185
+ // Edits whose operation is known not to be a proposal. A cache: losing it costs one read of an object.
186
+ const notProposals = new Set()
187
+
188
+ // -- the workspace of one call -------------------------------------------
189
+
190
+ function open(context) {
191
+ const { project, workspaceRoot, workspaceId, repositoryRoots } = context
192
+ if (typeof workspaceRoot !== 'string' || !path.isAbsolute(workspaceRoot) || !isIdentifier(workspaceId) || !Array.isArray(repositoryRoots) || project === null || typeof project !== 'object') {
193
+ throw new TypeError('the proposal adapter needs the project, the private workspace root, the workspace identity and the repository roots')
194
+ }
195
+ callerClock = typeof context.clock === 'function' ? context.clock : null
196
+ const stores = new Map()
197
+ const recoveryOf = (scopeId) => {
198
+ if (!stores.has(scopeId)) stores.set(scopeId, seams.createRecoveryStore({ workspaceRoot, workspaceId, scopeId, repositoryRoots }))
199
+ return stores.get(scopeId)
200
+ }
201
+ // The canonical graph as it is now and the corpus profile of this machine, built once per call. Throws typed.
202
+ let corpus = null
203
+ const currentCorpus = () => (corpus ??= {
204
+ graph: seams.buildGraph({ project, eligibility }),
205
+ profile: seams.profileFor({ project, workspaceId, audienceAllow: readMachineSettings({ workspaceRoot, workspaceId })?.audienceAllow ?? [] }),
206
+ })
207
+ // true, false, or null when the canonical graph cannot be read now. The rule is the one source apply asks.
208
+ const visible = options.isVisible ? (identity) => options.isVisible(identity, context) : ({ repoId, nodeId }) => {
209
+ let built
210
+ try { built = currentCorpus() } catch (error) { if (isTyped(error)) return null; throw error }
211
+ if (!built.graph.nodes.some((node) => node.repo === repoId && node.id === nodeId)) return false
212
+ const decision = decideApply({ request: { mode: 'manual', editId: 'edit-route' }, workspace: { workspaceRoot, workspaceId }, graph: built.graph, profile: built.profile, object: { repoId, nodeId }, editClass: 'body-replacement' })
213
+ return decision.allowed === true || decision.code !== 'object-not-visible'
214
+ }
215
+ let objects = null
216
+ // Records are immutable, so the queue of a workspace, and what it has verified, is kept from one tick to the next.
217
+ const queueKey = JSON.stringify([workspaceRoot, workspaceId, repositoryRoots])
218
+ const queue = () => {
219
+ if (queues.get(queueKey) === undefined) { queues.clear(); queues.set(queueKey, openProposalQueue({ stateRoot: workspaceRoot, workspaceId, repositoryRoots, clock, crash: queueCrash })) }
220
+ return queues.get(queueKey)
221
+ }
222
+ if (observation.key !== queueKey) observation = { key: queueKey, run: createTickObservation({ seams, isGitIgnored, bounds }) }
223
+ return {
224
+ ...context, recoveryOf, visible, clock, env: env ?? context.env ?? process.env,
225
+ corpus: currentCorpus,
226
+ objects: () => (objects ??= objectStore({ stateRoot: workspaceRoot, workspaceId, repositoryRoots, clock })),
227
+ queue,
228
+ managedRoots: () => [workspaceRoot, ...[...stores.values()].map((store) => store.vaultRoot)],
229
+ }
230
+ }
231
+
232
+ // A pending edit as the operation the object store recorded for it, or null when that is not a proposed
233
+ // structural edit (or is not recorded yet). The live note is never read: only bytes that were preserved.
234
+ function describe(workspace, edit) {
235
+ const { workspaceId, repoId, nodeId } = edit.identity
236
+ const entry = recordedOperationOf(workspace.objects(), edit)
237
+ if (!entry) return null
238
+ if (entry.kind !== 'semantic-proposal' || entry.state !== 'proposed') { notProposals.add(edit.editId); return null }
239
+ const origin = entry.origins.find((item) => item.editId === edit.editId && item.scopeId === edit.scopeId)
240
+ const item = {
241
+ workspaceId, repoId, nodeId, editId: edit.editId, idempotencyKey: entry.idempotencyKey, scopeId: origin.scopeId, generationId: origin.generationId,
242
+ baseSourceDigest: entry.baseSourceDigest, observed: { digest: entry.observedDigest, byteLength: entry.observedByteLength, recoveryRef: origin.recoveryRef },
243
+ adapterOperationId: adapterOperationId({ workspaceId, repoId, nodeId, idempotencyKey: entry.idempotencyKey }), sourcePath: null, publishedNoteDigest: null, change: { code: 'unclassified', reason: 'manifest-unavailable' },
244
+ }
245
+ const manifest = manifestOf(workspace, origin.scopeId, origin.generationId)
246
+ const noteEntry = manifest?.notes.find((note) => note.repoId === repoId && note.nodeId === nodeId && note.path === edit.path)
247
+ if (!noteEntry) return item
248
+ const recordedPath = noteEntry.ext?.[EXT]?.source?.path
249
+ item.sourcePath = typeof recordedPath === 'string' ? recordedPath : null
250
+ item.publishedNoteDigest = noteEntry.noteDigest ?? null
251
+ const store = workspace.recoveryOf(origin.scopeId)
252
+ const [publishedNoteBytes, editedNoteBytes, baseSourceBytes] = [retained(store, noteEntry.noteDigest), retained(store, entry.observedDigest), retained(store, entry.baseSourceDigest)]
253
+ if (!publishedNoteBytes || !editedNoteBytes || !baseSourceBytes) { item.change = { code: 'unclassified', reason: 'preserved-bytes-unavailable' }; return item }
254
+ let outcome
255
+ try { outcome = applyEditLens({ manifest, repoId, nodeId, publishedNoteBytes, editedNoteBytes, baseSourceBytes }) } catch (error) { if (!isTyped(error)) throw error; outcome = { kind: 'refusal', code: error.code } }
256
+ item.change = describeChange(outcome)
257
+ return item
258
+ }
259
+
260
+ // -- one operation, under the lock of its repository ----------------------
261
+
262
+ function receiptOf({ item, storeId, sequence, proposalId, dedupe, backpressure }) {
263
+ const receipt = {
264
+ schema: 'atelier-obsidian-proposal-receipt/v1', contractVersion: '1.0.0', receiptId: `pr-${hashOf([item.adapterOperationId, String(sequence)]).slice(0, 48)}`,
265
+ repoId: item.repoId, storeId, adapterOperationId: item.adapterOperationId, editId: item.editId, proposalId, dedupe, backpressure, recordedAt: isoTime(clock),
266
+ }
267
+ assertObsidianContract('proposal-receipt', receipt)
268
+ return receipt
269
+ }
270
+
271
+ function settleOne(workspace, item, { requireOpenEdit = true, editState = 'open' } = {}) {
272
+ const queue = workspace.queue()
273
+ const { repoId, adapterOperationId: operationId } = item
274
+ const storeId = proposalStoreId(item)
275
+ const step = (name) => crash(name, { adapterOperationId: operationId, repoId })
276
+ const outcome = (status, record, extra = {}) => ({ status, code: record?.code ?? null, editId: item.editId, repoId, nodeId: item.nodeId, adapterOperationId: operationId, state: record?.state ?? null, proposalId: record?.proposalId ?? null, ...extra })
277
+ let history = queue.read(repoId, operationId)
278
+ const known = history !== null
279
+ const sequenceNext = () => (history?.records.length ?? 0) + 1
280
+ const write = (fields) => { const record = queue.append(repoId, operationId, fields); history = queue.read(repoId, operationId); return record }
281
+ const refused = (code) => {
282
+ const receipt = receiptOf({ item, storeId, sequence: sequenceNext(), proposalId: null, dedupe: known ? 'duplicate' : 'new', backpressure: 'refused' })
283
+ return outcome('refused', write({ ...identityFields(item, storeId), state: 'refused', code, proposalId: null, dedupe: null, nextAttemptAt: null, receipt }), { receipt })
284
+ }
285
+ const deferred = (code, attempts) => {
286
+ const record = write({ ...identityFields(item, storeId), state: 'backpressure', code, attempts, lastAttemptAt: isoTime(clock), nextAttemptAt: nextAttemptAt({ attempts, nowMs: Date.parse(isoTime(clock)), bounds }), proposalId: null, dedupe: null, receipt: null })
287
+ return outcome('deferred', record, { receipt: receiptOf({ item, storeId, sequence: record.sequence, proposalId: null, dedupe: known ? 'duplicate' : 'new', backpressure: 'deferred' }), exhausted: isExhausted(record, bounds) })
288
+ }
289
+
290
+ if (history && rules.isSettled({ head: history.head })) {
291
+ // Offered again: answered from the record. Nothing is read from the store and nothing is written.
292
+ const { head } = history
293
+ return outcome('duplicate', head, { receipt: { ...head.receipt, dedupe: 'duplicate' } })
294
+ }
295
+
296
+ let proposalId = history?.head.state === 'submitted' ? history.head.proposalId : null
297
+ let dedupe = history?.head.state === 'submitted' ? history.head.dedupe : null
298
+ let attempts = history?.head.attempts ?? 0
299
+ let store = null
300
+ let route = null
301
+
302
+ if (proposalId === null) {
303
+ // A person took the edit back, or made another over it: the request is theirs to withdraw.
304
+ if (requireOpenEdit && editState !== 'open') return known ? refused(`edit-${editState}`) : outcome('skipped', null, { code: `edit-${editState}` })
305
+ // The object store no longer says this is a proposed structural edit, so there is nothing to describe.
306
+ if (item.observed === null) return refused('operation-not-proposed')
307
+ const resolved = resolveProposalRoute({ project: workspace.project, workspaceId: workspace.workspaceId, identity: item, sourcePath: item.sourcePath, managedRoots: workspace.managedRoots(), isVisible: workspace.visible, isGitIgnored, env: env ?? workspace.env ?? process.env })
308
+ if (!resolved.ok && !resolved.transient) return refused(resolved.code)
309
+ // Recorded before anything is asked of the store.
310
+ if (!known) write({ ...identityFields(item, storeId), state: 'queued', code: null, proposalId: null, dedupe: null, receipt: null, nextAttemptAt: null })
311
+ step('queued')
312
+ if (!resolved.ok) return deferred(resolved.code, attempts + 1)
313
+ route = resolved.route
314
+ }
315
+
316
+ attempts += 1
317
+ const repositoryRoot = route?.repositoryRoot ?? repositoryRootOf(workspace.project, repoId)
318
+ if (repositoryRoot === null) return proposalId === null ? refused('repository-not-enrolled') : outcome('deferred', history.head, { code: 'repository-not-enrolled' })
319
+ try { store = openStore({ workspaceRoot: repositoryRoot, workspaceId: workspace.workspaceId }) } catch { return proposalId === null ? deferred('store-unavailable', attempts) : outcome('deferred', history.head, { code: 'store-unavailable' }) }
320
+
321
+ if (proposalId === null) {
322
+ const ledger = classifyLedgerRead(store.eventLedger.readAll(), limits)
323
+ if (ledger.state === 'corrupt') return refused(ledger.code)
324
+ if (ledger.state === 'full') { rules.relieve({ store, ledger }); return deferred(ledger.code, attempts) }
325
+ if (ledger.state !== 'readable') return deferred(ledger.code, attempts)
326
+
327
+ let existing = []
328
+ if (rules.lookBeforeCreate) {
329
+ const listed = store.listProposals()
330
+ if (!listed.ok) { const read = classifyLedgerRead(listed, limits); return read.state === 'corrupt' ? refused(read.code) : deferred(read.code, attempts) }
331
+ existing = rules.existingFor({ proposals: listed.proposals, item }).sort((left, right) => compareText(left.proposal.createdAt, right.proposal.createdAt) || compareText(left.proposal.id, right.proposal.id))
332
+ }
333
+ if (existing.length > 0) {
334
+ proposalId = existing[0].proposal.id
335
+ dedupe = 'recovered'
336
+ } else {
337
+ const body = rules.content({ item, route, change: item.change })
338
+ const fits = preflightAppend({ ledger, lineBytes: estimateEventLineBytes({ body, workspaceId: workspace.workspaceId, marginBytes: bounds.lineMarginBytes }), limits, bounds })
339
+ if (!fits.ok) {
340
+ if (fits.code === 'proposal-too-large') return refused(fits.code)
341
+ rules.relieve({ store, ledger })
342
+ return deferred(fits.code, attempts)
343
+ }
344
+ step('before-append')
345
+ const created = store.createProposal(body)
346
+ if (!created.ok) {
347
+ const code = classifyStoreRefusal(created.status)
348
+ return code === 'store-refused' || code === 'ledger-corrupt' ? refused(code) : deferred(code, attempts)
349
+ }
350
+ step('appended')
351
+ // What went in is what is there: the identity in the payload and the path, byte for byte.
352
+ if (created.record?.payload?.adapter?.operationId !== operationId || created.record?.proposal?.path !== route.sourcePath) return refused('identity-not-preserved')
353
+ proposalId = created.record.proposal.id
354
+ dedupe = 'new'
355
+ }
356
+ write({ ...identityFields(item, storeId), state: 'submitted', code: null, attempts, lastAttemptAt: isoTime(clock), nextAttemptAt: null, proposalId, dedupe, receipt: null })
357
+ step('submitted')
358
+ }
359
+
360
+ // Read back from the persisted store before it is acknowledged.
361
+ const read = store.readProposal(proposalId)
362
+ if (!read.ok || read.record?.payload?.adapter?.operationId !== operationId) {
363
+ if (read.status === 404 || read.ok) { write({ ...identityFields(item, storeId), state: 'queued', code: 'proposal-not-found', proposalId: null, dedupe: null, receipt: null, nextAttemptAt: null }); return outcome('deferred', history.head) }
364
+ const record = write({ state: 'submitted', code: classifyLedgerRead(read, limits).code, attempts, lastAttemptAt: isoTime(clock), nextAttemptAt: nextAttemptAt({ attempts, nowMs: Date.parse(isoTime(clock)), bounds }) })
365
+ return outcome('deferred', record)
366
+ }
367
+ const receipt = receiptOf({ item, storeId, sequence: sequenceNext(), proposalId, dedupe, backpressure: 'accepted' })
368
+ const record = write({ state: 'acknowledged', code: null, nextAttemptAt: null, receipt })
369
+ step('acknowledged')
370
+ return outcome('acknowledged', record, { receipt, dedupe })
371
+ }
372
+
373
+ const identityFields = (item, storeId) => ({
374
+ nodeId: item.nodeId, editId: item.editId, idempotencyKey: item.idempotencyKey, scopeId: item.scopeId, generationId: item.generationId, storeId,
375
+ sourcePath: isRoutableSourcePath(item.sourcePath) ? item.sourcePath : null,
376
+ })
377
+
378
+ function repositoryRootOf(project, repoId) {
379
+ const named = (project?.repos ?? []).filter((item) => item?.name === repoId && !item.external && typeof item.path === 'string')
380
+ if (named.length !== 1) return null
381
+ try { return fs.realpathSync.native(named[0].path) } catch { return null }
382
+ }
383
+
384
+ async function withRepositoryLock(workspace, repoId, operation) {
385
+ const queue = workspace.queue()
386
+ queue.ensureRepository(repoId)
387
+ let lock
388
+ for (let attempt = 0; ; attempt += 1) {
389
+ try { lock = await rules.acquireLock({ workspaceRoot: queue.stateRoot, directory: queue.lockDirectory(repoId), workspaceId: workspace.workspaceId, purpose: PROPOSAL_LOCK_PURPOSE, clock, proveAbandoned }); break } catch (error) {
390
+ if (error.code !== 'EEXIST' || attempt >= 8) throw error
391
+ }
392
+ }
393
+ if (!lock.acquired) return { locked: false, reason: lock.reason }
394
+ let crashed = false
395
+ try { return { locked: true, value: await operation() } } catch (error) { crashed = error?.crashSeam === true; throw error } finally {
396
+ // A process that died released nothing; the test that stands for it says so.
397
+ if (!crashed) lock.release()
398
+ }
399
+ }
400
+
401
+ const itemOfHead = (head, edit) => ({
402
+ workspaceId: head.workspaceId, repoId: head.repoId, nodeId: head.nodeId, editId: head.editId, idempotencyKey: head.idempotencyKey, scopeId: head.scopeId, generationId: head.generationId,
403
+ adapterOperationId: head.adapterOperationId, sourcePath: head.sourcePath, baseSourceDigest: null, observed: null, publishedNoteDigest: null, change: { code: 'unclassified', reason: null }, edit,
404
+ })
405
+
406
+ // One repository: its lock, then each of its operations in turn. What goes wrong with one repository, or with one
407
+ // operation, is reported by its code and stops nothing else.
408
+ async function settleRepository(workspace, repoId, works, report) {
409
+ try {
410
+ const held = await withRepositoryLock(workspace, repoId, () => {
411
+ for (const work of works) {
412
+ try { report.outcomes.push(settleOne(workspace, work.item, work)) } catch (error) {
413
+ if (error?.crashSeam === true || !isTyped(error)) throw error
414
+ report.outcomes.push({ status: 'failed', code: error.code, editId: work.item.editId, repoId, nodeId: work.item.nodeId, adapterOperationId: work.item.adapterOperationId, state: null, proposalId: null })
415
+ }
416
+ }
417
+ })
418
+ if (!held.locked) report.repositories.push({ repoId, code: 'adapter-lock-held', reason: held.reason })
419
+ } catch (error) {
420
+ if (error?.crashSeam === true || !isTyped(error)) throw error
421
+ report.repositories.push({ repoId, code: error.code })
422
+ }
423
+ }
424
+
425
+ return {
426
+ id: PROPOSAL_ADAPTER_ID,
427
+
428
+ // The observation of a tick, before `propose`: every open pending edit the object store does not know yet is
429
+ // observed from its preserved bytes against the source as it is now and recorded as what it is, a bounded number
430
+ // per tick and never twice. `retryRefused` offers once more the edits whose observation refused before anything
431
+ // could be recorded. Writes no source and no vault. Returns codes and identifiers only.
432
+ async observe(context, { retryRefused = false } = {}) {
433
+ const workspace = open(context)
434
+ const edits = Array.isArray(context.edits) ? context.edits : []
435
+ return { adapterId: PROPOSAL_ADAPTER_ID, observed: observation.run(workspace, { edits, retryRefused }) }
436
+ },
437
+
438
+ // The tick. `context` is { project, workspaceRoot, workspaceId, repositoryRoots, edits }: the pending edit
439
+ // records of the engine, open and closed. Returns codes and identifiers only.
440
+ async propose(context) {
441
+ const workspace = open(context)
442
+ const nowMs = Date.parse(isoTime(clock))
443
+ const report = { adapterId: PROPOSAL_ADAPTER_ID, examined: 0, outcomes: [], repositories: [] }
444
+ const edits = Array.isArray(context.edits) ? context.edits : []
445
+ const { operations: heads, unreadable } = workspace.queue().heads()
446
+ for (const item of unreadable) report.repositories.push({ repoId: null, code: item.code })
447
+ const byEdit = new Map(edits.map((edit) => [edit.editId, edit]))
448
+ const knownEdits = new Set(heads.map((head) => head.editId))
449
+ const works = new Map()
450
+ const add = (repoId, work) => { if (!works.has(repoId)) works.set(repoId, []); if (works.get(repoId).length < bounds.maxBatchPerRepository) works.get(repoId).push(work) }
451
+
452
+ // Operations the queue already knows, when they are due.
453
+ for (const head of [...heads].sort((left, right) => compareText(left.recordedAt, right.recordedAt) || compareText(left.adapterOperationId, right.adapterOperationId))) {
454
+ if (!rules.handOver({ head, nowMs, bounds })) continue
455
+ const edit = byEdit.get(head.editId) ?? null
456
+ // An operation whose edit this caller did not hand over is not this caller's to decide: it waits as it is.
457
+ if (edit === null && head.state !== 'submitted') continue
458
+ const editState = edit === null || edit.closedAt === null ? 'open' : ['withdrawn', 'superseded'].includes(edit.state) ? edit.state : 'closed'
459
+ let item = itemOfHead(head, edit)
460
+ if (edit !== null && head.state !== 'submitted') {
461
+ try { item = { ...item, ...(describe(workspace, edit) ?? {}) } } catch (error) { if (!isTyped(error)) throw error; report.outcomes.push({ status: 'failed', code: error.code, editId: head.editId, repoId: head.repoId, nodeId: head.nodeId, adapterOperationId: head.adapterOperationId, state: head.state, proposalId: null }); continue }
462
+ }
463
+ add(head.repoId, { item, editState })
464
+ }
465
+
466
+ // Pending edits the queue does not know: a bounded number per tick, starting where the last tick stopped.
467
+ const fresh = edits.filter((edit) => edit.closedAt === null && !knownEdits.has(edit.editId) && !notProposals.has(edit.editId)).sort((left, right) => compareText(left.observedAt, right.observedAt) || compareText(left.editId, right.editId))
468
+ const start = fresh.length === 0 ? 0 : cursor % fresh.length
469
+ const looked = [...fresh.slice(start), ...fresh.slice(0, start)].slice(0, bounds.maxExaminedPerTick)
470
+ cursor = fresh.length > bounds.maxExaminedPerTick ? start + looked.length : 0
471
+ for (const edit of looked) {
472
+ report.examined += 1
473
+ let item
474
+ try { item = describe(workspace, edit) } catch (error) { if (!isTyped(error)) throw error; report.outcomes.push({ status: 'failed', code: error.code, editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId, adapterOperationId: null, state: null, proposalId: null }); continue }
475
+ if (item !== null) add(item.repoId, { item, editState: 'open' })
476
+ }
477
+
478
+ for (const [repoId, list] of [...works].sort(([left], [right]) => compareText(left, right))) await settleRepository(workspace, repoId, list, report)
479
+ return report
480
+ },
481
+
482
+ // One edit operation document, offered directly: what a caller that already holds the operation uses, and what
483
+ // the tests of routing and identity use. `sourcePath` is the repository-relative path the manifest records.
484
+ async offer(context, { operation, sourcePath, change = { code: 'unclassified', reason: null }, publishedNoteDigest = null }) {
485
+ assertObsidianContract('edit-operation', operation)
486
+ if (operation.kind !== 'semantic-proposal' || operation.state !== 'proposed') return { status: 'refused', code: 'invalid-operation', editId: operation.editId, repoId: operation.repoId, nodeId: operation.nodeId, adapterOperationId: null, state: null, proposalId: null }
487
+ const workspace = open(context)
488
+ const item = {
489
+ workspaceId: operation.workspaceId, repoId: operation.repoId, nodeId: operation.nodeId, editId: operation.editId, idempotencyKey: operation.idempotencyKey, scopeId: operation.origin.scopeId, generationId: operation.origin.generationId,
490
+ baseSourceDigest: operation.baseSourceDigest, observed: { ...operation.observed }, adapterOperationId: adapterOperationId(operation), sourcePath, publishedNoteDigest, change,
491
+ }
492
+ if (item.workspaceId !== workspace.workspaceId) return { status: 'refused', code: 'foreign-workspace', editId: item.editId, repoId: item.repoId, nodeId: item.nodeId, adapterOperationId: item.adapterOperationId, state: null, proposalId: null }
493
+ const report = { outcomes: [], repositories: [] }
494
+ await settleRepository(workspace, item.repoId, [{ item, requireOpenEdit: false }], report)
495
+ return report.outcomes[0] ?? { status: 'deferred', code: report.repositories[0]?.code ?? 'adapter-lock-held', editId: item.editId, repoId: item.repoId, nodeId: item.nodeId, adapterOperationId: item.adapterOperationId, state: null, proposalId: null }
496
+ },
497
+
498
+ // A refused operation, or one whose retries are spent, asked for again by a person or an agent acting for them.
499
+ async requeue(context, { repoId, adapterOperationId: operationId }) {
500
+ const workspace = open(context)
501
+ if (!isIdentifier(repoId) || !isAdapterOperationId(operationId)) return { requeued: false, code: 'invalid-operation' }
502
+ const held = await withRepositoryLock(workspace, repoId, () => {
503
+ const history = workspace.queue().read(repoId, operationId)
504
+ if (history === null) return { requeued: false, code: 'unknown-operation' }
505
+ if (history.head.state !== 'refused' && !isExhausted(history.head, bounds)) return { requeued: false, code: 'operation-not-refused', state: history.head.state }
506
+ const record = workspace.queue().append(repoId, operationId, { state: 'queued', code: null, attempts: 0, lastAttemptAt: null, nextAttemptAt: null, proposalId: null, dedupe: null, receipt: null })
507
+ return { requeued: true, state: record.state, sequence: record.sequence }
508
+ })
509
+ return held.locked ? held.value : { requeued: false, code: 'adapter-lock-held' }
510
+ },
511
+
512
+ // Read-only. Per repository: how many operations are in each state, the codes of the refused and the waiting,
513
+ // and how much room the ledger of its store has. No note text, no source text, no path of this machine.
514
+ status(context) {
515
+ const workspace = open(context)
516
+ const { operations, unreadable } = workspace.queue().heads()
517
+ const enrolled = (workspace.project.repos ?? []).filter((repo) => !repo.external && typeof repo.path === 'string' && isIdentifier(repo.name)).map((repo) => repo.name)
518
+ const repositories = [...new Set([...enrolled, ...operations.map((record) => record.repoId)])].sort(compareText).map((repoId) => {
519
+ const own = operations.filter((record) => record.repoId === repoId)
520
+ const count = (state) => own.filter((record) => record.state === state).length
521
+ const codes = (state) => Object.fromEntries([...new Set(own.filter((record) => record.state === state).map((record) => record.code))].sort(compareText).map((code) => [code, own.filter((record) => record.state === state && record.code === code).length]))
522
+ return {
523
+ repoId, enrolled: enrolled.includes(repoId), storeId: proposalStoreId({ workspaceId: workspace.workspaceId, repoId }),
524
+ queued: count('queued'), submitted: count('submitted'), acknowledged: count('acknowledged'), backpressure: count('backpressure'), refused: count('refused'),
525
+ exhausted: own.filter((record) => isExhausted(record, bounds)).length, backpressureCodes: codes('backpressure'), refusedCodes: codes('refused'),
526
+ ledger: ledgerOf(workspace, repoId),
527
+ }
528
+ })
529
+ return { schema: 'atelier-obsidian-proposal-status/v1', workspaceId: workspace.workspaceId, adapterId: PROPOSAL_ADAPTER_ID, limits: { ...limits }, reserve: { events: bounds.reserveEvents, bytes: bounds.reserveBytes }, repositories, unreadable: unreadable.map(({ adapterOperationId: operationId, code }) => ({ adapterOperationId: operationId, code })) }
530
+ },
531
+
532
+ // Every operation of the queue: identifiers, states, codes and times.
533
+ list(context) {
534
+ const { operations } = open(context).queue().heads()
535
+ return operations.sort((left, right) => compareText(left.repoId, right.repoId) || compareText(left.recordedAt, right.recordedAt) || compareText(left.adapterOperationId, right.adapterOperationId))
536
+ .map(({ adapterOperationId: operationId, repoId, nodeId, editId, state, code, attempts, nextAttemptAt: next, proposalId, recordedAt }) => ({ adapterOperationId: operationId, repoId, nodeId, editId, state, code, attempts, nextAttemptAt: next, exhausted: state === 'backpressure' && attempts >= bounds.maxAttempts, proposalId, recordedAt }))
537
+ },
538
+
539
+ // One operation: its record, its receipt, and how the reviewers of the repository have settled its proposal so
540
+ // far. That last part is shown and nothing more: no status of a proposal makes this adapter do anything.
541
+ show(context, { adapterOperationId: operationId }) {
542
+ const workspace = open(context)
543
+ const head = workspace.queue().heads().operations.find((record) => record.adapterOperationId === operationId)
544
+ if (!head) return null
545
+ let review = null
546
+ if (head.proposalId !== null) {
547
+ const repositoryRoot = repositoryRootOf(workspace.project, head.repoId)
548
+ if (repositoryRoot !== null && fs.existsSync(path.join(repositoryRoot, PROPOSAL_STORE_DIRECTORY))) {
549
+ const read = openStore({ workspaceRoot: repositoryRoot, workspaceId: workspace.workspaceId }).readProposal(head.proposalId)
550
+ review = read.ok ? { status: read.record.proposal.status, updatedAt: read.record.proposal.updatedAt } : { status: 'unreadable', updatedAt: null }
551
+ if (read.ok) rules.onReviewStatus({ status: read.record.proposal.status, record: read.record, head, repositoryRoot })
552
+ }
553
+ }
554
+ return { operation: head, review }
555
+ },
556
+ }
557
+
558
+ function ledgerOf(workspace, repoId) {
559
+ const repositoryRoot = repositoryRootOf(workspace.project, repoId)
560
+ if (repositoryRoot === null) return { state: 'not-enrolled', events: null, bytes: null, headroom: null }
561
+ // A store nobody made yet is not made to be asked: it has all of its room.
562
+ if (!fs.existsSync(path.join(repositoryRoot, PROPOSAL_STORE_DIRECTORY))) return { state: 'absent', events: 0, bytes: 0, headroom: { events: limits.maxEvents, bytes: limits.maxBytes } }
563
+ try {
564
+ const { state, events, bytes, headroom } = classifyLedgerRead(openStore({ workspaceRoot: repositoryRoot, workspaceId: workspace.workspaceId }).eventLedger.readAll(), limits)
565
+ return { state, events, bytes, headroom }
566
+ } catch { return { state: 'unavailable', events: null, bytes: null, headroom: null } }
567
+ }
568
+ }
569
+ }
570
+
571
+ export const createProposalAdapter = createProposalAdapterForOracleTests()
572
+ export { OPEN_QUEUE_STATES }