@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,129 @@
1
+ import { refuse } from './errors.mjs'
2
+
3
+ // Whether the installed Obsidian may be published through and opened.
4
+ //
5
+ // Nothing here looks at this machine. What is installed is learned through an
6
+ // injected `appProbe`:
7
+ //
8
+ // appProbe.inspect() -> { installed, cli, running, version, noVaultOpen? }
9
+ // appProbe.vaultState({ vaultRoot }) -> { answered, indexReady }
10
+ //
11
+ // `installed` and `cli` are booleans, `running` is true, false or null (not
12
+ // known) and `version` is the app's own version text or null. `noVaultOpen`
13
+ // is true when the command-line tool answered that no vault is open. The
14
+ // production probe is a separate module that only the real command-line
15
+ // entries load; every test passes its own.
16
+
17
+ // The publication protocol sets the view's undocumented `lastSavedData` field.
18
+ // 1.13.7 is the only app version that protocol was proven on, so it is the
19
+ // floor. Raising the ceiling needs the protocol cases re-run on that release.
20
+ export const MINIMUM_APP_VERSION = '1.13.7'
21
+
22
+ export const APP_OUTCOMES = Object.freeze(['qualified', 'app-missing', 'app-cli-unavailable', 'app-version-unsupported'])
23
+
24
+ const VERSION = /^v?(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})(?:-([0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*))?(?:\+[0-9A-Za-z.-]+)?(?=$|[\s(])/
25
+
26
+ // { major, minor, patch, prerelease: string[] } or null. Only a version at the
27
+ // very start of the text counts; anything else is not a version.
28
+ export function parseAppVersion(text) {
29
+ if (typeof text !== 'string' || text.length > 200) return null
30
+ const match = VERSION.exec(text.trim())
31
+ if (!match) return null
32
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] === undefined ? [] : match[4].split('.') }
33
+ }
34
+
35
+ function comparePrerelease(left, right) {
36
+ // No prerelease ranks above any prerelease of the same version.
37
+ if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1
38
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
39
+ if (left[index] === undefined) return -1
40
+ if (right[index] === undefined) return 1
41
+ const [a, b] = [left[index], right[index]]
42
+ const [aNumeric, bNumeric] = [/^\d+$/.test(a), /^\d+$/.test(b)]
43
+ if (aNumeric && bNumeric) { if (Number(a) !== Number(b)) return Number(a) < Number(b) ? -1 : 1 } else if (aNumeric !== bNumeric) return aNumeric ? -1 : 1
44
+ else if (a !== b) return a < b ? -1 : 1
45
+ }
46
+ return 0
47
+ }
48
+
49
+ // Pure. -1, 0 or 1; null when either side is not a version.
50
+ export function compareAppVersions(left, right) {
51
+ const [a, b] = [parseAppVersion(left), parseAppVersion(right)]
52
+ if (a === null || b === null) return null
53
+ for (const part of ['major', 'minor', 'patch']) if (a[part] !== b[part]) return a[part] < b[part] ? -1 : 1
54
+ return comparePrerelease(a.prerelease, b.prerelease)
55
+ }
56
+
57
+ // Pure. A version that cannot be read does not meet the floor.
58
+ export function meetsMinimumAppVersion(version, floor = MINIMUM_APP_VERSION) {
59
+ const order = compareAppVersions(version, floor)
60
+ return order !== null && order >= 0
61
+ }
62
+
63
+ // While the app runs with no vault open, its command-line tool answers every
64
+ // command, `version` too, with this line (observed on 1.13.7, on either output
65
+ // stream and with either exit status). It is not a version.
66
+ const NO_VAULT_OPEN = /^vault not found\.?$/i
67
+
68
+ // Pure. What one `version` call of the command-line tool answered:
69
+ // { version, noVaultOpen }. `exited` is false for a call that failed or exited
70
+ // non-zero; its output is then read for the no-vault answer, never for a version.
71
+ export function readVersionAnswer({ stdout = '', stderr = '', exited = true } = {}) {
72
+ const lines = [stdout, stderr].flatMap((text) => (typeof text === 'string' ? text.split('\n') : [])).map((line) => line.trim())
73
+ if (lines.some((line) => NO_VAULT_OPEN.test(line))) return { version: null, noVaultOpen: true }
74
+ const text = typeof stdout === 'string' ? stdout.trim() : ''
75
+ return { version: exited && text !== '' ? text : null, noVaultOpen: false }
76
+ }
77
+
78
+ // Pure. `requireVersion: false` lets an installed app that is positively not
79
+ // running pass without a version: nothing is published through an app that
80
+ // does not run, and the version is asked for again once it does. An app whose
81
+ // command-line tool says no vault is open runs, whatever the process table
82
+ // said, and answers nothing else: `no-vault-open`, which does not qualify.
83
+ export function qualifyApp(observation, { requireVersion = true, floor = MINIMUM_APP_VERSION } = {}) {
84
+ const seen = observation !== null && typeof observation === 'object' ? observation : {}
85
+ const base = { floor, version: typeof seen.version === 'string' ? seen.version.slice(0, 80) : null, running: seen.running === true ? true : seen.running === false ? false : null }
86
+ if (seen.installed !== true) return { ...base, outcome: 'app-missing', reason: 'no-app-found' }
87
+ if (seen.cli !== true) return { ...base, outcome: 'app-cli-unavailable', reason: 'cli-capability-absent' }
88
+ if (seen.noVaultOpen === true) return { ...base, outcome: 'app-version-unsupported', reason: 'no-vault-open' }
89
+ if (base.version === null) {
90
+ if (!requireVersion && base.running === false) return { ...base, outcome: 'qualified', reason: 'app-not-running-version-not-needed', versionChecked: false }
91
+ return { ...base, outcome: 'app-version-unsupported', reason: 'version-unknown' }
92
+ }
93
+ if (parseAppVersion(base.version) === null) return { ...base, outcome: 'app-version-unsupported', reason: 'version-unreadable' }
94
+ if (!meetsMinimumAppVersion(base.version, floor)) return { ...base, outcome: 'app-version-unsupported', reason: 'below-minimum-version' }
95
+ return { ...base, outcome: 'qualified', reason: 'meets-minimum-version', versionChecked: true }
96
+ }
97
+
98
+ export async function inspectApp(appProbe) {
99
+ if (typeof appProbe?.inspect !== 'function') throw new TypeError('an appProbe with inspect() is required')
100
+ try { return await appProbe.inspect() } catch { return { installed: false, cli: false, running: null, version: null } }
101
+ }
102
+
103
+ // The adapter factory of a service that reaches a real app. The editor adapter
104
+ // is constructed only after the app qualified; otherwise the factory refuses,
105
+ // typed, and the engine records that reason and publishes nothing. The answer
106
+ // is remembered briefly so a tick over many views asks once.
107
+ export function createQualifiedAdapterFactory({ appProbe, createAdapter, floor = MINIMUM_APP_VERSION, maxAgeMs = 10_000, now = () => Date.now() } = {}) {
108
+ if (typeof appProbe?.inspectSync !== 'function') throw new TypeError('the qualified adapter factory needs an appProbe with inspectSync()')
109
+ if (typeof createAdapter !== 'function') throw new TypeError('the qualified adapter factory needs createAdapter')
110
+ let last = null
111
+ const qualification = () => {
112
+ if (last === null || now() - last.at > maxAgeMs) {
113
+ let observation
114
+ try { observation = appProbe.inspectSync() } catch { observation = null }
115
+ last = { at: now(), result: qualifyApp(observation, { requireVersion: false, floor }) }
116
+ }
117
+ return last.result
118
+ }
119
+ const factory = (input) => {
120
+ const result = qualification()
121
+ // No app at all is not an unqualified app: the publisher's own path needs none, and its adapter finds no process.
122
+ if (result.outcome !== 'qualified' && result.outcome !== 'app-missing') refuse(result.outcome, 'the installed Obsidian does not qualify; nothing is published through it', { reason: result.reason, floor: result.floor, version: result.version })
123
+ return createAdapter(input)
124
+ }
125
+ factory.qualification = qualification
126
+ // What was last learned, without asking again: for a status answer.
127
+ factory.lastQualification = () => last?.result ?? null
128
+ return factory
129
+ }
@@ -0,0 +1,105 @@
1
+ import { execFile, spawnSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { defaultCliPath, defaultObsidianProcessProbe } from '../../projection/obsidian/publication/transport.mjs'
5
+ import { readVersionAnswer } from './app-capability.mjs'
6
+
7
+ // The production seams of `obsidian open` and of the service's adapter
8
+ // factory: the only code that asks the installed Obsidian anything or asks the
9
+ // operating system to open it.
10
+ //
11
+ // This module is imported by exactly two places, both real command-line
12
+ // entries and both behind an explicit `--adapter=obsidian-cli`: the service
13
+ // entry (service-main.mjs) and the command entry (src/commands/obsidian.mjs,
14
+ // `production: true`). No test imports it, and importing it leaves a trace
15
+ // below that the test suite asserts is absent.
16
+ //
17
+ // Status: written against the documented command-line interface and NOT yet
18
+ // exercised against a running app. The acceptance work qualifies it on an
19
+ // isolated host before any of it is relied on; until then every answer it
20
+ // cannot establish is the failing one.
21
+ globalThis[Symbol.for('mnstry.atelier.obsidian.production-seams-loaded')] = true
22
+
23
+ const CLI_TIMEOUT_MS = 5000
24
+
25
+ function installedAppPath(platform) {
26
+ if (platform === 'darwin') return '/Applications/Obsidian.app'
27
+ return null
28
+ }
29
+
30
+ function cliExists(cliPath) {
31
+ if (!path.isAbsolute(cliPath)) return null // found through PATH or not at all: only running it tells
32
+ try { return fs.statSync(cliPath).isFile() } catch { return false }
33
+ }
34
+
35
+ const NOT_ASKED = Object.freeze({ version: null, noVaultOpen: false })
36
+
37
+ const observationOf = ({ platform, cliPath, processes, answer }) => {
38
+ const appPath = installedAppPath(platform)
39
+ const present = cliExists(cliPath)
40
+ const answered = answer.version !== null || answer.noVaultOpen
41
+ return {
42
+ installed: appPath === null ? (present !== false && (answered || present === true)) : fs.existsSync(appPath),
43
+ cli: present === true || answered,
44
+ running: processes === 'running' ? true : processes === 'absent' ? false : null,
45
+ version: answer.version,
46
+ ...(answer.noVaultOpen ? { noVaultOpen: true } : {}),
47
+ }
48
+ }
49
+
50
+ export function createProductionAppProbe({ platform = process.platform, env = process.env, cliPath = defaultCliPath(platform) } = {}) {
51
+ const options = { env, timeout: CLI_TIMEOUT_MS, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024 }
52
+ return {
53
+ // For the service's adapter factory, which is synchronous. The version is asked for only while an app runs.
54
+ // Both output streams are read, whatever the exit status: the answer with no vault open is not a version.
55
+ inspectSync() {
56
+ const processes = defaultObsidianProcessProbe({ platform })
57
+ if (processes === 'absent') return observationOf({ platform, cliPath, processes, answer: NOT_ASKED })
58
+ let answer = NOT_ASKED
59
+ try {
60
+ const reply = spawnSync(cliPath, ['version'], { ...options, stdio: ['ignore', 'pipe', 'pipe'] })
61
+ answer = readVersionAnswer({ stdout: reply.stdout, stderr: reply.stderr, exited: reply.error === undefined && reply.status === 0 })
62
+ } catch { answer = NOT_ASKED }
63
+ return observationOf({ platform, cliPath, processes, answer })
64
+ },
65
+ inspect() {
66
+ const processes = defaultObsidianProcessProbe({ platform })
67
+ if (processes === 'absent') return Promise.resolve(observationOf({ platform, cliPath, processes, answer: NOT_ASKED }))
68
+ return new Promise((resolve) => {
69
+ execFile(cliPath, ['version'], options, (error, stdout, stderr) => resolve(observationOf({ platform, cliPath, processes, answer: readVersionAnswer({ stdout, stderr, exited: !error }) })))
70
+ })
71
+ },
72
+ // Whether the app answers for exactly this vault and has finished reading it.
73
+ vaultState({ vaultRoot }) {
74
+ const code = '(()=>JSON.stringify({basePath:app.vault.adapter.basePath,ready:app.metadataCache.initialized===true}))()'
75
+ return new Promise((resolve) => {
76
+ execFile(cliPath, ['eval', `code=${code}`], options, (error, stdout) => {
77
+ if (error) return resolve({ answered: false, indexReady: false })
78
+ try {
79
+ const text = String(stdout)
80
+ const value = JSON.parse(JSON.parse(text.slice(text.indexOf('=> ') + 3)))
81
+ return resolve({ answered: value.basePath === vaultRoot, indexReady: value.basePath === vaultRoot && value.ready === true })
82
+ } catch { return resolve({ answered: false, indexReady: false }) }
83
+ })
84
+ })
85
+ },
86
+ }
87
+ }
88
+
89
+ // Asks the operating system to open the vault in Obsidian. No shell.
90
+ export function createProductionLauncher({ platform = process.platform, env = process.env } = {}) {
91
+ const command = platform === 'darwin' ? '/usr/bin/open' : platform === 'linux' ? 'xdg-open' : null
92
+ return {
93
+ open({ vaultRoot }) {
94
+ if (command === null) return Promise.resolve({ launched: false, reason: 'launcher-platform-unqualified' })
95
+ const uri = `obsidian://open?path=${encodeURIComponent(vaultRoot)}`
96
+ return new Promise((resolve) => {
97
+ execFile(command, [uri], { env, timeout: 15_000 }, (error) => resolve(error ? { launched: false, reason: 'os-open-failed' } : { launched: true, reason: 'os-open-accepted' }))
98
+ })
99
+ },
100
+ }
101
+ }
102
+
103
+ export function createProductionAppSeams(options = {}) {
104
+ return { appProbe: createProductionAppProbe(options), launcher: createProductionLauncher(options) }
105
+ }
@@ -0,0 +1,4 @@
1
+ import { createProposalAdapterContribution } from '../../../projection/obsidian/proposals/contribution.mjs'
2
+
3
+ // The proposal adapter: what the engine hands structural edits to on a tick, and `atelier obsidian proposals`.
4
+ export default createProposalAdapterContribution()
@@ -0,0 +1,4 @@
1
+ import { createSelectionContribution } from '../../../projection/obsidian/selection-ui/contribution.mjs'
2
+
3
+ // The selection, conflict and apply-policy operations: `atelier obsidian selection`, `conflicts` and `apply-policy`.
4
+ export default createSelectionContribution()
@@ -0,0 +1,4 @@
1
+ import { createSourceApplyContribution } from '../../../projection/obsidian/edits/contribution.mjs'
2
+
3
+ // The source apply operation: what the engine dispatches to in automatic mode, and `atelier obsidian apply`.
4
+ export default createSourceApplyContribution()
@@ -0,0 +1,27 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
+ import { refuse } from './errors.mjs'
5
+
6
+ // Where later work plugs in without touching a shared file: every module in
7
+ // `contributions/` beside this file is a contribution. Its default export is
8
+ // `{ id, register({ extensions, operations }) }`. The command and the service
9
+ // entry both load the directory, so an apply operation registered there is
10
+ // the one the service dispatches to and the one `status` reports.
11
+ //
12
+ // Only regular `.mjs` files directly in that directory are loaded, in name
13
+ // order. The directory ships with the package; nothing outside it is read.
14
+
15
+ export const CONTRIBUTIONS_DIRECTORY = path.join(path.dirname(fileURLToPath(import.meta.url)), 'contributions')
16
+
17
+ export async function loadContributions({ directory = CONTRIBUTIONS_DIRECTORY } = {}) {
18
+ let entries
19
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }) } catch (error) { if (error.code === 'ENOENT') return []; throw error }
20
+ const contributions = []
21
+ for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith('.mjs')).sort((left, right) => (left.name < right.name ? -1 : 1))) {
22
+ const loaded = await import(pathToFileURL(path.join(directory, entry.name)).href)
23
+ if (loaded.default === null || typeof loaded.default !== 'object' || typeof loaded.default.register !== 'function') refuse('invalid-extension', 'a contribution module must export { id, register } by default', { file: entry.name })
24
+ contributions.push(loaded.default)
25
+ }
26
+ return contributions
27
+ }
@@ -0,0 +1,37 @@
1
+ import { refuse } from './errors.mjs'
2
+
3
+ // Helpers shared by this module's private documents. Each document is
4
+ // versioned by its `schema` string, closed (an unknown key refuses) and
5
+ // written in one canonical form, so equal state is equal bytes.
6
+
7
+ export const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
8
+
9
+ function sortKeys(value) {
10
+ if (Array.isArray(value)) return value.map(sortKeys)
11
+ if (!isPlainObject(value)) return value
12
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])]))
13
+ }
14
+
15
+ // Keys sorted at every depth, two-space indentation, one final newline.
16
+ export function canonicalJson(value) {
17
+ return `${JSON.stringify(sortKeys(value), null, 2)}\n`
18
+ }
19
+
20
+ export function closedObject(value, { required = [], optional = [] }, code, label) {
21
+ if (!isPlainObject(value)) refuse(code, `${label} must be an object`)
22
+ const allowed = new Set([...required, ...optional])
23
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key))
24
+ if (unknown.length > 0) refuse(code, `${label} carries an unknown key`, { keys: unknown.sort() })
25
+ const missing = required.filter((key) => !Object.hasOwn(value, key))
26
+ if (missing.length > 0) refuse(code, `${label} lacks a required key`, { keys: missing })
27
+ return value
28
+ }
29
+
30
+ export const compareText = (left, right) => (left < right ? -1 : left > right ? 1 : 0)
31
+
32
+ export function isoTime(clock) {
33
+ const value = clock()
34
+ const date = value instanceof Date ? value : new Date(value)
35
+ if (Number.isNaN(date.getTime())) refuse('missing-clock', 'the injected clock did not return a time')
36
+ return date.toISOString()
37
+ }
@@ -0,0 +1,53 @@
1
+ import { projectExtMember } from '../../project/config.mjs'
2
+ import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, assertObsidianContract } from '../../projection/obsidian/contracts.mjs'
3
+ import { refuse } from './errors.mjs'
4
+
5
+ // Typed enablement. The project v1 schema is unchanged: a project without the
6
+ // ext member is valid and means "not configured", which is disabled. A member
7
+ // that is present must satisfy the closed ext-settings contract exactly. An
8
+ // unknown key or value refuses here; it is never ignored and never read as
9
+ // permission, so a newer or mistyped setting cannot switch anything on.
10
+
11
+ export const ENABLEMENT_STATES = Object.freeze(['disabled', 'enabled'])
12
+ export const DISABLED_REASONS = Object.freeze(['not-configured', 'disabled-in-settings'])
13
+
14
+ const SCOPE_SCHEMA = 'atelier-obsidian-scope/v1'
15
+
16
+ function scopeDocument({ scopeId, mode, selector, expansion, ext }) {
17
+ return {
18
+ schema: SCOPE_SCHEMA,
19
+ scopeId,
20
+ mode,
21
+ selector,
22
+ ...(expansion === undefined ? {} : { expansion }),
23
+ ...(ext === undefined ? {} : { ext }),
24
+ }
25
+ }
26
+
27
+ function assertOrRefuse(shape, document, code, message) {
28
+ try {
29
+ return assertObsidianContract(shape, document)
30
+ } catch (error) {
31
+ if (error instanceof ObsidianContractRefusal) refuse(code, message, { errors: error.detail?.errors ?? [] })
32
+ throw error
33
+ }
34
+ }
35
+
36
+ // Returns a frozen { state, reason, settings, scopes, defaultScopeId }.
37
+ // `scopes` are complete scope contract documents, in declared order, and are
38
+ // listed even when the integration is disabled so a caller can name them.
39
+ export function readObsidianEnablement(project) {
40
+ const member = projectExtMember(project, OBSIDIAN_EXT_KEY)
41
+ if (!member.present) {
42
+ return Object.freeze({ state: 'disabled', reason: 'not-configured', settings: null, scopes: Object.freeze([]), defaultScopeId: null })
43
+ }
44
+ const settings = assertOrRefuse('ext-settings', member.value, 'invalid-ext-settings', 'the project Obsidian settings do not satisfy their contract; nothing is enabled')
45
+ const scopes = settings.scopes.map((scope) => assertOrRefuse('scope', scopeDocument(scope), 'invalid-ext-settings', 'a declared scope does not satisfy the scope contract; nothing is enabled'))
46
+ return Object.freeze({
47
+ state: settings.enabled === true ? 'enabled' : 'disabled',
48
+ reason: settings.enabled === true ? 'enabled' : 'disabled-in-settings',
49
+ settings,
50
+ scopes: Object.freeze(scopes),
51
+ defaultScopeId: settings.defaultScopeId ?? null,
52
+ })
53
+ }