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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +86 -25
  3. package/SECURITY.md +6 -1
  4. package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
  5. package/contracts/atelier-coauthor.v1.schema.json +163 -0
  6. package/contracts/atelier-decision-request.v1.schema.json +476 -0
  7. package/contracts/atelier-decision-result.v1.schema.json +500 -0
  8. package/contracts/atelier-guide.v1.schema.json +227 -0
  9. package/contracts/atelier-intake.v1.schema.json +151 -0
  10. package/contracts/atelier-migration.v2.schema.json +61 -0
  11. package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
  12. package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
  13. package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
  14. package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
  15. package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
  16. package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
  17. package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
  18. package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
  19. package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
  20. package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
  21. package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
  22. package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
  23. package/contracts/atelier-presentation.v1.schema.json +1293 -0
  24. package/contracts/atelier-repository-observation.v1.schema.json +163 -0
  25. package/contracts/atelier-review.v1.schema.json +1601 -0
  26. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  27. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  28. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  29. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  30. package/docs/assurance-controls.md +12 -2
  31. package/docs/atelier-runtime.md +13 -2
  32. package/docs/atelier-sync.md +172 -0
  33. package/docs/blocks/claims.md +28 -18
  34. package/docs/blocks/will-not-do.md +12 -3
  35. package/docs/coauthor-session.md +57 -0
  36. package/docs/connected-composition.md +75 -0
  37. package/docs/decisions.md +167 -0
  38. package/docs/exact-upgrades.md +159 -0
  39. package/docs/extension-pack-lifecycle.md +33 -0
  40. package/docs/guided-upgrades.md +99 -0
  41. package/docs/install.md +65 -9
  42. package/docs/intake-and-guides.md +65 -0
  43. package/docs/integration-and-review-plan.md +409 -0
  44. package/docs/integration-contract-decisions.md +22 -0
  45. package/docs/local-review.md +84 -0
  46. package/docs/local-services.md +227 -0
  47. package/docs/local-state.md +107 -0
  48. package/docs/obsidian-contract.md +687 -0
  49. package/docs/obsidian.md +387 -0
  50. package/docs/portable-presentation.md +328 -0
  51. package/docs/presentation-consumer-boundaries.md +75 -0
  52. package/docs/presentation-review-disposition.md +47 -0
  53. package/docs/project-command-inventory.md +38 -0
  54. package/docs/project-options.md +53 -0
  55. package/docs/release-engineering.md +99 -2
  56. package/docs/review-portability.md +55 -0
  57. package/docs/skill-steward.md +156 -0
  58. package/docs/upgrade.md +70 -2
  59. package/docs/vault-service.md +292 -0
  60. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  61. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  62. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  63. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  64. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  65. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  66. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  67. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  68. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  69. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  70. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  71. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  72. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  73. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  74. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  75. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  76. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  77. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  78. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  79. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  80. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  81. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  82. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  83. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  84. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  85. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  86. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  87. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  88. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  89. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  90. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  91. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  92. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  93. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  94. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  95. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  96. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  97. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  98. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  99. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  100. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  101. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  102. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  103. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  104. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  105. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  106. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  107. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  108. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  109. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  110. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  111. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  112. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  113. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  114. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  115. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  116. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  117. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  118. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  119. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  120. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  121. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  122. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  123. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  125. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  127. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  128. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  129. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  130. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  131. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  132. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  133. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  134. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  135. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  136. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  137. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  138. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  139. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  140. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  141. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  142. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  143. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  144. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  145. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  146. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  147. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  148. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  149. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  150. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  151. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  152. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  153. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  154. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  159. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  160. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  161. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  162. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  163. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  164. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  165. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  166. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  167. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  168. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  169. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  170. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  171. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  172. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  173. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  174. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  175. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  176. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  177. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  178. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  179. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  181. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  182. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  183. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  186. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  187. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  188. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  189. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  190. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  191. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  192. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  193. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  194. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  195. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  196. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  197. package/fixtures/obsidian/edits/workspace.json +65 -0
  198. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  199. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  200. package/fixtures/obsidian/materialization/workspace.json +54 -0
  201. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  202. package/fixtures/ui/presentation/pane.v1.json +7 -0
  203. package/fixtures/ui/presentation/reference.v1.json +293 -0
  204. package/package.json +61 -6
  205. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  206. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  207. package/skills/claude/atelier-open/SKILL.md +5 -0
  208. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  209. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  210. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  211. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  212. package/skills/codex/atelier-open/SKILL.md +5 -0
  213. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  214. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  215. package/src/access/preview-enforcer.mjs +84 -0
  216. package/src/boundary/content-rules.mjs +9 -4
  217. package/src/boundary/policy.mjs +74 -42
  218. package/src/cli/project-options.mjs +58 -0
  219. package/src/cli/run.mjs +61 -3
  220. package/src/coauthor/session.mjs +161 -0
  221. package/src/coauthor/store.mjs +173 -0
  222. package/src/collaboration/event-ledger.mjs +16 -33
  223. package/src/collaboration/inspection-bundle.mjs +223 -0
  224. package/src/collaboration/review-contracts.mjs +25 -0
  225. package/src/collaboration/review-store.mjs +305 -0
  226. package/src/commands/coauthor.mjs +26 -0
  227. package/src/commands/config.mjs +6 -1
  228. package/src/commands/extension-pack.mjs +15 -8
  229. package/src/commands/init.mjs +44 -5
  230. package/src/commands/obsidian.mjs +368 -0
  231. package/src/commands/review.mjs +147 -0
  232. package/src/commands/setup.mjs +51 -14
  233. package/src/commands/skills.mjs +199 -0
  234. package/src/commands/sync.mjs +107 -0
  235. package/src/composition/registry.mjs +27 -0
  236. package/src/composition/wire.mjs +122 -0
  237. package/src/contracts/corpus.mjs +35 -0
  238. package/src/decisions/contracts.d.mts +96 -0
  239. package/src/decisions/contracts.mjs +321 -0
  240. package/src/disclosure/content-scan.mjs +13 -0
  241. package/src/export/atelier-export-contract.mjs +16 -1
  242. package/src/extension-packs/lifecycle.mjs +154 -0
  243. package/src/graph/graph.mjs +64 -2
  244. package/src/graph/knowledge-graph.mjs +548 -24
  245. package/src/guides/contracts.mjs +54 -0
  246. package/src/index.mjs +29 -0
  247. package/src/intake/store.mjs +119 -0
  248. package/src/preview/channel.mjs +89 -0
  249. package/src/project/config.mjs +50 -49
  250. package/src/project/durable-state.mjs +161 -0
  251. package/src/project/file-class.mjs +166 -0
  252. package/src/project/private-state.mjs +11 -1
  253. package/src/projection/obsidian/contracts.mjs +571 -0
  254. package/src/projection/obsidian/edits/align.mjs +169 -0
  255. package/src/projection/obsidian/edits/apply.mjs +889 -0
  256. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  257. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  258. package/src/projection/obsidian/edits/index.mjs +19 -0
  259. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  260. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  261. package/src/projection/obsidian/edits/observe.mjs +123 -0
  262. package/src/projection/obsidian/edits/policy.mjs +123 -0
  263. package/src/projection/obsidian/edits/regions.mjs +648 -0
  264. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  265. package/src/projection/obsidian/materialize/index.mjs +7 -0
  266. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  267. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  268. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  269. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  270. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  271. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  272. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  273. package/src/projection/obsidian/proposals/index.mjs +23 -0
  274. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  275. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  276. package/src/projection/obsidian/proposals/router.mjs +168 -0
  277. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  278. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  279. package/src/projection/obsidian/publication/index.mjs +5 -0
  280. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  281. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  282. package/src/projection/obsidian/publication/transport.mjs +139 -0
  283. package/src/projection/obsidian/recovery/index.mjs +6 -0
  284. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  285. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  286. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  287. package/src/projection/obsidian/recovery/store.mjs +234 -0
  288. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  289. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  290. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  291. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  292. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  293. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  294. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  295. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  296. package/src/projection/project.mjs +28 -19
  297. package/src/readiness-protocols/evidence.mjs +282 -0
  298. package/src/readiness-protocols/runtime.mjs +4 -3
  299. package/src/readiness-protocols/source-read.mjs +39 -0
  300. package/src/runtime/git-adapter.mjs +189 -0
  301. package/src/runtime/local-state.mjs +439 -0
  302. package/src/runtime/obsidian/app-capability.mjs +110 -0
  303. package/src/runtime/obsidian/app-production-seams.mjs +96 -0
  304. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  305. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  306. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  307. package/src/runtime/obsidian/contributions.mjs +27 -0
  308. package/src/runtime/obsidian/documents.mjs +37 -0
  309. package/src/runtime/obsidian/enablement.mjs +53 -0
  310. package/src/runtime/obsidian/engine.mjs +532 -0
  311. package/src/runtime/obsidian/errors.mjs +14 -0
  312. package/src/runtime/obsidian/extension-points.mjs +103 -0
  313. package/src/runtime/obsidian/index.mjs +34 -0
  314. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  315. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  316. package/src/runtime/obsidian/observation.mjs +158 -0
  317. package/src/runtime/obsidian/opening.mjs +243 -0
  318. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  319. package/src/runtime/obsidian/pipeline.mjs +122 -0
  320. package/src/runtime/obsidian/private-lock.mjs +156 -0
  321. package/src/runtime/obsidian/process-identity.mjs +58 -0
  322. package/src/runtime/obsidian/service-client.mjs +66 -0
  323. package/src/runtime/obsidian/service-main.mjs +89 -0
  324. package/src/runtime/obsidian/service-record.mjs +169 -0
  325. package/src/runtime/obsidian/service-server.mjs +121 -0
  326. package/src/runtime/obsidian/service.mjs +216 -0
  327. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  328. package/src/runtime/obsidian/state-store.mjs +180 -0
  329. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  330. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  331. package/src/runtime/obsidian/watchers.mjs +36 -0
  332. package/src/runtime/repository-observation.mjs +491 -0
  333. package/src/runtime/supervisor.mjs +803 -0
  334. package/src/server/local-sidecar.mjs +44 -0
  335. package/src/server/server.mjs +1 -0
  336. package/src/skills/steward.mjs +780 -0
  337. package/src/ui/presentation/browser.mjs +278 -0
  338. package/src/ui/presentation/contract.mjs +116 -0
  339. package/src/ui/presentation/index.mjs +7 -0
  340. package/src/ui/presentation/native.mjs +153 -0
  341. package/src/ui/presentation/proof.mjs +57 -0
  342. package/src/ui/presentation/schema-check.mjs +30 -0
  343. package/src/ui/presentation/schema.generated.mjs +1371 -0
  344. package/src/ui/presentation/state.mjs +43 -0
  345. package/src/ui/presentation/styles.mjs +67 -0
  346. package/src/ui/presentation/tokens.mjs +74 -0
  347. package/src/ui/presentation/web.mjs +85 -0
  348. package/src/ui/review-page.mjs +32 -0
  349. package/src/upgrade/explanation.mjs +33 -0
  350. package/src/upgrade/provenance.mjs +338 -0
  351. package/src/upgrade/transaction-files.mjs +103 -0
  352. package/src/upgrade/transaction.mjs +475 -0
  353. package/src/upgrade/upgrade.mjs +63 -28
  354. package/src/vault/hosts.mjs +21 -0
  355. package/src/vault/identity.mjs +19 -0
  356. package/src/vault/index.mjs +7 -0
  357. package/src/vault/interface.mjs +11 -0
  358. package/src/vault/metadata.mjs +31 -0
  359. package/src/vault/privacy.mjs +148 -0
  360. package/src/vault/probe.mjs +89 -0
  361. package/src/vault/service.mjs +131 -0
  362. package/src/vault/source.mjs +45 -0
  363. package/src/vault/storage.mjs +23 -0
  364. package/templates/distribution-workspace/.claude/launch.json +6 -2
  365. package/templates/distribution-workspace/gitignore +1 -0
  366. package/templates/distribution-workspace/project/README.md +2 -2
  367. package/templates/external-project-workspace/README.md +45 -0
  368. package/templates/external-project-workspace/answers.example.json +4 -0
  369. package/templates/external-project-workspace/atelier.project.json +40 -0
  370. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  371. package/templates/external-project-workspace/gitignore +8 -0
  372. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  373. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  374. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  375. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  376. package/templates/external-project-workspace/source/README.md +11 -0
  377. package/templates/launch.json +6 -2
  378. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  379. package/templates/private-domain-workspace/domain/README.md +2 -2
  380. package/templates/private-domain-workspace/gitignore +1 -0
  381. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  382. package/templates/shared-project-workspace/gitignore +1 -0
  383. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { canonicalize } from '../attestation/jcs.mjs';
4
+ import { validateJsonSchema } from '../export/atelier-export-contract.mjs';
5
+
6
+ const schema = JSON.parse(fs.readFileSync(new URL('../../contracts/atelier-guide.v1.schema.json', import.meta.url), 'utf8'));
7
+ export function guideDocumentDigest(value) { return createHash('sha256').update(canonicalize(value)).digest('hex'); }
8
+ export function validateGuideDocument(value, kind) {
9
+ if (!['offer', 'capability', 'consent', 'engagement'].includes(kind)) return ['unknown guide document kind'];
10
+ const errors = validateJsonSchema({ $schema: schema.$schema, $defs: schema.$defs, $ref: `#/$defs/${kind}` }, value);
11
+ if (kind === 'capability' && !errors.length) {
12
+ const url = new URL(value.serviceRef);
13
+ if (url.username || url.password || url.search || url.hash) errors.push('service reference must not contain credentials, query or fragment');
14
+ }
15
+ return errors;
16
+ }
17
+ function requireValid(value, kind) {
18
+ if (validateGuideDocument(value, kind).length) throw new Error(`invalid guide ${kind}`);
19
+ }
20
+ export function createGuideEngagement(offer) {
21
+ requireValid(offer, 'offer');
22
+ return { schema: 'mnstry.atelier-guide-engagement@v1', offerDigest: guideDocumentDigest(offer),
23
+ revision: 0, status: 'proposed', authority: 'local-assertion-only' };
24
+ }
25
+ export function transitionGuideEngagement(state, { expectedRevision, action, offerDigest }) {
26
+ requireValid(state, 'engagement');
27
+ if (expectedRevision !== state.revision || offerDigest !== state.offerDigest) throw new Error('stale or mismatched guide engagement');
28
+ const routes = { proposed: { accept: 'accepted', revoke: 'revoked' }, accepted: { pause: 'paused', revoke: 'revoked' }, paused: { resume: 'accepted', revoke: 'revoked' }, revoked: {} };
29
+ const status = Object.hasOwn(routes[state.status], action) ? routes[state.status][action] : null;
30
+ if (!status) throw new Error('guide transition refused');
31
+ return { ...state, revision: state.revision + 1, status };
32
+ }
33
+
34
+ // No connection, credentials, invocation, payment, or permission grant occurs.
35
+ // A host must independently authenticate participants and revalidate revocation,
36
+ // consent, policy, schema and exact bytes immediately before any disclosure.
37
+ export function assessGuideInvocation({ offer, capability, engagement, consent, payload, now }) {
38
+ const blockers = [];
39
+ for (const [kind, value] of Object.entries({ offer, capability, engagement, consent })) {
40
+ if (validateGuideDocument(value, kind).length) blockers.push(`invalid ${kind}`);
41
+ }
42
+ if (blockers.length) return { status: 'blocked', blockers, executionAuthority: false };
43
+ let bytes;
44
+ try { bytes = canonicalize(payload); } catch { blockers.push('payload is not canonical JSON'); }
45
+ if (!Number.isFinite(now)) blockers.push('explicit current time required');
46
+ if (engagement.status !== 'accepted') blockers.push('engagement is not accepted');
47
+ if (engagement.offerDigest !== guideDocumentDigest(offer) || consent.offerDigest !== engagement.offerDigest) blockers.push('offer binding mismatch');
48
+ if (consent.engagementRevision !== engagement.revision) blockers.push('consent revision is stale');
49
+ if (!offer.capabilityIds.includes(capability.id) || consent.capabilityDigest !== guideDocumentDigest(capability)) blockers.push('capability binding mismatch');
50
+ if (consent.status !== 'approved') blockers.push('disclosure consent is not approved');
51
+ if (now < Date.parse(consent.validFrom) || now >= Date.parse(consent.expiresAt) || Date.parse(consent.validFrom) >= Date.parse(consent.expiresAt)) blockers.push('consent is not currently valid');
52
+ if (bytes !== undefined && (Buffer.byteLength(bytes) > capability.maxInputBytes || guideDocumentDigest(payload) !== consent.payloadDigest)) blockers.push('payload binding or byte limit mismatch');
53
+ return { status: blockers.length ? 'blocked' : 'eligible-for-host-validation', blockers, executionAuthority: false };
54
+ }
package/src/index.mjs CHANGED
@@ -98,3 +98,32 @@ export {
98
98
  export {
99
99
  analysisAdapterDryRun,
100
100
  } from './analysis/adapter.mjs'
101
+
102
+ export {
103
+ GitCommandError,
104
+ classifyRemoteAuthentication,
105
+ inspectGitEngine,
106
+ parseGitVersion,
107
+ resolveGitExecutable,
108
+ runGit,
109
+ sanitizeRemoteUrl,
110
+ } from './runtime/git-adapter.mjs'
111
+
112
+ export {
113
+ ATELIER_REPOSITORY_OBSERVATION_SCHEMA,
114
+ classifyFilesystemRoot,
115
+ observeRepository,
116
+ resolveRepositoryRoot,
117
+ validateRepositoryObservation,
118
+ } from './runtime/repository-observation.mjs'
119
+
120
+ export {
121
+ ATELIER_COMMIT_PLAN_SCHEMA,
122
+ enrollRepository,
123
+ executeUserConfirmedCommit,
124
+ operationTrace,
125
+ planUserConfirmedCommit,
126
+ reconcileRepository,
127
+ runtimeStatus,
128
+ setRepositoryPaused,
129
+ } from './runtime/supervisor.mjs'
@@ -0,0 +1,119 @@
1
+ import { withPrivateLock, publishPrivateFile } from '../project/durable-state.mjs';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { execFileSync } from 'node:child_process';
6
+ import { canonicalize } from '../attestation/jcs.mjs';
7
+ import { validateJsonSchema } from '../export/atelier-export-contract.mjs';
8
+ import { ensureContainedPrivateDirectory, openRegularFileNoFollow } from '../project/private-state.mjs';
9
+
10
+ export const INTAKE_MAX_BYTES = 16 * 1024 * 1024;
11
+ const schema = JSON.parse(fs.readFileSync(new URL('../../contracts/atelier-intake.v1.schema.json', import.meta.url), 'utf8'));
12
+ export function validateIntakeDocument(value) { return validateJsonSchema(schema, value); }
13
+ function valid(value) { if (validateIntakeDocument(value).length) throw new Error('invalid intake document'); return value; }
14
+ export function intakeDigest(bytes) { return createHash('sha256').update(bytes).digest('hex'); }
15
+ const idPattern = /^[a-z][a-z0-9-]{0,63}$/;
16
+ const digestPattern = /^[0-9a-f]{64}$/;
17
+ function digest(value) { if (!digestPattern.test(value ?? '')) throw new Error('invalid content digest'); return value; }
18
+ function id(value) { if (!idPattern.test(value ?? '')) throw new Error('invalid intake identifier'); return value; }
19
+ function read(file) {
20
+ const fd = openRegularFileNoFollow(file);
21
+ try {
22
+ if (fs.fstatSync(fd).size > INTAKE_MAX_BYTES) throw new Error('intake byte ceiling exceeded');
23
+ const bytes = fs.readFileSync(fd);
24
+ if (bytes.length > INTAKE_MAX_BYTES) throw new Error('intake byte ceiling exceeded');
25
+ return bytes;
26
+ } finally { fs.closeSync(fd); }
27
+ }
28
+ function immutable(file, bytes) {
29
+ publishPrivateFile(file, bytes);
30
+ if (!read(file).equals(Buffer.from(bytes))) throw new Error('immutable intake readback mismatch');
31
+ }
32
+
33
+ // Local integrity storage, not a worker, semantic acceptance service, or import
34
+ // authority. Callers retain their source trees and choose their existing extractor.
35
+ export function createIntakeStore({ workspaceRoot = process.cwd() } = {}) {
36
+ const root = fs.realpathSync(workspaceRoot);
37
+ function placement() {
38
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')));
39
+ try {
40
+ if (execFileSync('git', ['-C', root, 'ls-files', '-z', '--', '.atelier-local'], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })) throw new Error('tracked state');
41
+ execFileSync('git', ['-C', root, 'check-ignore', '--quiet', '.atelier-local/'], { env, stdio: 'ignore' });
42
+ } catch { throw new Error('intake requires ignored, untracked .atelier-local/ in a Git workspace'); }
43
+ }
44
+ placement();
45
+ function directory(...parts) {
46
+ return ensureContainedPrivateDirectory({ workspaceRoot: root, directory: path.join(root, '.atelier-local', 'intake', ...parts), label: 'intake state' });
47
+ }
48
+ function locked(fn) {
49
+ placement();
50
+ const file = path.join(directory(), 'operation.lock');
51
+ return withPrivateLock(file, fn);
52
+ }
53
+ function sourceFile(ref) {
54
+ if (typeof ref !== 'string' || !ref || path.isAbsolute(ref) || ref.includes('\\') ||
55
+ ref.split('/').some(p => !p || p === '.' || p === '..' || p.startsWith('.'))) throw new Error('source must be a visible workspace-relative file');
56
+ let file = root;
57
+ for (const part of ref.split('/')) {
58
+ file = path.join(file, part);
59
+ if (fs.lstatSync(file).isSymbolicLink()) throw new Error('redirected intake source refused');
60
+ }
61
+ return file;
62
+ }
63
+ function blob(blobId) {
64
+ const bytes = read(path.join(directory('blobs'), digest(blobId)));
65
+ if (intakeDigest(bytes) !== blobId) throw new Error('stored blob digest mismatch');
66
+ return bytes;
67
+ }
68
+ function record(dir, name, value) { valid(value); immutable(path.join(dir, name), canonicalize(value) + '\n'); return value; }
69
+ return Object.freeze({
70
+ ingest({ ref, expectedDigest }) {
71
+ return locked(() => {
72
+ digest(expectedDigest);
73
+ const file = sourceFile(ref), bytes = read(file);
74
+ if (intakeDigest(bytes) !== expectedDigest) throw new Error('source digest mismatch');
75
+ immutable(path.join(directory('blobs'), expectedDigest), bytes);
76
+ if (intakeDigest(read(sourceFile(ref))) !== expectedDigest) throw new Error('source changed during intake');
77
+ const manifest = { schema: 'mnstry.atelier-intake-source@v1', sourceRef: ref, blobId: expectedDigest, bytes: bytes.length, originalMutation: false };
78
+ return record(directory('sources'), intakeDigest(canonicalize(manifest)) + '.json', manifest);
79
+ });
80
+ },
81
+ beginAttempt({ attemptId, blobId, extractorId, extractorVersion, configurationDigest }) {
82
+ return locked(() => {
83
+ id(attemptId); id(extractorId); digest(configurationDigest); blob(blobId);
84
+ if (typeof extractorVersion !== 'string' || !/^[a-zA-Z0-9._-]{1,64}$/.test(extractorVersion)) throw new Error('invalid extractor version');
85
+ const manifest = { schema: 'mnstry.atelier-intake-attempt@v1', attemptId, blobId, extractorId, extractorVersion, configurationDigest };
86
+ return record(directory('attempts', attemptId), 'attempt.json', manifest);
87
+ });
88
+ },
89
+ completeAttempt({ attemptId, output, expectedOutputDigest }) {
90
+ return locked(() => {
91
+ id(attemptId); digest(expectedOutputDigest);
92
+ if (typeof output !== 'string' || Buffer.byteLength(output) > INTAKE_MAX_BYTES) throw new Error('output must be bounded UTF-8 text');
93
+ if (intakeDigest(output) !== expectedOutputDigest) throw new Error('output digest mismatch');
94
+ const dir = directory('attempts', attemptId);
95
+ const attemptBytes = read(path.join(dir, 'attempt.json'));
96
+ const attempt = valid(JSON.parse(attemptBytes));
97
+ if (attempt.attemptId !== attemptId || attempt.schema !== 'mnstry.atelier-intake-attempt@v1') throw new Error('attempt identity mismatch');
98
+ blob(attempt.blobId);
99
+ immutable(path.join(dir, 'output.txt'), output);
100
+ // Completion is written last. A crash before it is incomplete, not success.
101
+ return record(dir, 'completion.json', { schema: 'mnstry.atelier-intake-completion@v1', attemptId,
102
+ attemptDigest: intakeDigest(attemptBytes), outputDigest: expectedOutputDigest, bytes: Buffer.byteLength(output),
103
+ integrity: 'verified', semanticAcceptance: 'pending' });
104
+ });
105
+ },
106
+ readCompletion(attemptId) {
107
+ placement(); id(attemptId);
108
+ const dir = directory('attempts', attemptId), receipt = valid(JSON.parse(read(path.join(dir, 'completion.json'))));
109
+ if (receipt.schema !== 'mnstry.atelier-intake-completion@v1' || receipt.attemptId !== attemptId ||
110
+ receipt.integrity !== 'verified' || receipt.semanticAcceptance !== 'pending') throw new Error('invalid completion receipt');
111
+ const attemptBytes = read(path.join(dir, 'attempt.json')), output = read(path.join(dir, 'output.txt'));
112
+ if (intakeDigest(attemptBytes) !== receipt.attemptDigest || intakeDigest(output) !== receipt.outputDigest || output.length !== receipt.bytes) throw new Error('completion integrity mismatch');
113
+ const attempt = valid(JSON.parse(attemptBytes));
114
+ if (attempt.attemptId !== attemptId || attempt.schema !== 'mnstry.atelier-intake-attempt@v1') throw new Error('attempt identity mismatch');
115
+ blob(attempt.blobId);
116
+ return receipt;
117
+ },
118
+ });
119
+ }
@@ -0,0 +1,89 @@
1
+ // Browser-only channel. Host/session/service authority never enters this module.
2
+ import { parseRequest, parseBoundedJSON, closed, need, isRef, isDigest, MODES } from '../composition/wire.mjs';
3
+ const HELLO = 'atelier.preview.mount/experimental-v1';
4
+ function origin(value) { const u = new URL(value); need(u.origin !== 'null' && u.protocol === 'https:' && u.origin === value); return value; }
5
+ function resultCheck(bytes, request, generation) {
6
+ const result = parseBoundedJSON(bytes);
7
+ closed(result, ['state'], ['projectedData']);
8
+ const s = result.state;
9
+ closed(s, ['schema', 'requestId', 'placementId', 'sourceDigest', 'mode', 'state', 'sessionGeneration', 'publicMessage']);
10
+ need(s.schema === 'atelier-preview-state/proposal-v1' && s.requestId === request.requestId && s.placementId === request.placementId && s.sourceDigest === request.sourceDigest && s.mode === request.mode);
11
+ need(s.sessionGeneration === generation && ['loading', 'ready', 'unauthenticated', 'forbidden', 'expired', 'unavailable', 'stale'].includes(s.state));
12
+ need(typeof s.publicMessage === 'string' && s.publicMessage.length <= 160);
13
+ need(s.state === 'ready' ? Object.hasOwn(result, 'projectedData') : !Object.hasOwn(result, 'projectedData'));
14
+ return result;
15
+ }
16
+ function fail(request, generation) { return { state: { schema: 'atelier-preview-state/proposal-v1', requestId: request.requestId, placementId: request.placementId,
17
+ sourceDigest: request.sourceDigest, mode: request.mode, state: 'unavailable', sessionGeneration: generation, publicMessage: 'Preview unavailable.' } }; }
18
+
19
+ export function mountPreview({ frame, previewOrigin, requestBytes, generation, handle, onState = () => {} }) {
20
+ origin(previewOrigin); need(new URL(frame.src).origin === previewOrigin && previewOrigin !== window.location.origin);
21
+ const template = parseRequest(requestBytes); need(!template.intentRef && Number.isSafeInteger(generation) && generation >= 0 && typeof handle === 'function');
22
+ const nonce = crypto.randomUUID(), channel = new MessageChannel(), controller = new AbortController();
23
+ let active = true, ready = false, sequence = 0, latest = 0;
24
+ const invalidate = () => {
25
+ if (!active) return; active = false; controller.abort();
26
+ channel.port1.postMessage(JSON.stringify({type:'invalidate',nonce})); channel.port1.close(); frame.removeEventListener('load', invalidate);
27
+ onState('stale');
28
+ };
29
+ frame.addEventListener('load', invalidate); // Future navigation invalidates this mount.
30
+ channel.port1.onmessage = async event => {
31
+ if (!active) return;
32
+ try {
33
+ const v = parseBoundedJSON(event.data);
34
+ if (!ready) { closed(v, ['type', 'nonce']); need(v.type === 'ready' && v.nonce === nonce); ready = true; onState('connected'); return; }
35
+ closed(v, ['type', 'nonce', 'sequence', 'request']);
36
+ need(v.type === 'request' && v.nonce === nonce && Number.isSafeInteger(v.sequence) && v.sequence > sequence);
37
+ const request = parseRequest(v.request);
38
+ need(request.placementId === template.placementId && request.sourceDigest === template.sourceDigest && request.mode === template.mode);
39
+ sequence = v.sequence; latest = sequence; const thisSequence = sequence;
40
+ let result;
41
+ try { result = resultCheck(JSON.stringify(await handle(v.request, controller.signal)), request, generation); }
42
+ catch { result = fail(request, generation); }
43
+ if (!active || controller.signal.aborted || thisSequence !== latest) return;
44
+ channel.port1.postMessage(JSON.stringify({ type: 'result', nonce, sequence: thisSequence, result }));
45
+ } catch { invalidate(); }
46
+ };
47
+ channel.port1.start();
48
+ frame.contentWindow.postMessage(JSON.stringify({ type: HELLO, nonce, generation, request: requestBytes }), previewOrigin, [channel.port2]);
49
+ return Object.freeze({ dispose: invalidate });
50
+ }
51
+
52
+ export function receivePreview({ shellOrigin, onMount, onResult, onInvalidate = () => {} }) {
53
+ origin(shellOrigin); need(shellOrigin !== window.location.origin);
54
+ let active = null;
55
+ function clear() { active?.port.close(); active = null; onInvalidate(); }
56
+ function receive(event) {
57
+ if (event.origin !== shellOrigin || event.source !== window.parent) return;
58
+ try {
59
+ const v = parseBoundedJSON(event.data);
60
+ closed(v, ['type', 'nonce', 'generation', 'request']);
61
+ need(v.type === HELLO && isRef(v.nonce) && Number.isSafeInteger(v.generation) && v.generation >= 0 && event.ports.length === 1);
62
+ const request = parseRequest(v.request); need(!request.intentRef && isDigest(request.sourceDigest) && MODES.includes(request.mode));
63
+ clear();
64
+ const mount = { port: event.ports[0], nonce: v.nonce, generation: v.generation, request, sequence: 0, settled: 0 };
65
+ active = mount;
66
+ mount.port.onmessage = e => {
67
+ if (active !== mount) return;
68
+ try {
69
+ const packet = parseBoundedJSON(e.data);
70
+ if (packet?.type === 'invalidate') { closed(packet, ['type','nonce']); need(packet.nonce === mount.nonce); clear(); return; }
71
+ closed(packet, ['type', 'nonce', 'sequence', 'result']);
72
+ need(packet.type === 'result' && packet.nonce === mount.nonce && packet.sequence === mount.sequence && packet.sequence > mount.settled);
73
+ const result = resultCheck(JSON.stringify(packet.result), mount.pending, mount.generation);
74
+ mount.settled = packet.sequence; onResult(result);
75
+ } catch { clear(); }
76
+ };
77
+ mount.port.start(); mount.port.postMessage(JSON.stringify({ type: 'ready', nonce: mount.nonce }));
78
+ onMount(request);
79
+ } catch { clear(); }
80
+ }
81
+ window.addEventListener('message', receive);
82
+ return Object.freeze({ request(intent) {
83
+ if (!active) return false;
84
+ const sequence = ++active.sequence;
85
+ const request = { ...active.request, requestId: crypto.randomUUID(), ...(intent ? { schema: 'atelier-component-intent/proposal-v1', intentRef: intent.ref, payload: intent.payload } : {}) };
86
+ parseRequest(JSON.stringify(request)); active.pending = request;
87
+ active.port.postMessage(JSON.stringify({ type: 'request', nonce: active.nonce, sequence, request: JSON.stringify(request) })); return true;
88
+ }, dispose() { clear(); window.removeEventListener('message', receive); } });
89
+ }
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { spawnSync } from 'node:child_process'
4
+ import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
5
+ import { parseProjectOptions } from '../cli/project-options.mjs'
4
6
 
5
7
  export const PROJECT_CONFIG_ARG_PREFIX = '--project-config='
6
8
  export const PROJECT_CONFIG_ENV = 'MNSTRY_ATELIER_PROJECT_CONFIG'
@@ -94,14 +96,7 @@ export function writeJson(file, value) {
94
96
  }
95
97
 
96
98
  export function projectConfigArg(argv = process.argv.slice(2), prefix = PROJECT_CONFIG_ARG_PREFIX) {
97
- for (let index = 0; index < argv.length; index += 1) {
98
- const arg = argv[index]
99
- if (typeof arg !== 'string') continue
100
- if (arg.startsWith(prefix)) return arg.slice(prefix.length).trim() || null
101
- if ((arg === '--project-config' || arg === '--project') && argv[index + 1]) return String(argv[index + 1]).trim() || null
102
- if (arg.startsWith('--project=')) return arg.slice('--project='.length).trim() || null
103
- }
104
- return null
99
+ return parseProjectOptions(argv, prefix).project
105
100
  }
106
101
 
107
102
  export function stripProjectConfigArgs(argv = [], prefix = PROJECT_CONFIG_ARG_PREFIX) {
@@ -170,22 +165,7 @@ export function readLocalOverlay({ configDir, env = process.env, cwd = process.c
170
165
  }
171
166
 
172
167
  export function parseRepoPathOverrides(argv = process.argv.slice(2), cwd = process.cwd()) {
173
- const out = new Map()
174
- for (let index = 0; index < argv.length; index += 1) {
175
- const arg = argv[index]
176
- let value = null
177
- if (arg === '--repo-path' && argv[index + 1]) {
178
- value = argv[index + 1]
179
- index += 1
180
- } else if (typeof arg === 'string' && arg.startsWith('--repo-path=')) {
181
- value = arg.slice('--repo-path='.length)
182
- }
183
- if (!value || !value.includes('=')) continue
184
- const [name, ...rest] = value.split('=')
185
- const repoPath = rest.join('=')
186
- if (firstString(name) && firstString(repoPath)) out.set(name.trim(), resolvePathValue(repoPath, cwd))
187
- }
188
- return out
168
+ return new Map([...parseProjectOptions(argv).repoPaths].map(([name, value]) => [name, resolvePathValue(value, cwd)]))
189
169
  }
190
170
 
191
171
  function overlayRepoPath(overlay, repoName) {
@@ -195,25 +175,25 @@ function overlayRepoPath(overlay, repoName) {
195
175
  return firstString(direct, repo.path, repo.localPath)
196
176
  }
197
177
 
198
- function resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay, cliRepoPaths }) {
178
+ function resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay, cliRepoPaths, gitExecutable, env }) {
199
179
  if (repoName && cliRepoPaths.has(repoName)) return cliRepoPaths.get(repoName)
200
180
  const overlayPath = overlayRepoPath(overlay, repoName)
201
181
  const fromOverlay = resolvePathValue(overlayPath, configDir)
202
182
  if (fromOverlay) return fromOverlay
203
183
  const fromConfig = resolvePathValue(repo.path, configDir) || resolvePathValue(repo.path, workspaceRoot)
204
184
  if (fromConfig) return fromConfig
205
- return discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })
185
+ return discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable, env })
206
186
  }
207
187
 
208
- function repoPathSource({ repo, repoName, overlay, cliRepoPaths, workspaceRoot, configDir }) {
188
+ function repoPathSource({ repo, repoName, overlay, cliRepoPaths, workspaceRoot, configDir, gitExecutable, env }) {
209
189
  if (repoName && cliRepoPaths.has(repoName)) return 'cli'
210
190
  if (overlayRepoPath(overlay, repoName)) return 'local-overlay'
211
191
  if (firstString(repo.path)) return 'tracked-config'
212
- if (discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })) return 'sibling-discovery'
192
+ if (discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable, env })) return 'sibling-discovery'
213
193
  return null
214
194
  }
215
195
 
216
- function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot }) {
196
+ function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable = 'git', env = process.env }) {
217
197
  if (!repoName) return null
218
198
  const candidates = [
219
199
  path.join(workspaceRoot, repoName),
@@ -222,14 +202,14 @@ function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot }) {
222
202
  ]
223
203
  for (const candidate of candidates) {
224
204
  if (!fs.existsSync(candidate)) continue
225
- if (repo.remote && !gitRemoteMatches(candidate, repo.remote)) continue
205
+ if (repo.remote && !gitRemoteMatches(candidate, repo.remote, { gitExecutable, env })) continue
226
206
  return path.resolve(candidate)
227
207
  }
228
208
  return null
229
209
  }
230
210
 
231
- function gitRemoteMatches(repoPath, expected) {
232
- const result = spawnSync('git', ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8' })
211
+ function gitRemoteMatches(repoPath, expected, { gitExecutable = 'git', env = process.env } = {}) {
212
+ const result = spawnSync(gitExecutable, ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
233
213
  if (result.status !== 0) return false
234
214
  const actual = normalizeRemote(result.stdout.trim())
235
215
  return actual === normalizeRemote(expected)
@@ -243,9 +223,9 @@ function normalizeRemote(value) {
243
223
  .toLowerCase()
244
224
  }
245
225
 
246
- export function gitRemoteUrl(repoPath) {
226
+ export function gitRemoteUrl(repoPath, { gitExecutable = 'git', env = process.env } = {}) {
247
227
  if (!repoPath || !fs.existsSync(repoPath)) return null
248
- const result = spawnSync('git', ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8' })
228
+ const result = spawnSync(gitExecutable, ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
249
229
  return result.status === 0 ? firstString(result.stdout) : null
250
230
  }
251
231
 
@@ -268,7 +248,9 @@ export function resolveProjectConfig({
268
248
  env = process.env,
269
249
  cwd = process.cwd(),
270
250
  configArgPrefix = PROJECT_CONFIG_ARG_PREFIX,
251
+ gitExecutable = 'git',
271
252
  configEnv = PROJECT_CONFIG_ENV,
253
+ writeLocalState = true,
272
254
  defaults = {},
273
255
  } = {}) {
274
256
  const argPath = projectConfigArg(argv, configArgPrefix)
@@ -373,7 +355,7 @@ export function resolveProjectConfig({
373
355
  localOverlay,
374
356
  repos: (Array.isArray(config.repos) ? config.repos : []).map((repo) => {
375
357
  const repoName = firstString(repo.name) || (repo.path ? path.basename(repo.path) : null)
376
- const repoPath = resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay: localOverlay.overlay, cliRepoPaths })
358
+ const repoPath = resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay: localOverlay.overlay, cliRepoPaths, gitExecutable, env })
377
359
  const external = isExternalRepo(repo)
378
360
  return {
379
361
  ...repo,
@@ -381,16 +363,16 @@ export function resolveProjectConfig({
381
363
  path: repoPath,
382
364
  external,
383
365
  readBoundary: external ? null : firstString(repo.readBoundary) || 'team',
384
- pathSource: repoPath ? repoPathSource({ repo, repoName, overlay: localOverlay.overlay, cliRepoPaths, workspaceRoot, configDir }) : null,
366
+ pathSource: repoPath ? repoPathSource({ repo, repoName, overlay: localOverlay.overlay, cliRepoPaths, workspaceRoot, configDir, gitExecutable, env }) : null,
385
367
  }
386
368
  }),
387
369
  }
388
- resolved.localState = ensureLocalState(resolved, { write: true })
370
+ resolved.localState = ensureLocalState(resolved, { write: writeLocalState, gitExecutable, env })
389
371
  return resolved
390
372
  }
391
373
 
392
- export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd() } = {}) {
393
- const project = resolveProjectConfig({ argv, env, cwd })
374
+ export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd(), gitExecutable = 'git', writeLocalState = true } = {}) {
375
+ const project = resolveProjectConfig({ argv, env, cwd, gitExecutable, writeLocalState })
394
376
  // Fail closed at CLI entry, but only when a real config file was loaded; the
395
377
  // defaults/no-file path resolves with an empty config that would spuriously
396
378
  // fail document validation.
@@ -419,35 +401,35 @@ export function localStateRoot(projectOrDir) {
419
401
  return path.join(configDir || process.cwd(), LOCAL_STATE_DIR)
420
402
  }
421
403
 
422
- function gitRootFor(dir) {
423
- const result = spawnSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' })
404
+ function gitRootFor(dir, { gitExecutable = 'git', env = process.env } = {}) {
405
+ const result = spawnSync(gitExecutable, ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
424
406
  return result.status === 0 ? result.stdout.trim() : null
425
407
  }
426
408
 
427
- function gitPrefixFor(dir) {
428
- const result = spawnSync('git', ['-C', dir, 'rev-parse', '--show-prefix'], { encoding: 'utf8' })
409
+ function gitPrefixFor(dir, { gitExecutable = 'git', env = process.env } = {}) {
410
+ const result = spawnSync(gitExecutable, ['-C', dir, 'rev-parse', '--show-prefix'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
429
411
  return result.status === 0 ? result.stdout.trim() : null
430
412
  }
431
413
 
432
- function isIgnoredByGit(gitRoot, rel) {
433
- const result = spawnSync('git', ['-C', gitRoot, 'check-ignore', '-q', rel], { encoding: 'utf8' })
414
+ function isIgnoredByGit(gitRoot, rel, { gitExecutable = 'git', env = process.env } = {}) {
415
+ const result = spawnSync(gitExecutable, ['-C', gitRoot, 'check-ignore', '-q', rel], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
434
416
  return result.status === 0
435
417
  }
436
418
 
437
- export function ensureLocalState(project, { write = false } = {}) {
419
+ export function ensureLocalState(project, { write = false, gitExecutable = 'git', env = process.env } = {}) {
438
420
  const root = localStateRoot(project)
439
- const gitRoot = gitRootFor(project.configDir)
421
+ const gitRoot = gitRootFor(project.configDir, { gitExecutable, env })
440
422
  // Git owns the repository-relative spelling. Deriving it by comparing
441
423
  // filesystem paths breaks when Windows exposes the cwd through an 8.3 short
442
424
  // name (RUNNER~1) but Git reports the same root through its long name.
443
- const gitPrefix = gitRoot ? gitPrefixFor(project.configDir) : null
425
+ const gitPrefix = gitRoot ? gitPrefixFor(project.configDir, { gitExecutable, env }) : null
444
426
  const rel = gitRoot && gitPrefix !== null ? `${gitPrefix}${LOCAL_STATE_DIR}` : LOCAL_STATE_DIR
445
427
  // A directory-only ignore rule such as `.atelier-local/` is not evaluated
446
428
  // consistently by Git for an absent directory on every host. Probe a
447
429
  // hypothetical child as well: it proves the directory rule before Atelier
448
430
  // creates any local state, including on Git for Windows.
449
431
  const ignored = gitRoot && gitPrefix !== null
450
- ? isIgnoredByGit(gitRoot, `${rel}/.atelier-ignore-probe`) || isIgnoredByGit(gitRoot, `${rel}/`) || isIgnoredByGit(gitRoot, rel)
432
+ ? isIgnoredByGit(gitRoot, `${rel}/.atelier-ignore-probe`, { gitExecutable, env }) || isIgnoredByGit(gitRoot, `${rel}/`, { gitExecutable, env }) || isIgnoredByGit(gitRoot, rel, { gitExecutable, env })
451
433
  : gitRoot === null
452
434
  const report = {
453
435
  root,
@@ -502,6 +484,19 @@ function extErrors(value, label) {
502
484
  .map(([key]) => `${label}.${key} must be an object; ext members are namespaced objects`)
503
485
  }
504
486
 
487
+ // Hands one namespaced ext member to its structured consumer, uninterpreted.
488
+ // The project validator above deliberately does not look inside a member, so
489
+ // the consumer validates it against its own closed contract and fails closed.
490
+ // `present` separates "no such member" (the consumer's feature is simply not
491
+ // configured) from a member that exists with any value at all, including a
492
+ // malformed one, which the consumer must refuse rather than ignore.
493
+ export function projectExtMember(project, key) {
494
+ const ext = project?.config?.ext
495
+ if (ext == null) return { present: false, value: undefined }
496
+ if (typeof ext !== 'object' || Array.isArray(ext)) return { present: true, value: undefined }
497
+ return Object.hasOwn(ext, key) ? { present: true, value: ext[key] } : { present: false, value: undefined }
498
+ }
499
+
505
500
  export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {}) {
506
501
  const errors = []
507
502
  if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return ['project config must be a JSON object']
@@ -563,6 +558,7 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
563
558
  }
564
559
  if (alignment.appRepo != null && !firstString(alignment.appRepo)) errors.push('alignment.appRepo must be a non-empty string')
565
560
 
561
+ const repoNames = new Set()
566
562
  for (const [index, repo] of (Array.isArray(doc.repos) ? doc.repos : []).entries()) {
567
563
  if (!repo || typeof repo !== 'object' || Array.isArray(repo)) {
568
564
  errors.push(`repos[${index}] must be an object`)
@@ -571,6 +567,11 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
571
567
  errors.push(...unknownKeyErrors(repo, `repos[${index}]`, new Set(['name', 'path', 'readBoundary', 'role', 'kind', 'remote', 'identity', 'aliases', 'required', 'ext'])))
572
568
  errors.push(...extErrors(repo.ext, `repos[${index}].ext`))
573
569
  if (!firstString(repo.name)) errors.push(`repos[${index}].name is required`)
570
+ else {
571
+ const key = repo.name.toLowerCase()
572
+ if (repoNames.has(key)) errors.push(`repos[${index}].name duplicates another repository name`)
573
+ repoNames.add(key)
574
+ }
574
575
  if (repo.identity != null) {
575
576
  const identity = asObject(repo.identity)
576
577
  errors.push(...unknownKeyErrors(identity, `repos[${index}].identity`, new Set(['provider', 'id', 'ext'])))