@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
@@ -5,9 +5,17 @@ import fs from 'node:fs'
5
5
  import http from 'node:http'
6
6
  import path from 'node:path'
7
7
  import { fileURLToPath } from 'node:url'
8
+ import {
9
+ atomicReplacePrivateText,
10
+ ensureContainedPrivateDirectory,
11
+ readRegularTextNoFollow,
12
+ } from '../project/private-state.mjs'
8
13
  import {
9
14
  expectedOriginForRequest,
10
15
  htmlDocumentHeaders,
16
+ isLoopbackHost,
17
+ loadPublishedWorkspaceManifest,
18
+ publishedStaticPathVerdict,
11
19
  requestHeader,
12
20
  resolveWorkspacePath,
13
21
  staticHeaders,
@@ -22,6 +30,10 @@ import {
22
30
  renderProposalListPageHtml,
23
31
  } from '../ui/html-primitives.mjs'
24
32
 
33
+ import { createReviewStore } from '../collaboration/review-store.mjs'
34
+ import { loadBoundRun, currentRunEligibility } from '../readiness-protocols/evidence.mjs'
35
+ import { renderReviewPage } from '../ui/review-page.mjs'
36
+
25
37
  export const ATELIER_CONTEXT_SCHEMA = 'atelier-context@v1'
26
38
  export const ATELIER_PRESENCE_SCHEMA = 'atelier-presence@v1'
27
39
  export const ATELIER_RESOLVE_SCHEMA = 'atelier-resolve@v1'
@@ -43,39 +55,37 @@ function nowMs() {
43
55
 
44
56
  function readJsonFile(file, fallback) {
45
57
  try {
46
- return JSON.parse(fs.readFileSync(file, 'utf8'))
58
+ return JSON.parse(readRegularTextNoFollow(file))
47
59
  } catch {
48
60
  return fallback
49
61
  }
50
62
  }
51
63
 
52
64
  function secureWriteJson(file, payload, mode = 0o600) {
53
- fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
54
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
55
- fs.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, { mode })
56
- fs.renameSync(tmp, file)
57
- try {
58
- fs.chmodSync(file, mode)
59
- } catch {
60
- // Best effort on filesystems that do not support chmod.
61
- }
65
+ atomicReplacePrivateText(file, `${JSON.stringify(payload, null, 2)}\n`, mode)
62
66
  }
63
67
 
64
68
  function readOrCreateNonce(noncePath) {
65
- const existing = fs.existsSync(noncePath) ? fs.readFileSync(noncePath, 'utf8').trim() : ''
69
+ let existing = ''
70
+ if (fs.existsSync(noncePath)) {
71
+ const stat = fs.lstatSync(noncePath)
72
+ if (!stat.isFile()) throw new Error('nonce state leaf is not a regular file')
73
+ existing = readRegularTextNoFollow(noncePath).trim()
74
+ }
66
75
  if (/^[a-f0-9]{64}$/.test(existing)) return existing
67
76
  const nonce = crypto.randomBytes(32).toString('hex')
68
- fs.writeFileSync(noncePath, `${nonce}\n`, { mode: 0o600 })
77
+ atomicReplacePrivateText(noncePath, `${nonce}\n`)
69
78
  return nonce
70
79
  }
71
80
 
72
- function json(res, code, obj) {
81
+ function json(res, code, obj, headers = {}) {
73
82
  const body = Buffer.from(`${JSON.stringify(obj, null, 2)}\n`)
74
83
  res.writeHead(code, {
75
84
  'Content-Type': 'application/json; charset=utf-8',
76
85
  'Content-Length': String(body.length),
77
86
  'Cache-Control': 'no-store',
78
87
  'X-Content-Type-Options': 'nosniff',
88
+ ...headers,
79
89
  })
80
90
  res.end(body)
81
91
  }
@@ -200,21 +210,40 @@ function doctorEnvelope() {
200
210
  export function createAtelierSidecarServer({
201
211
  workspaceRoot = process.cwd(),
202
212
  stateDir = workspaceRoot,
213
+ reviewProject = null,
203
214
  port = 0,
204
215
  presenceTtlMs = Number(process.env.ATELIER_PRESENCE_TTL_MS || 5 * 60 * 1000),
205
216
  } = {}) {
206
217
  const root = path.resolve(workspaceRoot)
207
218
  const rootReal = fs.realpathSync(root)
219
+ const stateDirReal = ensureContainedPrivateDirectory({
220
+ workspaceRoot: root,
221
+ directory: stateDir,
222
+ label: 'Atelier state directory',
223
+ })
224
+ const publication = loadPublishedWorkspaceManifest(root)
225
+ const review = reviewProject ? createReviewStore(reviewProject) : null
208
226
  const workspaceId = workspaceIdForRoot(root)
209
- const noncePath = path.join(stateDir, '.atelier-nonce')
210
- const presencePath = path.join(stateDir, '.atelier-presence.json')
227
+ const noncePath = path.join(stateDirReal, '.atelier-nonce')
228
+ const presencePath = path.join(stateDirReal, '.atelier-presence.json')
211
229
  const mutationNonce = readOrCreateNonce(noncePath)
212
230
  const proposals = createProposalStore({
213
231
  workspaceRoot: root,
214
- proposalsDir: path.join(stateDir, '.atelier-proposals'),
232
+ proposalsDir: path.join(stateDirReal, '.atelier-proposals'),
215
233
  workspaceId,
216
234
  })
217
235
 
236
+ function resolvePublishedPath({ rel, requireFile = false, requireHtml = false } = {}) {
237
+ const resolved = resolveWorkspacePath({
238
+ workspaceRoot: root,
239
+ workspaceRootReal: rootReal,
240
+ rel,
241
+ requireFile,
242
+ requireHtml,
243
+ })
244
+ return publishedStaticPathVerdict({ resolved, publication })
245
+ }
246
+
218
247
  function readPresence() {
219
248
  const presence = readJsonFile(presencePath, defaultPresence())
220
249
  return {
@@ -229,12 +258,7 @@ export function createAtelierSidecarServer({
229
258
  }
230
259
 
231
260
  function recordView(rel, hints = {}) {
232
- const resolved = resolveWorkspacePath({
233
- workspaceRoot: root,
234
- workspaceRootReal: rootReal,
235
- rel,
236
- requireHtml: true,
237
- })
261
+ const resolved = resolvePublishedPath({ rel, requireHtml: true })
238
262
  if (!resolved.ok) return { ok: false, error: 'unknown workspace html file' }
239
263
  const entry = {
240
264
  sessionId: cleanIdentity(hints.sessionId, 120),
@@ -295,12 +319,7 @@ export function createAtelierSidecarServer({
295
319
  const viewId = cleanIdentity(url.searchParams.get('viewId'), 120)
296
320
  const rel = cleanIdentity(url.searchParams.get('path') || 'index.html', 500)
297
321
  const expectedWorkspaceId = cleanIdentity(url.searchParams.get('expectedWorkspaceId'), 120)
298
- const resolved = resolveWorkspacePath({
299
- workspaceRoot: root,
300
- workspaceRootReal: rootReal,
301
- rel,
302
- requireHtml: true,
303
- })
322
+ const resolved = resolvePublishedPath({ rel, requireHtml: true })
304
323
  const current = resolveCurrentContext({ sessionId })
305
324
  const workspaceVerified = Boolean(expectedWorkspaceId && expectedWorkspaceId === workspaceId)
306
325
  const caps = capabilityContract()
@@ -353,12 +372,31 @@ export function createAtelierSidecarServer({
353
372
  json(res, 400, { ok: false, error: error.message })
354
373
  return
355
374
  }
375
+ // Browsers send Origin on POST; same-origin GET normally omits it. Keep
376
+ // the strict origin gate for nonce disclosure instead of relaxing the
377
+ // existing session-auth GET contract or accepting unclassified reads.
378
+ if (review && url.pathname === '/api/review/session') {
379
+ if (!trustedMutationRequest(req.headers) || !body || Array.isArray(body) || Object.keys(body).length) {
380
+ json(res, 403, { ok: false, error: 'review session requires a same-origin empty request' })
381
+ return
382
+ }
383
+ const published = resolvePublishedPath({ rel: 'index.html', requireHtml: true })
384
+ if (!published.ok) { json(res, 404, { ok: false, error: 'published workspace unavailable' }); return }
385
+ json(res, 200, { ok: true, schema: ATELIER_SESSION_AUTH_SCHEMA, workspaceId, mutationNonce })
386
+ return
387
+ }
356
388
  const auth = requireMutationAuth(req, body, mutationNonce)
357
389
  if (!auth.ok) {
358
390
  json(res, auth.status, { ok: false, error: auth.error })
359
391
  return
360
392
  }
361
393
 
394
+ if (review && url.pathname === '/api/review/contributions') {
395
+ const result = review.contribute(body)
396
+ json(res, result.status, result)
397
+ return
398
+ }
399
+
362
400
  if (url.pathname === '/api/seen') {
363
401
  const result = recordView(body.rel || body.path, body)
364
402
  json(res, 200, result.ok ? { ok: true, entry: result.entry } : result)
@@ -366,12 +404,7 @@ export function createAtelierSidecarServer({
366
404
  }
367
405
 
368
406
  if (url.pathname === '/api/proposals') {
369
- const resolved = resolveWorkspacePath({
370
- workspaceRoot: root,
371
- workspaceRootReal: rootReal,
372
- rel: body.path || body.rel,
373
- requireHtml: true,
374
- })
407
+ const resolved = resolvePublishedPath({ rel: body.path || body.rel, requireHtml: true })
375
408
  if (!resolved.ok) {
376
409
  json(res, 200, { ok: false, error: 'unknown workspace html file' })
377
410
  return
@@ -392,6 +425,7 @@ export function createAtelierSidecarServer({
392
425
  schema: result.record.schema,
393
426
  workspaceId,
394
427
  proposal: result.record.proposal,
428
+ diagnostics: result.diagnostics,
395
429
  })
396
430
  return
397
431
  }
@@ -416,6 +450,7 @@ export function createAtelierSidecarServer({
416
450
  proposal: result.record.proposal,
417
451
  diff: result.record.diff,
418
452
  copyable: result.record.copyable,
453
+ diagnostics: result.diagnostics,
419
454
  })
420
455
  return
421
456
  }
@@ -430,12 +465,7 @@ export function createAtelierSidecarServer({
430
465
  }
431
466
 
432
467
  const rel = url.pathname === '/' ? 'index.html' : url.pathname.slice(1)
433
- const resolved = resolveWorkspacePath({
434
- workspaceRoot: root,
435
- workspaceRootReal: rootReal,
436
- rel,
437
- requireFile: true,
438
- })
468
+ const resolved = resolvePublishedPath({ rel, requireFile: true })
439
469
  if (!resolved.ok) {
440
470
  text(res, resolved.status || 403, `${resolved.error || 'forbidden'}\n`)
441
471
  return
@@ -463,11 +493,40 @@ export function createAtelierSidecarServer({
463
493
  return
464
494
  }
465
495
 
496
+ if (!['GET', 'HEAD', 'POST'].includes(req.method || '')) {
497
+ json(res, 405, { ok: false, error: 'method not allowed' }, { Allow: 'GET, HEAD, POST' })
498
+ return
499
+ }
500
+
501
+ if (!trustedReadRequest(req.headers, { expectedOrigin })) {
502
+ json(res, 403, { ok: false, error: 'cross-origin or unclassified request refused' })
503
+ return
504
+ }
505
+
466
506
  if (req.method === 'POST') {
467
507
  await handlePost(req, res, url)
468
508
  return
469
509
  }
470
510
 
511
+ if (review && url.pathname === '/review') {
512
+ const body = renderReviewPage()
513
+ res.writeHead(200, htmlDocumentHeaders(Buffer.byteLength(body)))
514
+ res.end(body)
515
+ return
516
+ }
517
+ if (review && url.pathname.startsWith('/api/review/')) {
518
+ let result
519
+ if (url.pathname === '/api/review/records') result = review.records()
520
+ else if (url.pathname === '/api/review/document') result = review.document(url.searchParams.get('repo'), url.searchParams.get('path'))
521
+ else if (url.pathname === '/api/review/run') {
522
+ const bound = loadBoundRun(reviewProject, url.searchParams.get('id'))
523
+ result = bound.ok ? {...bound, current:currentRunEligibility(reviewProject,bound)} : bound
524
+ } else if (url.pathname === '/api/review/handoff') result = review.handoff(url.searchParams.get('id'))
525
+ else result = {ok:false,status:404,error:'unknown review action'}
526
+ json(res, result.status, result)
527
+ return
528
+ }
529
+
471
530
  if (url.pathname === '/api/health') {
472
531
  json(res, 200, { ok: true, workspaceId })
473
532
  return
@@ -478,12 +537,7 @@ export function createAtelierSidecarServer({
478
537
  return
479
538
  }
480
539
  const rel = url.searchParams.get('path') || 'index.html'
481
- const resolved = resolveWorkspacePath({
482
- workspaceRoot: root,
483
- workspaceRootReal: rootReal,
484
- rel,
485
- requireHtml: true,
486
- })
540
+ const resolved = resolvePublishedPath({ rel, requireHtml: true })
487
541
  if (!resolved.ok) {
488
542
  json(res, 404, { ok: false, error: 'unknown workspace html file' })
489
543
  return
@@ -509,12 +563,7 @@ export function createAtelierSidecarServer({
509
563
  return
510
564
  }
511
565
  if (url.pathname === '/api/resolve') {
512
- const resolved = resolveWorkspacePath({
513
- workspaceRoot: root,
514
- workspaceRootReal: rootReal,
515
- rel: url.searchParams.get('path') || 'index.html',
516
- requireHtml: true,
517
- })
566
+ const resolved = resolvePublishedPath({ rel: url.searchParams.get('path') || 'index.html', requireHtml: true })
518
567
  json(res, resolved.ok ? 200 : (resolved.status || 404), {
519
568
  ok: resolved.ok,
520
569
  schema: ATELIER_RESOLVE_SCHEMA,
@@ -533,37 +582,49 @@ export function createAtelierSidecarServer({
533
582
  return
534
583
  }
535
584
  if (url.pathname === '/api/proposals') {
585
+ const listed = proposals.listProposals()
586
+ if (!listed.ok) {
587
+ json(res, listed.status, { ok: false, error: listed.error, diagnostics: listed.diagnostics })
588
+ return
589
+ }
536
590
  json(res, 200, {
537
591
  ok: true,
538
592
  schema: 'atelier-proposals@v1',
539
593
  workspaceId,
540
- proposals: proposals.listProposals(),
594
+ proposals: listed.proposals,
541
595
  })
542
596
  return
543
597
  }
544
598
  const proposalReadMatch = url.pathname.match(/^\/api\/proposals\/([^/]+)$/)
545
599
  if (proposalReadMatch) {
546
- const record = proposals.readProposal(proposalReadMatch[1])
547
- json(res, record ? 200 : 404, record ? {
600
+ const result = proposals.readProposal(proposalReadMatch[1])
601
+ const record = result.record
602
+ json(res, result.ok ? 200 : result.status, result.ok ? {
548
603
  ok: true,
549
604
  schema: record.schema,
550
605
  workspaceId,
551
606
  proposal: record.proposal,
552
607
  diff: record.diff,
553
608
  copyable: record.copyable,
554
- } : { ok: false, error: 'proposal not found' })
609
+ } : { ok: false, error: result.error, diagnostics: result.diagnostics })
555
610
  return
556
611
  }
557
612
  if (url.pathname === '/proposals') {
558
- const body = renderProposalListPageHtml(proposals.listProposals())
613
+ const listed = proposals.listProposals()
614
+ if (!listed.ok) {
615
+ text(res, listed.status, `${listed.error}\n`)
616
+ return
617
+ }
618
+ const body = renderProposalListPageHtml(listed.proposals)
559
619
  res.writeHead(200, htmlDocumentHeaders(Buffer.byteLength(body)))
560
620
  res.end(body)
561
621
  return
562
622
  }
563
623
  const proposalPageMatch = url.pathname.match(/^\/proposals\/([^/]+)$/)
564
624
  if (proposalPageMatch) {
565
- const body = renderProposalDetailPageHtml(proposals.readProposal(proposalPageMatch[1]))
566
- res.writeHead(200, htmlDocumentHeaders(Buffer.byteLength(body)))
625
+ const result = proposals.readProposal(proposalPageMatch[1])
626
+ const body = renderProposalDetailPageHtml(result.record)
627
+ res.writeHead(result.ok ? 200 : result.status, htmlDocumentHeaders(Buffer.byteLength(body)))
567
628
  res.end(body)
568
629
  return
569
630
  }
@@ -595,8 +656,12 @@ export function createAtelierSidecarServer({
595
656
  workspaceRootReal: rootReal,
596
657
  workspaceId,
597
658
  mutationNonce,
659
+ publication,
598
660
  proposals,
599
661
  listen(listenPort = port, host = '127.0.0.1') {
662
+ if (!isLoopbackHost(host)) {
663
+ return Promise.reject(new Error(`non-loopback listen host refused: ${host}`))
664
+ }
600
665
  // A busy port is an ordinary condition, not a crash: surface it as a
601
666
  // rejection the CLI can print, instead of an unhandled 'error' event.
602
667
  return new Promise((resolve, reject) => {
@@ -615,6 +680,7 @@ export function createAtelierSidecarServer({
615
680
  })
616
681
  },
617
682
  close() {
683
+ if (!server.listening) return Promise.resolve()
618
684
  return new Promise((resolve, reject) => {
619
685
  server.close((error) => {
620
686
  if (error) reject(error)
@@ -37,6 +37,9 @@ export const LOOPBACK_HOSTS = new Set([
37
37
  '[::1]',
38
38
  ])
39
39
 
40
+ export const ATELIER_MANIFEST_SCHEMA = 'mnstry.atelier-manifest@v1'
41
+ export const PUBLISHED_STATIC_EXTENSIONS = new Set(['.html', '.js', '.mjs', '.css', '.svg', '.png', '.jpg', '.jpeg', '.webp'])
42
+
40
43
  export function parseHostHeader(value) {
41
44
  const raw = String(value || '').trim().toLowerCase()
42
45
  if (!raw) return ''
@@ -66,7 +69,7 @@ export function expectedOriginForRequest(headers, fallbackPort = null) {
66
69
  }
67
70
 
68
71
  export function sameOrigin(origin, expectedOrigin) {
69
- if (!origin) return true
72
+ if (!origin) return false
70
73
  if (!expectedOrigin) return false
71
74
  try {
72
75
  return new URL(origin).origin === new URL(expectedOrigin).origin
@@ -77,7 +80,7 @@ export function sameOrigin(origin, expectedOrigin) {
77
80
 
78
81
  export function trustedFetchSite(headers) {
79
82
  const value = requestHeader(headers, 'sec-fetch-site').toLowerCase()
80
- return value === '' || value === 'none' || value === 'same-origin' || value === 'same-site'
83
+ return value === 'none' || value === 'same-origin'
81
84
  }
82
85
 
83
86
  export function trustedHost(headers) {
@@ -85,11 +88,92 @@ export function trustedHost(headers) {
85
88
  }
86
89
 
87
90
  export function trustedReadRequest(headers, { expectedOrigin = expectedOriginForRequest(headers) } = {}) {
88
- return trustedHost(headers) && sameOrigin(requestHeader(headers, 'origin'), expectedOrigin)
91
+ const origin = requestHeader(headers, 'origin')
92
+ return trustedHost(headers) && trustedFetchSite(headers) && (!origin || sameOrigin(origin, expectedOrigin))
89
93
  }
90
94
 
91
95
  export function trustedMutationRequest(headers, { expectedOrigin = expectedOriginForRequest(headers) } = {}) {
92
- return trustedReadRequest(headers, { expectedOrigin }) && trustedFetchSite(headers)
96
+ const origin = requestHeader(headers, 'origin')
97
+ return Boolean(origin) && trustedReadRequest(headers, { expectedOrigin }) && sameOrigin(origin, expectedOrigin)
98
+ }
99
+
100
+ function manifestPathValue(value) {
101
+ if (typeof value === 'string') return value
102
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return ''
103
+ return value.path || value.output || value.href || ''
104
+ }
105
+
106
+ function secretShapedPath(value) {
107
+ return /(?:^|[._-])(?:secret|credential|token|password|nonce|private[._-]?key)(?:$|[._-])/i.test(value)
108
+ || /(?:^|\/)\.env(?:\.|$)/i.test(value)
109
+ }
110
+
111
+ function addManifestPath(paths, value, label) {
112
+ const candidate = manifestPathValue(value)
113
+ const normalized = normalizeWorkspacePath(candidate, { defaultFile: '' })
114
+ if (!candidate || !normalized.ok || /^[a-z][a-z0-9+.-]*:/i.test(candidate)) {
115
+ throw new Error(`atelier.manifest.json ${label} must be a workspace-relative path`)
116
+ }
117
+ if (deniedStaticStatePath(normalized.path) || normalized.path.split('/').some((segment) => segment.startsWith('.')) || secretShapedPath(normalized.path)) {
118
+ throw new Error(`atelier.manifest.json ${label} enrolls a hidden, state, or secret-shaped path`)
119
+ }
120
+ if (!PUBLISHED_STATIC_EXTENSIONS.has(path.extname(normalized.path).toLowerCase())) {
121
+ throw new Error(`atelier.manifest.json ${label} enrolls an unsupported static file type`)
122
+ }
123
+ paths.add(normalized.path)
124
+ }
125
+
126
+ export function loadPublishedWorkspaceManifest(workspaceRoot) {
127
+ const root = path.resolve(workspaceRoot)
128
+ const rootReal = fs.realpathSync(root)
129
+ const manifestPath = path.join(root, 'atelier.manifest.json')
130
+ if (!fs.existsSync(manifestPath)) throw new Error('served workspace must contain generated atelier.manifest.json')
131
+ const manifestReal = fs.realpathSync(manifestPath)
132
+ if (!pathContainedBy(rootReal, manifestReal)) throw new Error('atelier.manifest.json realpath escapes served workspace')
133
+ let manifest
134
+ try {
135
+ manifest = JSON.parse(fs.readFileSync(manifestReal, 'utf8'))
136
+ } catch (error) {
137
+ throw new Error(`atelier.manifest.json must be valid JSON: ${error.message}`)
138
+ }
139
+ if (manifest?.schema !== ATELIER_MANIFEST_SCHEMA) {
140
+ throw new Error(`atelier.manifest.json schema must be ${ATELIER_MANIFEST_SCHEMA}`)
141
+ }
142
+ const paths = new Set()
143
+ if (typeof manifest.entry !== 'string') throw new Error('atelier.manifest.json entry must be a workspace-relative HTML path')
144
+ addManifestPath(paths, manifest.entry, 'entry')
145
+ if (path.extname(manifest.entry).toLowerCase() !== '.html') throw new Error('atelier.manifest.json entry must be an HTML file')
146
+ for (const key of ['files', 'assets']) {
147
+ if (manifest[key] == null) continue
148
+ if (!Array.isArray(manifest[key])) throw new Error(`atelier.manifest.json ${key} must be an array`)
149
+ for (const [index, value] of manifest[key].entries()) addManifestPath(paths, value, `${key}[${index}]`)
150
+ }
151
+ for (const rel of paths) {
152
+ const resolved = resolveWorkspacePath({ workspaceRoot: root, workspaceRootReal: rootReal, rel, requireFile: true })
153
+ if (!resolved.ok) throw new Error(`atelier.manifest.json enrolled path is unavailable: ${rel}`)
154
+ const realRel = path.relative(rootReal, resolved.real).replaceAll('\\', '/')
155
+ if (realRel !== rel) throw new Error(`atelier.manifest.json enrolled path must not be a symlink: ${rel}`)
156
+ }
157
+ return Object.freeze({ manifest, manifestPath: manifestReal, paths })
158
+ }
159
+
160
+ export function publishedStaticPathVerdict({ resolved, publication } = {}) {
161
+ if (!resolved?.ok) return resolved
162
+ const realRel = path.relative(resolved.workspaceRootReal, resolved.real).replaceAll('\\', '/')
163
+ const segments = realRel.split('/')
164
+ if (segments.some((segment) => segment.startsWith('.')) || deniedStaticStatePath(realRel)) {
165
+ return { ok: false, status: 403, error: 'hidden or local state path is not published', path: resolved.path }
166
+ }
167
+ if (realRel === 'atelier.manifest.json' || secretShapedPath(realRel)) {
168
+ return { ok: false, status: 403, error: 'secret-shaped path is not published', path: resolved.path }
169
+ }
170
+ if (!PUBLISHED_STATIC_EXTENSIONS.has(path.extname(realRel).toLowerCase())) {
171
+ return { ok: false, status: 403, error: 'static file type is not published', path: resolved.path }
172
+ }
173
+ if (!publication?.paths?.has(realRel)) {
174
+ return { ok: false, status: 404, error: 'path is not enrolled by atelier.manifest.json', path: resolved.path }
175
+ }
176
+ return resolved
93
177
  }
94
178
 
95
179
  export function pathContainedBy(root, candidate) {
@@ -191,6 +275,7 @@ export function resolveWorkspacePath({
191
275
  real,
192
276
  stat,
193
277
  workspaceContained: true,
278
+ workspaceRootReal,
194
279
  }
195
280
  }
196
281
 
@@ -17,6 +17,7 @@ export async function runServerCommand(argv = process.argv.slice(2)) {
17
17
  const sidecar = createAtelierSidecarServer({
18
18
  workspaceRoot: project.outputRoot,
19
19
  stateDir: project.outputRoot,
20
+ reviewProject: argv.includes('--review') ? project : null,
20
21
  // argv > PORT env > default. Supervisors that assign a free port (preview
21
22
  // panes, dev harnesses) pass it via PORT, and an Atelier that ignores that
22
23
  // fights whatever already holds the canonical port instead of coexisting.
@@ -26,11 +27,12 @@ export async function runServerCommand(argv = process.argv.slice(2)) {
26
27
  if (args.smoke) {
27
28
  const address = await sidecar.listen()
28
29
  const base = `http://127.0.0.1:${address.port}`
30
+ const headers = { Origin: base, 'Sec-Fetch-Site': 'same-origin' }
29
31
  // @atelier-egress-local-computed
30
- const health = await fetch(`${base}/api/health`).then((res) => res.json())
32
+ const health = await fetch(`${base}/api/health`, { headers }).then((res) => res.json())
31
33
  if (!health.ok) throw new Error('health check failed')
32
34
  // @atelier-egress-local-computed
33
- const page = await fetch(`${base}/index.html`).then((res) => res.text())
35
+ const page = await fetch(`${base}/`, { headers }).then((res) => res.text())
34
36
  if (!page.includes('<meta name="mnstry:atelier"')) throw new Error('projection smoke failed')
35
37
  await sidecar.close()
36
38
  console.log('[atelier:browser:smoke] local projection and health endpoint passed')