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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. package/CHANGELOG.md +372 -0
  2. package/README.md +402 -205
  3. package/SECURITY.md +6 -1
  4. package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
  5. package/contracts/atelier-coauthor.v1.schema.json +163 -0
  6. package/contracts/atelier-decision-request.v1.schema.json +476 -0
  7. package/contracts/atelier-decision-result.v1.schema.json +500 -0
  8. package/contracts/atelier-guide.v1.schema.json +227 -0
  9. package/contracts/atelier-intake.v1.schema.json +151 -0
  10. package/contracts/atelier-migration.v2.schema.json +61 -0
  11. package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
  12. package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
  13. package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
  14. package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
  15. package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
  16. package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
  17. package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
  18. package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
  19. package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
  20. package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
  21. package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
  22. package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
  23. package/contracts/atelier-presentation.v1.schema.json +1293 -0
  24. package/contracts/atelier-repository-observation.v1.schema.json +163 -0
  25. package/contracts/atelier-review.v1.schema.json +1601 -0
  26. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  27. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  28. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  29. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  30. package/contracts/public-api-baseline.json +57 -0
  31. package/docs/assurance-controls.md +49 -0
  32. package/docs/atelier-runtime.md +28 -2
  33. package/docs/atelier-sync.md +172 -0
  34. package/docs/blocks/audience-visibility.md +9 -0
  35. package/docs/blocks/claims.md +61 -0
  36. package/docs/blocks/conformance-admission.md +15 -0
  37. package/docs/blocks/will-not-do.md +18 -0
  38. package/docs/coauthor-session.md +57 -0
  39. package/docs/connected-composition.md +75 -0
  40. package/docs/continuity.md +3 -4
  41. package/docs/decisions.md +167 -0
  42. package/docs/design.md +167 -0
  43. package/docs/exact-upgrades.md +159 -0
  44. package/docs/extension-pack-lifecycle.md +33 -0
  45. package/docs/guided-upgrades.md +99 -0
  46. package/docs/install.md +99 -16
  47. package/docs/intake-and-guides.md +65 -0
  48. package/docs/integration-and-review-plan.md +409 -0
  49. package/docs/integration-contract-decisions.md +22 -0
  50. package/docs/knowledge-graph.md +8 -4
  51. package/docs/local-review.md +84 -0
  52. package/docs/local-services.md +328 -0
  53. package/docs/local-state.md +107 -0
  54. package/docs/obsidian-contract.md +690 -0
  55. package/docs/obsidian.md +423 -0
  56. package/docs/portable-presentation.md +328 -0
  57. package/docs/presentation-consumer-boundaries.md +75 -0
  58. package/docs/presentation-review-disposition.md +47 -0
  59. package/docs/project-command-inventory.md +38 -0
  60. package/docs/project-options.md +53 -0
  61. package/docs/release-engineering.md +174 -12
  62. package/docs/repo-boundary-guard.md +12 -2
  63. package/docs/review-portability.md +55 -0
  64. package/docs/skill-steward.md +156 -0
  65. package/docs/upgrade.md +146 -5
  66. package/docs/vault-service.md +292 -0
  67. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  68. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  69. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  70. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  71. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  72. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  73. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  74. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  75. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  76. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  77. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  78. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  79. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  80. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  81. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  82. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  83. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  84. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  85. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  86. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  87. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  88. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  89. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  90. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  91. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  92. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  93. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  94. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  95. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  96. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  97. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  98. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  99. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  100. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  101. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  102. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  103. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  104. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  105. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  106. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  107. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  108. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  109. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  110. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  111. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  112. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  113. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  114. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  115. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  116. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  117. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  118. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  119. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  120. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  121. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  122. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  123. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  125. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  127. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  128. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  129. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  130. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  131. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  132. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  133. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  134. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  135. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  136. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  137. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  138. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  139. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  140. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  141. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  142. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  143. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  144. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  145. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  146. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  147. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  148. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  149. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  150. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  151. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  152. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  153. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  154. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  159. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  160. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  161. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  162. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  163. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  164. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  165. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  166. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  167. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  168. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  169. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  170. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  171. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  172. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  173. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  174. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  175. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  176. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  177. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  178. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  179. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  181. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  182. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  183. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  186. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  187. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  188. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  189. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  190. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  191. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  192. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  193. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  194. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  195. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  196. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  197. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  198. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  199. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  200. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  201. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  202. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  203. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  204. package/fixtures/obsidian/edits/workspace.json +65 -0
  205. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  206. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  207. package/fixtures/obsidian/materialization/workspace.json +54 -0
  208. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  209. package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
  210. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
  211. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
  212. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
  213. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
  214. package/fixtures/ui/presentation/pane.v1.json +7 -0
  215. package/fixtures/ui/presentation/reference.v1.json +293 -0
  216. package/package.json +86 -10
  217. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  218. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  219. package/skills/claude/atelier-local-service/SKILL.md +47 -0
  220. package/skills/claude/atelier-open/SKILL.md +11 -5
  221. package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
  222. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  223. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  224. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  225. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  226. package/skills/codex/atelier-local-service/SKILL.md +47 -0
  227. package/skills/codex/atelier-open/SKILL.md +11 -5
  228. package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
  229. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  230. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  231. package/src/access/preview-enforcer.mjs +84 -0
  232. package/src/boundary/content-rules.mjs +283 -20
  233. package/src/boundary/policy.mjs +213 -91
  234. package/src/cli/execute-command.mjs +36 -0
  235. package/src/cli/project-options.mjs +58 -0
  236. package/src/cli/run.mjs +78 -10
  237. package/src/coauthor/session.mjs +161 -0
  238. package/src/coauthor/store.mjs +173 -0
  239. package/src/collaboration/event-ledger.mjs +348 -0
  240. package/src/collaboration/index.mjs +17 -0
  241. package/src/collaboration/inspection-bundle.mjs +223 -0
  242. package/src/collaboration/proposals.mjs +265 -65
  243. package/src/collaboration/review-contracts.mjs +25 -0
  244. package/src/collaboration/review-store.mjs +305 -0
  245. package/src/commands/attestation.mjs +20 -6
  246. package/src/commands/coauthor.mjs +26 -0
  247. package/src/commands/config.mjs +6 -1
  248. package/src/commands/disclosure.mjs +133 -0
  249. package/src/commands/distribution.mjs +2 -1
  250. package/src/commands/extension-pack.mjs +17 -9
  251. package/src/commands/init.mjs +46 -6
  252. package/src/commands/obsidian.mjs +371 -0
  253. package/src/commands/review.mjs +147 -0
  254. package/src/commands/server.mjs +1 -4
  255. package/src/commands/setup.mjs +51 -14
  256. package/src/commands/skills.mjs +199 -0
  257. package/src/commands/sync.mjs +107 -0
  258. package/src/composition/registry.mjs +27 -0
  259. package/src/composition/wire.mjs +122 -0
  260. package/src/contracts/corpus.mjs +35 -0
  261. package/src/decisions/contracts.d.mts +96 -0
  262. package/src/decisions/contracts.mjs +321 -0
  263. package/src/disclosure/content-scan.mjs +206 -0
  264. package/src/egress/check.mjs +7 -38
  265. package/src/egress/forbidden-egress.mjs +32 -18
  266. package/src/export/atelier-export-contract.mjs +16 -1
  267. package/src/extension-packs/lifecycle.mjs +154 -0
  268. package/src/graph/graph.mjs +176 -316
  269. package/src/graph/knowledge-graph.mjs +643 -43
  270. package/src/guides/contracts.mjs +54 -0
  271. package/src/harness/context-client.mjs +9 -1
  272. package/src/index.mjs +41 -0
  273. package/src/intake/store.mjs +119 -0
  274. package/src/preview/channel.mjs +89 -0
  275. package/src/project/config.mjs +112 -52
  276. package/src/project/durable-state.mjs +161 -0
  277. package/src/project/file-class.mjs +180 -0
  278. package/src/project/package-root.mjs +10 -0
  279. package/src/project/path-match.mjs +38 -15
  280. package/src/project/private-state.mjs +120 -0
  281. package/src/projection/obsidian/contracts.mjs +571 -0
  282. package/src/projection/obsidian/edits/align.mjs +169 -0
  283. package/src/projection/obsidian/edits/apply.mjs +889 -0
  284. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  285. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  286. package/src/projection/obsidian/edits/index.mjs +19 -0
  287. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  288. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  289. package/src/projection/obsidian/edits/observe.mjs +123 -0
  290. package/src/projection/obsidian/edits/policy.mjs +123 -0
  291. package/src/projection/obsidian/edits/regions.mjs +648 -0
  292. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  293. package/src/projection/obsidian/materialize/index.mjs +7 -0
  294. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  295. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  296. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  297. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  298. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  299. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  300. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  301. package/src/projection/obsidian/proposals/index.mjs +23 -0
  302. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  303. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  304. package/src/projection/obsidian/proposals/router.mjs +168 -0
  305. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  306. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  307. package/src/projection/obsidian/publication/index.mjs +5 -0
  308. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  309. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  310. package/src/projection/obsidian/publication/transport.mjs +160 -0
  311. package/src/projection/obsidian/recovery/index.mjs +6 -0
  312. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  313. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  314. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  315. package/src/projection/obsidian/recovery/store.mjs +234 -0
  316. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  317. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  318. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  319. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  320. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  321. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  322. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  323. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  324. package/src/projection/project.mjs +28 -19
  325. package/src/readiness-protocols/evidence.mjs +282 -0
  326. package/src/readiness-protocols/runtime.mjs +4 -3
  327. package/src/readiness-protocols/source-read.mjs +39 -0
  328. package/src/runtime/git-adapter.mjs +189 -0
  329. package/src/runtime/local-state.mjs +439 -0
  330. package/src/runtime/obsidian/app-capability.mjs +129 -0
  331. package/src/runtime/obsidian/app-production-seams.mjs +105 -0
  332. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  333. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  334. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  335. package/src/runtime/obsidian/contributions.mjs +27 -0
  336. package/src/runtime/obsidian/documents.mjs +37 -0
  337. package/src/runtime/obsidian/enablement.mjs +53 -0
  338. package/src/runtime/obsidian/engine.mjs +532 -0
  339. package/src/runtime/obsidian/errors.mjs +14 -0
  340. package/src/runtime/obsidian/extension-points.mjs +103 -0
  341. package/src/runtime/obsidian/index.mjs +34 -0
  342. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  343. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  344. package/src/runtime/obsidian/observation.mjs +158 -0
  345. package/src/runtime/obsidian/opening.mjs +263 -0
  346. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  347. package/src/runtime/obsidian/pipeline.mjs +122 -0
  348. package/src/runtime/obsidian/private-lock.mjs +156 -0
  349. package/src/runtime/obsidian/process-identity.mjs +58 -0
  350. package/src/runtime/obsidian/service-client.mjs +66 -0
  351. package/src/runtime/obsidian/service-entry-path.mjs +9 -0
  352. package/src/runtime/obsidian/service-main.mjs +91 -0
  353. package/src/runtime/obsidian/service-record.mjs +169 -0
  354. package/src/runtime/obsidian/service-server.mjs +121 -0
  355. package/src/runtime/obsidian/service.mjs +216 -0
  356. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  357. package/src/runtime/obsidian/state-store.mjs +180 -0
  358. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  359. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  360. package/src/runtime/obsidian/watchers.mjs +36 -0
  361. package/src/runtime/repository-observation.mjs +491 -0
  362. package/src/runtime/supervisor.mjs +803 -0
  363. package/src/server/local-sidecar.mjs +125 -59
  364. package/src/server/security.mjs +89 -4
  365. package/src/server/server.mjs +4 -2
  366. package/src/skills/steward.mjs +780 -0
  367. package/src/support/feedback-report.mjs +4 -3
  368. package/src/ui/presentation/browser.mjs +278 -0
  369. package/src/ui/presentation/contract.mjs +116 -0
  370. package/src/ui/presentation/index.mjs +7 -0
  371. package/src/ui/presentation/native.mjs +153 -0
  372. package/src/ui/presentation/proof.mjs +57 -0
  373. package/src/ui/presentation/schema-check.mjs +30 -0
  374. package/src/ui/presentation/schema.generated.mjs +1371 -0
  375. package/src/ui/presentation/state.mjs +43 -0
  376. package/src/ui/presentation/styles.mjs +67 -0
  377. package/src/ui/presentation/tokens.mjs +74 -0
  378. package/src/ui/presentation/web.mjs +85 -0
  379. package/src/ui/review-page.mjs +32 -0
  380. package/src/upgrade/explanation.mjs +33 -0
  381. package/src/upgrade/provenance.mjs +338 -0
  382. package/src/upgrade/transaction-files.mjs +103 -0
  383. package/src/upgrade/transaction.mjs +475 -0
  384. package/src/upgrade/upgrade.mjs +65 -29
  385. package/src/vault/hosts.mjs +21 -0
  386. package/src/vault/identity.mjs +19 -0
  387. package/src/vault/index.mjs +7 -0
  388. package/src/vault/interface.mjs +11 -0
  389. package/src/vault/metadata.mjs +31 -0
  390. package/src/vault/privacy.mjs +148 -0
  391. package/src/vault/probe.mjs +89 -0
  392. package/src/vault/service.mjs +131 -0
  393. package/src/vault/source.mjs +45 -0
  394. package/src/vault/storage.mjs +23 -0
  395. package/templates/distribution-workspace/.claude/launch.json +6 -2
  396. package/templates/distribution-workspace/gitignore +1 -0
  397. package/templates/distribution-workspace/project/README.md +2 -2
  398. package/templates/external-project-workspace/README.md +45 -0
  399. package/templates/external-project-workspace/answers.example.json +4 -0
  400. package/templates/external-project-workspace/atelier.project.json +40 -0
  401. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  402. package/templates/external-project-workspace/gitignore +8 -0
  403. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  404. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  405. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  406. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  407. package/templates/external-project-workspace/source/README.md +11 -0
  408. package/templates/launch.json +6 -2
  409. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  410. package/templates/private-domain-workspace/domain/README.md +2 -2
  411. package/templates/private-domain-workspace/gitignore +1 -0
  412. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  413. package/templates/shared-project-workspace/gitignore +1 -0
  414. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -1,12 +1,18 @@
1
1
  import crypto from 'node:crypto'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
+ import { createCollaborationEventLedger } from './event-ledger.mjs'
5
+ import {
6
+ atomicReplacePrivateText,
7
+ ensureContainedPrivateDirectory,
8
+ readRegularTextNoFollow,
9
+ } from '../project/private-state.mjs'
4
10
 
5
11
  export const ATELIER_PROPOSAL_SCHEMA = 'atelier-proposal@v1'
6
12
  export const ATELIER_PROPOSALS_SCHEMA = 'atelier-proposals@v1'
7
13
  export const PROPOSAL_REVIEW_STATUSES = new Set(['reviewed', 'accepted', 'rejected', 'superseded'])
8
-
9
- const DIRECT_WRITE_ACTION_RE = /(?:^|[._:-])(apply|write|commit|persist|mutate|db|database)(?:$|[._:-])/i
14
+ export const COPY_ONLY_PROPOSAL_CAPABILITY = 'proposal.copy-only'
15
+ const PROPOSAL_ID_PATTERN = /^proposal-[a-z0-9]+(?:-[a-z0-9]+)*$/
10
16
 
11
17
  function nowIso() {
12
18
  return new Date().toISOString()
@@ -20,6 +26,10 @@ function stableCompare(left, right) {
20
26
  return String(left).localeCompare(String(right), 'en')
21
27
  }
22
28
 
29
+ function isRecord(value) {
30
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
31
+ }
32
+
23
33
  function safeJsonText(value, max = 50000) {
24
34
  if (value == null) return ''
25
35
  if (typeof value === 'string') return value.slice(0, max)
@@ -30,24 +40,20 @@ function safeJsonText(value, max = 50000) {
30
40
  }
31
41
  }
32
42
 
33
- function ensurePrivateDir(dir) {
34
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
35
- try {
36
- fs.chmodSync(dir, 0o700)
37
- } catch {
38
- // Best effort on filesystems that do not support chmod.
39
- }
43
+ function secureWriteJson(file, payload) {
44
+ atomicReplacePrivateText(file, `${JSON.stringify(payload, null, 2)}\n`)
40
45
  }
41
46
 
42
- function secureWriteJson(file, payload) {
43
- ensurePrivateDir(path.dirname(file))
44
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
45
- fs.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 })
46
- fs.renameSync(tmp, file)
47
+ function readRegularJson(file) {
48
+ return JSON.parse(readRegularTextNoFollow(file))
49
+ }
50
+
51
+ function writeSnapshotProjectionWith(writer, file, payload) {
47
52
  try {
48
- fs.chmodSync(file, 0o600)
49
- } catch {
50
- // Best effort on filesystems that do not support chmod.
53
+ writer(file, payload)
54
+ return []
55
+ } catch (error) {
56
+ return [{ code: 'proposal-snapshot-write-failed', message: `compatibility snapshot was not updated: ${error.message}` }]
51
57
  }
52
58
  }
53
59
 
@@ -61,15 +67,25 @@ function proposalId(seed = crypto.randomBytes(16).toString('hex')) {
61
67
  return `proposal-${crypto.createHash('sha256').update(seed).digest('hex').slice(0, 32)}`
62
68
  }
63
69
 
64
- export function actionIsCopyOnly(action) {
65
- const value = String(action || '')
66
- return Boolean(value) && !DIRECT_WRITE_ACTION_RE.test(value)
70
+ export function validateCopyOnlyProposalAuthority(input = {}) {
71
+ const authority = input.authority && typeof input.authority === 'object' ? input.authority : {}
72
+ const capability = input.capability ?? authority.capability
73
+ const directWrite = input.directWrite ?? authority.directWrite
74
+ const applyEndpoint = input.applyEndpoint ?? authority.applyEndpoint
75
+ const issues = []
76
+ if (capability != null && capability !== COPY_ONLY_PROPOSAL_CAPABILITY) {
77
+ issues.push(`capability must be ${COPY_ONLY_PROPOSAL_CAPABILITY}`)
78
+ }
79
+ if (directWrite != null && directWrite !== false) issues.push('direct-write capability must be false')
80
+ if (applyEndpoint != null) issues.push('applyEndpoint must be null')
81
+ return issues.length ? { ok: false, status: 409, issues } : { ok: true, status: 200 }
67
82
  }
68
83
 
69
84
  export function copyOnlyActionSummary(action) {
70
85
  return {
71
86
  action: String(action || ''),
72
- copyOnly: actionIsCopyOnly(action),
87
+ capability: COPY_ONLY_PROPOSAL_CAPABILITY,
88
+ copyOnly: true,
73
89
  directWrite: false,
74
90
  applyEndpoint: null,
75
91
  }
@@ -107,15 +123,24 @@ export function acceptedProposalCopy(record) {
107
123
 
108
124
  export function createProposalStore({
109
125
  workspaceRoot = process.cwd(),
110
- proposalsDir = path.join(workspaceRoot, '.atelier-proposals'),
126
+ proposalsDir: requestedProposalsDir = path.join(workspaceRoot, '.atelier-proposals'),
111
127
  workspaceId = null,
128
+ snapshotWriter = secureWriteJson,
112
129
  } = {}) {
113
130
  const workspaceRootReal = fs.realpathSync(workspaceRoot)
114
- ensurePrivateDir(proposalsDir)
131
+ const proposalsDir = ensureContainedPrivateDirectory({
132
+ workspaceRoot,
133
+ directory: requestedProposalsDir,
134
+ label: 'proposal state directory',
135
+ })
136
+ const eventLedger = createCollaborationEventLedger({
137
+ workspaceRoot,
138
+ ledgerPath: path.join(proposalsDir, 'events.ndjson'),
139
+ })
115
140
 
116
141
  function proposalPath(id) {
117
- const clean = String(id || '').replace(/[^a-z0-9-]/gi, '')
118
- if (!clean) throw new Error('proposal id is required')
142
+ const clean = String(id || '')
143
+ if (clean.length > 200 || !PROPOSAL_ID_PATTERN.test(clean)) throw new Error('proposal id is invalid')
119
144
  const file = path.join(proposalsDir, `${clean}.json`)
120
145
  const candidateDir = fs.realpathSync(proposalsDir)
121
146
  if (!pathContainedBy(workspaceRootReal, candidateDir)) {
@@ -124,47 +149,189 @@ export function createProposalStore({
124
149
  return file
125
150
  }
126
151
 
127
- // Read is a lookup, not an assertion: an unusable id and an unreadable file
128
- // are both "no such proposal", never a throw. Callers render 404 from null.
129
- // listProposals() has always guarded its parse; this is the same contract.
130
- function readProposal(id) {
152
+ function reduceProposal(state, event) {
153
+ if (event.type === 'proposal-created' || event.type === 'proposal-imported') {
154
+ return event.payload.record
155
+ }
156
+ if (event.type === 'proposal-reviewed' && state) {
157
+ if (event.payload.record) return event.payload.record
158
+ const next = {
159
+ ...state,
160
+ proposal: {
161
+ ...state.proposal,
162
+ status: event.payload.status,
163
+ updatedAt: event.at,
164
+ review: event.payload.review,
165
+ eventVersion: event.version,
166
+ },
167
+ }
168
+ if (event.payload.copyable) next.copyable = event.payload.copyable
169
+ else delete next.copyable
170
+ return next
171
+ }
172
+ return state
173
+ }
174
+
175
+ function reduceProposalEvents(id, events) {
176
+ if (events.length === 0) {
177
+ return { ok: false, status: 404, error: 'proposal not found', record: null }
178
+ }
179
+ let state = null
180
+ let previousVersion = 0
181
+ for (const event of events) {
182
+ if (state === null) {
183
+ if (event.version !== 1 || !['proposal-created', 'proposal-imported'].includes(event.type)) {
184
+ return { ok: false, status: 422, error: 'proposal ledger does not begin with a canonical record', record: null }
185
+ }
186
+ const record = event.payload?.record
187
+ if (
188
+ !isRecord(record) || record.schema !== ATELIER_PROPOSAL_SCHEMA ||
189
+ !isRecord(record.proposal) || record.proposal.id !== id ||
190
+ !['proposed', ...PROPOSAL_REVIEW_STATUSES].includes(record.proposal.status)
191
+ ) {
192
+ return { ok: false, status: 422, error: 'proposal ledger record is invalid', record: null }
193
+ }
194
+ } else {
195
+ const checkpoint = event.payload?.record
196
+ const checkpointValid = (
197
+ isRecord(checkpoint) && checkpoint.schema === ATELIER_PROPOSAL_SCHEMA &&
198
+ isRecord(checkpoint.proposal) && checkpoint.proposal.id === id &&
199
+ checkpoint.proposal.status === event.payload?.status &&
200
+ checkpoint.proposal.eventVersion === event.version
201
+ )
202
+ const hasCompactionGap = event.version > previousVersion + 1
203
+ if (
204
+ event.type !== 'proposal-reviewed' || !PROPOSAL_REVIEW_STATUSES.has(event.payload?.status) ||
205
+ !isRecord(event.payload?.review) ||
206
+ (checkpoint !== undefined && !checkpointValid) ||
207
+ (hasCompactionGap && !checkpointValid) ||
208
+ (!hasCompactionGap && !canTransitionProposal(state.proposal.status, event.payload.status))
209
+ ) {
210
+ return { ok: false, status: 422, error: 'proposal ledger review sequence is invalid', record: null }
211
+ }
212
+ }
213
+ state = reduceProposal(state, event)
214
+ previousVersion = event.version
215
+ }
216
+ return { ok: true, status: 200, record: state }
217
+ }
218
+
219
+ function materializedProposal(id) {
220
+ const result = eventLedger.eventsFor(id)
221
+ if (!result.ok) return { ...result, record: null }
222
+ return { ...result, ...reduceProposalEvents(id, result.events) }
223
+ }
224
+
225
+ function readCompatibilitySnapshot(id) {
131
226
  let file
132
227
  try {
133
228
  file = proposalPath(id)
134
229
  } catch {
135
- return null
230
+ return { ok: false, status: 404, error: 'proposal not found', record: null }
136
231
  }
137
- if (!fs.existsSync(file)) return null
232
+ if (!fs.existsSync(file)) return { ok: false, status: 404, error: 'proposal not found', record: null }
138
233
  try {
139
- return JSON.parse(fs.readFileSync(file, 'utf8'))
140
- } catch {
141
- return null
234
+ const resolved = fs.realpathSync(file)
235
+ if (!pathContainedBy(fs.realpathSync(proposalsDir), resolved)) {
236
+ throw new Error('proposal snapshot escapes workspace')
237
+ }
238
+ const record = readRegularJson(file)
239
+ if (record?.proposal?.id !== id) throw new Error('proposal snapshot id does not match its filename')
240
+ return { ok: true, status: 200, record, source: 'compatibility-snapshot' }
241
+ } catch (error) {
242
+ return { ok: false, status: 422, error: `proposal snapshot cannot be read: ${error.message}`, record: null }
243
+ }
244
+ }
245
+
246
+ // Read is a lookup, not an assertion: an unusable id and an unreadable file
247
+ // are both "no such proposal", never a throw. New records materialize from
248
+ // the append-only ledger. Per-proposal JSON remains a compatibility snapshot.
249
+ function readProposal(id) {
250
+ if (String(id || '').length > 200 || !PROPOSAL_ID_PATTERN.test(String(id || ''))) {
251
+ return { ok: false, status: 404, error: 'proposal not found', record: null }
142
252
  }
253
+ const materialized = materializedProposal(id)
254
+ if (materialized.ok) {
255
+ if (materialized.record?.proposal?.id !== id) {
256
+ return { ok: false, status: 422, error: 'proposal ledger identity mismatch', record: null }
257
+ }
258
+ return materialized
259
+ }
260
+ if (materialized.status !== 404) return materialized
261
+ return readCompatibilitySnapshot(id)
143
262
  }
144
263
 
145
264
  function listProposals() {
146
- if (!fs.existsSync(proposalsDir)) return []
147
- return fs.readdirSync(proposalsDir)
148
- .filter((name) => name.endsWith('.json'))
149
- .sort(stableCompare)
150
- .map((name) => {
151
- try {
152
- return JSON.parse(fs.readFileSync(path.join(proposalsDir, name), 'utf8'))
153
- } catch {
154
- return null
155
- }
156
- })
157
- .filter(Boolean)
265
+ if (!fs.existsSync(proposalsDir)) return { ok: true, status: 200, proposals: [] }
266
+ const ledger = eventLedger.readAll()
267
+ if (!ledger.ok) return { ...ledger, proposals: [] }
268
+ const ledgerEvents = new Map()
269
+ for (const event of ledger.events) {
270
+ if (!PROPOSAL_ID_PATTERN.test(event.aggregateId)) {
271
+ return { ok: false, status: 422, error: 'proposal ledger contains an invalid identity', proposals: [] }
272
+ }
273
+ const events = ledgerEvents.get(event.aggregateId) ?? []
274
+ events.push(event)
275
+ ledgerEvents.set(event.aggregateId, events)
276
+ }
277
+ const ledgerRecords = new Map()
278
+ for (const [id, events] of ledgerEvents) {
279
+ const reduced = reduceProposalEvents(id, events)
280
+ if (!reduced.ok) return { ...reduced, proposals: [] }
281
+ ledgerRecords.set(id, reduced.record)
282
+ }
283
+ const ledgerIds = [...ledgerRecords.keys()]
284
+ const snapshotEntries = fs.readdirSync(proposalsDir, { withFileTypes: true })
285
+ .filter((entry) => entry.name.endsWith('.json'))
286
+ const unsafeSnapshot = snapshotEntries.find((entry) => !entry.isFile())
287
+ if (unsafeSnapshot) {
288
+ return { ok: false, status: 422, error: 'proposal snapshot cannot be read: state leaf is not a regular file', proposals: [] }
289
+ }
290
+ const snapshotIds = snapshotEntries.map((entry) => entry.name.slice(0, -'.json'.length))
291
+ const readIds = [...new Set([...ledgerIds, ...snapshotIds])].sort(stableCompare)
292
+ const reads = readIds.map((id) => {
293
+ const record = ledgerRecords.get(id)
294
+ if (record) return { ok: true, status: 200, record, source: 'event-ledger' }
295
+ return readCompatibilitySnapshot(id)
296
+ })
297
+ const failed = reads.find((result) => !result.ok)
298
+ if (failed) return { ...failed, proposals: [] }
299
+ if (reads.some((result, index) => result.record?.proposal?.id !== readIds[index])) {
300
+ return { ok: false, status: 422, error: 'proposal identity does not match its state key', proposals: [] }
301
+ }
302
+ const proposals = reads
303
+ .map((result) => result.record)
158
304
  .sort((left, right) => stableCompare(right.proposal?.updatedAt || '', left.proposal?.updatedAt || ''))
305
+ return { ok: true, status: 200, proposals, diagnostics: ledger.diagnostics, stats: ledger.stats }
306
+ }
307
+
308
+ function ensureLedgerSeed(id, record) {
309
+ const existing = eventLedger.eventsFor(id)
310
+ if (!existing.ok) return existing
311
+ if (existing.events.length > 0) return { ok: true, version: existing.currentVersion }
312
+ const seeded = eventLedger.append({
313
+ aggregateId: id,
314
+ expectedVersion: 0,
315
+ type: 'proposal-imported',
316
+ actor: 'atelier compatibility importer',
317
+ at: record.proposal?.createdAt || nowIso(),
318
+ payload: { record },
319
+ })
320
+ if (!seeded.ok) return seeded
321
+ return { ok: true, version: seeded.event.version }
159
322
  }
160
323
 
161
324
  function createProposal(body = {}) {
162
325
  const action = cleanIdentity(body.action || body.proposal?.action || 'copy.repoPath', 120)
163
- if (!actionIsCopyOnly(action)) {
326
+ const authority = validateCopyOnlyProposalAuthority({
327
+ ...(body.proposal && typeof body.proposal === 'object' ? body.proposal : {}),
328
+ ...body,
329
+ })
330
+ if (!authority.ok) {
164
331
  return {
165
332
  ok: false,
166
- status: 409,
167
- error: 'direct-write proposal action refused',
333
+ status: authority.status,
334
+ error: `proposal authority refused: ${authority.issues.join('; ')}`,
168
335
  }
169
336
  }
170
337
 
@@ -196,19 +363,30 @@ export function createProposalStore({
196
363
  ignored: true,
197
364
  },
198
365
  authority: copyOnlyActionSummary(action),
366
+ eventVersion: 1,
199
367
  },
200
368
  diff: safeJsonText(body.diff || body.proposal?.diff || ''),
201
369
  payload: body.proposal && typeof body.proposal === 'object' ? body.proposal : {},
202
370
  }
203
- secureWriteJson(proposalPath(id), record)
204
- return { ok: true, status: 200, record }
371
+ const appended = eventLedger.append({
372
+ aggregateId: id,
373
+ expectedVersion: 0,
374
+ type: 'proposal-created',
375
+ actor: cleanIdentity(body.actor || body.proposal?.createdBy || 'local contributor', 160),
376
+ at: createdAt,
377
+ payload: { record },
378
+ })
379
+ if (!appended.ok) {
380
+ return { ok: false, status: appended.status, error: appended.error }
381
+ }
382
+ const diagnostics = writeSnapshotProjectionWith(snapshotWriter, proposalPath(id), record)
383
+ return { ok: true, status: 200, record, diagnostics }
205
384
  }
206
385
 
207
386
  function reviewProposal(id, body = {}) {
208
- const record = readProposal(id)
209
- if (!record) {
210
- return { ok: false, status: 404, error: 'proposal not found' }
211
- }
387
+ const read = readProposal(id)
388
+ if (!read.ok) return read
389
+ const record = read.record
212
390
  if (body.proposalId && body.proposalId !== id) {
213
391
  return { ok: false, status: 409, error: 'ambiguous proposal review refused' }
214
392
  }
@@ -232,27 +410,49 @@ export function createProposalStore({
232
410
  }
233
411
 
234
412
  const updatedAt = nowIso()
235
- record.proposal = {
413
+ const review = {
414
+ reviewer: cleanIdentity(body.reviewer || 'unknown reviewer', 160),
415
+ notes: cleanIdentity(body.notes, 2000),
416
+ reviewedAt: updatedAt,
417
+ }
418
+ const nextProposal = {
236
419
  ...record.proposal,
237
420
  status: nextStatus,
238
421
  updatedAt,
239
- review: {
240
- reviewer: cleanIdentity(body.reviewer || 'unknown reviewer', 160),
241
- notes: cleanIdentity(body.notes, 2000),
242
- reviewedAt: updatedAt,
243
- },
422
+ review,
244
423
  }
424
+ const nextRecord = { ...record, proposal: nextProposal }
245
425
  if (nextStatus === 'accepted') {
246
- record.copyable = acceptedProposalCopy(record)
426
+ nextRecord.copyable = acceptedProposalCopy(nextRecord)
247
427
  } else {
248
- delete record.copyable
428
+ delete nextRecord.copyable
429
+ }
430
+ const seeded = ensureLedgerSeed(id, record)
431
+ if (!seeded.ok) return seeded
432
+ nextRecord.proposal.eventVersion = seeded.version + 1
433
+ const appended = eventLedger.append({
434
+ aggregateId: id,
435
+ expectedVersion: seeded.version,
436
+ type: 'proposal-reviewed',
437
+ actor: review.reviewer,
438
+ at: updatedAt,
439
+ payload: {
440
+ status: nextStatus,
441
+ review,
442
+ record: nextRecord,
443
+ ...(nextRecord.copyable ? { copyable: nextRecord.copyable } : {}),
444
+ },
445
+ })
446
+ if (!appended.ok) {
447
+ return { ok: false, status: appended.status, error: appended.error }
249
448
  }
250
- secureWriteJson(proposalPath(id), record)
251
- return { ok: true, status: 200, record }
449
+ const diagnostics = writeSnapshotProjectionWith(snapshotWriter, proposalPath(id), nextRecord)
450
+ return { ok: true, status: 200, record: nextRecord, diagnostics }
252
451
  }
253
452
 
254
453
  return {
255
454
  proposalsDir,
455
+ eventLedger,
256
456
  proposalPath,
257
457
  readProposal,
258
458
  listProposals,
@@ -0,0 +1,25 @@
1
+ import fs from 'node:fs'
2
+ import Ajv2020 from 'ajv/dist/2020.js'
3
+ import addFormats from 'ajv-formats'
4
+ const ajv = new Ajv2020({ allErrors: true, strict: false })
5
+ addFormats(ajv)
6
+ for (const name of ['atelier-review'])
7
+ ajv.addSchema(
8
+ JSON.parse(
9
+ fs.readFileSync(
10
+ new URL(`../../contracts/${name}.v1.schema.json`, import.meta.url),
11
+ 'utf8',
12
+ ),
13
+ ),
14
+ )
15
+ const validators = new Map()
16
+ export function validReviewArtifact(kind, value) {
17
+ if (!validators.has(kind))
18
+ validators.set(
19
+ kind,
20
+ ajv.compile({
21
+ $ref: `https://mnstry.ai/schemas/atelier/atelier-review.v1.schema.json#/$defs/${kind}`,
22
+ }),
23
+ )
24
+ return validators.get(kind)(value)
25
+ }