@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,180 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../project/private-state.mjs'
4
+ import { PATH_REGISTRY_SCHEMA } from '../../projection/obsidian/materialize/path-registry.mjs'
5
+ import { canonicalJson, closedObject, compareText } from './documents.mjs'
6
+ import { refuse } from './errors.mjs'
7
+ import { APPLY_RESULT_STATUSES } from './extension-points.mjs'
8
+
9
+ // Private, versioned documents of the maintenance engine, under
10
+ //
11
+ // <data>/obsidian/<workspace-id>/state/maintenance/
12
+ // freshness.json per-scope freshness, read by the opening commands
13
+ // pending-edits.json edits made in a vault, preserved and waiting
14
+ // late-writers.json writes that arrived in a displaced file after publication
15
+ // path-registry.json the workspace path registry prepareView grows
16
+ //
17
+ // None of the frozen contracts describes these, so each is a closed document
18
+ // of this module: an unknown key, state or schema refuses on read and on
19
+ // write. Files are owner-only and replaced atomically. A document is written
20
+ // only when its bytes change.
21
+
22
+ export const FRESHNESS_SCHEMA = 'atelier-obsidian-freshness/v1'
23
+ export const PENDING_EDITS_SCHEMA = 'atelier-obsidian-pending-edits/v1'
24
+ export const LATE_WRITERS_SCHEMA = 'atelier-obsidian-late-writers/v1'
25
+
26
+ export const FRESHNESS_STATES = Object.freeze(['current', 'updating', 'held-for-your-edit', 'stale', 'publisher-conflict', 'disabled'])
27
+ export const CHANGE_CLASSES = Object.freeze(['source-body', 'sidecar', 'asset', 'config', 'ext-settings', 'scope', 'eligibility', 'vault-note'])
28
+
29
+ // queued waiting for a person (manual) or for the next dispatch (automatic)
30
+ // apply-unavailable dispatched, and no apply operation exists yet
31
+ // apply-failed dispatched, and the operation refused, conflicted or failed
32
+ // retry-exhausted the retry budget is spent; still pending, no longer dispatched
33
+ // applied the operation reported the edit applied to source
34
+ // withdrawn the note is back at the bytes it was generated with
35
+ // superseded the note was edited again; a newer record carries the newer bytes
36
+ export const OPEN_EDIT_STATES = Object.freeze(['queued', 'apply-unavailable', 'apply-failed', 'retry-exhausted'])
37
+ export const CLOSED_EDIT_STATES = Object.freeze(['applied', 'withdrawn', 'superseded'])
38
+ export const EDIT_STATES = Object.freeze([...OPEN_EDIT_STATES, ...CLOSED_EDIT_STATES])
39
+
40
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
41
+ const DIGEST = /^sha256:[0-9a-f]{64}$/
42
+ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
43
+ const EDIT_ID = /^edit-[0-9a-f]{32}$/
44
+
45
+ const isIdentifier = (value) => typeof value === 'string' && IDENTIFIER.test(value)
46
+ const isDigest = (value) => typeof value === 'string' && DIGEST.test(value)
47
+ const isTimestamp = (value) => typeof value === 'string' && TIMESTAMP.test(value)
48
+ const isRelative = (value) => typeof value === 'string' && value !== '' && !value.startsWith('/') && !value.includes('\\') && !value.split('/').some((part) => part === '' || part === '.' || part === '..')
49
+ const isCount = (value) => Number.isInteger(value) && value >= 0
50
+
51
+ function check(condition, code, message) {
52
+ if (!condition) refuse(code, message)
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Validators
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export function validateFreshness(document, workspaceId) {
60
+ const code = 'invalid-freshness-state'
61
+ closedObject(document, { required: ['schema', 'workspaceId', 'enablement', 'maintenanceMode', 'lastTickAt', 'lastFullReconciliationAt', 'scopes'] }, code, 'the freshness document')
62
+ check(document.schema === FRESHNESS_SCHEMA, code, 'the freshness document names an unknown schema')
63
+ check(document.workspaceId === workspaceId, code, 'the freshness document belongs to another workspace')
64
+ check(['enabled', 'disabled', 'refused'].includes(document.enablement), code, 'enablement is unknown')
65
+ check(['manual', 'automatic'].includes(document.maintenanceMode), code, 'maintenanceMode is unknown')
66
+ check(isTimestamp(document.lastTickAt) && (document.lastFullReconciliationAt === null || isTimestamp(document.lastFullReconciliationAt)), code, 'a tick time is not a UTC timestamp')
67
+ check(Array.isArray(document.scopes), code, 'scopes must be a list')
68
+ const seen = new Set()
69
+ for (const scope of document.scopes) {
70
+ closedObject(scope, { required: ['scopeId', 'state', 'reason', 'generationId', 'preparedGenerationId', 'verified', 'heldNotes', 'changeClasses', 'retainedEdits', 'checkedAt'] }, code, 'a scope freshness entry')
71
+ check(isIdentifier(scope.scopeId) && !seen.has(scope.scopeId), code, 'a scope identity is malformed or repeated')
72
+ seen.add(scope.scopeId)
73
+ check(FRESHNESS_STATES.includes(scope.state), code, 'a scope carries an unknown freshness state')
74
+ check(typeof scope.reason === 'string' && scope.reason !== '' && scope.reason.length <= 160, code, 'a scope reason must be a short code')
75
+ check((scope.generationId === null || isIdentifier(scope.generationId)) && (scope.preparedGenerationId === null || isIdentifier(scope.preparedGenerationId)), code, 'a generation identity is malformed')
76
+ check(typeof scope.verified === 'boolean', code, 'verified must be a boolean')
77
+ check(Array.isArray(scope.heldNotes) && scope.heldNotes.every(isRelative), code, 'heldNotes must list vault-relative paths')
78
+ // `current` is a claim about exact generations and verified bytes; a document that claims it otherwise refuses.
79
+ check(scope.state !== 'current' || (scope.verified && scope.generationId !== null && scope.generationId === scope.preparedGenerationId && scope.heldNotes.length === 0), code, 'a scope claims current without an exact verified generation')
80
+ check(Array.isArray(scope.changeClasses) && scope.changeClasses.every((item) => CHANGE_CLASSES.includes(item)), code, 'changeClasses names an unknown class')
81
+ check(isCount(scope.retainedEdits) && isTimestamp(scope.checkedAt), code, 'a scope entry is malformed')
82
+ }
83
+ return document
84
+ }
85
+
86
+ export function validatePendingEdits(document, workspaceId) {
87
+ const code = 'invalid-pending-edits'
88
+ closedObject(document, { required: ['schema', 'workspaceId', 'edits'] }, code, 'the pending edit document')
89
+ check(document.schema === PENDING_EDITS_SCHEMA, code, 'the pending edit document names an unknown schema')
90
+ check(document.workspaceId === workspaceId, code, 'the pending edit document belongs to another workspace')
91
+ check(Array.isArray(document.edits), code, 'edits must be a list')
92
+ const seen = new Set()
93
+ for (const edit of document.edits) {
94
+ closedObject(edit, { required: ['editId', 'identity', 'scopeId', 'generationId', 'path', 'baseNoteDigest', 'observedDigest', 'objectRef', 'observedAt', 'state', 'attempts', 'retryBudget', 'lastAttemptAt', 'lastResult', 'closedAt'] }, code, 'a pending edit')
95
+ check(typeof edit.editId === 'string' && EDIT_ID.test(edit.editId) && !seen.has(edit.editId), code, 'an edit identity is malformed or repeated')
96
+ seen.add(edit.editId)
97
+ closedObject(edit.identity, { required: ['workspaceId', 'repoId', 'nodeId'] }, code, 'a pending edit identity')
98
+ check(edit.identity.workspaceId === workspaceId && isIdentifier(edit.identity.repoId) && isIdentifier(edit.identity.nodeId), code, 'a pending edit identity is malformed')
99
+ check(isIdentifier(edit.scopeId) && isIdentifier(edit.generationId) && isRelative(edit.path), code, 'a pending edit origin is malformed')
100
+ check(isDigest(edit.baseNoteDigest) && isDigest(edit.observedDigest) && edit.baseNoteDigest !== edit.observedDigest, code, 'a pending edit needs two different digests')
101
+ // The edited bytes are preserved before an edit is recorded, so a record without its object refuses.
102
+ check(edit.objectRef === `recovery/objects/${edit.observedDigest.slice('sha256:'.length)}.bin`, code, 'a pending edit must name the retained object of its observed bytes')
103
+ check(isTimestamp(edit.observedAt) && EDIT_STATES.includes(edit.state) && isCount(edit.attempts), code, 'a pending edit is malformed')
104
+ check(edit.retryBudget === null || (isCount(edit.retryBudget) && edit.retryBudget <= 16), code, 'retryBudget is out of range')
105
+ check(edit.lastAttemptAt === null || isTimestamp(edit.lastAttemptAt), code, 'lastAttemptAt is malformed')
106
+ if (edit.lastResult !== null) {
107
+ closedObject(edit.lastResult, { required: ['status', 'code', 'operationId', 'policyDigest'] }, code, 'a pending edit result')
108
+ check(APPLY_RESULT_STATUSES.includes(edit.lastResult.status) && typeof edit.lastResult.code === 'string' && typeof edit.lastResult.operationId === 'string' && isDigest(edit.lastResult.policyDigest), code, 'a pending edit result is malformed')
109
+ }
110
+ check(CLOSED_EDIT_STATES.includes(edit.state) ? isTimestamp(edit.closedAt) : edit.closedAt === null, code, 'closedAt must be set exactly when an edit is closed')
111
+ check(edit.state !== 'applied' || edit.lastResult?.status === 'applied', code, 'an edit is applied only when the apply operation said so')
112
+ }
113
+ return document
114
+ }
115
+
116
+ export function validateLateWriters(document, workspaceId) {
117
+ const code = 'invalid-late-writers'
118
+ closedObject(document, { required: ['schema', 'workspaceId', 'findings'] }, code, 'the late writer document')
119
+ check(document.schema === LATE_WRITERS_SCHEMA && document.workspaceId === workspaceId && Array.isArray(document.findings), code, 'the late writer document is malformed')
120
+ for (const finding of document.findings) {
121
+ closedObject(finding, { required: ['scopeId', 'journalId', 'unit', 'notePath', 'digestAtMove', 'observedDigest', 'objectRef', 'detectedAt'] }, code, 'a late writer finding')
122
+ check(isIdentifier(finding.scopeId) && isIdentifier(finding.journalId) && isCount(finding.unit) && (finding.notePath === null || isRelative(finding.notePath)), code, 'a late writer finding is malformed')
123
+ check((finding.digestAtMove === null || isDigest(finding.digestAtMove)) && isDigest(finding.observedDigest) && isRelative(finding.objectRef) && isTimestamp(finding.detectedAt), code, 'a late writer finding is malformed')
124
+ }
125
+ return document
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Store
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export function sortEdits(edits) {
133
+ return [...edits].sort((left, right) => compareText(left.identity.repoId, right.identity.repoId) || compareText(left.identity.nodeId, right.identity.nodeId)
134
+ || compareText(left.scopeId, right.scopeId) || compareText(left.observedAt, right.observedAt) || compareText(left.editId, right.editId))
135
+ }
136
+
137
+ export function createMaintenanceStateStore({ workspaceRoot, workspaceId }) {
138
+ const directory = () => ensureContainedPrivateDirectory({ workspaceRoot, directory: path.join(workspaceRoot, 'state', 'maintenance'), label: 'Obsidian maintenance state' })
139
+ const file = (name) => path.join(workspaceRoot, 'state', 'maintenance', name)
140
+
141
+ function read(name, validate, code) {
142
+ let text
143
+ try { text = readRegularTextNoFollow(file(name)) } catch (error) {
144
+ if (error.code === 'ENOENT') return null
145
+ refuse(code, 'a maintenance state document cannot be read', { cause: error.code ?? String(error.message) })
146
+ }
147
+ let document
148
+ try { document = JSON.parse(text) } catch { refuse(code, 'a maintenance state document is not JSON') }
149
+ return validate(document, workspaceId)
150
+ }
151
+
152
+ function write(name, document, validate) {
153
+ validate(document, workspaceId)
154
+ const text = canonicalJson(document)
155
+ let existing = null
156
+ try { existing = fs.readFileSync(file(name), 'utf8') } catch (error) { if (error.code !== 'ENOENT') throw error }
157
+ if (existing === text) return false
158
+ atomicReplacePrivateText(path.join(directory(), name), text)
159
+ return true
160
+ }
161
+
162
+ const registryValidator = (document) => {
163
+ if (document?.schema !== PATH_REGISTRY_SCHEMA || document.workspaceId !== workspaceId || !Array.isArray(document.entries)) refuse('invalid-path-registry', 'the persisted path registry is malformed')
164
+ return document
165
+ }
166
+
167
+ return {
168
+ workspaceRoot,
169
+ workspaceId,
170
+ exists: () => fs.existsSync(path.join(workspaceRoot, 'state', 'maintenance')),
171
+ readFreshness: () => read('freshness.json', validateFreshness, 'invalid-freshness-state'),
172
+ writeFreshness: (document) => write('freshness.json', { ...document, scopes: [...document.scopes].sort((left, right) => compareText(left.scopeId, right.scopeId)) }, validateFreshness),
173
+ readPendingEdits: () => read('pending-edits.json', validatePendingEdits, 'invalid-pending-edits') ?? { schema: PENDING_EDITS_SCHEMA, workspaceId, edits: [] },
174
+ writePendingEdits: (document) => write('pending-edits.json', { ...document, edits: sortEdits(document.edits) }, validatePendingEdits),
175
+ readLateWriters: () => read('late-writers.json', validateLateWriters, 'invalid-late-writers') ?? { schema: LATE_WRITERS_SCHEMA, workspaceId, findings: [] },
176
+ writeLateWriters: (document) => write('late-writers.json', document, validateLateWriters),
177
+ readPathRegistry: () => read('path-registry.json', registryValidator, 'invalid-path-registry'),
178
+ writePathRegistry: (document) => write('path-registry.json', document, registryValidator),
179
+ }
180
+ }
@@ -0,0 +1,33 @@
1
+ import { readObsidianEnablement } from './enablement.mjs'
2
+ import { ObsidianMaintenanceRefusal } from './errors.mjs'
3
+ import { readServiceRecord } from './service-record.mjs'
4
+ import { resolveServiceWorkspace } from './service.mjs'
5
+ import { createMaintenanceStateStore } from './state-store.mjs'
6
+
7
+ // What Git synchronization may say about Obsidian maintenance: a report, read
8
+ // from persisted state, and a hint. Null when the project does not declare
9
+ // the integration or has it switched off, so the output of `sync` is then
10
+ // exactly what it always was. Nothing here starts or asks a service, ticks an
11
+ // engine, publishes a view or writes a file.
12
+ export function maintenanceNoticeFor(project, { dataRoot, env = process.env, platform = process.platform } = {}) {
13
+ let enablement
14
+ try { enablement = readObsidianEnablement(project) } catch (error) {
15
+ if (!(error instanceof ObsidianMaintenanceRefusal)) throw error
16
+ return { state: 'refused', code: error.code, hint: 'atelier obsidian status' }
17
+ }
18
+ if (enablement.state !== 'enabled') return null
19
+ const notice = { state: 'enabled', maintainedBy: 'the Obsidian maintenance service, not by sync', hint: 'atelier obsidian status' }
20
+ try {
21
+ const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
22
+ if (!workspace?.workspaceRoot) return { ...notice, service: 'never-started', scopes: [] }
23
+ const freshness = createMaintenanceStateStore(workspace).readFreshness()
24
+ // A record is not proof that a service runs; only `obsidian service status` proves that.
25
+ return {
26
+ ...notice, service: readServiceRecord(workspace) === null ? 'no-record' : 'recorded-not-verified', lastTickAt: freshness?.lastTickAt ?? null,
27
+ scopes: (freshness?.scopes ?? []).map(({ scopeId, state, reason, checkedAt }) => ({ scopeId, lastKnownState: state, reason, checkedAt })),
28
+ }
29
+ } catch (error) {
30
+ if (!(error instanceof ObsidianMaintenanceRefusal)) throw error
31
+ return { ...notice, service: 'unknown', code: error.code, scopes: [] }
32
+ }
33
+ }
@@ -0,0 +1,84 @@
1
+ // Calls one `tick` again and again, never two at a time.
2
+ //
3
+ // The next tick is scheduled only when the previous one has settled, so ticks
4
+ // cannot overlap however long one takes. A tick that throws does not end the
5
+ // loop and does not change the next tick: the error is handed to `onOutcome`,
6
+ // and the delay before the next attempt doubles per consecutive failure up to
7
+ // a ceiling, so a disk that stays full is retried slowly instead of in a tight
8
+ // loop. One success returns the loop to its interval.
9
+ //
10
+ // `tickNow()` asks for a tick that starts after the request: while a tick is
11
+ // in flight the request is answered by one follow-up tick, shared by everyone
12
+ // who asked meanwhile.
13
+
14
+ export const DEFAULT_TICK_INTERVAL_MS = 30 * 1000
15
+ export const DEFAULT_MAX_BACKOFF_MS = 15 * 60 * 1000
16
+
17
+ export const TICK_LOOP_PRIMITIVES = Object.freeze({
18
+ // Whether the loop goes on after a tick threw.
19
+ continuesAfterError: () => true,
20
+ delayAfter: ({ intervalMs, maxBackoffMs, consecutiveFailures }) => (consecutiveFailures === 0 ? intervalMs : Math.min(maxBackoffMs, intervalMs * 2 ** Math.min(consecutiveFailures, 20))),
21
+ })
22
+
23
+ export function createTickLoopForOracleTests({ tick, intervalMs = DEFAULT_TICK_INTERVAL_MS, maxBackoffMs = DEFAULT_MAX_BACKOFF_MS, setTimer = setTimeout, clearTimer = clearTimeout, onOutcome = () => {} }, primitives = TICK_LOOP_PRIMITIVES) {
24
+ if (typeof tick !== 'function') throw new TypeError('the tick loop needs a tick')
25
+ if (!Number.isInteger(intervalMs) || intervalMs < 1 || !Number.isInteger(maxBackoffMs) || maxBackoffMs < intervalMs) throw new TypeError('the tick loop needs a positive interval and a backoff ceiling no smaller than it')
26
+ const rules = { ...TICK_LOOP_PRIMITIVES, ...primitives }
27
+ let timer = null
28
+ let inFlight = null
29
+ let followUp = null // { promise, resolve } while somebody waits for a tick that starts after now
30
+ let started = false
31
+ let stopped = false
32
+ let consecutiveFailures = 0
33
+ let ticks = 0
34
+
35
+ function schedule() {
36
+ if (stopped || !started || timer !== null) return
37
+ timer = setTimer(() => { timer = null; void run() }, rules.delayAfter({ intervalMs, maxBackoffMs, consecutiveFailures }))
38
+ }
39
+
40
+ async function once() {
41
+ let outcome
42
+ try { outcome = { ok: true, report: await tick() } } catch (error) { outcome = { ok: false, error } }
43
+ ticks += 1
44
+ consecutiveFailures = outcome.ok ? 0 : consecutiveFailures + 1
45
+ try { await onOutcome({ ...outcome, consecutiveFailures }) } catch { /* recording an outcome never ends the loop */ }
46
+ if (!outcome.ok && !rules.continuesAfterError()) stopped = true
47
+ return outcome
48
+ }
49
+
50
+ function run() {
51
+ if (stopped) return Promise.resolve({ ok: false, stopped: true })
52
+ if (inFlight) return inFlight
53
+ if (timer !== null) { clearTimer(timer); timer = null }
54
+ inFlight = once().finally(() => {
55
+ inFlight = null
56
+ const waiting = followUp
57
+ followUp = null
58
+ if (waiting) run().then(waiting.resolve)
59
+ else schedule()
60
+ })
61
+ return inFlight
62
+ }
63
+
64
+ return {
65
+ start() { if (!started && !stopped) { started = true; void run() } },
66
+ tickNow() {
67
+ if (stopped) return Promise.resolve({ ok: false, stopped: true })
68
+ if (!inFlight) return run()
69
+ if (!followUp) { let resolve; const promise = new Promise((settle) => { resolve = settle }); followUp = { promise, resolve } }
70
+ return followUp.promise
71
+ },
72
+ // Lets the tick in flight finish; starts no other.
73
+ async stop() {
74
+ stopped = true
75
+ if (timer !== null) { clearTimer(timer); timer = null }
76
+ if (inFlight) await inFlight.catch(() => {})
77
+ },
78
+ state: () => ({ started, stopped, ticking: inFlight !== null, ticks, consecutiveFailures, nextDelayMs: rules.delayAfter({ intervalMs, maxBackoffMs, consecutiveFailures }) }),
79
+ }
80
+ }
81
+
82
+ export function createTickLoop(options) {
83
+ return createTickLoopForOracleTests(options, TICK_LOOP_PRIMITIVES)
84
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs'
2
+
3
+ // Filesystem watchers, as hints only.
4
+ //
5
+ // factory({ roots: [{ id, path, recursive }], onEvent }) -> { close() }
6
+ // onEvent({ rootId, relative }) relative is a '/'-separated path under the
7
+ // root, or null for "something under it"
8
+ //
9
+ // A watcher may drop, repeat or delay events and may fail to start at all; the
10
+ // engine never depends on one. It only hashes a named file sooner than the
11
+ // next stat scan would have. Tests inject a factory they drive by hand.
12
+
13
+ export function createNullWatcherFactory() {
14
+ return () => ({ close() {} })
15
+ }
16
+
17
+ // fs.watch, recursive where the platform supports it. It does not keep the
18
+ // process alive, and a root that cannot be watched is reported, not thrown:
19
+ // digest reconciliation still covers it.
20
+ export function createFsWatcherFactory({ watch = fs.watch, onUnavailable = () => {} } = {}) {
21
+ return ({ roots, onEvent }) => {
22
+ const watchers = []
23
+ for (const root of roots) {
24
+ try {
25
+ const watcher = watch(root.path, { recursive: root.recursive !== false, persistent: false }, (_eventType, filename) => {
26
+ onEvent({ rootId: root.id, relative: typeof filename === 'string' && filename !== '' ? filename.split('\\').join('/') : null })
27
+ })
28
+ watcher.on('error', () => onUnavailable({ rootId: root.id }))
29
+ watchers.push(watcher)
30
+ } catch {
31
+ onUnavailable({ rootId: root.id })
32
+ }
33
+ }
34
+ return { close() { for (const watcher of watchers.splice(0)) try { watcher.close() } catch { /* already closed */ } } }
35
+ }
36
+ }