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

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 (560) hide show
  1. package/CHANGELOG.md +1005 -0
  2. package/README.md +149 -9
  3. package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
  4. package/contracts/atelier-architecture.v1.schema.json +258 -0
  5. package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
  6. package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
  7. package/contracts/atelier-build.v1.schema.json +1076 -0
  8. package/contracts/atelier-capability.v1.schema.json +1252 -0
  9. package/contracts/atelier-coordination.v1.schema.json +1050 -0
  10. package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
  11. package/contracts/atelier-harness.v1.schema.json +234 -0
  12. package/contracts/atelier-ingestion.v1.schema.json +806 -0
  13. package/contracts/atelier-inquiry.v1.schema.json +1772 -0
  14. package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
  15. package/contracts/atelier-interaction.v1.schema.json +553 -0
  16. package/contracts/atelier-judgment.v1.schema.json +690 -0
  17. package/contracts/atelier-knowledge.v1.schema.json +1512 -0
  18. package/contracts/atelier-learning.v1.schema.json +1095 -0
  19. package/contracts/atelier-migration.v3.schema.json +64 -0
  20. package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
  21. package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
  22. package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
  23. package/contracts/atelier-template-adoption.v1.schema.json +223 -0
  24. package/contracts/atelier-template-profile.v1.schema.json +1168 -0
  25. package/contracts/atelier-trackable.v1.schema.json +781 -0
  26. package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
  27. package/contracts/compat-baseline.json +1 -1
  28. package/docs/architecture.md +89 -0
  29. package/docs/assurance-controls.md +4 -3
  30. package/docs/blocks/capability-stewardship.md +17 -0
  31. package/docs/blocks/claims.md +16 -3
  32. package/docs/blocks/inquiry-harnesses.md +19 -0
  33. package/docs/blocks/learning-harnesses.md +17 -0
  34. package/docs/capability-stewardship.md +269 -0
  35. package/docs/coordination.md +51 -0
  36. package/docs/decision-practices.md +111 -0
  37. package/docs/decisions.md +29 -0
  38. package/docs/evidence-navigation.md +237 -0
  39. package/docs/exact-upgrades.md +3 -2
  40. package/docs/ingestion.md +181 -0
  41. package/docs/inquiry-harnesses.md +211 -0
  42. package/docs/install.md +80 -3
  43. package/docs/intake-and-guides.md +5 -0
  44. package/docs/integration-contract-decisions.md +24 -2
  45. package/docs/interaction-and-reflection.md +72 -0
  46. package/docs/knowledge-graph.md +20 -0
  47. package/docs/knowledge-health/api.md +30 -0
  48. package/docs/knowledge-health/assessment/api.md +96 -0
  49. package/docs/knowledge-health/assessment/capabilities.md +40 -0
  50. package/docs/knowledge-health/assessment/evaluation.md +27 -0
  51. package/docs/knowledge-health/assessment/facilitator.md +30 -0
  52. package/docs/knowledge-health/assessment/quickstart.md +61 -0
  53. package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
  54. package/docs/knowledge-health/devday/facilitator.md +16 -0
  55. package/docs/knowledge-health/devday/participant-response.md +22 -0
  56. package/docs/knowledge-health/devday/public-default.md +107 -0
  57. package/docs/knowledge-health/devday/quickstart.md +18 -0
  58. package/docs/knowledge-health/devday/support.md +22 -0
  59. package/docs/knowledge-health/exercise.md +17 -0
  60. package/docs/knowledge-health/facilitator.md +20 -0
  61. package/docs/knowledge-health/quickstart.md +94 -0
  62. package/docs/knowledge-health/readme.md +15 -0
  63. package/docs/knowledge-health/support.md +34 -0
  64. package/docs/knowledge-setup.md +248 -0
  65. package/docs/knowledge-workspace.md +264 -0
  66. package/docs/learning-harnesses.md +238 -0
  67. package/docs/learning.md +215 -0
  68. package/docs/local-services.md +242 -26
  69. package/docs/obsidian-contract.md +878 -36
  70. package/docs/obsidian-plugin.md +619 -0
  71. package/docs/obsidian.md +809 -64
  72. package/docs/optional-ingestion.md +83 -0
  73. package/docs/personal-workspace.md +257 -0
  74. package/docs/practical-judgment.md +92 -0
  75. package/docs/progressive-ingestion-acceptance.md +89 -0
  76. package/docs/progressive-ingestion.md +235 -0
  77. package/docs/project-command-inventory.md +1 -0
  78. package/docs/release-engineering.md +56 -1
  79. package/docs/repo-boundary-guard.md +94 -0
  80. package/docs/research-starter-quickstart.md +66 -0
  81. package/docs/research-starter.md +114 -0
  82. package/docs/semantic-ingestion.md +198 -0
  83. package/docs/semantic-operation.md +317 -0
  84. package/docs/skill-steward.md +6 -0
  85. package/docs/template-profile.md +164 -0
  86. package/docs/template-upgrades.md +114 -0
  87. package/docs/trackables.md +111 -0
  88. package/docs/upgrade.md +68 -3
  89. package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
  90. package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
  91. package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
  92. package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
  93. package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
  94. package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
  95. package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
  96. package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
  97. package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
  98. package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
  99. package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
  100. package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
  101. package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
  102. package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
  103. package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
  104. package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
  105. package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
  106. package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
  107. package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
  108. package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
  109. package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
  110. package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
  111. package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
  112. package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
  113. package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
  114. package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
  115. package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
  116. package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
  117. package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
  118. package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
  119. package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
  120. package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
  121. package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
  122. package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
  123. package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
  124. package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
  125. package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
  126. package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
  127. package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
  128. package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
  129. package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
  130. package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
  131. package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
  132. package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
  133. package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
  134. package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
  135. package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
  136. package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
  137. package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
  138. package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
  139. package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
  140. package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
  141. package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
  142. package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
  143. package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
  144. package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
  145. package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
  146. package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
  147. package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
  148. package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
  149. package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
  150. package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
  151. package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
  152. package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
  153. package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
  154. package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
  155. package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
  156. package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
  157. package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
  158. package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
  159. package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
  160. package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
  161. package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
  162. package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
  163. package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
  164. package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
  165. package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
  166. package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
  167. package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
  168. package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
  169. package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
  170. package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
  171. package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
  172. package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
  173. package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
  174. package/fixtures/atelier-interaction/act/valid/document.json +24 -0
  175. package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
  176. package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
  177. package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
  178. package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
  179. package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
  180. package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
  181. package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
  182. package/fixtures/atelier-judgment/case/valid/document.json +75 -0
  183. package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
  184. package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
  185. package/fixtures/atelier-judgment/native-decision.md +3 -0
  186. package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
  187. package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
  188. package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
  189. package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
  190. package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
  191. package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
  192. package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
  193. package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
  194. package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
  195. package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
  196. package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
  197. package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
  198. package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
  199. package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
  200. package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
  201. package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
  202. package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
  203. package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
  204. package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
  205. package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
  206. package/fixtures/atelier-learning/invalid/authority.json +14 -0
  207. package/fixtures/atelier-learning/valid/capture.json +13 -0
  208. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
  209. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
  210. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
  211. package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
  212. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
  213. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
  214. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
  215. package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
  216. package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
  217. package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
  218. package/fixtures/atelier-trackable/request/valid/document.json +22 -0
  219. package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
  220. package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
  221. package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
  222. package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
  223. package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
  224. package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
  225. package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
  226. package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
  227. package/fixtures/capability-packages/adoption.example.json +70 -0
  228. package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
  229. package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
  230. package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
  231. package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
  232. package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
  233. package/fixtures/capability-packages/research/capability-package.json +88 -0
  234. package/fixtures/capability-packages/research/capability-release.json +134 -0
  235. package/fixtures/capability-packages/research/evaluation.md +15 -0
  236. package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
  237. package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
  238. package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
  239. package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
  240. package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
  241. package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
  242. package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
  243. package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
  244. package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
  245. package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
  246. package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
  247. package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
  248. package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
  249. package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
  250. package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
  251. package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
  252. package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
  253. package/fixtures/evidence-navigation/valid/claim.json +36 -0
  254. package/fixtures/evidence-navigation/valid/host.json +16 -0
  255. package/fixtures/evidence-navigation/valid/profile.json +16 -0
  256. package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
  257. package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
  258. package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
  259. package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
  260. package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
  261. package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
  262. package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
  263. package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
  264. package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
  265. package/fixtures/harnesses/learning-cycle.json +780 -0
  266. package/fixtures/inquiry/workshop.json +675 -0
  267. package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
  268. package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
  269. package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
  270. package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
  271. package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
  272. package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
  273. package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
  274. package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
  275. package/fixtures/knowledge-health/calibration-suite.json +26 -0
  276. package/fixtures/knowledge-health/calibration-trial.json +37 -0
  277. package/fixtures/knowledge-health/corrected/checklist.md +18 -0
  278. package/fixtures/knowledge-health/devday/participant-record.json +19 -0
  279. package/fixtures/knowledge-health/expected-summary.json +27 -0
  280. package/fixtures/knowledge-health/workshop/README.md +20 -0
  281. package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
  282. package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
  283. package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
  284. package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
  285. package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
  286. package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
  287. package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
  288. package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
  289. package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
  290. package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
  291. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
  292. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
  293. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
  294. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
  295. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
  296. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
  297. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
  298. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
  299. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
  300. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
  301. package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
  302. package/fixtures/obsidian/edits/workspace.json +1 -1
  303. package/fixtures/obsidian/materialization/expected-full.json +21 -21
  304. package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
  305. package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
  306. package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
  307. package/fixtures/personal-workspace/overlay.json +24 -0
  308. package/fixtures/templates/local-library.v1.json +75 -0
  309. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
  310. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
  311. package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
  312. package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
  313. package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
  314. package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
  315. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
  316. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
  317. package/package.json +64 -6
  318. package/plugins/obsidian/main.js +444 -0
  319. package/plugins/obsidian/manifest.json +9 -0
  320. package/plugins/obsidian/styles.css +22 -0
  321. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  322. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  323. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  324. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  325. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  326. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  327. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  328. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  329. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  330. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  331. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  332. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  333. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  334. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  335. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  336. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  337. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  338. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  339. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  340. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  341. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  342. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  343. package/src/architecture/catalog.mjs +355 -0
  344. package/src/architecture/index.mjs +93 -0
  345. package/src/boundary/content-rules.mjs +4 -2
  346. package/src/boundary/delegation-command.mjs +158 -0
  347. package/src/boundary/delegation.mjs +396 -0
  348. package/src/boundary/policy.mjs +52 -10
  349. package/src/build/git.mjs +18 -0
  350. package/src/build/index.mjs +3 -0
  351. package/src/build/ledger.mjs +88 -0
  352. package/src/capabilities/adoption.mjs +381 -0
  353. package/src/capabilities/evidence.mjs +140 -0
  354. package/src/capabilities/files.mjs +124 -0
  355. package/src/capabilities/index.mjs +4 -0
  356. package/src/capabilities/instructions.mjs +250 -0
  357. package/src/capabilities/package.mjs +136 -0
  358. package/src/cli/command-failure.mjs +36 -0
  359. package/src/cli/execute-command.mjs +16 -2
  360. package/src/cli/run.mjs +65 -9
  361. package/src/coauthor/store.mjs +43 -2
  362. package/src/collaboration/proposals.mjs +189 -0
  363. package/src/commands/architecture.mjs +27 -0
  364. package/src/commands/boundary.mjs +3 -0
  365. package/src/commands/capability.mjs +73 -0
  366. package/src/commands/enroll.mjs +3 -0
  367. package/src/commands/harness.mjs +79 -0
  368. package/src/commands/ingest.mjs +39 -0
  369. package/src/commands/init.mjs +4 -1
  370. package/src/commands/inquiry.mjs +66 -0
  371. package/src/commands/knowledge-semantic.mjs +128 -0
  372. package/src/commands/knowledge.mjs +119 -0
  373. package/src/commands/learn.mjs +61 -0
  374. package/src/commands/obsidian.mjs +832 -62
  375. package/src/commands/practice.mjs +24 -0
  376. package/src/commands/setup.mjs +22 -2
  377. package/src/commands/trackable.mjs +19 -0
  378. package/src/contracts/corpus.mjs +36 -0
  379. package/src/coordination/index.mjs +110 -0
  380. package/src/egress/forbidden-egress.mjs +2 -0
  381. package/src/evidence-navigation/contracts.mjs +60 -0
  382. package/src/evidence-navigation/evaluate.mjs +69 -0
  383. package/src/evidence-navigation/generation.mjs +22 -0
  384. package/src/evidence-navigation/index.mjs +2 -0
  385. package/src/evidence-navigation/local.mjs +167 -0
  386. package/src/evidence-navigation/schema.generated.mjs +604 -0
  387. package/src/evidence-navigation/types.mjs +12 -0
  388. package/src/export/atelier-export-contract.mjs +35 -8
  389. package/src/graph/enroll.mjs +248 -0
  390. package/src/graph/graph.mjs +17 -1
  391. package/src/graph/knowledge-graph.mjs +58 -8
  392. package/src/harnesses/contracts.mjs +41 -0
  393. package/src/harnesses/exchange.mjs +154 -0
  394. package/src/harnesses/feedback.mjs +16 -0
  395. package/src/harnesses/history.mjs +37 -0
  396. package/src/harnesses/index.mjs +4 -0
  397. package/src/harnesses/store.mjs +29 -0
  398. package/src/ingestion/contracts.mjs +41 -0
  399. package/src/ingestion/evaluation.mjs +119 -0
  400. package/src/ingestion/optional/composition/index.mjs +171 -0
  401. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  402. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  403. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  404. package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
  405. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  406. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  407. package/src/ingestion/optional/index.mjs +9 -0
  408. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  409. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  410. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  411. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  412. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  413. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  414. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  415. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  416. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  417. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  418. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  419. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  420. package/src/ingestion/processors.mjs +218 -0
  421. package/src/ingestion/semantic.mjs +203 -0
  422. package/src/ingestion/store.mjs +372 -0
  423. package/src/inquiry/beliefs.mjs +32 -0
  424. package/src/inquiry/contracts.mjs +23 -0
  425. package/src/inquiry/index.mjs +5 -0
  426. package/src/inquiry/ledger.mjs +135 -0
  427. package/src/inquiry/lenses.mjs +15 -0
  428. package/src/inquiry/projection.mjs +77 -0
  429. package/src/inquiry/store.mjs +35 -0
  430. package/src/intake/read-scope.mjs +12 -0
  431. package/src/intake/store.mjs +69 -6
  432. package/src/interaction/index.mjs +91 -0
  433. package/src/judgment/index.mjs +82 -0
  434. package/src/judgment/practice-evaluation.mjs +70 -0
  435. package/src/judgment/practice.mjs +144 -0
  436. package/src/knowledge/context.mjs +217 -0
  437. package/src/knowledge/index.mjs +5 -0
  438. package/src/knowledge/ingestion.mjs +79 -0
  439. package/src/knowledge/intake.mjs +33 -0
  440. package/src/knowledge/ledger.mjs +110 -0
  441. package/src/knowledge/participatory/guidance.mjs +36 -0
  442. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  443. package/src/knowledge/participatory/index.mjs +2 -0
  444. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  445. package/src/knowledge/plan.mjs +67 -0
  446. package/src/knowledge/projection.mjs +43 -0
  447. package/src/knowledge/semantic-operation.mjs +567 -0
  448. package/src/knowledge/sessions.mjs +467 -0
  449. package/src/knowledge/workspace.mjs +295 -0
  450. package/src/knowledge-health/assessment.mjs +165 -0
  451. package/src/knowledge-health/capabilities.mjs +350 -0
  452. package/src/knowledge-health/context.mjs +62 -0
  453. package/src/knowledge-health/example.mjs +47 -0
  454. package/src/knowledge-health/index.mjs +10 -0
  455. package/src/knowledge-health/owner-join.mjs +88 -0
  456. package/src/knowledge-health/proof/common.mjs +24 -0
  457. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  458. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  459. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  460. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  461. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  462. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  463. package/src/knowledge-health/rule-identity.mjs +44 -0
  464. package/src/knowledge-health/workshop-composition.mjs +19 -0
  465. package/src/knowledge-health/workshop-profile.mjs +176 -0
  466. package/src/knowledge-health/workshop.mjs +99 -0
  467. package/src/learning/adapters.mjs +68 -0
  468. package/src/learning/assistance.mjs +127 -0
  469. package/src/learning/contracts.mjs +65 -0
  470. package/src/learning/store.mjs +272 -0
  471. package/src/personal-workspace/index.mjs +391 -0
  472. package/src/personal-workspace/selection.mjs +385 -0
  473. package/src/project/config.mjs +8 -0
  474. package/src/project/durable-state.mjs +3 -1
  475. package/src/project/file-class.mjs +1 -1
  476. package/src/project/private-state.mjs +10 -1
  477. package/src/projection/obsidian/contracts.mjs +224 -18
  478. package/src/projection/obsidian/edits/apply.mjs +63 -19
  479. package/src/projection/obsidian/edits/regions.mjs +137 -42
  480. package/src/projection/obsidian/materialize/index.mjs +6 -3
  481. package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
  482. package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
  483. package/src/projection/obsidian/materialize/redaction.mjs +327 -0
  484. package/src/projection/obsidian/materialize/settings.mjs +113 -34
  485. package/src/projection/obsidian/personal-workspace.mjs +436 -0
  486. package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
  487. package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
  488. package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
  489. package/src/projection/obsidian/proposals/adapter.mjs +47 -8
  490. package/src/projection/obsidian/proposals/observation.mjs +20 -7
  491. package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
  492. package/src/projection/obsidian/publication/index.mjs +2 -1
  493. package/src/projection/obsidian/publication/publisher.mjs +312 -36
  494. package/src/projection/obsidian/publication/transport.mjs +63 -8
  495. package/src/projection/obsidian/publication/vault-list.mjs +200 -0
  496. package/src/projection/obsidian/recovery/index.mjs +4 -1
  497. package/src/projection/obsidian/recovery/restart.mjs +3 -1
  498. package/src/projection/obsidian/recovery/store.mjs +168 -8
  499. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  500. package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
  501. package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
  502. package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
  503. package/src/readiness/readiness.mjs +10 -3
  504. package/src/reflection/index.mjs +56 -0
  505. package/src/runtime/obsidian/app-capability.mjs +104 -12
  506. package/src/runtime/obsidian/app-production-seams.mjs +161 -42
  507. package/src/runtime/obsidian/app-registration.mjs +313 -0
  508. package/src/runtime/obsidian/app-restart.mjs +154 -0
  509. package/src/runtime/obsidian/engine.mjs +288 -29
  510. package/src/runtime/obsidian/extension-points.mjs +1 -1
  511. package/src/runtime/obsidian/index.mjs +15 -8
  512. package/src/runtime/obsidian/launch-plan.mjs +91 -0
  513. package/src/runtime/obsidian/lifecycle.mjs +193 -22
  514. package/src/runtime/obsidian/login-item.mjs +332 -0
  515. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  516. package/src/runtime/obsidian/opening.mjs +369 -63
  517. package/src/runtime/obsidian/pending-edits.mjs +10 -0
  518. package/src/runtime/obsidian/pipeline.mjs +71 -3
  519. package/src/runtime/obsidian/plugin-channel.mjs +289 -0
  520. package/src/runtime/obsidian/plugin-choice.mjs +182 -0
  521. package/src/runtime/obsidian/plugin-drift.mjs +124 -0
  522. package/src/runtime/obsidian/plugin-presence.mjs +61 -0
  523. package/src/runtime/obsidian/process-identity.mjs +20 -10
  524. package/src/runtime/obsidian/project-views.mjs +282 -0
  525. package/src/runtime/obsidian/questions.mjs +48 -0
  526. package/src/runtime/obsidian/release-watch.mjs +56 -0
  527. package/src/runtime/obsidian/service-main.mjs +113 -17
  528. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  529. package/src/runtime/obsidian/service-managers.mjs +275 -0
  530. package/src/runtime/obsidian/service-record.mjs +105 -5
  531. package/src/runtime/obsidian/service-server.mjs +50 -12
  532. package/src/runtime/obsidian/service.mjs +134 -7
  533. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  534. package/src/runtime/obsidian/state-store.mjs +13 -1
  535. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  536. package/src/runtime/obsidian/vault-location.mjs +300 -0
  537. package/src/runtime/obsidian/view-counts.mjs +34 -0
  538. package/src/server/local-sidecar.mjs +193 -7
  539. package/src/server/server.mjs +45 -6
  540. package/src/template-bindings/project.mjs +92 -0
  541. package/src/templates/profile.mjs +225 -0
  542. package/src/trackables/domain.mjs +175 -0
  543. package/src/trackables/store.mjs +114 -0
  544. package/src/ui/knowledge-page.mjs +1388 -0
  545. package/src/upgrade/explanation.mjs +1 -1
  546. package/src/upgrade/template-participant.mjs +171 -0
  547. package/src/upgrade/transaction.mjs +72 -25
  548. package/src/upgrade/upgrade.mjs +2 -1
  549. package/templates/external-project-workspace/pack-lifecycle.json +6 -1
  550. package/templates/knowledge-workspace/.gitattributes +2 -0
  551. package/templates/knowledge-workspace/AGENTS.md +27 -0
  552. package/templates/knowledge-workspace/README.md +44 -0
  553. package/templates/knowledge-workspace/atelier.project.json +22 -0
  554. package/templates/knowledge-workspace/gitignore +6 -0
  555. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  556. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  557. package/templates/knowledge-workspace/records/forecast.md +22 -0
  558. package/templates/knowledge-workspace/records/inspection.md +25 -0
  559. package/templates/knowledge-workspace/records/telescope.md +24 -0
  560. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
4
5
  import { atomicReplacePrivateText, ensureContainedPrivateDirectory, openRegularFileNoFollow, readRegularTextNoFollow } from '../../project/private-state.mjs'
5
6
  import { ObsidianContractRefusal, assertObsidianContract } from '../../projection/obsidian/contracts.mjs'
6
7
  import { canonicalJson, closedObject } from './documents.mjs'
@@ -10,11 +11,14 @@ import { LOOPBACK_HOSTS } from './service-client.mjs'
10
11
  // Private documents of the maintenance service of one workspace, under
11
12
  //
12
13
  // <data>/obsidian/<workspace-id>/state/service/
13
- // runtime.json the adapter record of the running service (service-state v1)
14
- // settings.json what this machine chose: loopback host, port, startup consent
15
- // last-error.json the last tick that failed for a reason nobody typed
16
- // service.log the operational log of the service process
17
- // start-lock/ serializes `start` for this workspace
14
+ // runtime.json the adapter record of the running service (service-state v1)
15
+ // settings.json what this machine chose: loopback host, port, startup consent
16
+ // last-error.json the last tick that failed for a reason nobody typed
17
+ // last-startup.json how the last start by the login item ended: started, or the refusal's code
18
+ // login-item.json the installed login item (login-item.mjs)
19
+ // service.log the operational log of the service process
20
+ // login-item.log what a login item's service printed before it could open service.log
21
+ // start-lock/ serializes `start` for this workspace
18
22
  //
19
23
  // Owner-only, replaced atomically, outside every repository and every vault.
20
24
  // Every document is validated on every read. One that does not validate, or
@@ -23,7 +27,9 @@ import { LOOPBACK_HOSTS } from './service-client.mjs'
23
27
 
24
28
  export const SERVICE_SETTINGS_SCHEMA = 'atelier-obsidian-service-settings/v1'
25
29
  export const SERVICE_ERROR_SCHEMA = 'atelier-obsidian-service-last-error/v1'
30
+ export const LAST_STARTUP_SCHEMA = 'atelier-obsidian-last-startup/v1'
26
31
  export const CONSENT_COVERAGES = Object.freeze(['service', 'service-and-startup'])
32
+ export const STARTUP_OUTCOMES = Object.freeze(['started', 'refused'])
27
33
 
28
34
  const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
29
35
  const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
@@ -40,6 +46,7 @@ export function servicePaths(workspaceRoot) {
40
46
  return {
41
47
  stateLocation: path.join(workspaceRoot, 'state'), directory, record: path.join(directory, 'runtime.json'), settings: path.join(directory, 'settings.json'),
42
48
  lastError: path.join(directory, 'last-error.json'), log: path.join(directory, 'service.log'), startLock: path.join(directory, 'start-lock'),
49
+ lastStartup: path.join(directory, 'last-startup.json'), loginItem: path.join(directory, 'login-item.json'), loginItemLog: path.join(directory, 'login-item.log'),
43
50
  }
44
51
  }
45
52
 
@@ -60,6 +67,72 @@ export function executableIdentity(entryPath) {
60
67
  return { path: resolved, digest: `sha256:${createHash('sha256').update(fs.readFileSync(resolved)).digest('hex')}` }
61
68
  }
62
69
 
70
+ // The package this module ships in.
71
+ const PACKAGE_ROOT = fileURLToPath(new URL('../../../', import.meta.url))
72
+ const byName = (left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
73
+ const releases = new Map()
74
+
75
+ // The release that runs, beyond its entry module: the version of the package
76
+ // and a digest of the runtime it ships, by relative path and content. That is
77
+ // what the service loads: every module under `src/`, the `contracts/` it
78
+ // reads, and the plugin under `plugins/` it publishes into every vault it
79
+ // maintains; outside those it reads nothing else of the package but the
80
+ // version in `package.json`. A release that changes any of it, and not only
81
+ // the entry, differs. A package without `plugins/` reads as one with an empty
82
+ // one.
83
+
84
+ // Every regular file a release identity covers, in a fixed order: `visit(relative, absolute)`. A missing `src/` or
85
+ // `contracts/` throws (the package cannot be read); a missing `plugins/` is empty.
86
+ export function walkRelease(root, visit) {
87
+ const walk = (relative, { optional = false } = {}) => {
88
+ let entries
89
+ try { entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true }) } catch (error) { if (optional && error.code === 'ENOENT') return; throw error }
90
+ for (const entry of entries.sort(byName)) {
91
+ const child = `${relative}/${entry.name}`
92
+ if (entry.isDirectory()) walk(child)
93
+ else if (entry.isFile()) visit(child, path.join(root, child))
94
+ }
95
+ }
96
+ for (const part of ['src', 'contracts']) walk(part)
97
+ walk('plugins', { optional: true })
98
+ }
99
+
100
+ // The release identity of the package at `root`, read now and never cached: { version, digest }.
101
+ export function readReleaseIdentity({ root }) {
102
+ if (typeof root !== 'string' || !path.isAbsolute(root)) throw new TypeError('a release identity is read from the absolute root of a package')
103
+ const hash = createHash('sha256')
104
+ walkRelease(root, (relative, absolute) => { hash.update(`${relative}\0${createHash('sha256').update(fs.readFileSync(absolute)).digest('hex')}\n`) })
105
+ let version = null
106
+ try { const read = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version; version = typeof read === 'string' ? read : null } catch { version = null }
107
+ return Object.freeze({ version, digest: `sha256:${hash.digest('hex')}` })
108
+ }
109
+
110
+ // readReleaseIdentity, computed once per process and package root, so a service keeps the identity of the code it
111
+ // loaded.
112
+ export function releaseIdentity({ root = PACKAGE_ROOT } = {}) {
113
+ if (!releases.has(root)) releases.set(root, readReleaseIdentity({ root }))
114
+ return releases.get(root)
115
+ }
116
+
117
+ const ENTRY_IN_PACKAGE = Object.freeze(['src', 'runtime', 'obsidian', 'service-main.mjs'])
118
+
119
+ // The root of the package whose service entry is `entryPath` (`<root>/src/runtime/obsidian/service-main.mjs`), as that
120
+ // path names it, without resolving links; null for any other entry.
121
+ export function packageRootOfEntry(entryPath) {
122
+ if (typeof entryPath !== 'string' || !path.isAbsolute(entryPath)) return null
123
+ const parts = path.resolve(entryPath).split(path.sep)
124
+ if (parts.length <= ENTRY_IN_PACKAGE.length || ENTRY_IN_PACKAGE.some((part, index) => parts[parts.length - ENTRY_IN_PACKAGE.length + index] !== part)) return null
125
+ return parts.slice(0, parts.length - ENTRY_IN_PACKAGE.length).join(path.sep) || path.sep
126
+ }
127
+
128
+ // The release that starting `entryPath` would run, read now from the package that path names (so a link pointed
129
+ // elsewhere, or a `file:` install, is the package it leads to now); for an entry that is not a package's service entry,
130
+ // this package's own, as it was loaded.
131
+ export function releaseOfEntry(entryPath) {
132
+ const root = packageRootOfEntry(entryPath)
133
+ return root === null ? releaseIdentity() : readReleaseIdentity({ root })
134
+ }
135
+
63
136
  // ---------------------------------------------------------------------------
64
137
  // The adapter record
65
138
  // ---------------------------------------------------------------------------
@@ -159,6 +232,33 @@ export function writeLastServiceError({ workspaceRoot, workspaceId, document })
159
232
  return document
160
233
  }
161
234
 
235
+ // ---------------------------------------------------------------------------
236
+ // How the last start by a login item ended
237
+ // ---------------------------------------------------------------------------
238
+
239
+ // { schema, workspaceId, at, outcome: 'started' | 'refused', code }: the code of the refusal, null for a start. Written by
240
+ // the service when it runs with `--startup`, so a login item that exits cleanly on a refusal (and is therefore not
241
+ // started again in a loop) still says why.
242
+ function validateLastStartup(document, workspaceId) {
243
+ const code = 'invalid-service-last-startup'
244
+ closedObject(document, { required: ['schema', 'workspaceId', 'at', 'outcome', 'code'] }, code, 'the last startup')
245
+ const ok = document.schema === LAST_STARTUP_SCHEMA && document.workspaceId === workspaceId && TIMESTAMP.test(document.at) && STARTUP_OUTCOMES.includes(document.outcome)
246
+ && (document.outcome === 'started' ? document.code === null : typeof document.code === 'string' && /^[A-Za-z0-9_.:-]{1,64}$/.test(document.code))
247
+ if (!ok) refuse(code, 'the last startup is malformed')
248
+ return document
249
+ }
250
+
251
+ export function readLastStartup({ workspaceRoot, workspaceId }) {
252
+ const document = readJson(servicePaths(workspaceRoot).lastStartup, 'invalid-service-last-startup', 'the last startup')
253
+ return document === null ? null : validateLastStartup(document, workspaceId)
254
+ }
255
+
256
+ export function writeLastStartup({ workspaceRoot, workspaceId, document }) {
257
+ validateLastStartup(document, workspaceId)
258
+ atomicReplacePrivateText(path.join(serviceDirectory(workspaceRoot), 'last-startup.json'), canonicalJson(document))
259
+ return document
260
+ }
261
+
162
262
  // The log is bounded: past the ceiling it becomes the one previous log, and a new one begins.
163
263
  export const SERVICE_LOG_MAX_BYTES = 1024 * 1024
164
264
 
@@ -1,31 +1,50 @@
1
1
  import { createHash, timingSafeEqual } from 'node:crypto'
2
2
  import http from 'node:http'
3
+ import { isContractIdentifier } from '../../projection/obsidian/contracts.mjs'
4
+ import { PLUGIN_MAX_REQUEST_BYTES, pluginCommandOf, validatePluginRequest } from '../../projection/obsidian/plugin-bridge/channel.mjs'
3
5
  import { requestHeader, sameOrigin } from '../../server/security.mjs'
4
6
  import { isPlainObject } from './documents.mjs'
5
7
  import { HEALTH_SCHEMA, LOOPBACK_HOSTS, authorityOf } from './service-client.mjs'
6
8
 
7
- // The listener of the maintenance service: four fixed operations and nothing
8
- // else. There is no file serving, no command, no evaluation and no route that
9
- // takes a path, a name or code from a request.
9
+ // The listener of the maintenance service: four fixed operations, the five
10
+ // fixed commands of Atelier's plugin, and nothing else. There is no file
11
+ // serving, no command, no evaluation and no route that takes a path, a name or
12
+ // code from a request.
10
13
  //
11
14
  // GET /health who answers here; open, and says nothing else
12
15
  // GET /status service and per-view freshness summary; bearer required
13
16
  // POST /tick run one tick now; bearer required
14
17
  // POST /stop finish the tick in flight and exit; bearer required
15
18
  //
19
+ // POST /plugin/challenge | /plugin/hello | /plugin/lease | /plugin/release |
20
+ // /plugin/status
21
+ // Atelier's plugin inside one vault, authenticated by a
22
+ // handshake over that vault's key (plugin-bridge/channel.mjs)
23
+ //
16
24
  // Every request, before its operation is even looked up:
17
25
  //
18
26
  // - `Host` is exactly the literal loopback authority this listener is bound
19
27
  // to, so a name that resolves to loopback (DNS rebinding) is refused;
20
28
  // - a `Sec-Fetch-Site` other than `none` or `same-origin`, or an `Origin`
21
29
  // that is not this listener itself, is refused: no web page drives this;
22
- // - the path is one of the four, exactly, with no query; the method is the
30
+ // - the path is one of the nine, exactly, with no query; the method is the
23
31
  // one that path has.
24
32
  //
25
- // Everything but health needs the per-runtime random bearer that exists only
26
- // in the owner-only record. A POST body is JSON of at most 1 KiB naming the
27
- // runtime it is meant for, so a request aimed at an earlier runtime on the
28
- // same port does nothing.
33
+ // Everything but health and the plugin's commands needs the per-runtime
34
+ // random bearer that exists only in the owner-only record. A POST body is JSON
35
+ // of at most 1 KiB naming the runtime it is meant for, so a request aimed at an
36
+ // earlier runtime on the same port does nothing (409). A tick may also name one
37
+ // view (`scopeId`, a contract identifier), which is then prepared and published
38
+ // once more on that tick; any other member is refused (400,
39
+ // `request-member-unknown`).
40
+ //
41
+ // A plugin command carries no bearer: the plugin never sends its vault's key.
42
+ // Its body, JSON of at most 1 KiB with exactly the fields of its command, is
43
+ // judged by the plugin channel (plugin-channel.mjs): a challenge the service
44
+ // answers only for a key it holds, a hello that proves the same key, and
45
+ // session commands under the key the handshake derived. What it grants is
46
+ // these commands for that one vault. The runtime bearer is not accepted there,
47
+ // and nothing a plugin holds reaches status, tick or stop.
29
48
 
30
49
  export const MAX_REQUEST_BYTES = 1024
31
50
  export const SERVICE_OPERATIONS = Object.freeze({ '/health': 'GET', '/status': 'GET', '/tick': 'POST', '/stop': 'POST' })
@@ -35,6 +54,7 @@ const digestOf = (value) => createHash('sha256').update(String(value)).digest()
35
54
  // The decisions the request oracles are sensitive to; tests substitute broken ones to prove the oracles can fail.
36
55
  export const SERVER_PRIMITIVES = Object.freeze({
37
56
  bearerMatches: (presented, expected) => typeof presented === 'string' && presented !== '' && timingSafeEqual(digestOf(presented), digestOf(expected)),
57
+ maxPluginRequestBytes: PLUGIN_MAX_REQUEST_BYTES,
38
58
  hostMatches: (header, authority) => header === authority,
39
59
  originAllowed(headers, authority) {
40
60
  const site = requestHeader(headers, 'sec-fetch-site').toLowerCase()
@@ -73,7 +93,9 @@ function readBoundedJson(request, limit) {
73
93
  }
74
94
 
75
95
  // identity: { serviceName, workspaceId, runtimeId, pid, host, port, executableDigest, startedAt }
76
- // operations: { healthStatus(), status(), tick(), stop() }
96
+ // operations: { healthStatus(), status(), tick(), stop() }, and, where the
97
+ // service holds the plugin channel, { plugin(command, { body, authority }) ->
98
+ // { statusCode, body } }
77
99
  export function createServiceServerForOracleTests({ identity, bearer, operations }, primitives = SERVER_PRIMITIVES) {
78
100
  if (!LOOPBACK_HOSTS.includes(identity?.host)) throw new TypeError('the service listens on a literal loopback address only')
79
101
  if (typeof bearer !== 'string' || bearer.length < 32) throw new TypeError('the service needs its random bearer')
@@ -83,6 +105,19 @@ export function createServiceServerForOracleTests({ identity, bearer, operations
83
105
  const authority = authorityOf(identity.host, identity.port)
84
106
  if (!rules.hostMatches(requestHeader(request.headers, 'host'), authority)) return send(response, 403, { error: 'host-not-this-loopback-listener' })
85
107
  if (!rules.originAllowed(request.headers, authority)) return send(response, 403, { error: 'cross-site-request' })
108
+ const presented = /^Bearer ([A-Za-z0-9_-]+)$/.exec(requestHeader(request.headers, 'authorization'))?.[1] ?? null
109
+ const command = typeof operations.plugin === 'function' ? pluginCommandOf(request.url) : null
110
+ if (command !== null) {
111
+ if (request.method !== 'POST') return send(response, 405, { error: 'method-not-allowed' })
112
+ // No credential travels in a header here: a request that sends one was not made by the plugin.
113
+ if (requestHeader(request.headers, 'authorization') !== '') return send(response, 400, { error: 'plugin-command-takes-no-bearer' })
114
+ const payload = await readBoundedJson(request, rules.maxPluginRequestBytes)
115
+ if (!payload.ok) return send(response, payload.statusCode, { error: payload.code })
116
+ const checked = validatePluginRequest(command, payload.body)
117
+ if (!checked.ok) return send(response, checked.code === 'protocol-unsupported' ? 409 : 400, { error: checked.code })
118
+ const answer = await operations.plugin(command, { body: checked.body, authority })
119
+ return send(response, answer.statusCode, answer.body)
120
+ }
86
121
  const method = Object.hasOwn(SERVICE_OPERATIONS, request.url) ? SERVICE_OPERATIONS[request.url] : null
87
122
  if (method === null) return send(response, 404, { error: 'unknown-operation' })
88
123
  if (request.method !== method) return send(response, 405, { error: 'method-not-allowed' })
@@ -90,13 +125,16 @@ export function createServiceServerForOracleTests({ identity, bearer, operations
90
125
  const { serviceName, workspaceId, runtimeId, pid, host, port, executableDigest, startedAt } = identity
91
126
  return send(response, 200, { schema: HEALTH_SCHEMA, serviceName, workspaceId, runtimeId, pid, host, port, executableDigest, startedAt, status: operations.healthStatus() })
92
127
  }
93
- const presented = /^Bearer ([A-Za-z0-9_-]+)$/.exec(requestHeader(request.headers, 'authorization'))?.[1] ?? null
94
128
  if (!rules.bearerMatches(presented, bearer)) return send(response, 401, { error: 'bearer-required' })
95
129
  if (request.url === '/status') return send(response, 200, await operations.status())
96
130
  const payload = await readBoundedJson(request, rules.maxRequestBytes)
97
131
  if (!payload.ok) return send(response, payload.statusCode, { error: payload.code })
98
- if (Object.keys(payload.body).length !== 1 || payload.body.runtimeId !== identity.runtimeId) return send(response, 409, { error: 'request-names-another-runtime' })
99
- if (request.url === '/tick') return send(response, 200, await operations.tick())
132
+ const { runtimeId, scopeId, ...unknown } = payload.body
133
+ if (runtimeId !== identity.runtimeId) return send(response, 409, { error: 'request-names-another-runtime' })
134
+ // A member the operation does not take: a view on a stop, or anything else.
135
+ if (Object.keys(unknown).length > 0 || (scopeId !== undefined && request.url !== '/tick')) return send(response, 400, { error: 'request-member-unknown' })
136
+ if (scopeId !== undefined && !isContractIdentifier(scopeId)) return send(response, 400, { error: 'request-invalid' })
137
+ if (request.url === '/tick') return send(response, 200, await operations.tick(scopeId === undefined ? {} : { scopeId }))
100
138
  // The answer leaves first; the service then finishes its tick and exits.
101
139
  response.once('finish', () => { void operations.stop() })
102
140
  return send(response, 202, { stopping: true, runtimeId: identity.runtimeId, pid: identity.pid })
@@ -1,19 +1,26 @@
1
1
  import { randomBytes as cryptoRandomBytes } from 'node:crypto'
2
2
  import fs from 'node:fs'
3
+ import path from 'node:path'
3
4
  import { AtelierDiagnosticError } from '../../project/config.mjs'
4
5
  import { ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
6
+ import { prepareView as productionPrepareView } from '../../projection/obsidian/materialize/index.mjs'
7
+ import { preparePluginFiles } from '../../projection/obsidian/plugin-bridge/bundle.mjs'
8
+ import { publishView as productionPublishView } from '../../projection/obsidian/publication/publisher.mjs'
5
9
  import { isoTime } from './documents.mjs'
6
10
  import { createMaintenanceEngine } from './engine.mjs'
7
11
  import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
8
12
  import { assertOutsideRepositories, protectedRoots, readLocalPointer, resolveDataRoot, workspaceStateRoot } from './machine-settings.mjs'
13
+ import { createPluginChannel, createPluginSessions, ensurePluginBearer, pluginPresence } from './plugin-channel.mjs'
14
+ import { confirmPluginEntry, confirmPluginSeen, currentPluginChoice, decidePluginChoice, vaultFilePresent, viewVaultRoot } from './plugin-choice.mjs'
15
+ import { createPluginDriftObserver } from './plugin-drift.mjs'
9
16
  import { isProcessAlive } from './private-lock.mjs'
10
17
  import { probeHealth } from './service-client.mjs'
11
18
  import {
12
- SERVICE_ERROR_SCHEMA, executableIdentity, readLastServiceError, readServiceRecord, readServiceSettings, removeServiceRecord, serviceNameFor, servicePaths,
19
+ SERVICE_ERROR_SCHEMA, executableIdentity, readLastServiceError, readServiceRecord, readServiceSettings, releaseIdentity, removeServiceRecord, serviceNameFor, servicePaths,
13
20
  writeLastServiceError, writeServiceRecord,
14
21
  } from './service-record.mjs'
15
22
  import { createServiceServer } from './service-server.mjs'
16
- import { createMaintenanceStateStore } from './state-store.mjs'
23
+ import { OPEN_EDIT_STATES, createMaintenanceStateStore } from './state-store.mjs'
17
24
  import { DEFAULT_MAX_BACKOFF_MS, DEFAULT_TICK_INTERVAL_MS, createTickLoop } from './tick-loop.mjs'
18
25
  import { createFsWatcherFactory } from './watchers.mjs'
19
26
 
@@ -33,11 +40,25 @@ import { createFsWatcherFactory } from './watchers.mjs'
33
40
  // publication that was cut short is settled by the publisher's own restart
34
41
  // recovery on a later tick, never by deleting what it left behind.
35
42
  //
43
+ // It also holds the plugin channel (plugin-channel.mjs). Every view it
44
+ // prepares carries Atelier's plugin, whose data file names this listener and
45
+ // the view's bearer, unless the person turned the plugin off in that vault
46
+ // (plugin-choice.mjs): then the entry is left alone and only the plugin files
47
+ // that are still there are kept current. A plugin that holds a view open is
48
+ // reported by status and tells the adapter factory the app version it runs in.
49
+ //
36
50
  // `adapterFactory` has no default here either: whoever starts the service
37
51
  // decides whether it may reach a running app.
52
+ //
53
+ // Started by a login item (`startup`), the service also asks `releaseWatch`
54
+ // after every tick whether the package it runs from changed on disk; when it
55
+ // did, it finishes that tick and shuts down with the reason RELEASE_CHANGED,
56
+ // which its process turns into an exit code the service manager restarts, on
57
+ // the new release (release-watch.mjs, service-main.mjs).
38
58
 
39
59
  export const SERVICE_STATUS_SCHEMA = 'atelier-obsidian-service-status/v1'
40
60
  export const DEFAULT_SHUTDOWN_GRACE_MS = 30 * 1000
61
+ export const RELEASE_CHANGED = 'release-changed'
41
62
 
42
63
  // Where a workspace keeps its private state, without creating an identity.
43
64
  export function resolveServiceWorkspace({ project, dataRoot, env = process.env, platform = process.platform, create = false }) {
@@ -72,6 +93,11 @@ export async function runMaintenanceService(options = {}) {
72
93
  engineOptions = {}, createEngine = createMaintenanceEngine,
73
94
  // What the adapter factory last learned about the installed app, when it qualifies one. Codes and versions only.
74
95
  appStatus = null,
96
+ // Under `startup`: { changed() } (createReleaseWatch), asked after every tick.
97
+ releaseWatch = null,
98
+ // The entry as this process was started by it (`process.argv[1]`), when that is not its real path: a login item
99
+ // names the installed entry by its path. It is recorded, so the process table can still prove this process ours.
100
+ invokedAs = null,
75
101
  } = options
76
102
  if (typeof loadProject !== 'function') throw new TypeError('the service needs loadProject')
77
103
  if (typeof adapterFactory !== 'function') throw new TypeError('the service needs an adapterFactory')
@@ -94,10 +120,13 @@ export async function runMaintenanceService(options = {}) {
94
120
  }
95
121
 
96
122
  const executable = executableIdentity(entryPath)
123
+ const alias = typeof invokedAs === 'string' && path.isAbsolute(invokedAs) && path.resolve(invokedAs) !== executable.path ? path.resolve(invokedAs) : null
97
124
  const startedAt = isoTime(clock)
98
125
  const identity = { serviceName: serviceNameFor(workspaceId), workspaceId, runtimeId, pid, host, port, executableDigest: executable.digest, startedAt }
99
126
  const bearer = randomBytes(32).toString('base64url')
100
127
  const stateStore = createMaintenanceStateStore({ workspaceRoot, workspaceId })
128
+ // Session identities live in memory only and are always random, whatever `randomBytes` the service was given.
129
+ const pluginSessions = createPluginSessions()
101
130
 
102
131
  let stopping = false
103
132
  let consecutiveFailures = 0
@@ -105,7 +134,49 @@ export async function runMaintenanceService(options = {}) {
105
134
  let settleDone
106
135
  const done = new Promise((resolve) => { settleDone = resolve })
107
136
 
108
- const engine = createEngine({ watcherFactory: createFsWatcherFactory(), ...engineOptions, loadProject, dataRoot, adapterFactory, clock, env, platform, lockOwner: { host, port, runtimeId } })
137
+ // Every prepared view carries the plugin with this listener's address and the view's bearer, as the person's choice
138
+ // for that vault allows: the choice is read, and recorded when the vault shows it changed, from the community plugin
139
+ // list as it is now, and those very bytes travel with the view. A bearer or a choice that cannot be kept leaves that
140
+ // view without the plugin for this tick; the view itself is prepared as ever.
141
+ const pluginFor = (scopeId) => {
142
+ try {
143
+ const vaultRoot = viewVaultRoot(workspaceRoot, scopeId, workspaceId)
144
+ const { choice, community } = decidePluginChoice({ workspaceRoot, workspaceId, scopeId, vaultRoot, clock })
145
+ const off = choice.state === 'off'
146
+ const bearer = ensurePluginBearer({ workspaceRoot, workspaceId, scopeId, randomBytes, clock })
147
+ if (pluginChannel.bearers().get(scopeId) !== bearer) pluginChannel.bearersChanged()
148
+ const plugin = preparePluginFiles({ channel: { host, port }, scopeId, bearer, onlyIfPresent: off })
149
+ // Turned off: the files still there are kept current; a folder the person removed is not made again.
150
+ const files = off ? plugin.files.filter((file) => vaultFilePresent(vaultRoot, file.path)) : plugin.files
151
+ const kept = new Set(files.map((file) => file.path))
152
+ const unreadable = community.file.state === 'unreadable'
153
+ return {
154
+ files, ownership: { ...plugin.ownership, files: plugin.ownership.files.filter((entry) => kept.has(entry.path)) },
155
+ community: off || unreadable ? { entry: 'withheld', reason: off ? 'turned-off-in-this-vault' : 'list-unreadable' } : { entry: 'owned', existing: community.file.bytes },
156
+ }
157
+ } catch (error) {
158
+ log({ at: isoTime(clock), event: 'plugin-not-prepared', code: errorCode(error), name: errorName(error) })
159
+ return null
160
+ }
161
+ }
162
+ const prepareWithPlugin = (input) => (engineOptions.seams?.prepareView ?? productionPrepareView)({ ...input, plugin: pluginFor(input.scope.scopeId) })
163
+ // An entry offered to a vault and now in place is confirmed: from then on, a list without it is the person's decision.
164
+ const publishAndConfirm = async (input) => {
165
+ const result = await (engineOptions.seams?.publishView ?? productionPublishView)(input)
166
+ try { confirmPluginEntry({ workspaceRoot, workspaceId, scopeId: input.recoveryStore.scopeId, result, clock }) } catch (error) {
167
+ log({ at: isoTime(clock), event: 'plugin-entry-not-confirmed', code: errorCode(error), name: errorName(error) })
168
+ }
169
+ return result
170
+ }
171
+ // The app version a live plugin reports counts as checked for the view it holds open, while one launch of the plugin
172
+ // alone holds it: with two apps holding the vault, which one the command-line tool reaches is unknown.
173
+ const pluginReportOf = (scopeId) => { const report = pluginSessions.report(scopeId); return report?.instances === 1 ? report : null }
174
+ // The engine also asks the factory itself (`forget`, on a tick somebody asked for): the wrapper keeps its methods.
175
+ const pluginAwareAdapterFactory = Object.assign((input) => adapterFactory({ ...input, pluginReport: typeof input?.scope?.scopeId === 'string' ? pluginReportOf(input.scope.scopeId) : null }), adapterFactory)
176
+ const engine = createEngine({
177
+ watcherFactory: createFsWatcherFactory(), ...engineOptions, seams: { ...(engineOptions.seams ?? {}), prepareView: prepareWithPlugin, publishView: publishAndConfirm },
178
+ loadProject, dataRoot, adapterFactory: pluginAwareAdapterFactory, clock, env, platform, lockOwner: { host, port, runtimeId },
179
+ })
109
180
 
110
181
  const recordIsOurs = () => {
111
182
  try { const record = readServiceRecord({ workspaceRoot, workspaceId }); return record !== null && record.runtimeId === runtimeId && record.pid === pid } catch { return false }
@@ -128,12 +199,41 @@ export async function runMaintenanceService(options = {}) {
128
199
  writeLastServiceError({ workspaceRoot, workspaceId, document: { schema: SERVICE_ERROR_SCHEMA, workspaceId, runtimeId, code: errorCode(outcome.error), name: errorName(outcome.error), at, consecutiveFailures, totalFailures: (previous?.totalFailures ?? 0) + 1, resolvedAt: null } })
129
200
  }
130
201
 
202
+ // A plugin file a view's committed generation pins that is not on disk as pinned has the view prepared again, so the
203
+ // publisher writes it again; a drift is asked about once, not at every tick.
204
+ const pluginDrift = createPluginDriftObserver({ workspaceRoot, workspaceId })
205
+ const askForDriftedViews = () => {
206
+ try { for (const scopeId of pluginDrift.observe([...pluginChannel.bearers().keys()])) engine.requestPreparation(scopeId) } catch (error) {
207
+ log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
208
+ }
209
+ }
210
+ // What a publication of this tick could not change is no drift to ask about again (plugin-drift.mjs).
211
+ const settleDriftLooks = () => {
212
+ try { pluginDrift.settle([...pluginChannel.bearers().keys()]) } catch (error) {
213
+ log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
214
+ }
215
+ }
216
+
217
+ // A package that changed on disk: another release, loaded module by module into this process from then on.
218
+ const releaseChanged = () => {
219
+ if (!startup || releaseWatch === null) return false
220
+ try { return releaseWatch.changed() === true } catch { return false }
221
+ }
222
+
131
223
  const loop = createTickLoop({
132
224
  intervalMs, maxBackoffMs, onOutcome: recordOutcome,
133
225
  async tick() {
134
226
  // The service runs only while its record names it: a replaced or removed record ends it, cleanly.
135
227
  if (!recordIsOurs()) { void shutdown('record-no-longer-names-this-runtime'); return { state: 'stopping', reason: 'record-no-longer-names-this-runtime' } }
136
- return engine.tick()
228
+ askForDriftedViews()
229
+ try {
230
+ const report = await engine.tick()
231
+ if (report?.state === 'ticked') settleDriftLooks()
232
+ return report
233
+ } finally {
234
+ // After the tick, whether it succeeded or not: the next one runs in a process of the release now on disk.
235
+ if (!stopping && releaseChanged()) { log({ at: isoTime(clock), event: RELEASE_CHANGED }); void shutdown(RELEASE_CHANGED) }
236
+ }
137
237
  },
138
238
  })
139
239
 
@@ -150,6 +250,26 @@ export async function runMaintenanceService(options = {}) {
150
250
  }
151
251
 
152
252
  const healthStatus = () => (stopping ? 'stopped' : consecutiveFailures > 0 ? 'degraded' : 'healthy')
253
+
254
+ // One view as the plugin is told about it: its freshness entry with held notes counted, and its open pending edits.
255
+ function pluginStatusOf(scopeId) {
256
+ let entry = null
257
+ try { entry = stateStore.readFreshness()?.scopes.find((item) => item.scopeId === scopeId) ?? null } catch (error) { if (!isTyped(error)) throw error; return { view: null, pendingEdits: null } }
258
+ let open = null
259
+ try { open = stateStore.readPendingEdits().edits.filter((edit) => edit.scopeId === scopeId && OPEN_EDIT_STATES.includes(edit.state)).length } catch (error) { if (!isTyped(error)) throw error }
260
+ const view = entry === null ? null : {
261
+ state: entry.state, reason: entry.reason, verified: entry.verified, generationId: entry.generationId, preparedGenerationId: entry.preparedGenerationId,
262
+ heldNoteCount: entry.heldNotes.length, retainedEdits: entry.retainedEdits, checkedAt: entry.checkedAt,
263
+ }
264
+ return { view, pendingEdits: open === null ? null : { open } }
265
+ }
266
+ // A plugin that holds a view open shows that its app has the view's entry: an entry only offered so far is confirmed.
267
+ const pluginSeen = (scopeId) => {
268
+ try { confirmPluginSeen({ workspaceRoot, workspaceId, scopeId, clock }) } catch (error) {
269
+ log({ at: isoTime(clock), event: 'plugin-entry-not-confirmed', code: errorCode(error), name: errorName(error) })
270
+ }
271
+ }
272
+ const pluginChannel = createPluginChannel({ workspaceRoot, workspaceId, runtimeId, sessions: pluginSessions, statusOf: pluginStatusOf, serviceStatus: healthStatus, onSessionOpened: pluginSeen })
153
273
  const server = createServiceServer({
154
274
  identity, bearer,
155
275
  operations: {
@@ -158,14 +278,21 @@ export async function runMaintenanceService(options = {}) {
158
278
  let lastError = null
159
279
  try { lastError = readLastServiceError({ workspaceRoot, workspaceId }) } catch (error) { if (!isTyped(error)) throw error; lastError = { unreadable: error.code } }
160
280
  if (lastError?.schema) { const { schema: _schema, workspaceId: _workspace, ...shown } = lastError; lastError = shown }
161
- return { schema: SERVICE_STATUS_SCHEMA, service: { ...identity, status: healthStatus() }, loop: loop.state(), lastTick, lastError, freshness: freshnessSummary(), ...(typeof appStatus === 'function' ? { app: appStatus() } : {}) }
281
+ return {
282
+ schema: SERVICE_STATUS_SCHEMA, service: { ...identity, status: healthStatus() }, loop: loop.state(), lastTick, lastError, freshness: freshnessSummary(),
283
+ plugins: pluginPresence({ sessions: pluginSessions, scopeIds: [...pluginChannel.bearers().keys()], entryOf: (scopeId) => currentPluginChoice({ workspaceRoot, workspaceId, scopeId }).state }),
284
+ ...(typeof appStatus === 'function' ? { app: appStatus() } : {}),
285
+ }
162
286
  },
163
- async tick() {
287
+ // A tick asked for one view (`open` asks for its own) prepares and publishes that view once more.
288
+ async tick({ scopeId } = {}) {
289
+ if (scopeId !== undefined) engine.requestPreparation?.(scopeId)
164
290
  const outcome = await loop.tickNow()
165
291
  if (outcome.stopped) return { ok: false, stopped: true }
166
292
  return outcome.ok ? { ok: true, ...summary(outcome.report) } : { ok: false, error: { code: errorCode(outcome.error), name: errorName(outcome.error) } }
167
293
  },
168
294
  stop: () => shutdown('stop-requested'),
295
+ plugin: (command, request) => pluginChannel.handle(command, request),
169
296
  },
170
297
  })
171
298
 
@@ -202,7 +329,7 @@ export async function runMaintenanceService(options = {}) {
202
329
  workspaceRoot, workspaceId,
203
330
  record: {
204
331
  schema: 'atelier-obsidian-service-state/v1', contractVersion: '1.0.0', workspaceId, serviceName: identity.serviceName, host, port, runtimeId, pid,
205
- executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
332
+ executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity(), ...(alias === null ? {} : { invokedAs: alias }) } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
206
333
  health: { status: 'healthy', checkedAt: startedAt }, consent: settings.consent, ext: { bearer },
207
334
  },
208
335
  })