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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. package/CHANGELOG.md +372 -0
  2. package/README.md +402 -205
  3. package/SECURITY.md +6 -1
  4. package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
  5. package/contracts/atelier-coauthor.v1.schema.json +163 -0
  6. package/contracts/atelier-decision-request.v1.schema.json +476 -0
  7. package/contracts/atelier-decision-result.v1.schema.json +500 -0
  8. package/contracts/atelier-guide.v1.schema.json +227 -0
  9. package/contracts/atelier-intake.v1.schema.json +151 -0
  10. package/contracts/atelier-migration.v2.schema.json +61 -0
  11. package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
  12. package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
  13. package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
  14. package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
  15. package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
  16. package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
  17. package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
  18. package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
  19. package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
  20. package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
  21. package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
  22. package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
  23. package/contracts/atelier-presentation.v1.schema.json +1293 -0
  24. package/contracts/atelier-repository-observation.v1.schema.json +163 -0
  25. package/contracts/atelier-review.v1.schema.json +1601 -0
  26. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  27. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  28. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  29. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  30. package/contracts/public-api-baseline.json +57 -0
  31. package/docs/assurance-controls.md +49 -0
  32. package/docs/atelier-runtime.md +28 -2
  33. package/docs/atelier-sync.md +172 -0
  34. package/docs/blocks/audience-visibility.md +9 -0
  35. package/docs/blocks/claims.md +61 -0
  36. package/docs/blocks/conformance-admission.md +15 -0
  37. package/docs/blocks/will-not-do.md +18 -0
  38. package/docs/coauthor-session.md +57 -0
  39. package/docs/connected-composition.md +75 -0
  40. package/docs/continuity.md +3 -4
  41. package/docs/decisions.md +167 -0
  42. package/docs/design.md +167 -0
  43. package/docs/exact-upgrades.md +159 -0
  44. package/docs/extension-pack-lifecycle.md +33 -0
  45. package/docs/guided-upgrades.md +99 -0
  46. package/docs/install.md +99 -16
  47. package/docs/intake-and-guides.md +65 -0
  48. package/docs/integration-and-review-plan.md +409 -0
  49. package/docs/integration-contract-decisions.md +22 -0
  50. package/docs/knowledge-graph.md +8 -4
  51. package/docs/local-review.md +84 -0
  52. package/docs/local-services.md +328 -0
  53. package/docs/local-state.md +107 -0
  54. package/docs/obsidian-contract.md +690 -0
  55. package/docs/obsidian.md +423 -0
  56. package/docs/portable-presentation.md +328 -0
  57. package/docs/presentation-consumer-boundaries.md +75 -0
  58. package/docs/presentation-review-disposition.md +47 -0
  59. package/docs/project-command-inventory.md +38 -0
  60. package/docs/project-options.md +53 -0
  61. package/docs/release-engineering.md +174 -12
  62. package/docs/repo-boundary-guard.md +12 -2
  63. package/docs/review-portability.md +55 -0
  64. package/docs/skill-steward.md +156 -0
  65. package/docs/upgrade.md +146 -5
  66. package/docs/vault-service.md +292 -0
  67. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  68. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  69. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  70. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  71. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  72. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  73. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  74. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  75. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  76. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  77. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  78. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  79. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  80. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  81. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  82. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  83. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  84. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  85. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  86. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  87. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  88. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  89. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  90. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  91. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  92. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  93. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  94. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  95. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  96. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  97. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  98. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  99. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  100. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  101. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  102. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  103. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  104. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  105. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  106. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  107. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  108. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  109. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  110. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  111. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  112. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  113. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  114. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  115. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  116. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  117. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  118. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  119. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  120. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  121. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  122. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  123. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  125. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  127. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  128. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  129. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  130. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  131. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  132. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  133. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  134. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  135. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  136. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  137. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  138. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  139. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  140. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  141. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  142. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  143. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  144. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  145. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  146. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  147. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  148. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  149. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  150. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  151. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  152. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  153. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  154. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  159. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  160. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  161. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  162. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  163. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  164. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  165. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  166. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  167. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  168. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  169. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  170. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  171. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  172. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  173. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  174. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  175. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  176. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  177. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  178. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  179. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  181. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  182. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  183. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  186. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  187. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  188. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  189. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  190. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  191. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  192. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  193. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  194. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  195. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  196. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  197. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  198. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  199. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  200. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  201. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  202. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  203. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  204. package/fixtures/obsidian/edits/workspace.json +65 -0
  205. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  206. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  207. package/fixtures/obsidian/materialization/workspace.json +54 -0
  208. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  209. package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
  210. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
  211. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
  212. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
  213. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
  214. package/fixtures/ui/presentation/pane.v1.json +7 -0
  215. package/fixtures/ui/presentation/reference.v1.json +293 -0
  216. package/package.json +86 -10
  217. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  218. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  219. package/skills/claude/atelier-local-service/SKILL.md +47 -0
  220. package/skills/claude/atelier-open/SKILL.md +11 -5
  221. package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
  222. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  223. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  224. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  225. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  226. package/skills/codex/atelier-local-service/SKILL.md +47 -0
  227. package/skills/codex/atelier-open/SKILL.md +11 -5
  228. package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
  229. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  230. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  231. package/src/access/preview-enforcer.mjs +84 -0
  232. package/src/boundary/content-rules.mjs +283 -20
  233. package/src/boundary/policy.mjs +213 -91
  234. package/src/cli/execute-command.mjs +36 -0
  235. package/src/cli/project-options.mjs +58 -0
  236. package/src/cli/run.mjs +78 -10
  237. package/src/coauthor/session.mjs +161 -0
  238. package/src/coauthor/store.mjs +173 -0
  239. package/src/collaboration/event-ledger.mjs +348 -0
  240. package/src/collaboration/index.mjs +17 -0
  241. package/src/collaboration/inspection-bundle.mjs +223 -0
  242. package/src/collaboration/proposals.mjs +265 -65
  243. package/src/collaboration/review-contracts.mjs +25 -0
  244. package/src/collaboration/review-store.mjs +305 -0
  245. package/src/commands/attestation.mjs +20 -6
  246. package/src/commands/coauthor.mjs +26 -0
  247. package/src/commands/config.mjs +6 -1
  248. package/src/commands/disclosure.mjs +133 -0
  249. package/src/commands/distribution.mjs +2 -1
  250. package/src/commands/extension-pack.mjs +17 -9
  251. package/src/commands/init.mjs +46 -6
  252. package/src/commands/obsidian.mjs +371 -0
  253. package/src/commands/review.mjs +147 -0
  254. package/src/commands/server.mjs +1 -4
  255. package/src/commands/setup.mjs +51 -14
  256. package/src/commands/skills.mjs +199 -0
  257. package/src/commands/sync.mjs +107 -0
  258. package/src/composition/registry.mjs +27 -0
  259. package/src/composition/wire.mjs +122 -0
  260. package/src/contracts/corpus.mjs +35 -0
  261. package/src/decisions/contracts.d.mts +96 -0
  262. package/src/decisions/contracts.mjs +321 -0
  263. package/src/disclosure/content-scan.mjs +206 -0
  264. package/src/egress/check.mjs +7 -38
  265. package/src/egress/forbidden-egress.mjs +32 -18
  266. package/src/export/atelier-export-contract.mjs +16 -1
  267. package/src/extension-packs/lifecycle.mjs +154 -0
  268. package/src/graph/graph.mjs +176 -316
  269. package/src/graph/knowledge-graph.mjs +643 -43
  270. package/src/guides/contracts.mjs +54 -0
  271. package/src/harness/context-client.mjs +9 -1
  272. package/src/index.mjs +41 -0
  273. package/src/intake/store.mjs +119 -0
  274. package/src/preview/channel.mjs +89 -0
  275. package/src/project/config.mjs +112 -52
  276. package/src/project/durable-state.mjs +161 -0
  277. package/src/project/file-class.mjs +180 -0
  278. package/src/project/package-root.mjs +10 -0
  279. package/src/project/path-match.mjs +38 -15
  280. package/src/project/private-state.mjs +120 -0
  281. package/src/projection/obsidian/contracts.mjs +571 -0
  282. package/src/projection/obsidian/edits/align.mjs +169 -0
  283. package/src/projection/obsidian/edits/apply.mjs +889 -0
  284. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  285. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  286. package/src/projection/obsidian/edits/index.mjs +19 -0
  287. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  288. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  289. package/src/projection/obsidian/edits/observe.mjs +123 -0
  290. package/src/projection/obsidian/edits/policy.mjs +123 -0
  291. package/src/projection/obsidian/edits/regions.mjs +648 -0
  292. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  293. package/src/projection/obsidian/materialize/index.mjs +7 -0
  294. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  295. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  296. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  297. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  298. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  299. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  300. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  301. package/src/projection/obsidian/proposals/index.mjs +23 -0
  302. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  303. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  304. package/src/projection/obsidian/proposals/router.mjs +168 -0
  305. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  306. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  307. package/src/projection/obsidian/publication/index.mjs +5 -0
  308. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  309. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  310. package/src/projection/obsidian/publication/transport.mjs +160 -0
  311. package/src/projection/obsidian/recovery/index.mjs +6 -0
  312. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  313. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  314. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  315. package/src/projection/obsidian/recovery/store.mjs +234 -0
  316. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  317. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  318. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  319. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  320. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  321. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  322. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  323. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  324. package/src/projection/project.mjs +28 -19
  325. package/src/readiness-protocols/evidence.mjs +282 -0
  326. package/src/readiness-protocols/runtime.mjs +4 -3
  327. package/src/readiness-protocols/source-read.mjs +39 -0
  328. package/src/runtime/git-adapter.mjs +189 -0
  329. package/src/runtime/local-state.mjs +439 -0
  330. package/src/runtime/obsidian/app-capability.mjs +129 -0
  331. package/src/runtime/obsidian/app-production-seams.mjs +105 -0
  332. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  333. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  334. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  335. package/src/runtime/obsidian/contributions.mjs +27 -0
  336. package/src/runtime/obsidian/documents.mjs +37 -0
  337. package/src/runtime/obsidian/enablement.mjs +53 -0
  338. package/src/runtime/obsidian/engine.mjs +532 -0
  339. package/src/runtime/obsidian/errors.mjs +14 -0
  340. package/src/runtime/obsidian/extension-points.mjs +103 -0
  341. package/src/runtime/obsidian/index.mjs +34 -0
  342. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  343. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  344. package/src/runtime/obsidian/observation.mjs +158 -0
  345. package/src/runtime/obsidian/opening.mjs +263 -0
  346. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  347. package/src/runtime/obsidian/pipeline.mjs +122 -0
  348. package/src/runtime/obsidian/private-lock.mjs +156 -0
  349. package/src/runtime/obsidian/process-identity.mjs +58 -0
  350. package/src/runtime/obsidian/service-client.mjs +66 -0
  351. package/src/runtime/obsidian/service-entry-path.mjs +9 -0
  352. package/src/runtime/obsidian/service-main.mjs +91 -0
  353. package/src/runtime/obsidian/service-record.mjs +169 -0
  354. package/src/runtime/obsidian/service-server.mjs +121 -0
  355. package/src/runtime/obsidian/service.mjs +216 -0
  356. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  357. package/src/runtime/obsidian/state-store.mjs +180 -0
  358. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  359. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  360. package/src/runtime/obsidian/watchers.mjs +36 -0
  361. package/src/runtime/repository-observation.mjs +491 -0
  362. package/src/runtime/supervisor.mjs +803 -0
  363. package/src/server/local-sidecar.mjs +125 -59
  364. package/src/server/security.mjs +89 -4
  365. package/src/server/server.mjs +4 -2
  366. package/src/skills/steward.mjs +780 -0
  367. package/src/support/feedback-report.mjs +4 -3
  368. package/src/ui/presentation/browser.mjs +278 -0
  369. package/src/ui/presentation/contract.mjs +116 -0
  370. package/src/ui/presentation/index.mjs +7 -0
  371. package/src/ui/presentation/native.mjs +153 -0
  372. package/src/ui/presentation/proof.mjs +57 -0
  373. package/src/ui/presentation/schema-check.mjs +30 -0
  374. package/src/ui/presentation/schema.generated.mjs +1371 -0
  375. package/src/ui/presentation/state.mjs +43 -0
  376. package/src/ui/presentation/styles.mjs +67 -0
  377. package/src/ui/presentation/tokens.mjs +74 -0
  378. package/src/ui/presentation/web.mjs +85 -0
  379. package/src/ui/review-page.mjs +32 -0
  380. package/src/upgrade/explanation.mjs +33 -0
  381. package/src/upgrade/provenance.mjs +338 -0
  382. package/src/upgrade/transaction-files.mjs +103 -0
  383. package/src/upgrade/transaction.mjs +475 -0
  384. package/src/upgrade/upgrade.mjs +65 -29
  385. package/src/vault/hosts.mjs +21 -0
  386. package/src/vault/identity.mjs +19 -0
  387. package/src/vault/index.mjs +7 -0
  388. package/src/vault/interface.mjs +11 -0
  389. package/src/vault/metadata.mjs +31 -0
  390. package/src/vault/privacy.mjs +148 -0
  391. package/src/vault/probe.mjs +89 -0
  392. package/src/vault/service.mjs +131 -0
  393. package/src/vault/source.mjs +45 -0
  394. package/src/vault/storage.mjs +23 -0
  395. package/templates/distribution-workspace/.claude/launch.json +6 -2
  396. package/templates/distribution-workspace/gitignore +1 -0
  397. package/templates/distribution-workspace/project/README.md +2 -2
  398. package/templates/external-project-workspace/README.md +45 -0
  399. package/templates/external-project-workspace/answers.example.json +4 -0
  400. package/templates/external-project-workspace/atelier.project.json +40 -0
  401. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  402. package/templates/external-project-workspace/gitignore +8 -0
  403. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  404. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  405. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  406. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  407. package/templates/external-project-workspace/source/README.md +11 -0
  408. package/templates/launch.json +6 -2
  409. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  410. package/templates/private-domain-workspace/domain/README.md +2 -2
  411. package/templates/private-domain-workspace/gitignore +1 -0
  412. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  413. package/templates/shared-project-workspace/gitignore +1 -0
  414. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,96 @@
1
+ export type JsonValue = null | boolean | number | string | JsonValue[] | JsonObject
2
+ export interface JsonObject { [key: string]: JsonValue }
3
+ export interface DecisionExtension { ext?: JsonObject }
4
+ export interface DecisionScope extends DecisionExtension {
5
+ workspaceId: string
6
+ /** Host-reported provenance; this reference does not grant authorization. */
7
+ authorizationRef: string
8
+ }
9
+ export interface DecisionEvidence extends DecisionExtension {
10
+ id: string
11
+ sourceRef: string
12
+ }
13
+ export interface DecisionQuestionBase extends DecisionExtension {
14
+ instructions: string
15
+ evidenceIds: string[]
16
+ }
17
+ export interface ChoiceQuestion extends DecisionQuestionBase {
18
+ type: 'choice'
19
+ criteria: Record<string, string>
20
+ }
21
+ export interface ScoreQuestion extends DecisionQuestionBase {
22
+ type: 'score'
23
+ criteria: string[]
24
+ }
25
+ export interface BooleanQuestion extends DecisionQuestionBase {
26
+ type: 'boolean'
27
+ criteria: DecisionExtension & { true: string; false: string }
28
+ }
29
+ export type DecisionQuestion = ChoiceQuestion | ScoreQuestion | BooleanQuestion
30
+ export interface DecisionRequest extends DecisionExtension {
31
+ schema: 'atelier-decision-request@v1'
32
+ contractVersion?: `1.${number}.${number}`
33
+ id: string
34
+ task: string
35
+ rubricVersion: string
36
+ scope: DecisionScope
37
+ state: string
38
+ evidence: DecisionEvidence[]
39
+ questions: Record<string, DecisionQuestion>
40
+ }
41
+ export interface ChoiceAnswer extends DecisionExtension {
42
+ type: 'choice'
43
+ choice: string
44
+ probabilities: Record<string, number>
45
+ /** A provider statistic, not a measured probability of semantic correctness. */
46
+ confidence: number
47
+ }
48
+ export interface ScoreAnswer extends DecisionExtension {
49
+ type: 'score'
50
+ /** The zero-index weighted expectation of the ordered criteria. */
51
+ score: number
52
+ probabilities: number[]
53
+ confidence: number
54
+ }
55
+ export interface BooleanAnswer extends DecisionExtension {
56
+ type: 'boolean'
57
+ probability: number
58
+ }
59
+ export type DecisionAnswer = ChoiceAnswer | ScoreAnswer | BooleanAnswer
60
+ export interface DecisionUsage extends DecisionExtension {
61
+ inputTokens: number
62
+ outputTokens: number
63
+ }
64
+ export interface DecisionResultBase extends DecisionExtension {
65
+ schema: 'atelier-decision-result@v1'
66
+ contractVersion?: `1.${number}.${number}`
67
+ requestId: string
68
+ requestDigest: string
69
+ task: string
70
+ rubricVersion: string
71
+ scope: DecisionScope
72
+ provider: DecisionExtension & { id: string; model: string }
73
+ authority: 'proposal-only'
74
+ mode: 'shadow' | 'advisory'
75
+ usage: DecisionUsage | null
76
+ elapsedMs: number
77
+ }
78
+ export interface AssessedDecisionResult extends DecisionResultBase {
79
+ status: 'assessed'
80
+ answers: Record<string, DecisionAnswer>
81
+ reason?: never
82
+ }
83
+ export type DecisionAbstentionReason = 'insufficient-evidence' | 'ambiguous' | 'no-match' | 'budget-exhausted' | 'provider-unavailable' | 'timeout' | 'invalid-response' | 'unauthorized'
84
+ export interface AbstainedDecisionResult extends DecisionResultBase {
85
+ status: 'abstained'
86
+ answers: Record<string, never>
87
+ reason: DecisionAbstentionReason
88
+ }
89
+ export type DecisionResult = AssessedDecisionResult | AbstainedDecisionResult
90
+ export interface DecisionValidation { ok: boolean; errors: string[] }
91
+ export function validateDecisionRequest(input: unknown): DecisionValidation
92
+ export function validateDecisionResult(request: unknown, result: unknown): DecisionValidation
93
+ /** Checks questions and answers without state, hashing, or evidence membership. */
94
+ export function validateDecisionAnswers(questions: unknown, answers: unknown): DecisionValidation
95
+ /** Throws a generic TypeError for invalid input. The digest is lowercase SHA-256. */
96
+ export function decisionRequestDigest(request: unknown): string
@@ -0,0 +1,321 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
4
+ const RESERVED = new Set(['__proto__', 'constructor', 'prototype'])
5
+ const VERSION = /^1\.[0-9]+\.[0-9]+$/
6
+ const REASONS = new Set(['insufficient-evidence', 'ambiguous', 'no-match', 'budget-exhausted', 'provider-unavailable', 'timeout', 'invalid-response', 'unauthorized'])
7
+ const TOLERANCE = 1e-6
8
+
9
+ // Inspect descriptors before reading values. Invalid in-process inputs such as
10
+ // accessors, symbols, sparse arrays, cycles and custom prototypes are refused
11
+ // without calling their conversion methods or returning their content.
12
+ function jsonSnapshot(input) {
13
+ const ancestors = new Set()
14
+ let nodes = 0
15
+ let bytes = 0
16
+ function count(value) {
17
+ bytes += value
18
+ if (bytes > 16777216) throw new TypeError()
19
+ }
20
+ function copy(value, depth) {
21
+ if (++nodes > 100000 || depth > 32) throw new TypeError()
22
+ if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value)) {
23
+ if (typeof value === 'string' && Buffer.byteLength(value) > 16777216) throw new TypeError()
24
+ count(Buffer.byteLength(JSON.stringify(value)))
25
+ return value
26
+ }
27
+ if (!value || typeof value !== 'object' || ancestors.has(value)) throw new TypeError()
28
+ const array = Array.isArray(value)
29
+ const prototype = Object.getPrototypeOf(value)
30
+ if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) throw new TypeError()
31
+ const descriptors = Object.getOwnPropertyDescriptors(value)
32
+ const keys = Reflect.ownKeys(descriptors)
33
+ if (keys.some((key) => typeof key !== 'string' || RESERVED.has(key))) throw new TypeError()
34
+ count(2)
35
+ ancestors.add(value)
36
+ let result
37
+ if (array) {
38
+ const length = descriptors.length?.value
39
+ if (!Number.isSafeInteger(length) || length < 0 || length > 100000 || keys.length !== length + 1) throw new TypeError()
40
+ result = []
41
+ for (let index = 0; index < length; index++) {
42
+ const descriptor = descriptors[String(index)]
43
+ if (!descriptor || !Object.hasOwn(descriptor, 'value') || !descriptor.enumerable) throw new TypeError()
44
+ if (index) count(1)
45
+ result.push(copy(descriptor.value, depth + 1))
46
+ }
47
+ } else {
48
+ result = Object.create(null)
49
+ for (const [index, key] of keys.entries()) {
50
+ const descriptor = descriptors[key]
51
+ if (!Object.hasOwn(descriptor, 'value') || !descriptor.enumerable) throw new TypeError()
52
+ if (Buffer.byteLength(key) > 16777216) throw new TypeError()
53
+ count(Buffer.byteLength(JSON.stringify(key)) + 1 + (index ? 1 : 0))
54
+ result[key] = copy(descriptor.value, depth + 1)
55
+ }
56
+ }
57
+ ancestors.delete(value)
58
+ return result
59
+ }
60
+ return copy(input, 0)
61
+ }
62
+
63
+ function object(value) {
64
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
65
+ }
66
+
67
+ function text(value, maximum, allowEmpty = false) {
68
+ if (typeof value !== 'string' || (!allowEmpty && !value.trim())) return false
69
+ let length = 0
70
+ for (const character of value) {
71
+ void character
72
+ if (++length > maximum) return false
73
+ }
74
+ return true
75
+ }
76
+
77
+ function identifier(value) {
78
+ return typeof value === 'string' && ID.test(value) && !RESERVED.has(value)
79
+ }
80
+
81
+ function report() {
82
+ const errors = []
83
+ return {
84
+ errors,
85
+ check(condition, label) {
86
+ if (!condition && errors.length < 64) errors.push(label)
87
+ return condition
88
+ },
89
+ }
90
+ }
91
+
92
+ function closed(value, required, optional, label, checks) {
93
+ if (!checks.check(object(value), `${label}: expected object`)) return false
94
+ const allowed = new Set([...required, ...optional, 'ext'])
95
+ checks.check(required.every((key) => Object.hasOwn(value, key)), `${label}: required field missing`)
96
+ checks.check(Object.keys(value).every((key) => allowed.has(key)), `${label}: unknown field`)
97
+ if (Object.hasOwn(value, 'ext')) checks.check(object(value.ext), `${label}.ext: expected object`)
98
+ return true
99
+ }
100
+
101
+ function record(value, minimum, maximum, label, checks) {
102
+ if (!checks.check(object(value), `${label}: expected record`)) return false
103
+ const keys = Object.keys(value)
104
+ checks.check(keys.length >= minimum && keys.length <= maximum, `${label}: entry count outside limits`)
105
+ checks.check(keys.every(identifier), `${label}: invalid identifier`)
106
+ return keys.length <= maximum
107
+ }
108
+
109
+ function scope(value, label, checks) {
110
+ if (closed(value, ['workspaceId', 'authorizationRef'], [], label, checks)) {
111
+ checks.check(identifier(value.workspaceId), `${label}.workspaceId: invalid identifier`)
112
+ checks.check(identifier(value.authorizationRef), `${label}.authorizationRef: invalid identifier`)
113
+ }
114
+ }
115
+
116
+ function version(value, label, checks) {
117
+ if (Object.hasOwn(value, 'contractVersion')) {
118
+ checks.check(typeof value.contractVersion === 'string' && VERSION.test(value.contractVersion), `${label}.contractVersion: invalid version`)
119
+ }
120
+ }
121
+
122
+ function inspectRequest(request, checks) {
123
+ if (!closed(request, ['schema', 'id', 'task', 'rubricVersion', 'scope', 'state', 'evidence', 'questions'], ['contractVersion'], 'request', checks)) return
124
+ checks.check(request.schema === 'atelier-decision-request@v1', 'request.schema: unsupported schema')
125
+ version(request, 'request', checks)
126
+ for (const key of ['id', 'task', 'rubricVersion']) checks.check(identifier(request[key]), `request.${key}: invalid identifier`)
127
+ scope(request.scope, 'request.scope', checks)
128
+ checks.check(text(request.state, 32000, true), 'request.state: expected string within limit')
129
+ const evidenceIds = new Set()
130
+ if (checks.check(Array.isArray(request.evidence) && request.evidence.length >= 1 && request.evidence.length <= 256, 'request.evidence: expected 1..256 entries')) {
131
+ for (const evidence of request.evidence) {
132
+ if (!closed(evidence, ['id', 'sourceRef'], [], 'request.evidence[]', checks)) continue
133
+ checks.check(identifier(evidence.id), 'request.evidence[].id: invalid identifier')
134
+ checks.check(!evidenceIds.has(evidence.id), 'request.evidence: duplicate identifier')
135
+ evidenceIds.add(evidence.id)
136
+ checks.check(text(evidence.sourceRef, 2048), 'request.evidence[].sourceRef: expected nonempty reference within limit')
137
+ }
138
+ }
139
+ inspectQuestions(request.questions, checks, 'request.questions', evidenceIds)
140
+ }
141
+
142
+ function inspectQuestions(questions, checks, label, evidenceIds = null) {
143
+ if (!record(questions, 1, 64, label, checks)) return
144
+ for (const question of Object.values(questions)) {
145
+ if (!closed(question, ['type', 'instructions', 'evidenceIds', 'criteria'], [], `${label}[]`, checks)) continue
146
+ checks.check(text(question.instructions, 4000), `${label}[].instructions: expected nonempty string within limit`)
147
+ if (checks.check(Array.isArray(question.evidenceIds) && question.evidenceIds.length >= 1 && question.evidenceIds.length <= 256, `${label}[].evidenceIds: expected 1..256 references`)) {
148
+ checks.check(new Set(question.evidenceIds).size === question.evidenceIds.length, `${label}[].evidenceIds: duplicate reference`)
149
+ checks.check(question.evidenceIds.every(identifier), `${label}[].evidenceIds: invalid reference`)
150
+ if (evidenceIds !== null) checks.check(question.evidenceIds.every((id) => evidenceIds.has(id)), `${label}[].evidenceIds: unknown reference`)
151
+ }
152
+ if (question.type === 'choice') {
153
+ if (record(question.criteria, 2, 64, `${label}[].criteria`, checks)) {
154
+ checks.check(Object.values(question.criteria).every((value) => text(value, 2000)), `${label}[].criteria: expected nonempty descriptions within limit`)
155
+ }
156
+ } else if (question.type === 'score') {
157
+ checks.check(Array.isArray(question.criteria) && question.criteria.length >= 2 && question.criteria.length <= 10 && question.criteria.every((value) => text(value, 2000)), `${label}[].criteria: expected 2..10 nonempty ordered descriptions within limit`)
158
+ } else if (question.type === 'boolean') {
159
+ if (closed(question.criteria, ['true', 'false'], [], `${label}[].criteria`, checks)) {
160
+ checks.check(text(question.criteria.true, 2000) && text(question.criteria.false, 2000), `${label}[].criteria: expected nonempty descriptions within limit`)
161
+ }
162
+ } else {
163
+ checks.check(false, `${label}[].type: unsupported type`)
164
+ }
165
+ }
166
+ }
167
+
168
+ function canonical(value) {
169
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`
170
+ if (object(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`
171
+ return JSON.stringify(value)
172
+ }
173
+
174
+ function digest(request) {
175
+ return createHash('sha256').update(canonical(request), 'utf8').digest('hex')
176
+ }
177
+
178
+ function inspected(input) {
179
+ const checks = report()
180
+ let request
181
+ try {
182
+ request = jsonSnapshot(input)
183
+ inspectRequest(request, checks)
184
+ } catch {
185
+ checks.check(false, 'request: expected bounded plain JSON')
186
+ }
187
+ return { request, checks }
188
+ }
189
+
190
+ /** Validate structure and evidence references; this does not authorize egress. */
191
+ export function validateDecisionRequest(input) {
192
+ const { checks } = inspected(input)
193
+ return { ok: checks.errors.length === 0, errors: checks.errors }
194
+ }
195
+
196
+ /** Hash a valid request, including its scope, evidence, rubric and extensions. */
197
+ export function decisionRequestDigest(input) {
198
+ const { request, checks } = inspected(input)
199
+ if (checks.errors.length) throw new TypeError('Invalid decision request')
200
+ return digest(request)
201
+ }
202
+
203
+ function probability(value) {
204
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1
205
+ }
206
+
207
+ function distribution(values, label, checks) {
208
+ if (!checks.check(values.every(probability), `${label}: values must be probabilities`)) return false
209
+ return checks.check(Math.abs(values.reduce((sum, value) => sum + value, 0) - 1) <= TOLERANCE, `${label}: probabilities must sum to one`)
210
+ }
211
+
212
+ function sameKeys(left, right) {
213
+ const leftKeys = Object.keys(left).sort()
214
+ const rightKeys = Object.keys(right).sort()
215
+ return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index])
216
+ }
217
+
218
+ function answer(question, value, checks, label) {
219
+ if (question.type === 'choice') {
220
+ if (!closed(value, ['type', 'choice', 'probabilities', 'confidence'], [], label, checks)) return
221
+ checks.check(value.type === 'choice', `${label}.type: question type mismatch`)
222
+ checks.check(probability(value.confidence), `${label}.confidence: expected probability`)
223
+ checks.check(identifier(value.choice) && Object.hasOwn(question.criteria, value.choice), `${label}.choice: unknown choice`)
224
+ if (record(value.probabilities, 2, 64, `${label}.probabilities`, checks)) {
225
+ checks.check(sameKeys(value.probabilities, question.criteria), `${label}.probabilities: criteria keys mismatch`)
226
+ const values = Object.values(value.probabilities)
227
+ if (distribution(values, `${label}.probabilities`, checks) && Object.hasOwn(value.probabilities, value.choice)) {
228
+ checks.check(value.probabilities[value.choice] === Math.max(...values), `${label}.choice: choice must have maximum probability`)
229
+ }
230
+ }
231
+ } else if (question.type === 'score') {
232
+ if (!closed(value, ['type', 'score', 'probabilities', 'confidence'], [], label, checks)) return
233
+ checks.check(value.type === 'score', `${label}.type: question type mismatch`)
234
+ checks.check(probability(value.confidence), `${label}.confidence: expected probability`)
235
+ checks.check(typeof value.score === 'number' && Number.isFinite(value.score) && value.score >= 0 && value.score <= question.criteria.length - 1, `${label}.score: outside score range`)
236
+ if (checks.check(Array.isArray(value.probabilities) && value.probabilities.length === question.criteria.length, `${label}.probabilities: criteria count mismatch`)) {
237
+ if (distribution(value.probabilities, `${label}.probabilities`, checks)) {
238
+ const expectation = value.probabilities.reduce((sum, value, index) => sum + value * index, 0)
239
+ checks.check(Math.abs(value.score - expectation) <= TOLERANCE, `${label}.score: weighted expectation mismatch`)
240
+ }
241
+ }
242
+ } else {
243
+ if (!closed(value, ['type', 'probability'], [], label, checks)) return
244
+ checks.check(value.type === 'boolean', `${label}.type: question type mismatch`)
245
+ checks.check(probability(value.probability), `${label}.probability: expected probability`)
246
+ }
247
+ }
248
+
249
+ function inspectAnswers(questions, answers, checks, label) {
250
+ if (!record(answers, 0, 64, label, checks)) return
251
+ checks.check(sameKeys(answers, questions), `${label}: question keys mismatch`)
252
+ for (const [id, question] of Object.entries(questions)) {
253
+ if (Object.hasOwn(answers, id)) answer(question, answers[id], checks, `${label}[]`)
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Validate question/answer shape and distributions without state or hashing.
259
+ * Evidence reference syntax is checked; membership and authority belong to
260
+ * the host that holds the source snapshot and transient request binding.
261
+ */
262
+ export function validateDecisionAnswers(inputQuestions, inputAnswers) {
263
+ const checks = report()
264
+ let questions
265
+ try {
266
+ questions = jsonSnapshot(inputQuestions)
267
+ inspectQuestions(questions, checks, 'questions')
268
+ } catch {
269
+ checks.check(false, 'questions: expected bounded plain JSON')
270
+ }
271
+ if (checks.errors.length) return { ok: false, errors: checks.errors }
272
+ try {
273
+ inspectAnswers(questions, jsonSnapshot(inputAnswers), checks, 'answers')
274
+ } catch {
275
+ checks.check(false, 'answers: expected bounded plain JSON')
276
+ }
277
+ return { ok: checks.errors.length === 0, errors: checks.errors }
278
+ }
279
+
280
+ function inspectResult(request, result, checks) {
281
+ if (!closed(result, ['schema', 'requestId', 'requestDigest', 'task', 'rubricVersion', 'scope', 'provider', 'authority', 'mode', 'status', 'answers', 'usage', 'elapsedMs'], ['reason', 'contractVersion'], 'result', checks)) return
282
+ checks.check(result.schema === 'atelier-decision-result@v1', 'result.schema: unsupported schema')
283
+ version(result, 'result', checks)
284
+ checks.check(result.requestId === request.id, 'result.requestId: request binding mismatch')
285
+ checks.check(result.requestDigest === digest(request), 'result.requestDigest: request binding mismatch')
286
+ checks.check(result.task === request.task && result.rubricVersion === request.rubricVersion, 'result: task or rubric binding mismatch')
287
+ scope(result.scope, 'result.scope', checks)
288
+ checks.check(canonical(result.scope) === canonical(request.scope), 'result.scope: request binding mismatch')
289
+ if (closed(result.provider, ['id', 'model'], [], 'result.provider', checks)) {
290
+ checks.check(identifier(result.provider.id), 'result.provider.id: invalid identifier')
291
+ checks.check(text(result.provider.model, 128), 'result.provider.model: expected nonempty model within limit')
292
+ }
293
+ checks.check(result.authority === 'proposal-only', 'result.authority: proposal-only required')
294
+ checks.check(result.mode === 'shadow' || result.mode === 'advisory', 'result.mode: unsupported mode')
295
+ checks.check(typeof result.elapsedMs === 'number' && Number.isFinite(result.elapsedMs) && result.elapsedMs >= 0, 'result.elapsedMs: expected finite nonnegative number')
296
+ if (result.usage !== null && closed(result.usage, ['inputTokens', 'outputTokens'], [], 'result.usage', checks)) {
297
+ checks.check([result.usage.inputTokens, result.usage.outputTokens].every((value) => Number.isSafeInteger(value) && value >= 0), 'result.usage: expected nonnegative safe integers')
298
+ }
299
+ if (!record(result.answers, 0, 64, 'result.answers', checks)) return
300
+ if (result.status === 'assessed') {
301
+ checks.check(!Object.hasOwn(result, 'reason'), 'result.reason: not allowed for assessed result')
302
+ inspectAnswers(request.questions, result.answers, checks, 'result.answers')
303
+ } else if (result.status === 'abstained') {
304
+ checks.check(Object.keys(result.answers).length === 0, 'result.answers: abstention must have no answers')
305
+ checks.check(REASONS.has(result.reason), 'result.reason: unsupported abstention reason')
306
+ } else {
307
+ checks.check(false, 'result.status: unsupported status')
308
+ }
309
+ }
310
+
311
+ /** Validate integrity and distributions; validity never establishes correctness. */
312
+ export function validateDecisionResult(input, output) {
313
+ const { request, checks } = inspected(input)
314
+ if (checks.errors.length) return { ok: false, errors: ['request: invalid decision request'] }
315
+ try {
316
+ inspectResult(request, jsonSnapshot(output), checks)
317
+ } catch {
318
+ checks.check(false, 'result: expected bounded plain JSON')
319
+ }
320
+ return { ok: checks.errors.length === 0, errors: checks.errors }
321
+ }
@@ -0,0 +1,206 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ export const STRUCTURAL_DISCLOSURE_PATTERNS = Object.freeze([
6
+ { pattern: new RegExp('\\/Users\\/'), label: 'absolute user path' },
7
+ { pattern: new RegExp('\\/var\\/folders\\/'), label: 'machine-local temp path' },
8
+ { pattern: new RegExp('\\.' + 'codex'), label: 'agent-local state path' },
9
+ {
10
+ pattern: new RegExp([
11
+ 'BEGIN ',
12
+ '(?:[A-Z0-9]+ ){0,4}',
13
+ 'PRIVATE KEY(?: BLOCK)?',
14
+ '|BEGIN (?:RSA|OPENSSH) KEY',
15
+ ].join('')),
16
+ label: 'private key material',
17
+ },
18
+ { pattern: /"d"\s*:\s*"[A-Za-z0-9_-]{20,}"/, label: 'JWK private key material' },
19
+ {
20
+ pattern: /\b(api[_-]?key|secret|password|(?<!id-)token)\b\s*[:=]/i,
21
+ label: 'secret-like assignment',
22
+ },
23
+ ])
24
+
25
+ export function compileDisclosurePatterns(patternDocs = []) {
26
+ if (!Array.isArray(patternDocs)) throw new Error('denylist document must contain a patterns array')
27
+ return patternDocs.map(({ pattern, flags = '', label }) => {
28
+ if (typeof label !== 'string' || label.length === 0) throw new Error('denylist pattern label must be non-empty')
29
+ if (typeof pattern !== 'string' || pattern.length === 0) throw new Error(`denylist pattern failed to compile: ${label}`)
30
+ try {
31
+ return { pattern: new RegExp(pattern, String(flags).replace(/[gy]/g, '')), label }
32
+ } catch {
33
+ throw new Error(`denylist pattern failed to compile: ${label}`)
34
+ }
35
+ })
36
+ }
37
+
38
+ export function scanDisclosureText(text, { denylistPatterns = [] } = {}) {
39
+ if (typeof text !== 'string' || Buffer.byteLength(text) > 4 * 1024 * 1024) throw new Error('disclosure text unavailable or exceeds bounds')
40
+ const findings = []
41
+ const patterns = [...STRUCTURAL_DISCLOSURE_PATTERNS, ...denylistPatterns]
42
+ for (const [index,line] of text.split('\n').entries()) {
43
+ for (const {pattern,label} of patterns) {
44
+ pattern.lastIndex = 0
45
+ if (pattern.test(line)) findings.push({label,line:index+1})
46
+ }
47
+ }
48
+ return {ok:findings.length===0,findings}
49
+ }
50
+
51
+ export function scanDisclosureContent({
52
+ root = process.cwd(),
53
+ staged = false,
54
+ denylistPatterns = [],
55
+ failOnBinary = false,
56
+ } = {}) {
57
+ const resolvedRoot = path.resolve(root)
58
+ assertGitCheckout(resolvedRoot)
59
+ const patterns = [...STRUCTURAL_DISCLOSURE_PATTERNS, ...denylistPatterns]
60
+ const findings = []
61
+ const skippedBinary = []
62
+ let scannedFiles = 0
63
+
64
+ for (const entry of stagedEntries(resolvedRoot, staged)) {
65
+ const buffer = entry.buffer ?? readTrackedFile(resolvedRoot, entry.path)
66
+ if (buffer === null) continue
67
+ const text = decodeText(buffer)
68
+ if (text === null) {
69
+ skippedBinary.push(entry.path)
70
+ if (failOnBinary) findings.push({ label: 'binary content cannot be disclosure-scanned', path: entry.path, line: null })
71
+ continue
72
+ }
73
+
74
+ scannedFiles += 1
75
+ const lines = text.split('\n')
76
+ for (let index = 0; index < lines.length; index += 1) {
77
+ for (const { pattern, label } of patterns) {
78
+ if (pattern.test(lines[index])) findings.push({ label, path: entry.path, line: index + 1 })
79
+ }
80
+ }
81
+ }
82
+
83
+ return {
84
+ ok: findings.length === 0,
85
+ staged,
86
+ scannedFiles,
87
+ skippedBinary,
88
+ findings,
89
+ }
90
+ }
91
+
92
+ function decodeText(buffer) {
93
+ if (buffer.includes(0)) return null
94
+ try {
95
+ return new TextDecoder('utf-8', { fatal: true }).decode(buffer)
96
+ } catch {
97
+ return null
98
+ }
99
+ }
100
+
101
+ export function isPathTracked(root, candidatePath) {
102
+ const resolvedRoot = path.resolve(root)
103
+ const resolvedCandidate = path.resolve(candidatePath)
104
+ const relative = path.relative(resolvedRoot, resolvedCandidate)
105
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return false
106
+ const gitRelative = relative.split(path.sep).join('/')
107
+ const result = execFileSync('git', ['-C', resolvedRoot, 'ls-files', '-z', '--', gitRelative], {
108
+ encoding: 'utf8',
109
+ stdio: ['ignore', 'pipe', 'pipe'],
110
+ })
111
+ if (result.split('\0').filter(Boolean).includes(gitRelative)) return true
112
+
113
+ // Git pathspec matching is case-sensitive even when the checkout filesystem
114
+ // is not. Compare filesystem identities so alternate-cased callers cannot
115
+ // make a tracked file appear local and untracked.
116
+ return execFileSync('git', [
117
+ '-C',
118
+ resolvedRoot,
119
+ 'ls-files',
120
+ '-z',
121
+ '--',
122
+ `:(icase,literal)${gitRelative}`,
123
+ ], {
124
+ encoding: 'utf8',
125
+ stdio: ['ignore', 'pipe', 'pipe'],
126
+ })
127
+ .split('\0')
128
+ .filter(Boolean)
129
+ .some((trackedPath) => sameExistingFile(resolvedCandidate, path.join(resolvedRoot, trackedPath)))
130
+ }
131
+
132
+ function sameExistingFile(left, right) {
133
+ try {
134
+ if (fs.realpathSync.native(left) === fs.realpathSync.native(right)) return true
135
+ const leftStat = fs.statSync(left)
136
+ const rightStat = fs.statSync(right)
137
+ return leftStat.ino !== 0 && leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino
138
+ } catch {
139
+ return false
140
+ }
141
+ }
142
+
143
+ export function isPathIgnored(root, candidatePath) {
144
+ const resolvedRoot = path.resolve(root)
145
+ const relative = path.relative(resolvedRoot, path.resolve(candidatePath))
146
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return false
147
+ const gitRelative = relative.split(path.sep).join('/')
148
+ try {
149
+ execFileSync('git', ['-C', resolvedRoot, 'check-ignore', '--quiet', '--', gitRelative], {
150
+ stdio: ['ignore', 'ignore', 'ignore'],
151
+ })
152
+ return true
153
+ } catch {
154
+ return false
155
+ }
156
+ }
157
+
158
+ function assertGitCheckout(root) {
159
+ try {
160
+ execFileSync('git', ['-C', root, 'rev-parse', '--git-dir'], {
161
+ stdio: ['ignore', 'ignore', 'ignore'],
162
+ })
163
+ } catch {
164
+ throw new Error(`disclosure root is not a Git checkout`)
165
+ }
166
+ }
167
+
168
+ function stagedEntries(root, staged) {
169
+ if (!staged) {
170
+ return git(root, ['ls-files', '-z'])
171
+ .split('\0')
172
+ .filter(Boolean)
173
+ .map((filePath) => ({ path: filePath }))
174
+ }
175
+
176
+ return git(root, ['diff', '--cached', '--name-only', '--diff-filter=ACMR', '-z'])
177
+ .split('\0')
178
+ .filter(Boolean)
179
+ .map((filePath) => ({
180
+ path: filePath,
181
+ buffer: execFileSync('git', ['-C', root, 'show', `:${filePath}`], {
182
+ stdio: ['ignore', 'pipe', 'pipe'],
183
+ maxBuffer: 64 * 1024 * 1024,
184
+ }),
185
+ }))
186
+ }
187
+
188
+ function readTrackedFile(root, relativePath) {
189
+ const absolutePath = path.join(root, relativePath)
190
+ let stat
191
+ try {
192
+ stat = fs.lstatSync(absolutePath)
193
+ } catch {
194
+ return null
195
+ }
196
+ if (!stat.isFile()) return null
197
+ return fs.readFileSync(absolutePath)
198
+ }
199
+
200
+ function git(root, args) {
201
+ return execFileSync('git', ['-C', root, ...args], {
202
+ encoding: 'utf8',
203
+ stdio: ['ignore', 'pipe', 'pipe'],
204
+ maxBuffer: 64 * 1024 * 1024,
205
+ })
206
+ }
@@ -1,44 +1,13 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
1
+ import { checkForbiddenEgress } from './forbidden-egress.mjs'
3
2
 
4
- const ROOT = path.resolve(new URL('../..', import.meta.url).pathname)
5
- const SCAN_DIRS = ['bin', 'src']
6
- const FORBIDDEN = [
7
- { pattern: /fetch\(\s*['"]https?:\/\/(?!127\.0\.0\.1|localhost|\[::1\])/, label: 'non-localhost fetch' },
8
- { pattern: /https?\.request\s*\(/, label: 'http request primitive' },
9
- { pattern: /new\s+WebSocket\s*\(/, label: 'websocket egress primitive' },
10
- { pattern: /net\.connect\s*\(/, label: 'net connect primitive' },
11
- { pattern: /dns\./, label: 'dns primitive' },
12
- { pattern: /\bcurl\s+https?:\/\//, label: 'shell network call' },
13
- ]
14
-
15
- function walk(dir, files = []) {
16
- if (!fs.existsSync(dir)) return files
17
- for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
18
- const abs = path.join(dir, ent.name)
19
- if (ent.isDirectory()) walk(abs, files)
20
- else if (/\.(mjs|js|sh)$/.test(ent.name)) files.push(abs)
21
- }
22
- return files
23
- }
24
-
25
- export function checkForbiddenEgress(root = ROOT) {
26
- const failures = []
27
- for (const rel of SCAN_DIRS) {
28
- for (const file of walk(path.join(root, rel))) {
29
- const text = fs.readFileSync(file, 'utf8')
30
- for (const rule of FORBIDDEN) {
31
- if (rule.pattern.test(text)) failures.push(`${path.relative(root, file)}:${rule.label}`)
32
- }
33
- }
34
- }
35
- return failures
36
- }
3
+ export { checkForbiddenEgress } from './forbidden-egress.mjs'
37
4
 
38
5
  export function runEgressCommand() {
39
- const failures = checkForbiddenEgress()
40
- if (failures.length) {
41
- console.error(failures.join('\n'))
6
+ const findings = checkForbiddenEgress()
7
+ if (findings.length) {
8
+ for (const finding of findings) {
9
+ console.error(`[atelier-egress] ${finding.file}:${finding.line} ${finding.type}: ${finding.detail}`)
10
+ }
42
11
  process.exit(1)
43
12
  }
44
13
  console.log('[egress:check] no forbidden non-localhost egress found in package runtime paths')