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

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 +413 -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 +698 -0
  55. package/docs/obsidian.md +484 -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 +155 -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 +212 -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 +138 -0
  331. package/src/runtime/obsidian/app-production-seams.mjs +107 -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 +280 -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 +93 -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
@@ -4,14 +4,14 @@ import path from 'node:path'
4
4
  import { buildGraph } from '../graph/graph.mjs'
5
5
  import { commandProject, firstString, parseArgs, readJson, resolvePathValue, writeJson } from '../project/config.mjs'
6
6
  import { matchesPathPattern } from '../project/path-match.mjs'
7
+ import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
7
8
  import {
8
- diffForPushRange,
9
- parseAddedContent,
10
- parsePushRefUpdates,
9
+ CHECK_AGGREGATE_MAX_BYTES,
10
+ parsePushRefInput,
11
11
  resolveContentRules,
12
- scanAddedContent,
12
+ scanPushUpdate,
13
+ scanStagedRepository,
13
14
  scanTree,
14
- stagedDiff,
15
15
  validateContentRuleExceptions,
16
16
  validateContentRules,
17
17
  } from './content-rules.mjs'
@@ -146,7 +146,7 @@ export function validateBoundaryPolicy(policy, project = null) {
146
146
  const declaredRepos = isObject(repos) ? repos : {}
147
147
  for (const [index, exception] of policy.contentRuleExceptions.entries()) {
148
148
  const name = firstString(exception?.repo)
149
- if (name && Object.keys(declaredRepos).length && !declaredRepos[name]) {
149
+ if (name && Object.keys(declaredRepos).length && !Object.hasOwn(declaredRepos, name)) {
150
150
  errors.push(`contentRuleExceptions[${index}].repo ${name} is not declared in repos`)
151
151
  }
152
152
  }
@@ -163,7 +163,7 @@ export function validateBoundaryPolicy(policy, project = null) {
163
163
  }
164
164
  if (!firstString(actor.privateDomainRepo)) errors.push(`actors.${actorId}.privateDomainRepo is required`)
165
165
  if (firstString(actor.privateDomainRepo) && isObject(repos)) {
166
- const privateRepo = repos[actor.privateDomainRepo]
166
+ const privateRepo = Object.hasOwn(repos, actor.privateDomainRepo) ? repos[actor.privateDomainRepo] : null
167
167
  if (!privateRepo) {
168
168
  errors.push(`actors.${actorId}.privateDomainRepo ${actor.privateDomainRepo} is not declared in repos`)
169
169
  } else if (privateRepo.kind !== 'private_domain') {
@@ -188,7 +188,7 @@ export function validateBoundaryPolicy(policy, project = null) {
188
188
  errors.push(...validAudienceList(repo.forbiddenAudiences, `repos.${repoName}.forbiddenAudiences`))
189
189
  if (repo.kind === 'private_domain') {
190
190
  if (!firstString(repo.ownerActor)) errors.push(`repos.${repoName}.ownerActor is required for private_domain repos`)
191
- if (repo.ownerActor && isObject(actors) && !actors[repo.ownerActor]) errors.push(`repos.${repoName}.ownerActor ${repo.ownerActor} is not declared`)
191
+ if (repo.ownerActor && isObject(actors) && !Object.hasOwn(actors, repo.ownerActor)) errors.push(`repos.${repoName}.ownerActor ${repo.ownerActor} is not declared`)
192
192
  }
193
193
  }
194
194
 
@@ -197,41 +197,71 @@ export function validateBoundaryPolicy(policy, project = null) {
197
197
  if (repo.external) {
198
198
  // Declaring a boundary for a repo nobody manages is the confusion the
199
199
  // external kind exists to remove.
200
- if (repos[repo.name]) errors.push(`policy repos.${repo.name} must not be declared; it is an external (unmanaged) repo`)
200
+ if (Object.hasOwn(repos, repo.name)) errors.push(`policy repos.${repo.name} must not be declared; it is an external (unmanaged) repo`)
201
201
  continue
202
202
  }
203
- if (!repos[repo.name]) errors.push(`policy repos.${repo.name} must be declared`)
203
+ if (!Object.hasOwn(repos, repo.name)) errors.push(`policy repos.${repo.name} must be declared`)
204
204
  }
205
205
  }
206
206
 
207
207
  return errors
208
208
  }
209
209
 
210
- export function resolveCurrentActor({ policy, project, actor = null, env = process.env } = {}) {
210
+ function resolveExplicitActor({ policy, actor, env }) {
211
211
  const actors = policy?.actors ?? {}
212
- const explicit = actor || env.MNSTRY_ATELIER_ACTOR || env.GITHUB_ACTOR
213
- if (explicit && actors[explicit]) return { actorId: explicit, source: 'explicit' }
214
- const gitEmails = gitEmailsForProject(project)
215
- for (const [actorId, info] of Object.entries(actors)) {
216
- const actorEmails = new Set(asArray(info.gitEmails).map((email) => email.toLowerCase()))
217
- if (gitEmails.some((email) => actorEmails.has(email.toLowerCase()))) return { actorId, source: 'git-email', gitEmails }
212
+ const explicit = actor || env.MNSTRY_ATELIER_ACTOR
213
+ const unverified = (reason) => ({ actorId: null, source: 'unverified', reason })
214
+ if (actor && env.MNSTRY_ATELIER_ACTOR && actor !== env.MNSTRY_ATELIER_ACTOR) {
215
+ return unverified('conflicting-explicit-actors')
218
216
  }
219
- const login = env.GITHUB_ACTOR || ghLogin()
217
+ if (explicit && !Object.hasOwn(actors, explicit)) return unverified('unknown-explicit-actor')
218
+ return explicit ? { actorId: explicit, source: 'explicit' } : null
219
+ }
220
+
221
+ export function resolveCurrentActor({ policy, project, actor = null, env = process.env, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false } = {}) {
222
+ const actors = policy?.actors ?? {}
223
+ const explicit = resolveExplicitActor({ policy, actor, env })
224
+ if (explicit) return explicit
225
+ const unverified = (reason) => ({ actorId: null, source: 'unverified', reason })
226
+ // Without an explicit local selector, a platform login precedes Git metadata.
227
+ // It is platform attribution only; this function cannot authenticate its env.
228
+ const platformLogin = env.GITHUB_ACTOR
229
+ if (platformLogin) {
230
+ const matches = Object.entries(actors).filter(([, info]) =>
231
+ !String(info.githubLogin || '').endsWith('_GITHUB_LOGIN_PLACEHOLDER') &&
232
+ String(info.githubLogin || '').toLowerCase() === String(platformLogin).toLowerCase())
233
+ if (matches.length !== 1) return unverified(matches.length ? 'ambiguous-platform-actor' : 'unknown-platform-actor')
234
+ return { actorId: matches[0][0], source: 'github-login', githubLogin: platformLogin }
235
+ }
236
+ // Keep the legacy option accepted by the API, but history is never evidence
237
+ // of the person currently operating a checkout.
238
+ const gitEmails = gitEmailsForProject(project, { gitExecutable, env, allowHistoryActorResolution: false })
239
+ const matches = Object.entries(actors).filter(([, info]) => {
240
+ const actorEmails = new Set(asArray(info.gitEmails).map((email) => email.toLowerCase()))
241
+ return gitEmails.some((email) => actorEmails.has(email.toLowerCase()))
242
+ })
243
+ if (matches.length > 1) return unverified('ambiguous-git-actor')
244
+ if (matches.length === 1) return { actorId: matches[0][0], source: 'git-email', gitEmails }
245
+ const login = allowNetworkActorResolution ? ghLogin() : null
220
246
  if (login) {
221
- for (const [actorId, info] of Object.entries(actors)) {
222
- if (String(info.githubLogin || '').toLowerCase() === String(login).toLowerCase()) return { actorId, source: 'github-login', githubLogin: login }
223
- }
247
+ const matches = Object.entries(actors).filter(([, info]) => !String(info.githubLogin || '').endsWith('_GITHUB_LOGIN_PLACEHOLDER') && String(info.githubLogin || '').toLowerCase() === String(login).toLowerCase())
248
+ if (matches.length !== 1) return unverified(matches.length ? 'ambiguous-platform-actor' : 'unknown-platform-actor')
249
+ return { actorId: matches[0][0], source: 'github-login', githubLogin: login }
224
250
  }
225
- return { actorId: null, source: 'unverified', gitEmails, githubLogin: login || null }
251
+ return { actorId: null, source: 'unverified', gitEmails, githubLogin: null }
226
252
  }
227
253
 
228
- function gitEmailsForProject(project) {
254
+ function gitEmailsForProject(project, { gitExecutable = 'git', env = process.env, allowHistoryActorResolution = false } = {}) {
229
255
  const roots = unique([project?.repoOpsRoot, project?.workspaceRoot, ...managedRepos(project).map((repo) => repo.path)])
230
256
  const emails = []
231
257
  for (const root of roots) {
232
258
  if (!root || !fs.existsSync(root)) continue
233
- const result = spawnSync('git', ['-C', root, 'config', 'user.email'], { encoding: 'utf8' })
234
- if (result.status === 0 && result.stdout.trim()) emails.push(result.stdout.trim())
259
+ const probes = [['config', 'user.email']]
260
+ if (allowHistoryActorResolution) probes.push(['log', '-1', '--format=%ae'])
261
+ for (const args of probes) {
262
+ const result = spawnSync(gitExecutable, ['-C', root, ...args], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
263
+ if (result.status === 0 && result.stdout.trim()) emails.push(result.stdout.trim())
264
+ }
235
265
  }
236
266
  return unique(emails)
237
267
  }
@@ -245,7 +275,7 @@ function ghLogin() {
245
275
  }
246
276
 
247
277
  function repoPolicy(policy, repoName) {
248
- return policy?.repos?.[repoName] ?? null
278
+ return policy?.repos && Object.hasOwn(policy.repos, repoName) ? policy.repos[repoName] : null
249
279
  }
250
280
 
251
281
  function finding({ severity = 'error', code, message, repo = null, path: itemPath = null, node = null, details = {} }) {
@@ -276,15 +306,21 @@ function nodePlacementFindings({ node, policy }) {
276
306
  return findings
277
307
  }
278
308
 
279
- function actorFindings({ policy, project, actor }) {
309
+ function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors }) {
280
310
  const findings = []
281
- const current = resolveCurrentActor({ policy, project, actor })
282
- const severity = severityFor(policy)
283
- for (const [repoName, repo] of Object.entries(policy.repos ?? {})) {
284
- if (repo.kind !== 'private_domain') continue
285
- if (!repo.ownerActor) continue
311
+ const operatedRepos = new Set(managedRepos(project).map((repo) => repo.name))
312
+ const ownedRepos = Object.entries(policy.repos ?? {}).filter(([name, repo]) =>
313
+ operatedRepos.has(name) && repo?.kind === 'private_domain' && repo.ownerActor)
314
+ const explicit = resolveExplicitActor({ policy, actor, env: process.env })
315
+ if (explicit?.reason) return [finding({ severity: 'error', code: 'actor-resolution-refused', message: explicit.reason })]
316
+ // Shared-only work needs no derived identity and must not invoke Git or gh.
317
+ if (!ownedRepos.length) return findings
318
+ const current = explicit || resolveCurrentActor({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution })
319
+ const severity = forceActorErrors ? 'error' : severityFor(policy)
320
+ for (const [repoName, repo] of ownedRepos) {
286
321
  if (!current.actorId) {
287
- findings.push(finding({ severity, code: 'private-domain-actor-unverified', repo: repoName, message: `${repoName}: could not verify local actor for private domain repo owned by ${repo.ownerActor}` }))
322
+ findings.push(finding({ severity, code: 'private-domain-actor-unverified', repo: repoName,
323
+ message: `${repoName}: could not verify local actor for private domain repo owned by ${repo.ownerActor}${current.reason ? ` (${current.reason})` : ''}` }))
288
324
  } else if (current.actorId !== repo.ownerActor) {
289
325
  findings.push(finding({ severity, code: 'private-domain-actor-mismatch', repo: repoName, message: `${repoName}: private domain repo is owned by ${repo.ownerActor}, but current actor is ${current.actorId}` }))
290
326
  }
@@ -292,11 +328,11 @@ function actorFindings({ policy, project, actor }) {
292
328
  return findings
293
329
  }
294
330
 
295
- export function stagedPathsForProject(project) {
331
+ export function stagedPathsForProject(project, { gitExecutable = 'git' } = {}) {
296
332
  const paths = []
297
333
  for (const repo of managedRepos(project)) {
298
334
  if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) continue
299
- const result = spawnSync('git', ['-C', repo.path, 'diff', '--cached', '--name-only', '--diff-filter=ACMR'], { encoding: 'utf8' })
335
+ const result = spawnSync(gitExecutable, ['-C', repo.path, 'diff', '--cached', '--name-only', '--diff-filter=ACMR'], { encoding: 'utf8', env: sanitizedGitEnvironment() })
300
336
  if (result.status !== 0) continue
301
337
  for (const rel of result.stdout.split('\n').map((line) => line.trim()).filter(Boolean)) {
302
338
  paths.push({ repo: repo.name, repoRoot: repo.path, path: normalize(rel) })
@@ -308,7 +344,7 @@ export function stagedPathsForProject(project) {
308
344
  function forbiddenPathFindings({ policy, stagedPaths }) {
309
345
  const patterns = [...DEFAULT_FORBIDDEN_PATHS, ...asArray(policy.forbiddenPaths)]
310
346
  const findings = []
311
- const severity = severityFor(policy)
347
+ const severity = 'error'
312
348
  for (const item of stagedPaths) {
313
349
  const matched = patterns.find((pattern) => matchesPathPattern(pattern, item.path))
314
350
  if (matched) {
@@ -390,13 +426,20 @@ export function semanticChangesInFile(file) {
390
426
  return changes
391
427
  }
392
428
 
393
- function semanticDiffFindings({ policy, project }) {
429
+ function semanticDiffFindings({ policy, project, gitExecutable = 'git' }) {
394
430
  const findings = []
395
- const severity = severityFor(policy)
431
+ const severity = 'error'
396
432
  for (const repo of managedRepos(project)) {
397
- if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) continue
398
- const result = spawnSync('git', ['-C', repo.path, 'diff', '--cached', '--unified=0', '--', '*.md', '*.kg.json'], { encoding: 'utf8' })
399
- if (result.status !== 0 || !result.stdout.trim()) continue
433
+ if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) {
434
+ findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
435
+ continue
436
+ }
437
+ const result = spawnSync(gitExecutable, ['-C', repo.path, 'diff', '--cached', '--unified=0', '--', '*.md', '*.kg.json'], { encoding: 'utf8', env: sanitizedGitEnvironment() })
438
+ if (result.status !== 0) {
439
+ findings.push(unscannableRepoFinding(repo, 'git diff --cached failed'))
440
+ continue
441
+ }
442
+ if (!result.stdout.trim()) continue
400
443
  for (const file of diffFileSections(result.stdout)) {
401
444
  const changes = semanticChangesInFile(file)
402
445
  if (!changes.length) continue
@@ -419,27 +462,32 @@ function semanticDiffFindings({ policy, project }) {
419
462
  return findings
420
463
  }
421
464
 
422
- function contentRuleFindings({ policy, project, files }) {
465
+ function stagedContentFindings({ policy, project, gitExecutable = 'git' }) {
423
466
  const rules = resolveContentRules(policy)
424
467
  const exceptions = asArray(policy?.contentRuleExceptions)
425
468
  const findings = []
426
- for (const entry of files) {
427
- for (const item of scanAddedContent({ files: entry.files, rules, exceptions, repo: entry.repo })) {
428
- // The rule id and line stay on the finding: they are what a person needs to
429
- // either fix the line or write the exception.
430
- findings.push({ ...finding({ ...item, details: { rule: item.rule, line: item.line } }), rule: item.rule, line: item.line })
469
+ let totalBytes = 0
470
+ for (const repo of managedRepos(project)) {
471
+ if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) {
472
+ findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
473
+ continue
474
+ }
475
+ const result = scanStagedRepository({ repoRoot: repo.path, rules, exceptions, repo: repo.name, gitExecutable })
476
+ findings.push(...result.findings, ...result.diagnostics)
477
+ totalBytes += result.bytes
478
+ if (totalBytes > CHECK_AGGREGATE_MAX_BYTES) {
479
+ findings.push(finding({
480
+ severity: 'error',
481
+ code: 'content-scan-incomplete',
482
+ repo: repo.name,
483
+ message: `staged evidence exceeds the ${CHECK_AGGREGATE_MAX_BYTES}-byte aggregate limit`,
484
+ }))
485
+ break
431
486
  }
432
487
  }
433
488
  return findings
434
489
  }
435
490
 
436
- function stagedContentFindings({ policy, project }) {
437
- const files = managedRepos(project)
438
- .filter((repo) => repo.path && fs.existsSync(path.join(repo.path, '.git')))
439
- .map((repo) => ({ repo: repo.name, files: parseAddedContent(stagedDiff(repo.path)) }))
440
- return contentRuleFindings({ policy, project, files })
441
- }
442
-
443
491
  function realPath(value) {
444
492
  try {
445
493
  return fs.realpathSync(path.resolve(value))
@@ -467,7 +515,7 @@ function repoForCwd(project, cwd) {
467
515
  * which reports without blocking, so an accepted usage elsewhere in the repo can
468
516
  * never strand unrelated work on the machine.
469
517
  */
470
- export function checkPushContent({ project, policy, repo, updates, cwd = process.cwd() } = {}) {
518
+ export function checkPushContent({ project, policy, repo, updates, cwd = process.cwd(), gitRunner = spawnSync } = {}) {
471
519
  const target = repo ?? repoForCwd(project, cwd)
472
520
  if (!target) {
473
521
  // Fail closed. This hook is only installed into managed repos, so failing to
@@ -480,20 +528,41 @@ export function checkPushContent({ project, policy, repo, updates, cwd = process
480
528
  })
481
529
  return { ok: false, repo: null, findings: [unresolved], errors: [unresolved], warnings: [] }
482
530
  }
483
- const files = []
484
- for (const update of updates) files.push(...parseAddedContent(diffForPushRange(target.path, update)))
485
- const findings = contentRuleFindings({ policy, project, files: [{ repo: target.name, files }] })
531
+ const rules = resolveContentRules(policy)
532
+ const exceptions = asArray(policy?.contentRuleExceptions)
533
+ const findings = []
534
+ let totalBytes = 0
535
+ for (const update of updates) {
536
+ const result = scanPushUpdate({ repoRoot: target.path, update, rules, exceptions, repo: target.name, gitRunner })
537
+ findings.push(...result.findings, ...result.diagnostics)
538
+ totalBytes += result.bytes
539
+ if (totalBytes > CHECK_AGGREGATE_MAX_BYTES) {
540
+ findings.push(finding({
541
+ severity: 'error',
542
+ code: 'content-scan-incomplete',
543
+ repo: target.name,
544
+ message: `push evidence exceeds the ${CHECK_AGGREGATE_MAX_BYTES}-byte aggregate limit`,
545
+ }))
546
+ break
547
+ }
548
+ }
486
549
  const errors = findings.filter((item) => item.severity === 'error')
487
550
  return { ok: errors.length === 0, repo: target.name, updates, findings, errors, warnings: findings.filter((item) => item.severity !== 'error') }
488
551
  }
489
552
 
490
- export function auditContentRules({ project, policy } = {}) {
553
+ export function auditContentRules({ project, policy, source = 'working-tree' } = {}) {
491
554
  const rules = resolveContentRules(policy)
492
555
  const exceptions = asArray(policy?.contentRuleExceptions)
493
556
  const findings = []
557
+ const diagnostics = []
494
558
  for (const repo of managedRepos(project)) {
495
- if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) continue
496
- findings.push(...scanTree(repo.path, { rules, exceptions, repo: repo.name }))
559
+ if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) {
560
+ diagnostics.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
561
+ continue
562
+ }
563
+ const result = scanTree(repo.path, { rules, exceptions, repo: repo.name, source })
564
+ findings.push(...result.findings)
565
+ diagnostics.push(...result.diagnostics)
497
566
  }
498
567
  const accepted = []
499
568
  for (const repo of managedRepos(project)) {
@@ -502,7 +571,16 @@ export function auditContentRules({ project, policy } = {}) {
502
571
  accepted.push({ repo: repo.name, rule: exception.rule, paths: exception.paths, reason: exception.reason })
503
572
  }
504
573
  }
505
- return { schema: BOUNDARY_POLICY_SCHEMA, findings, accepted }
574
+ return { schema: BOUNDARY_POLICY_SCHEMA, source, findings, diagnostics, accepted }
575
+ }
576
+
577
+ function unscannableRepoFinding(repo, reason) {
578
+ return finding({
579
+ severity: 'error',
580
+ code: 'repo-unscannable',
581
+ repo: repo?.name ?? null,
582
+ message: `${repo?.name ?? '(unnamed repo)'} cannot be scanned: ${reason}`,
583
+ })
506
584
  }
507
585
 
508
586
  function promotionRecords(project, policy) {
@@ -557,24 +635,22 @@ function promotionFindings({ policy, project, graph }) {
557
635
  return findings
558
636
  }
559
637
 
560
- export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null } = {}) {
638
+ export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false, forceActorErrors = false } = {}) {
561
639
  const validationErrors = validateBoundaryPolicy(policy, project)
562
640
  let graph = null
563
- const findings = validationErrors.map((message) => finding({ severity: severityFor(policy), code: 'boundary-policy-invalid', message }))
564
- if (validationErrors.length === 0) {
565
- if (!stagedOnly) {
566
- graph = buildGraph(project)
567
- findings.push(...graph.errors.map((message) => finding({ severity: severityFor(policy), code: 'knowledge-graph-invalid', message })))
568
- for (const node of graph.nodes ?? []) findings.push(...nodePlacementFindings({ node, policy }))
569
- findings.push(...promotionFindings({ policy, project, graph }))
570
- }
571
- findings.push(...actorFindings({ policy, project, actor }))
572
- if (staged) {
573
- const stagedPaths = stagedPathsForProject(project)
574
- findings.push(...forbiddenPathFindings({ policy, stagedPaths }))
575
- findings.push(...semanticDiffFindings({ policy, project }))
576
- findings.push(...stagedContentFindings({ policy, project }))
577
- }
641
+ const findings = validationErrors.map((message) => finding({ severity: 'error', code: 'boundary-policy-invalid', message }))
642
+ if (!stagedOnly) {
643
+ graph = buildGraph(project)
644
+ findings.push(...graph.errors.map((message) => finding({ severity: 'error', code: 'knowledge-graph-invalid', message })))
645
+ for (const node of graph.nodes ?? []) findings.push(...nodePlacementFindings({ node, policy }))
646
+ findings.push(...promotionFindings({ policy, project, graph }))
647
+ }
648
+ findings.push(...actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors }))
649
+ if (staged) {
650
+ const stagedPaths = stagedPathsForProject(project, { gitExecutable })
651
+ findings.push(...forbiddenPathFindings({ policy, stagedPaths }))
652
+ findings.push(...semanticDiffFindings({ policy, project, gitExecutable }))
653
+ findings.push(...stagedContentFindings({ policy, project, gitExecutable }))
578
654
  }
579
655
  const errors = findings.filter((item) => item.severity === 'error')
580
656
  const warnings = findings.filter((item) => item.severity !== 'error')
@@ -616,12 +692,12 @@ export function installBoundaryHooks({ project, force = false } = {}) {
616
692
  const skipped = []
617
693
  for (const repo of managedRepos(project)) {
618
694
  if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) continue
619
- const hooksDir = path.join(repo.path, '.git', 'hooks')
695
+ const hooksDir = gitHooksDir(repo.path)
620
696
  fs.mkdirSync(hooksDir, { recursive: true })
621
697
  for (const hookName of ['pre-commit', 'pre-push']) {
622
698
  const hookPath = path.join(hooksDir, hookName)
623
699
  const existing = fs.existsSync(hookPath) ? fs.readFileSync(hookPath, 'utf8') : ''
624
- const script = hookScript(project.configPath, hookName)
700
+ const script = boundaryHookScript(project.configPath, hookName, repo.path)
625
701
  if (existing && !existing.includes('MNSTRY_ATELIER_BOUNDARY_GUARD') && !force) {
626
702
  const sidecar = `${hookPath}.mnstry-atelier-boundary`
627
703
  fs.writeFileSync(sidecar, script)
@@ -637,8 +713,32 @@ export function installBoundaryHooks({ project, force = false } = {}) {
637
713
  return { installed, skipped }
638
714
  }
639
715
 
640
- function hookScript(projectConfigPath, hookName) {
641
- const config = projectConfigPath ? `--project-config=${projectConfigPath.replaceAll('"', '\\"')}` : ''
716
+ function gitHooksDir(repoPath) {
717
+ const result = spawnSync('git', ['-C', repoPath, 'rev-parse', '--git-path', 'hooks'], { encoding: 'utf8' })
718
+ if (result.status !== 0 || !result.stdout.trim()) throw new Error(`cannot resolve Git hooks directory for ${repoPath}`)
719
+ const value = result.stdout.trim()
720
+ return path.isAbsolute(value) ? value : path.resolve(repoPath, value)
721
+ }
722
+
723
+ function shellSingleQuote(value) {
724
+ const text = String(value ?? '')
725
+ if (!text || /[\0\r\n]/.test(text)) throw new Error('project config path contains unsupported control characters')
726
+ return `'${text.replaceAll("'", `'"'"'`)}'`
727
+ }
728
+
729
+ export function boundaryHookScript(projectConfigPath, hookName, repoPath) {
730
+ const relativeConfigPath = projectConfigPath && repoPath ? path.relative(repoPath, projectConfigPath) : null
731
+ const configLivesInRepo = relativeConfigPath && relativeConfigPath !== '..' && !relativeConfigPath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeConfigPath)
732
+ const portableRelativeConfig = configLivesInRepo ? relativeConfigPath.replaceAll(path.sep, '/') : null
733
+ const configSetup = configLivesInRepo
734
+ ? `ATELIER_HOOK_REPO_ROOT="$(git rev-parse --show-toplevel)"
735
+ ATELIER_HOOK_PROJECT_CONFIG="$ATELIER_HOOK_REPO_ROOT"/${shellSingleQuote(portableRelativeConfig)}`
736
+ : ''
737
+ const config = configLivesInRepo
738
+ ? '--project-config="$ATELIER_HOOK_PROJECT_CONFIG"'
739
+ : projectConfigPath
740
+ ? `--project-config=${shellSingleQuote(projectConfigPath)}`
741
+ : ''
642
742
  // pre-push reads the ref updates git writes to stdin and judges only that range;
643
743
  // pre-commit judges the staged diff. Neither scans the whole tree — that view is
644
744
  // `atelier boundary audit`, which reports without blocking.
@@ -646,6 +746,7 @@ function hookScript(projectConfigPath, hookName) {
646
746
  return `#!/usr/bin/env bash
647
747
  # MNSTRY_ATELIER_BOUNDARY_GUARD ${hookName}
648
748
  set -euo pipefail
749
+ ${configSetup}
649
750
  if command -v atelier >/dev/null 2>&1; then
650
751
  exec atelier ${invocation}
651
752
  elif [ -x "./node_modules/.bin/atelier" ]; then
@@ -663,7 +764,7 @@ fi
663
764
  `
664
765
  }
665
766
 
666
- export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdin = readStdin() } = {}) {
767
+ export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdin = readBoundaryPushInput() } = {}) {
667
768
  const args = parseArgs(argv)
668
769
  const project = commandProject({ argv })
669
770
  const loaded = loadBoundaryPolicy(project)
@@ -671,12 +772,23 @@ export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdi
671
772
  console.error(loaded.errors.join('\n'))
672
773
  process.exit(1)
673
774
  }
674
- const updates = parsePushRefUpdates(stdin)
675
- if (!updates.length) {
775
+ const input = stdin && typeof stdin === 'object' && Object.hasOwn(stdin, 'ok')
776
+ ? stdin
777
+ : { ok: true, text: String(stdin ?? '') }
778
+ if (!input.ok) {
779
+ console.error('[boundary:push-check] could not read pre-push ref updates')
780
+ process.exit(1)
781
+ }
782
+ const parsed = parsePushRefInput(input.text)
783
+ if (!parsed.ok) {
784
+ console.error(`[boundary:push-check] invalid pre-push input: ${parsed.issues.join('; ')}`)
785
+ process.exit(1)
786
+ }
787
+ if (parsed.kind === 'empty') {
676
788
  console.log('[boundary:push-check] no ref updates on stdin · nothing to judge')
677
789
  process.exit(0)
678
790
  }
679
- const report = checkPushContent({ project, policy: loaded.policy, updates, cwd: process.cwd() })
791
+ const report = checkPushContent({ project, policy: loaded.policy, updates: parsed.updates, cwd: process.cwd() })
680
792
  if (args.json) {
681
793
  console.log(JSON.stringify(report, null, 2))
682
794
  } else {
@@ -691,6 +803,10 @@ export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdi
691
803
  process.exit(report.ok ? 0 : 1)
692
804
  }
693
805
 
806
+ export function resolveBoundaryAuditSource(args = {}) {
807
+ return args.head ? 'head' : firstString(args.source) || 'working-tree'
808
+ }
809
+
694
810
  export function runBoundaryAuditCommand(argv = process.argv.slice(2)) {
695
811
  const args = parseArgs(argv)
696
812
  const project = commandProject({ argv })
@@ -699,23 +815,29 @@ export function runBoundaryAuditCommand(argv = process.argv.slice(2)) {
699
815
  console.error(loaded.errors.join('\n'))
700
816
  process.exit(1)
701
817
  }
702
- const report = auditContentRules({ project, policy: loaded.policy })
818
+ const source = resolveBoundaryAuditSource(args)
819
+ if (!['working-tree', 'head'].includes(source)) {
820
+ console.error('boundary audit --source must be working-tree or head')
821
+ process.exit(1)
822
+ }
823
+ const report = auditContentRules({ project, policy: loaded.policy, source })
703
824
  if (args.json) {
704
825
  console.log(JSON.stringify(report, null, 2))
705
826
  } else {
706
- console.log(`[boundary:audit] ${report.findings.length} content-rule matches across the tree · ${report.accepted.length} declared exceptions`)
827
+ console.log(`[boundary:audit] ${report.findings.length} content-rule matches · ${report.diagnostics.length} incomplete reads · ${report.accepted.length} declared exceptions · source ${report.source}`)
707
828
  for (const item of report.findings.slice(0, 100)) console.log(`${item.severity} ${item.rule}: ${item.message}`)
829
+ for (const item of report.diagnostics.slice(0, 100)) console.log(`${item.severity} ${item.code}: ${item.message}`)
708
830
  for (const item of report.accepted) console.log(`accepted ${item.rule} in ${item.repo} (${item.paths.join(', ')}): ${item.reason}`)
709
831
  }
710
832
  // Reporting only. A pre-existing accepted usage must never block unrelated work.
711
833
  process.exit(0)
712
834
  }
713
835
 
714
- function readStdin() {
836
+ export function readBoundaryPushInput(reader = () => fs.readFileSync(0, 'utf8')) {
715
837
  try {
716
- return fs.readFileSync(0, 'utf8')
838
+ return { ok: true, text: reader() }
717
839
  } catch {
718
- return ''
840
+ return { ok: false, text: '', error: 'stdin-read-failed' }
719
841
  }
720
842
  }
721
843
 
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ const [, , requestedScript, ...args] = process.argv
7
+
8
+ function renderError(error) {
9
+ const debug = process.env.ATELIER_DEBUG === '1'
10
+ if (debug) {
11
+ console.error(error?.stack || String(error))
12
+ return Number.isInteger(error?.exitCode) ? error.exitCode : 1
13
+ }
14
+ if (typeof error?.code === 'string' && error.code) {
15
+ console.error(`[${error.code}] ${error.message}`)
16
+ if (error.hint) console.error(`Next: ${error.hint}`)
17
+ return Number.isInteger(error.exitCode) ? error.exitCode : 2
18
+ }
19
+ console.error('[internal-error] command failed without a safe diagnostic')
20
+ console.error('Next: rerun with ATELIER_DEBUG=1 to inspect the stack locally.')
21
+ return 1
22
+ }
23
+
24
+ if (!requestedScript) {
25
+ console.error('[command-missing] no command module was selected')
26
+ process.exit(2)
27
+ }
28
+
29
+ const scriptPath = path.resolve(requestedScript)
30
+ process.argv = [process.execPath, scriptPath, ...args]
31
+
32
+ try {
33
+ await import(pathToFileURL(scriptPath).href)
34
+ } catch (error) {
35
+ process.exitCode = renderError(error)
36
+ }
@@ -0,0 +1,58 @@
1
+ // Shared project-location syntax. Command-specific arguments remain owned by
2
+ // their command; this parser does not make unknown options globally valid.
3
+ export const PROJECT_OPTION_SPEC = Object.freeze({
4
+ project: 'value',
5
+ 'project-config': 'value',
6
+ 'repo-path': 'value',
7
+ })
8
+
9
+ function invalid(message) {
10
+ const error = new Error(message)
11
+ error.code = 'project-option-invalid'
12
+ error.exitCode = 2
13
+ throw error
14
+ }
15
+
16
+ export function projectOptionAt(argv, index, configArgPrefix = '--project-config=') {
17
+ const arg = argv[index]
18
+ if (typeof arg !== 'string' || !arg.startsWith('--')) return null
19
+ const name = arg.slice(2).split('=')[0]
20
+ const custom = configArgPrefix !== '--project-config=' && arg.startsWith(configArgPrefix)
21
+ if (!custom && !Object.hasOwn(PROJECT_OPTION_SPEC, name)) return null
22
+ const inline = custom || arg.includes('=')
23
+ const value = custom ? arg.slice(configArgPrefix.length) : inline ? arg.slice(arg.indexOf('=') + 1) : argv[index + 1]
24
+ if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
25
+ invalid(`--${custom ? 'project-config' : name} requires a non-empty value`)
26
+ }
27
+ const option = custom ? 'project-config' : name
28
+ if (option === 'repo-path') {
29
+ const separator = value.indexOf('=')
30
+ if (separator < 1 || !value.slice(0, separator).trim() || !value.slice(separator + 1).trim()) {
31
+ invalid('--repo-path requires NAME=PATH')
32
+ }
33
+ }
34
+ return { name: option, value, nextIndex: index + (inline ? 1 : 2) }
35
+ }
36
+
37
+ export function parseProjectOptions(argv = [], configArgPrefix = '--project-config=') {
38
+ let project = null
39
+ const repoPaths = new Map()
40
+ const remaining = []
41
+ for (let index = 0; index < argv.length;) {
42
+ const option = projectOptionAt(argv, index, configArgPrefix)
43
+ if (!option) {
44
+ remaining.push(argv[index++])
45
+ continue
46
+ }
47
+ if (option.name === 'repo-path') {
48
+ const separator = option.value.indexOf('=')
49
+ repoPaths.set(option.value.slice(0, separator).trim(), option.value.slice(separator + 1))
50
+ } else if (project === null) {
51
+ // Preserve the resolver's first project selection and last named repo
52
+ // override behavior, including aliases in the same invocation.
53
+ project = option.value.trim()
54
+ }
55
+ index = option.nextIndex
56
+ }
57
+ return { project, repoPaths, remaining }
58
+ }