@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
@@ -0,0 +1,122 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../../project/private-state.mjs'
4
+ import { canonicalJson, closedObject, compareText } from '../../../runtime/obsidian/documents.mjs'
5
+ import { refuse } from '../../../runtime/obsidian/errors.mjs'
6
+ import { assertOutsideRepositories } from '../../../runtime/obsidian/machine-settings.mjs'
7
+ import { SCOPE_MODES, assertObsidianContract } from '../contracts.mjs'
8
+ import { SETTINGS_ROOT } from '../materialize/settings.mjs'
9
+ import { FOCUS_BOOKMARK_TYPE, FOCUS_QUERY_VERSION } from './focus.mjs'
10
+ import { SELECTION_SCHEMA } from './selection.mjs'
11
+
12
+ // Where a selector is kept: Atelier's own private state of the workspace,
13
+ //
14
+ // <data>/obsidian/<workspace-id>/state/selection/<scope-id>.json
15
+ //
16
+ // beside the maintenance and settings documents AOP-1 keeps there, through
17
+ // the same private-state primitives (owner-only, atomic replace, no symlink
18
+ // following) and the same guard that keeps private state outside every
19
+ // enrolled repository. It is not under any vault, and it is never one of the
20
+ // files the app's UI owns under `.obsidian/`.
21
+
22
+ export const SELECTION_STATE_SCHEMA = 'atelier-obsidian-selection-state/v1'
23
+ export const SELECTION_DIRECTORY = path.join('state', 'selection')
24
+
25
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
26
+ const DIGEST = /^sha256:[0-9a-f]{64}$/
27
+ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/
28
+ const segment = (identifier) => identifier.replaceAll(':', '_')
29
+
30
+ export function validateSelectionState(document, workspaceId) {
31
+ const code = 'invalid-selection-state'
32
+ closedObject(document, { required: ['schema', 'workspaceId', 'scopeId', 'mode', 'selector', 'expansion', 'focus', 'updatedAt'] }, code, 'the selection state')
33
+ if (document.schema !== SELECTION_STATE_SCHEMA) refuse(code, 'the selection state names an unknown schema')
34
+ if (document.workspaceId !== workspaceId) refuse(code, 'the selection state belongs to another workspace')
35
+ if (typeof document.scopeId !== 'string' || !IDENTIFIER.test(document.scopeId)) refuse(code, 'the selection state names no scope')
36
+ if (!SCOPE_MODES.includes(document.mode)) refuse(code, 'the selection state carries an unknown mode')
37
+ // The selector and the expansion are exactly a scope document's: the contract decides whether they are well formed.
38
+ assertObsidianContract('scope', { schema: 'atelier-obsidian-scope/v1', scopeId: document.scopeId, mode: document.mode, selector: document.selector, ...(document.expansion === null ? {} : { expansion: document.expansion }) })
39
+ if (document.focus !== null) {
40
+ closedObject(document.focus, { required: ['version', 'query', 'queryDigest', 'paths', 'bookmark'] }, code, 'the persisted focus')
41
+ if (document.focus.version !== FOCUS_QUERY_VERSION || typeof document.focus.query !== 'string' || document.focus.query === '' || !DIGEST.test(String(document.focus.queryDigest))) refuse(code, 'the persisted focus is malformed')
42
+ if (!Array.isArray(document.focus.paths) || document.focus.paths.length === 0 || document.focus.paths.some((item) => typeof item !== 'string')) refuse(code, 'the persisted focus names no path')
43
+ closedObject(document.focus.bookmark, { required: ['type', 'title', 'options'] }, code, 'the persisted bookmark payload')
44
+ if (document.focus.bookmark.type !== FOCUS_BOOKMARK_TYPE) refuse(code, 'the persisted bookmark payload is not a graph bookmark')
45
+ }
46
+ if (document.mode === 'focus' ? document.focus === null : document.focus !== null) refuse(code, 'a focus is persisted exactly for a focus scope')
47
+ if (typeof document.updatedAt !== 'string' || !TIMESTAMP.test(document.updatedAt)) refuse(code, 'updatedAt must be a UTC timestamp')
48
+ return document
49
+ }
50
+
51
+ // A path Atelier may write: under the workspace's private state and never under a vault's `.obsidian/`.
52
+ export function assertWritableSelectionPath({ workspaceRoot, file }) {
53
+ const relative = path.relative(workspaceRoot, file)
54
+ if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) refuse('selection-state-outside-workspace', 'the selection state is written under the workspace private state only')
55
+ const parts = relative.split(path.sep)
56
+ if (parts.includes(SETTINGS_ROOT) || parts[0] === 'vaults') refuse('ui-owned-file-refused', 'Atelier never writes a vault or a file under .obsidian/; the app owns those')
57
+ if (parts.length !== 3 || parts[0] !== 'state' || parts[1] !== 'selection') refuse('selection-state-outside-workspace', 'the selection state lives in state/selection')
58
+ return file
59
+ }
60
+
61
+ export function selectionStateDocument({ workspaceId, selection, now }) {
62
+ if (selection?.schema !== SELECTION_SCHEMA) refuse('invalid-selection', 'a resolved selection is persisted, nothing else')
63
+ return {
64
+ schema: SELECTION_STATE_SCHEMA,
65
+ workspaceId,
66
+ scopeId: selection.scope.scopeId,
67
+ mode: selection.scope.mode,
68
+ selector: selection.scope.selector,
69
+ expansion: selection.scope.expansion ?? null,
70
+ focus: selection.focus === null ? null : { version: selection.focus.version, query: selection.focus.query, queryDigest: selection.focus.queryDigest, paths: selection.focus.paths, bookmark: selection.focus.bookmark },
71
+ updatedAt: now,
72
+ }
73
+ }
74
+
75
+ const selectionFile = (workspaceRoot, scopeId) => path.join(workspaceRoot, SELECTION_DIRECTORY, `${segment(scopeId)}.json`)
76
+
77
+ // Writes only when the bytes change. Returns { file, changed, document }.
78
+ export function writeSelectionState({ workspaceRoot, workspaceId, repositoryRoots, selection, now }) {
79
+ const document = validateSelectionState(selectionStateDocument({ workspaceId, selection, now }), workspaceId)
80
+ assertOutsideRepositories({ managedRoot: workspaceRoot, repositoryRoots })
81
+ const file = assertWritableSelectionPath({ workspaceRoot, file: selectionFile(workspaceRoot, document.scopeId) })
82
+ const text = canonicalJson(document)
83
+ let existing = null
84
+ try { existing = readRegularTextNoFollow(file) } catch (error) { if (error.code !== 'ENOENT') throw error }
85
+ // A rewrite that changes only the time is not a change to the selection.
86
+ if (existing !== null) {
87
+ let previous = null
88
+ try { previous = JSON.parse(existing) } catch { previous = null }
89
+ if (previous !== null && canonicalJson({ ...previous, updatedAt: document.updatedAt }) === text) return { file, changed: false, document: previous }
90
+ }
91
+ fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 })
92
+ const directory = ensureContainedPrivateDirectory({ workspaceRoot, directory: path.join(workspaceRoot, SELECTION_DIRECTORY), label: 'Obsidian selection state' })
93
+ atomicReplacePrivateText(path.join(directory, path.basename(file)), text)
94
+ return { file, changed: true, document }
95
+ }
96
+
97
+ export function readSelectionState({ workspaceRoot, workspaceId, scopeId }) {
98
+ if (typeof scopeId !== 'string' || !IDENTIFIER.test(scopeId)) refuse('invalid-scope', 'name the scope whose selection to read')
99
+ let text
100
+ try { text = readRegularTextNoFollow(selectionFile(workspaceRoot, scopeId)) } catch (error) {
101
+ if (error.code === 'ENOENT') return null
102
+ refuse('invalid-selection-state', 'the selection state cannot be read', { cause: error.code ?? String(error.message) })
103
+ }
104
+ let document
105
+ try { document = JSON.parse(text) } catch { refuse('invalid-selection-state', 'the selection state is not JSON') }
106
+ const validated = validateSelectionState(document, workspaceId)
107
+ if (validated.scopeId !== scopeId) refuse('invalid-selection-state', 'the selection state names another scope')
108
+ return validated
109
+ }
110
+
111
+ export function listSelectionStates({ workspaceRoot, workspaceId }) {
112
+ let names
113
+ try { names = fs.readdirSync(path.join(workspaceRoot, SELECTION_DIRECTORY)) } catch (error) { if (error.code === 'ENOENT') return []; throw error }
114
+ const documents = []
115
+ for (const name of names.filter((item) => item.endsWith('.json')).sort(compareText)) {
116
+ const text = readRegularTextNoFollow(path.join(workspaceRoot, SELECTION_DIRECTORY, name))
117
+ let document
118
+ try { document = JSON.parse(text) } catch { refuse('invalid-selection-state', 'a selection state is not JSON', { file: name }) }
119
+ documents.push(validateSelectionState(document, workspaceId))
120
+ }
121
+ return documents.sort((left, right) => compareText(left.scopeId, right.scopeId))
122
+ }
@@ -0,0 +1,109 @@
1
+ import { refuse } from '../../../runtime/obsidian/errors.mjs'
2
+ import { EXPANSION_DIRECTIONS, EXPANSION_ORDERS, MAX_EXPANSION_DEPTH, ObsidianContractRefusal, SCOPE_MODES, assertObsidianContract, selectScope } from '../contracts.mjs'
3
+ import { allocateWorkspacePaths } from '../materialize/path-registry.mjs'
4
+ import { buildFocusQuery, focusBookmarkPayload } from './focus.mjs'
5
+
6
+ // Selection: a requested scope (full, scoped or focus, with an optional
7
+ // bounded expansion) becomes an exact scope document, the exact selected set
8
+ // and, for a focus, the graph query and bookmark payload derived from it.
9
+ //
10
+ // Selection is the contract's selectScope, unchanged: this module adds no
11
+ // selector, no default and no fallback. An empty result is reported as
12
+ // empty, or refused when the caller did not say empty is acceptable. It is
13
+ // never widened. The note paths are the ones the workspace path registry
14
+ // allocates, so a focus names the files a full vault of this workspace holds.
15
+
16
+ export const SELECTION_SCHEMA = 'atelier-obsidian-selection/v1'
17
+ export const SCOPE_CONTRACT_SCHEMA = 'atelier-obsidian-scope/v1'
18
+ export const EMPTY_SELECTION_REASONS = Object.freeze(['explicit-empty', 'no-visible-members'])
19
+
20
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
21
+
22
+ // The exact document: the caller's members, the two expansion defaults the
23
+ // contract allows filled in, and nothing else. A budget is never defaulted.
24
+ export function scopeDocumentOf({ scopeId, mode, selector, expansion, ext } = {}) {
25
+ if (typeof scopeId !== 'string' || scopeId === '') refuse('invalid-scope', 'a scope names itself with a scopeId')
26
+ if (!SCOPE_MODES.includes(mode)) refuse('invalid-scope', 'scope mode must be full, focus or scoped', { mode: String(mode).slice(0, 32) })
27
+ if (!isPlainObject(selector)) refuse('invalid-scope', 'a scope carries a selector object')
28
+ let normalizedExpansion
29
+ if (expansion !== undefined && expansion !== null) {
30
+ if (!isPlainObject(expansion)) refuse('invalid-expansion', 'expansion must be an object')
31
+ if (expansion.depth === undefined || expansion.maxNodes === undefined) refuse('missing-expansion-budget', 'expansion requires both a depth and a maxNodes budget; neither is defaulted')
32
+ // The budgets are checked here, before the contract, so a bad budget is an expansion fault and not a generic scope fault.
33
+ if (!Number.isInteger(expansion.depth) || expansion.depth < 1 || expansion.depth > MAX_EXPANSION_DEPTH) refuse('invalid-expansion', `depth must be an integer from 1 to ${MAX_EXPANSION_DEPTH}`)
34
+ if (!Number.isInteger(expansion.maxNodes) || expansion.maxNodes < 1) refuse('invalid-expansion', 'maxNodes must be a positive integer')
35
+ const direction = expansion.direction ?? 'outgoing'
36
+ const order = expansion.order ?? 'canonical-id'
37
+ if (!EXPANSION_DIRECTIONS.includes(direction)) refuse('invalid-expansion', 'unknown expansion direction')
38
+ if (!EXPANSION_ORDERS.includes(order)) refuse('invalid-expansion', 'unknown expansion order')
39
+ normalizedExpansion = { depth: expansion.depth, maxNodes: expansion.maxNodes, direction, order, ...(expansion.ext === undefined ? {} : { ext: expansion.ext }) }
40
+ }
41
+ const document = {
42
+ schema: SCOPE_CONTRACT_SCHEMA,
43
+ scopeId,
44
+ mode,
45
+ selector,
46
+ ...(normalizedExpansion === undefined ? {} : { expansion: normalizedExpansion }),
47
+ ...(ext === undefined ? {} : { ext }),
48
+ }
49
+ try { assertObsidianContract('scope', document) } catch (error) {
50
+ if (error instanceof ObsidianContractRefusal) refuse('invalid-scope', 'the requested scope does not satisfy the scope contract', { errors: error.detail?.errors ?? [] })
51
+ throw error
52
+ }
53
+ return document
54
+ }
55
+
56
+ function contractRefusalToTyped(error) {
57
+ if (error instanceof ObsidianContractRefusal) refuse(error.code, error.message.replace(`${error.code}: `, ''), error.detail ?? {})
58
+ throw error
59
+ }
60
+
61
+ // `pathRegistry` is the workspace's persisted path registry when the caller
62
+ // has one (AOP-1 keeps it in the maintenance state store); without it the
63
+ // paths are those a fresh workspace would allocate. Both are deterministic.
64
+ export function resolveSelection({ canonicalSnapshot, profile, scope, allowEmpty = false, pathRegistry = null, select = selectScope } = {}) {
65
+ const document = scopeDocumentOf(scope)
66
+ let result
67
+ try {
68
+ result = select({ canonicalSnapshot, profile, selector: document.selector, expansion: document.expansion, mode: document.mode })
69
+ } catch (error) { contractRefusalToTyped(error) }
70
+
71
+ const empty = result.nodes.length === 0
72
+ const emptyReason = empty ? (result.diagnostics.includes('selection-has-no-visible-members') ? 'no-visible-members' : 'explicit-empty') : null
73
+ if (empty && document.mode === 'focus') {
74
+ // A graph filter naming no note would show the whole vault or nothing, depending on the app. Neither is the request.
75
+ refuse('focus-selection-empty', 'the focus selects no visible note; a focus is never widened', { reason: emptyReason, unresolvedIds: result.unresolvedIds })
76
+ }
77
+ if (empty && allowEmpty !== true) refuse('selection-empty', 'the selection has no visible member; pass allowEmpty to produce an honest empty scope', { reason: emptyReason, unresolvedIds: result.unresolvedIds })
78
+
79
+ const workspaceId = typeof profile?.workspaceId === 'string' ? profile.workspaceId : 'ws-unnamed'
80
+ const nodeById = new Map(canonicalSnapshot.nodes.map((node) => [node.id, node]))
81
+ const vaultNodes = result.vaultNodes.map((id) => nodeById.get(id))
82
+ let pathOf
83
+ try { ({ pathOf } = allocateWorkspacePaths({ registry: pathRegistry, workspaceId, nodes: vaultNodes })) } catch (error) { contractRefusalToTyped(error) }
84
+ const notePaths = Object.fromEntries(result.nodes.map((id) => [id, pathOf(nodeById.get(id).repo, id)]))
85
+
86
+ let focus = null
87
+ if (document.mode === 'focus') {
88
+ const built = buildFocusQuery(result.nodes.map((id) => notePaths[id]))
89
+ focus = { ...built, bookmark: focusBookmarkPayload({ scopeId: document.scopeId, query: built.query }) }
90
+ }
91
+
92
+ return {
93
+ schema: SELECTION_SCHEMA,
94
+ scope: document,
95
+ mode: document.mode,
96
+ nodes: result.nodes,
97
+ edges: result.edges,
98
+ outsideSelectionEdges: result.outsideSelectionEdges,
99
+ vaultNodes: result.vaultNodes,
100
+ vaultEdges: result.vaultEdges,
101
+ truncated: result.truncated,
102
+ unresolvedIds: result.unresolvedIds,
103
+ diagnostics: result.diagnostics,
104
+ empty,
105
+ emptyReason,
106
+ notePaths,
107
+ focus,
108
+ }
109
+ }
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs'
2
+ import { isDeepStrictEqual } from 'node:util'
2
3
  import path from 'node:path'
3
4
  import { commandProject, readJson, writeJson } from '../project/config.mjs'
4
5
  import { summarizeReadinessJourney } from '../readiness-protocols/runtime.mjs'
@@ -50,7 +51,7 @@ export function buildProjectProjection(project) {
50
51
  const dimensionCards = readinessJourney.dimensions.map((item) => `<article class="readiness-card" data-protocol="${esc(item.protocolId)}">
51
52
  <p class="eyebrow">${esc(item.label)}</p>
52
53
  <h3>${esc(item.title)}</h3>
53
- <p><strong>${esc(item.status)}</strong> · ${esc(item.score)} / 100</p>
54
+ <p><strong>${esc(item.status)}</strong> · ${esc(item.score)} / 100 answers complete</p>
54
55
  <p>${esc(item.blockers[0] || 'review-ready')}</p>
55
56
  <button type="button" data-copy="${esc(item.agentPrompt)}">Copy protocol prompt</button>
56
57
  </article>`).join('\n')
@@ -93,6 +94,7 @@ ${branding.themeCss}</style>
93
94
  <div>
94
95
  <p class="eyebrow">MNSTRY Tenant Readiness</p>
95
96
  <h2 id="tenant-readiness-title">Readiness Journey</h2>
97
+ <p>Scores measure answer completeness, not evidence confidence, approval or runtime readiness.</p>
96
98
  <p>${esc(readinessJourney.dimensions.length)} dimensions · next protocol: ${esc(readinessJourney.nextProtocol || 'none')}</p>
97
99
  </div>
98
100
  <div class="readiness-score">${esc(readinessJourney.score)}%</div>
@@ -117,36 +119,43 @@ document.addEventListener('click', async (event) => {
117
119
  return { output, html, graph, readinessJourney }
118
120
  }
119
121
 
122
+ export function buildProjectManifest(project, projection) {
123
+ return {
124
+ schema: 'mnstry.atelier-manifest@v1',
125
+ generatedAt: 'deterministic',
126
+ graphPath: path.relative(project.outputRoot, project.graphPath).split(path.sep).join('/'),
127
+ entry: 'index.html',
128
+ tenantReadiness: {
129
+ score: projection.readinessJourney.score,
130
+ ready: projection.readinessJourney.ready,
131
+ dimensions: projection.readinessJourney.dimensions.map((item) => ({
132
+ key: item.key,
133
+ protocolId: item.protocolId,
134
+ status: item.status,
135
+ score: item.score,
136
+ })),
137
+ },
138
+ nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
139
+ }
140
+ }
141
+
120
142
  export function runProjectCommand(argv = process.argv.slice(2)) {
121
143
  const check = argv.includes('--check')
122
144
  const project = commandProject({ argv })
123
145
  const projection = buildProjectProjection(project)
124
146
  if (check) {
125
147
  const current = fs.existsSync(projection.output) ? fs.readFileSync(projection.output, 'utf8') : null
126
- if (current !== projection.html) {
148
+ const manifestPath = path.join(project.outputRoot, 'atelier.manifest.json')
149
+ let manifest = null
150
+ try { manifest = readJson(manifestPath) } catch { /* Missing or invalid artifacts are stale. */ }
151
+ if (current !== projection.html || !isDeepStrictEqual(manifest, JSON.parse(JSON.stringify(buildProjectManifest(project, projection))))) {
127
152
  console.error(`project projection is stale: ${projection.output}`)
128
153
  process.exit(1)
129
154
  }
130
155
  } else {
131
156
  fs.mkdirSync(path.dirname(projection.output), { recursive: true })
132
157
  fs.writeFileSync(projection.output, projection.html)
133
- writeJson(path.join(project.outputRoot, 'atelier.manifest.json'), {
134
- schema: 'mnstry.atelier-manifest@v1',
135
- generatedAt: 'deterministic',
136
- graphPath: project.graphPath,
137
- entry: 'index.html',
138
- tenantReadiness: {
139
- score: projection.readinessJourney.score,
140
- ready: projection.readinessJourney.ready,
141
- dimensions: projection.readinessJourney.dimensions.map((item) => ({
142
- key: item.key,
143
- protocolId: item.protocolId,
144
- status: item.status,
145
- score: item.score,
146
- })),
147
- },
148
- nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
149
- })
158
+ writeJson(path.join(project.outputRoot, 'atelier.manifest.json'), buildProjectManifest(project, projection))
150
159
  }
151
160
  console.log(`project projection: ${projection.graph.counts.nodes} nodes -> ${projection.output}`)
152
161
  }
@@ -0,0 +1,282 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { canonicalize } from '../attestation/jcs.mjs'
6
+ import { buildGraph } from '../graph/graph.mjs'
7
+ import {
8
+ loadExtensionPacks,
9
+ createProtocolRegistry,
10
+ } from '../extension-packs/loader.mjs'
11
+ import { readRegularTextNoFollow } from '../project/private-state.mjs'
12
+ import { createCollaborationEventLedger } from '../collaboration/event-ledger.mjs'
13
+ import { inspectPackLifecycle } from '../extension-packs/lifecycle.mjs'
14
+ import { readLocalOverlay } from '../project/config.mjs'
15
+ import { readBoundedSource } from './source-read.mjs'
16
+ export { readBoundedSource } from './source-read.mjs'
17
+ import { validReviewArtifact } from '../collaboration/review-contracts.mjs'
18
+ import { buildReadinessRun, answerForField } from './runtime.mjs'
19
+
20
+ export const hashEvidence = (value) =>
21
+ `sha256:${crypto
22
+ .createHash('sha256')
23
+ .update(canonicalize(value))
24
+ .digest('hex')}`
25
+ const hashBytes = (value) =>
26
+ `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
27
+ const packageRoot = fileURLToPath(new URL('../../', import.meta.url))
28
+ function evaluatorInventory() {
29
+ const files = ['package.json']
30
+ function visit(relative) {
31
+ for (const name of fs
32
+ .readdirSync(path.join(packageRoot, relative))
33
+ .sort()) {
34
+ const next = path.posix.join(relative, name),
35
+ stat = fs.lstatSync(path.join(packageRoot, next))
36
+ if (stat.isSymbolicLink()) throw new Error('redirected evaluator input')
37
+ if (stat.isDirectory()) visit(next)
38
+ else if (stat.isFile() && /\.(mjs|json)$/.test(name)) files.push(next)
39
+ }
40
+ }
41
+ visit('src')
42
+ visit('contracts')
43
+ return files
44
+ .sort()
45
+ .map((file) => ({
46
+ file,
47
+ digest: hashBytes(readRegularTextNoFollow(path.join(packageRoot, file))),
48
+ }))
49
+ }
50
+ const evaluator = hashEvidence(evaluatorInventory())
51
+
52
+ function currentInputs(project, protocolId, answers) {
53
+ if (
54
+ project.configPath &&
55
+ hashEvidence(JSON.parse(readRegularTextNoFollow(project.configPath))) !==
56
+ hashEvidence(project.config)
57
+ )
58
+ throw new Error('project config changed; resolve the project again')
59
+ if (hashEvidence(evaluatorInventory()) !== evaluator)
60
+ throw new Error('evaluator changed; restart before review')
61
+ if (
62
+ project.localOverlay &&
63
+ hashEvidence(readLocalOverlay({ configDir: project.configDir }).overlay) !==
64
+ hashEvidence(project.localOverlay.overlay)
65
+ )
66
+ throw new Error('local overlay changed; resolve again')
67
+ const loaded = loadExtensionPacks(project)
68
+ const packs = loaded.packs ?? loaded
69
+ const lifecycle = inspectPackLifecycle(project, packs)
70
+ if (!lifecycle.ok) throw new Error(lifecycle.errors.join('; '))
71
+ const registry = createProtocolRegistry({ packs })
72
+ const protocol = registry.protocolById(protocolId)
73
+ if (!protocol) throw new Error('protocol unavailable for evidence binding')
74
+ const graph = buildGraph(project)
75
+ if (graph.errors.length)
76
+ throw new Error('source graph is invalid; evidence capture refused')
77
+ if (graph.nodes.length > 256)
78
+ throw new Error('review evidence exceeds bounded source count')
79
+ const sources = [],
80
+ excerpts = new Map()
81
+ for (const node of graph.nodes) {
82
+ const repo = project.repos.find(
83
+ (entry) => entry.name === node.repo && !entry.external,
84
+ )
85
+ if (!repo?.path) throw new Error('source repository unavailable')
86
+ const source = readBoundedSource(repo.path, node.path)
87
+ const metadata = !/\.md$/i.test(node.path)
88
+ ? readBoundedSource(repo.path, `${node.path}.kg.json`).digest
89
+ : null
90
+ excerpts.set(node.id, source.text.slice(0, 2000))
91
+ sources.push({
92
+ id: node.id,
93
+ repo: node.repo,
94
+ path: node.path,
95
+ audience: node.audience,
96
+ digest: source.digest,
97
+ metadata,
98
+ })
99
+ }
100
+ sources.sort((a, b) =>
101
+ `${a.repo}/${a.path}`.localeCompare(`${b.repo}/${b.path}`, 'en'),
102
+ )
103
+ const claimEvidence = protocol.claimMappings.map((mapping) => {
104
+ const raw = answerForField(answers, mapping.evidence)
105
+ const references = (Array.isArray(raw) ? raw.flat(Infinity) : [raw])
106
+ .filter((value) => typeof value === 'string' && value.trim())
107
+ .map((value) => value.trim())
108
+ const resolved = [],
109
+ unresolved = []
110
+ for (const reference of references) {
111
+ const source = sources.find((item) => item.id === reference)
112
+ if (source)
113
+ resolved.push({
114
+ id: source.id,
115
+ repo: source.repo,
116
+ path: source.path,
117
+ digest: source.digest,
118
+ excerpt: excerpts.get(source.id),
119
+ })
120
+ else unresolved.push(reference)
121
+ }
122
+ return {
123
+ mappingId: mapping.id,
124
+ sourceFields: mapping.sourceFields,
125
+ answers: Object.fromEntries(
126
+ mapping.sourceFields.map((field) => [
127
+ field,
128
+ answerForField(answers, field) ?? null,
129
+ ]),
130
+ ),
131
+ evidenceField: mapping.evidence,
132
+ references,
133
+ resolved,
134
+ unresolved,
135
+ status: !references.length
136
+ ? 'missing'
137
+ : unresolved.length
138
+ ? 'unresolved'
139
+ : 'linked-source',
140
+ }
141
+ })
142
+ const readPolicy = (file) =>
143
+ fs.existsSync(file) ? hashBytes(readRegularTextNoFollow(file)) : null
144
+ return {
145
+ schema: 'atelier-review-evidence@v1',
146
+ protocolId,
147
+ protocol,
148
+ protocolDigest: hashEvidence(protocol),
149
+ answers,
150
+ answersDigest: hashEvidence(answers),
151
+ configDigest: hashEvidence(project.config),
152
+ lifecycleDigest: hashEvidence(lifecycle),
153
+ policyDigest: hashEvidence({
154
+ access: readPolicy(project.repoAccessPath),
155
+ boundary: readPolicy(project.boundaryPolicyPath),
156
+ }),
157
+ packs: packs
158
+ .map(({ id, version, digest }) => ({ id, version, digest }))
159
+ .sort((a, b) => a.id.localeCompare(b.id, 'en')),
160
+ evaluator: { id: 'answer-completeness/v1', digest: evaluator },
161
+ sources,
162
+ claimEvidence,
163
+ limitations: [
164
+ 'Answers are operator assertions; source presence does not substantiate each claim.',
165
+ 'No calibrated evidence confidence or runtime acceptance is established.',
166
+ ],
167
+ }
168
+ }
169
+
170
+ export function captureReviewEvidence(project, protocolId, answers) {
171
+ const first = currentInputs(project, protocolId, answers)
172
+ const second = currentInputs(project, protocolId, answers)
173
+ const digest = hashEvidence(first)
174
+ if (digest !== hashEvidence(second))
175
+ throw new Error('review inputs changed during capture')
176
+ const snapshot = { ...first, digest }
177
+ if (!validReviewArtifact('evidence', snapshot))
178
+ throw new Error('evidence contract invalid or exceeds bounds')
179
+ return snapshot
180
+ }
181
+
182
+ export function evidenceLedger(project) {
183
+ return createCollaborationEventLedger({
184
+ workspaceRoot: project.configDir,
185
+ ledgerPath: path.join(
186
+ project.configDir,
187
+ '.atelier-local/review/evidence.ndjson',
188
+ ),
189
+ })
190
+ }
191
+
192
+ export function recordReviewEvidence(project, run) {
193
+ const snapshot = captureReviewEvidence(project, run.protocolId, run.answers)
194
+ if (
195
+ hashEvidence(
196
+ buildReadinessRun({
197
+ project,
198
+ protocol: snapshot.protocol,
199
+ answers: run.answers,
200
+ createdAt: run.createdAt,
201
+ }),
202
+ ) !== hashEvidence(run)
203
+ )
204
+ throw new Error('run no longer matches its current protocol and evaluator')
205
+ const payload = {
206
+ schema: 'atelier-bound-run@v1',
207
+ run,
208
+ runDigest: hashEvidence(run),
209
+ snapshot,
210
+ }
211
+ if (!validBoundRun(payload)) throw new Error('bound run contract invalid')
212
+ const ledger = evidenceLedger(project)
213
+ const existing = ledger.eventsFor(run.runId)
214
+ if (!existing.ok) throw new Error(existing.error)
215
+ if (existing.events.length) {
216
+ if (hashEvidence(existing.events[0].payload) !== hashEvidence(payload))
217
+ throw new Error('run identity already has different evidence')
218
+ return existing.events[0].payload
219
+ }
220
+ const result = ledger.append({
221
+ aggregateId: run.runId,
222
+ expectedVersion: 0,
223
+ type: 'run-bound',
224
+ actor: 'local-runtime',
225
+ payload,
226
+ })
227
+ if (!result.ok) throw new Error(result.error)
228
+ return payload
229
+ }
230
+
231
+ export function validBoundRun(payload) {
232
+ if (!validReviewArtifact('bound', payload)) return false
233
+ const { digest, ...body } = payload.snapshot
234
+ const { run, snapshot } = payload
235
+ return (
236
+ payload.runDigest === hashEvidence(run) &&
237
+ digest === hashEvidence(body) &&
238
+ snapshot.protocolDigest === hashEvidence(snapshot.protocol) &&
239
+ snapshot.answersDigest === hashEvidence(snapshot.answers) &&
240
+ snapshot.answersDigest === hashEvidence(run.answers) &&
241
+ snapshot.protocol.id === run.protocolId &&
242
+ snapshot.protocolId === run.protocolId &&
243
+ snapshot.claimEvidence.length === run.claims.length &&
244
+ snapshot.claimEvidence.every(
245
+ (entry, index) =>
246
+ entry.mappingId === snapshot.protocol.claimMappings[index]?.id,
247
+ )
248
+ )
249
+ }
250
+
251
+ export function loadBoundRun(project, runId) {
252
+ const result = evidenceLedger(project).eventsFor(runId)
253
+ if (!result.ok) return result
254
+ if (result.events.length !== 1 || result.events[0].type !== 'run-bound')
255
+ return {
256
+ ok: false,
257
+ status: 409,
258
+ error: 'run lacks immutable review evidence',
259
+ }
260
+ const payload = result.events[0].payload
261
+ const { digest, ...body } = payload.snapshot ?? {}
262
+ if (
263
+ !validBoundRun(payload) ||
264
+ payload.schema !== 'atelier-bound-run@v1' ||
265
+ payload.run?.runId !== runId ||
266
+ payload.runDigest !== hashEvidence(payload.run) ||
267
+ digest !== hashEvidence(body)
268
+ )
269
+ return { ok: false, status: 422, error: 'run evidence is invalid' }
270
+ return { ok: true, status: 200, ...payload }
271
+ }
272
+
273
+ export function currentRunEligibility(project, bound) {
274
+ try {
275
+ return (
276
+ captureReviewEvidence(project, bound.run.protocolId, bound.run.answers)
277
+ .digest === bound.snapshot.digest
278
+ )
279
+ } catch {
280
+ return false
281
+ }
282
+ }
@@ -73,7 +73,7 @@ function nestedAnswer(value, parts) {
73
73
  return nestedAnswer(value[head], rest)
74
74
  }
75
75
 
76
- function answerForField(answers, fieldId) {
76
+ export function answerForField(answers, fieldId) {
77
77
  for (const key of pathVariants(fieldId)) {
78
78
  if (Object.hasOwn(answers, key)) return answers[key]
79
79
  }
@@ -119,9 +119,8 @@ export function claimsForProtocolRun({ protocol, answers, projectName, runId, cr
119
119
  provider: 'atelier-readiness',
120
120
  status: 'proposed',
121
121
  promoted: false,
122
- confidence: Object.values(answers).some(answerPresent) ? 0.7 : 0.2,
123
122
  evidence: [`readiness-run:${runId}`],
124
- notes: [`Proposed ${mapping.id} from ${protocol.title}.`],
123
+ notes: [`Proposed ${mapping.id} from ${protocol.title}.`, 'Answer presence is not evidence confidence; human judgment is required.'],
125
124
  createdAt,
126
125
  }
127
126
  })
@@ -147,6 +146,7 @@ export function buildReadinessRun({ project, protocol, answers = {}, createdAt =
147
146
  claims: claimsForProtocolRun({ protocol, answers: normalizedAnswers, projectName, runId, createdAt }),
148
147
  safety: {
149
148
  runtimeMutation: false,
149
+ runtimeImport: false,
150
150
  canonicalWrites: false,
151
151
  claimOnly: true,
152
152
  storage: 'ignored-local',
@@ -367,6 +367,7 @@ export function buildTenantPacket(project, { runs = listProtocolRuns(project), r
367
367
  ext: {
368
368
  'mnstry.atelier': {
369
369
  extensionPacks: packContribution,
370
+ scoreMeaning: 'Legacy readinessScore measures completed input dimensions; it is not evidence confidence or runtime admission.',
370
371
  },
371
372
  },
372
373
  }
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+ import { openRegularFileNoFollow } from '../project/private-state.mjs'
5
+ const hashBytes = (value) =>
6
+ `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
7
+
8
+ export function readBoundedSource(root, relative) {
9
+ if (
10
+ typeof relative !== 'string' ||
11
+ path.isAbsolute(relative) ||
12
+ relative.split(/[\\/]/).some((part) => part === '..' || part === '.git')
13
+ )
14
+ throw new Error('source reference escapes declared boundary')
15
+ const realRoot = fs.realpathSync(root)
16
+ const file = path.resolve(realRoot, relative)
17
+ let cursor = realRoot
18
+ for (const part of path.relative(realRoot, file).split(path.sep)) {
19
+ cursor = path.join(cursor, part)
20
+ if (fs.lstatSync(cursor).isSymbolicLink())
21
+ throw new Error('redirected source reference refused')
22
+ }
23
+ const descriptor = openRegularFileNoFollow(file)
24
+ try {
25
+ const before = fs.fstatSync(descriptor)
26
+ if (before.size > 1024 * 1024)
27
+ throw new Error('source exceeds review byte bound')
28
+ const bytes = fs.readFileSync(descriptor)
29
+ const after = fs.fstatSync(descriptor)
30
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
31
+ throw new Error('source changed during read')
32
+ return {
33
+ digest: hashBytes(bytes),
34
+ text: new TextDecoder('utf-8', { fatal: true }).decode(bytes),
35
+ }
36
+ } finally {
37
+ fs.closeSync(descriptor)
38
+ }
39
+ }