@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,7 +1,9 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { createRecoveryStore as createStore, readFileBytes, sha256Digest } from '../../projection/obsidian/recovery/store.mjs'
3
+ import { enclosingVaults, findVaultEntry, vaultRoute } from '../../projection/obsidian/publication/vault-list.mjs'
4
+ import { createRecoveryStore as createStore, readFileBytes, sha256Digest, vaultRootFor } from '../../projection/obsidian/recovery/store.mjs'
4
5
  import { inspectApp, qualifyApp } from './app-capability.mjs'
6
+ import { settingsWriteOutlook } from './app-registration.mjs'
5
7
  import { readObsidianEnablement } from './enablement.mjs'
6
8
  import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
7
9
  import { UNAVAILABLE_APPLY_OPERATION } from './extension-points.mjs'
@@ -24,25 +26,44 @@ import { OPEN_EDIT_STATES, createMaintenanceStateStore } from './state-store.mjs
24
26
  // that same generation and every note in the vault has the bytes that
25
27
  // generation was published with;
26
28
  // - the installed app is present, has its command-line capability, meets
27
- // the minimum version, was asked to open the vault, answers for exactly
28
- // this vault and has finished reading it.
29
+ // the minimum version, knows the vault as one of its vaults, was asked to
30
+ // open it, answers for exactly this vault and has finished reading it.
29
31
  //
30
- // Nothing here talks to an app or an operating system itself: `appProbe` and
31
- // `launcher` are injected, and there is no default for either.
32
+ // Nothing a person does by hand is needed on the way. The app is made to know
33
+ // the vault: through the app itself while it runs and answers, or, while no
34
+ // Obsidian runs, in the app's own vault list (app-registration.mjs), which is
35
+ // created for an Obsidian that never started, and whose command-line switch is
36
+ // turned on in the same write. A view that the app kept from being published
37
+ // (an app that runs without the vault, or that could not be qualified) is
38
+ // published again once the app holds the vault, through the app, before the
39
+ // answer is given.
40
+ //
41
+ // Two states of a running app cannot be got through that way: its command line
42
+ // is turned off, or it has no vault open and does not list this one. Only when
43
+ // the person asked for it (`restartApp`, `--restart-obsidian`; never implied),
44
+ // and only after a read-only check that the write can succeed and a copy of the
45
+ // settings file kept beside it, the app is quit (app-restart.mjs), the vault
46
+ // added with the switch on while it is quit, and the app started again plainly,
47
+ // which reopens every vault it had open. Without that, those states are
48
+ // answered, naming the flag.
49
+ //
50
+ // Nothing here talks to an app or an operating system itself: `appProbe`,
51
+ // `registry`, `launcher` and `quitter` are injected, and there is no default for
52
+ // any.
32
53
 
33
54
  export const OPENING_OUTCOMES = Object.freeze({
34
55
  current: { summary: 'the vault is the present generation, verified by read-back, and the app has it open', next: 'nothing to do' },
35
56
  updating: { summary: 'maintenance is publishing or has not finished; the vault is not confirmed current', next: 'run `obsidian open` again in a moment' },
36
57
  'held-for-your-edit': { summary: 'a note you edited is preserved and held; the view is not republished over it', next: 'apply or withdraw the pending edit, then open again' },
37
58
  'stale-readable': { summary: 'a last good vault exists and can be read, but it is not proven to be the present generation', next: 'see `obsidian status` for the reason; `obsidian open --allow-stale` opens it as it is' },
38
- 'not-prepared': { summary: 'no generation of this view has been published yet', next: 'see `obsidian status` for the reason' },
59
+ 'not-prepared': { summary: 'no generation of this view can be read back: none was published yet, or its vault cannot be found or used (the reason says which)', next: 'see `obsidian status` for the reason' },
39
60
  'app-missing': { summary: 'no Obsidian installation was found', next: 'install Obsidian, then open again' },
40
61
  'app-version-unsupported': { summary: 'the installed Obsidian is below the minimum supported version, or its version cannot be read', next: 'update Obsidian, then open again' },
41
62
  'app-cli-unavailable': { summary: 'the installed Obsidian has no usable command-line capability', next: 'enable the command-line interface in Obsidian, then open again' },
42
63
  indexing: { summary: 'the app answered for this vault but has not finished reading it', next: 'wait for Obsidian to finish indexing, then open again' },
43
64
  'publisher-conflict': { summary: 'another publisher or an uncoordinated editor holds this vault', next: 'close the other publisher or let it finish; it is retried automatically' },
44
- 'launch-failed': { summary: 'the operating system or the app did not open this vault', next: 'open the vault folder in Obsidian once by hand, then open again' },
45
- 'service-unavailable': { summary: 'the maintenance service of this workspace could not be started or reached as ours', next: 'see `obsidian service status`' },
65
+ 'launch-failed': { summary: 'the operating system or the app did not open this vault', next: 'run `obsidian open` again; if it fails the same way, start Obsidian with any vault open and open again' },
66
+ 'service-unavailable': { summary: 'the maintenance service of this workspace could not be started or reached as ours, or runs a later release than this command', next: 'see `obsidian service status`' },
46
67
  busy: { summary: 'the maintenance service runs but is in a long tick and did not answer in time', next: 'run `obsidian open` again in a moment; nothing was stopped or restarted' },
47
68
  disabled: { summary: 'the Obsidian integration is not enabled for this project, or this view is not configured', next: 'enable it in the project configuration' },
48
69
  })
@@ -57,34 +78,77 @@ export function describeOutcome(code) {
57
78
 
58
79
  // A reason whose next step is not its outcome's. With no vault open the app's
59
80
  // command line answers nothing, its version included, so the version floor
60
- // cannot be checked until a vault is open or the app is quit.
81
+ // cannot be checked, and a vault the app does not know yet cannot be added
82
+ // through it, until a vault is open or the app is quit.
61
83
  // `editor-uncoordinated` is the publisher stopping because an Obsidian that may
62
84
  // hold this vault could not be coordinated with. There is no other publisher to
63
85
  // wait for, and the cause is not known here: the app runs without this vault,
64
86
  // or with it but its command line did not answer, or the process table was
65
87
  // unknown (on Linux, any app that runs on a system Electron), or an app started
66
- // while the view was published with the app closed. Every cause clears once no
67
- // such process runs, and a view that was published before can instead be
68
- // opened in the app as it is, which then answers for it.
88
+ // while the view was published with the app closed. `open` makes the app hold
89
+ // the vault and publishes through it; every cause also clears once no such
90
+ // process runs.
69
91
  const UNCOORDINATED = 'an Obsidian that may hold this vault could not be coordinated with, so publication stopped'
70
92
  const QUIT = 'quit Obsidian (on Linux, also any app that runs on a system Electron)'
93
+ // Obsidian's settings file could not be used to add the vault while the app was quit: adding it through the app works instead.
94
+ const THROUGH_THE_APP = 'start Obsidian with any vault open, then open again: the vault is then added through the app'
71
95
  export const REASON_NEXT = Object.freeze({
72
- 'no-vault-open': 'open any vault in Obsidian, or quit Obsidian, then open again',
73
- 'editor-uncoordinated': `${UNCOORDINATED}; ${QUIT}, or open this view in it with \`atelier obsidian open --allow-stale\`; it is retried automatically`,
96
+ 'cli-turned-off': 'Obsidian\'s command line is turned off (the default of a new installation), and it runs, so its settings are its own: run `atelier obsidian open --restart-obsidian` to let Atelier quit Obsidian, turn its command line on, add this view\'s vault and start it again (the vaults it has open reopen); or turn it on yourself in Obsidian under Settings > General > Advanced > Command line interface, then open again',
97
+ 'no-vault-open': 'Obsidian runs with no vault open, so it can be asked nothing: run `atelier obsidian open --restart-obsidian` to let Atelier quit Obsidian (no vault is open, so nothing closes), add this view\'s vault and start it again; or open any vault in Obsidian, or quit Obsidian, then open again',
98
+ 'restart-platform-unqualified': 'Atelier restarts Obsidian on macOS only; turn its command line on yourself (Settings > General > Advanced > Command line interface), or open any vault in Obsidian, or quit it, then open again',
99
+ 'restart-unavailable': 'this command was given no way to quit Obsidian; quit Obsidian yourself, then open again',
100
+ 'app-main-process-unproven': 'Obsidian was not asked to quit: its main process could not be proven from the process table (`restart.detail` says why: more than one Obsidian main process, an Obsidian process of another user, a helper that is not its child, or a table that could not be read); quit Obsidian yourself, then open again',
101
+ 'app-did-not-quit': 'Obsidian was asked to quit and did not within the wait; nothing was written to its settings. Quit it yourself, or wait until it has quit, then open again',
102
+ 'app-not-started-again': 'Obsidian was quit to add this view\'s vault and could not be started again; start Obsidian yourself: it reopens the vaults it had open',
103
+ 'vault-open-cli-silent': 'Obsidian has this view\'s vault open, as Atelier\'s plugin in it shows, but its command line did not answer, so the vault can be neither found nor opened through it; make sure the command-line interface is turned on in Obsidian\'s settings, then open again',
104
+ 'editor-uncoordinated': `${UNCOORDINATED}; \`atelier obsidian open\` adds this view's vault to Obsidian and publishes through it, or ${QUIT}; it is retried automatically`,
105
+ 'obsidian-settings-missing': 'Obsidian has no settings file on this account, and the folder it would be in does not exist or is not yours, so Atelier cannot create it: open any vault in Obsidian (starting it if it is not running; it then writes its settings), then open again',
106
+ 'obsidian-settings-location-unknown': THROUGH_THE_APP,
107
+ 'obsidian-settings-unsafe': `Obsidian's settings file is a link or not a regular file, so it is not written; ${THROUGH_THE_APP}`,
108
+ 'obsidian-settings-not-owned': `Obsidian's settings file belongs to another user, so it is not written; ${THROUGH_THE_APP}`,
109
+ 'obsidian-settings-unreadable': `Obsidian's settings file cannot be read as JSON, so it is not written; ${THROUGH_THE_APP}`,
110
+ 'obsidian-settings-not-object': `Obsidian's settings file is not a JSON object, so it is not written; ${THROUGH_THE_APP}`,
111
+ 'obsidian-settings-unwritable': `Obsidian's settings directory cannot be written (space or permissions); ${THROUGH_THE_APP}`,
112
+ 'obsidian-settings-too-large': `with this view's vault, Obsidian's settings file would be larger than a settings file can be, so it is not written; ${THROUGH_THE_APP}`,
113
+ 'obsidian-sandboxed': `this Obsidian is a Flatpak or snap build, which keeps its vault list inside its sandbox, where Atelier does not write; ${THROUGH_THE_APP}`,
114
+ 'obsidian-settings-changed': 'Obsidian\'s settings file changed while the vault was being added, and nothing was written; open again',
115
+ 'app-may-be-running': 'Obsidian started while the vault was being added, and nothing was written; open again',
116
+ 'app-started-during-registration': 'Obsidian started just as this view\'s vault was added to its list and may not have read it; open again: a vault it missed is then added through it',
117
+ 'registration-not-read-back': 'this view\'s vault was added to Obsidian\'s settings file, but the file could not be read back to confirm it; open again',
118
+ 'addition-not-answered': 'Obsidian did not answer when asked to add this view\'s vault, and its vault list does not show it; open again, or quit Obsidian and open again',
119
+ 'app-did-not-list-its-vaults': 'Obsidian runs but did not answer with its vault list; open again, or quit Obsidian and open again',
120
+ 'app-refused-registration': 'Obsidian did not accept this view\'s vault folder as a vault; quit Obsidian and open again',
121
+ 'registration-not-verified': 'Obsidian answered, but its vault list does not show this view\'s vault; quit Obsidian and open again',
122
+ 'restarted-service-in-its-first-tick': 'the maintenance service ran an earlier release and was restarted on the installed one, which is still in its first tick; run `obsidian open` again in a moment',
123
+ 'service-outdated': 'the maintenance service runs an earlier release of Atelier that could not be replaced; run `atelier obsidian service stop`, then open again',
124
+ 'service-other-release': 'the maintenance service runs a later release of Atelier than this command, which never replaces a later release by itself; run `atelier obsidian service stop`, then open again, or open with the later release',
125
+ 'vault-open-in-several-windows': 'Obsidian\'s vault list marks this view\'s folder open under more than one entry (in another letter case, or through a link), so it may hold the vault in more than one window, and a publication coordinates with one window only; remove the extra entries from Obsidian\'s vault list (Obsidian keeps the last window it closed marked open, so closing windows does not clear this), then open again',
126
+ 'vault-allocation-lost': 'this view was published, but its vault is not where Atelier can find it: no record of a folder allocated for it (state/allocations/<view>.json in Atelier\'s private state), and no folder under the data root (vaults/<view>); restore that record from a backup if its vault was allocated elsewhere, or else make the folder vaults/<view> under the data root: the view is published there at the next change at its sources',
127
+ 'vault-allocation-moved': 'this view\'s vault is now reached through a link, or leads somewhere other than where it was allocated, and Atelier publishes only into the folder at the path it recorded; put that folder back at the path `atelier obsidian location show` names, with no link on the way',
128
+ 'vault-allocation-replaced': 'another folder, or a link, is where this view\'s vault was allocated, and Atelier publishes only into the folder it made; put the vault back there, or move what is there away: a folder that is gone is made again at the next tick, and the view published into it',
129
+ 'vault-note-missing': 'a note of the generation this view last published is missing from its vault; the view is published again, and the note written back, at the next change at its sources',
130
+ 'vault-root-moved': 'this view\'s vault was removed, moved or replaced by a link while it was being published, and nothing more was written into it; a vault allocated for the view is made again at the next tick and published into; a vault under the data root is asked for as lost until its folder is there again',
131
+ 'vault-allocation-missing': 'the folder allocated to this view is gone; the maintenance service makes it again at its next tick, and publishes the view into it',
132
+ // A publisher-conflict whose first blocking cause is a file Atelier writes in the vault's settings folder. One change
133
+ // is enough to stop a publication, and the reason names only the first file that did: nothing says that no other
134
+ // publisher or editor is involved.
135
+ 'plugin-file-changed': 'a file of Atelier\'s plugin in this vault (under .obsidian/plugins) changed while the view was published, by another program or by you, so the publication did not finish; it is retried automatically, and if this keeps happening, stop what keeps writing there (a sync tool, say)',
136
+ 'settings-changed': 'a settings file Atelier writes in this vault (the plugin list or a plugin setting under .obsidian) changed while the view was published, by Obsidian, another program or you, and was not written over; it is retried automatically, reading the file as it is then',
137
+ 'vault-inside-another-vault': 'Obsidian lists another vault at a folder that contains this view\'s vault; open never adds a vault inside another one, and never sends a call that could reach that vault instead: remove that vault from Obsidian\'s vault list, or keep Atelier\'s data root outside that folder, then open again',
74
138
  })
75
139
 
76
- // Before a first publication there is no vault to open in the app: the view is
77
- // published only while no such process runs.
78
- const FIRST_PUBLICATION_NEXT = Object.freeze({
79
- 'editor-uncoordinated': `${UNCOORDINATED}; ${QUIT} so the view is published, then \`atelier obsidian open\` starts Obsidian on it`,
140
+ // What `open` answers when it could not clear the refusal itself (the app held the vault and the publication still
141
+ // stopped, or no app answered to be asked): no step is left that open could take.
142
+ const AFTER_OPEN_NEXT = Object.freeze({
143
+ 'editor-uncoordinated': `${UNCOORDINATED}, and open could not clear it; ${QUIT} so the view is published, then open again`,
80
144
  })
81
145
 
82
146
  // The next step for an outcome and its reason; null for a code that is not an
83
- // opening outcome. `published` says whether a published generation of the view
84
- // reads back.
85
- export function nextStep(outcome, reason, { published = true } = {}) {
147
+ // opening outcome. `afterOpen` says that the answer is `open`'s own, after it
148
+ // tried what it can.
149
+ export function nextStep(outcome, reason, { afterOpen = false } = {}) {
86
150
  if (typeof reason === 'string') {
87
- if (!published && Object.hasOwn(FIRST_PUBLICATION_NEXT, reason)) return FIRST_PUBLICATION_NEXT[reason]
151
+ if (afterOpen && Object.hasOwn(AFTER_OPEN_NEXT, reason)) return AFTER_OPEN_NEXT[reason]
88
152
  if (Object.hasOwn(REASON_NEXT, reason)) return REASON_NEXT[reason]
89
153
  }
90
154
  return Object.hasOwn(OPENING_OUTCOMES, outcome) ? OPENING_OUTCOMES[outcome].next : null
@@ -98,16 +162,30 @@ export const OPENING_PRIMITIVES = Object.freeze({
98
162
  readBackAgrees: ({ entry, verification }) => verification.readable && verification.generationId === entry.preparedGenerationId && verification.intact,
99
163
  // Whether the app may be shown this vault at all.
100
164
  appQualifies: (qualification) => qualification.outcome === 'qualified',
165
+ // Whether a view that is not current was kept from publication by the app: then making the app hold the vault
166
+ // and asking for the view again can make it current. A vault open in several windows is asked for again once one
167
+ // window is left; while more are open, open names them instead.
168
+ // A view the service did not publish because the app's command line is turned off is one of them: once the app runs
169
+ // with it on and holds the vault, it is published through it.
170
+ keptByApp: (view) => ['publisher-conflict', 'stale-readable', 'not-prepared'].includes(view.outcome) && ['editor-uncoordinated', 'vault-open-in-several-windows', 'app-version-unsupported', 'app-cli-unavailable'].includes(view.reason),
101
171
  })
102
172
 
103
173
  const segment = (identifier) => identifier.replaceAll(':', '_')
104
- const STORE_AREAS = (scopeId) => [['vaults', segment(scopeId)], ['state', 'manifests', segment(scopeId)], ['state', 'journals', segment(scopeId)], ['state', 'locks'], ['recovery', 'objects'], ['staging']]
174
+ const STORE_AREAS = (scopeId) => [['state', 'manifests', segment(scopeId)], ['state', 'journals', segment(scopeId)], ['state', 'locks'], ['recovery', 'objects'], ['staging']]
105
175
 
106
176
  // Reads the trusted pointer of a view and every note of its vault. Read-only:
107
177
  // the store is only constructed when everything it would create already exists.
178
+ // `vaultRoot` is the folder the store publishes this view into, whenever the
179
+ // store could be constructed, even before a first generation.
108
180
  export function readBackTrustedGeneration({ workspaceRoot, workspaceId, scopeId, repositoryRoots, createRecoveryStore = createStore }) {
109
181
  const unreadable = (reason) => ({ readable: false, reason, generationId: null, intact: false, noteCount: 0, differing: 0, missing: 0 })
110
- if (!STORE_AREAS(scopeId).every((parts) => fs.existsSync(path.join(workspaceRoot, ...parts)))) return unreadable('no-published-vault')
182
+ // The view's vault wherever it is: allocated for the view, or under the data root.
183
+ let vault
184
+ try { vault = vaultRootFor({ workspaceRoot, workspaceId, scopeId }) } catch (error) {
185
+ if (typeof error?.code === 'string') return unreadable(error.code)
186
+ throw error
187
+ }
188
+ if (!fs.existsSync(vault.path) || !STORE_AREAS(scopeId).every((parts) => fs.existsSync(path.join(workspaceRoot, ...parts)))) return unreadable('no-published-vault')
111
189
  let store
112
190
  let trusted
113
191
  try {
@@ -117,7 +195,7 @@ export function readBackTrustedGeneration({ workspaceRoot, workspaceId, scopeId,
117
195
  if (typeof error?.code === 'string') return unreadable(error.code)
118
196
  throw error
119
197
  }
120
- if (!trusted.pointer || !trusted.manifest) return unreadable('no-trusted-generation')
198
+ if (!trusted.pointer || !trusted.manifest) return { ...unreadable('no-trusted-generation'), vaultRoot: store.vaultRoot }
121
199
  let differing = 0
122
200
  let missing = 0
123
201
  for (const [notePath, base] of trusted.bases) {
@@ -163,11 +241,19 @@ export function scopeReport({ workspace, scopeId, repositoryRoots, serviceState,
163
241
  reason = edited ? 'vault-differs-from-trusted-generation' : verification.readable ? 'trusted-generation-differs' : verification.reason
164
242
  } else if (serviceState !== 'healthy') { outcome = 'stale-readable'; reason = serviceState === 'busy' ? 'maintenance-busy-not-rechecked' : 'maintenance-not-running' }
165
243
  else { outcome = 'current'; reason = entry.reason }
244
+ // Where the view's vault is, published or not: allocated for the view, or under the data root.
245
+ let vault
246
+ try { const found = vaultRootFor({ ...workspace, scopeId }); vault = { path: found.path, origin: found.origin } } catch (error) {
247
+ if (typeof error?.code !== 'string') throw error
248
+ vault = { path: null, origin: 'unreadable', reason: error.code }
249
+ }
166
250
  return {
167
- scopeId, ...describeOutcome(outcome), next: nextStep(outcome, reason, { published: verification.readable }), reason,
251
+ scopeId, ...describeOutcome(outcome), next: nextStep(outcome, reason), reason, vault,
168
252
  freshness: entry === null ? null : { state: entry.state, reason: entry.reason, verified: entry.verified, generationId: entry.generationId, preparedGenerationId: entry.preparedGenerationId, heldNoteCount: entry.heldNotes.length, retainedEdits: entry.retainedEdits, checkedAt: entry.checkedAt },
169
253
  readBack: { readable: verification.readable, reason: verification.reason, generationId: verification.generationId, intact: verification.intact, noteCount: verification.noteCount, differing: verification.differing, missing: verification.missing },
170
254
  pendingEdits: pendingSummary(stateStore, scopeId, applyAvailable),
255
+ // Notes of the view worth naming, and the rule concerned; see "Notes that were laid out anyway" in docs/obsidian-contract.md.
256
+ ...(entry?.diagnostics ? { diagnostics: entry.diagnostics } : {}),
171
257
  vaultRoot: verification.vaultRoot ?? null,
172
258
  }
173
259
  }
@@ -183,12 +269,96 @@ const defaultSleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms)
183
269
  const READABLE = new Set(['stale-readable', 'held-for-your-edit', 'updating', 'publisher-conflict'])
184
270
  // After a launch: the app is still starting, or still opening the vault it was asked for.
185
271
  const NOT_UP_YET = new Set(['version-unknown', 'no-vault-open'])
272
+ // After a launch: how many rounds in a row the command line must be silent, with only the plugin giving the version,
273
+ // before `open` says the command line is what is missing.
274
+ const SILENT_ROUNDS = 2
275
+ const REGISTRY_OPERATIONS = ['listThroughApp', 'registerThroughApp', 'readSettings', 'registerInSettings']
276
+
277
+ const attempt = async (operation) => { try { return await operation() } catch { return null } }
278
+
279
+ // Makes the app know this vault as one of its vaults, by the state it is in:
280
+ //
281
+ // - answering its command line (it gave its version): its own list is read;
282
+ // a vault it does not list is added and opened through the app, and read
283
+ // back from its list (findVaultEntry);
284
+ // - running with no vault open: its command line answers nothing, and its
285
+ // settings file is the running app's, so it is only read: a vault it lists
286
+ // is opened by path, and one it does not list is `no-vault-open`. So is an
287
+ // app whose version only Atelier's plugin reported: its command line gave
288
+ // none, so nothing is asked through it, and a vault its settings do not
289
+ // show is `vault-open-cli-silent` (the plugin shows the vault open);
290
+ // - not running: the vault is added to its settings file, which is written
291
+ // only while no Obsidian runs (registerVaultInObsidianSettings). An app
292
+ // that started just after that write may have read its list before it, and
293
+ // would then not find the vault by path: that is answered, never launched.
294
+ //
295
+ // A vault inside a folder the list has as a vault already is never added
296
+ // (`vault-inside-another-vault`): the app would show its notes in that vault
297
+ // too, and a call run in its folder would reach that vault.
298
+ //
299
+ // { ok: true, path, how, open, vaults } with the path the app knows the vault
300
+ // by, whether its list flags it open (an app started plainly reopens it then),
301
+ // and the list it was found in, or { ok: false, reason }.
302
+ //
303
+ // The window the app opens for an added vault takes the next command-line
304
+ // call while it may still be loading, and can then answer that a command
305
+ // does not exist yet: the list is asked again, a bounded number of times,
306
+ // before an addition counts as not verified.
307
+ const VERIFY_ATTEMPTS = 10
308
+ async function ensureAppKnowsVault({ registry, observation, vaultRoot, sleep, pollMs, keepBackups = [] }) {
309
+ const inside = (vaults) => enclosingVaults({ vaults, vaultRoot }).length > 0
310
+ if (observation.noVaultOpen === true) {
311
+ const settings = await attempt(() => registry.readSettings())
312
+ const vaults = settings?.ok === true ? settings.vaults : null
313
+ const entry = findVaultEntry(vaults, vaultRoot)
314
+ if (entry) return { ok: true, path: entry.path, how: 'listed', open: entry.open === true, vaults }
315
+ return { ok: false, reason: inside(vaults) ? 'vault-inside-another-vault' : observation.fromPlugin === true ? 'vault-open-cli-silent' : 'no-vault-open' }
316
+ }
317
+ if (observation.answering === true) {
318
+ const listed = await attempt(() => registry.listThroughApp())
319
+ if (listed?.answered !== true) return { ok: false, reason: listed?.reason === 'no-vault-open' ? 'no-vault-open' : 'app-did-not-list-its-vaults' }
320
+ const known = findVaultEntry(listed.vaults, vaultRoot)
321
+ if (known) return { ok: true, path: known.path, how: 'listed', open: known.open === true, vaults: listed.vaults }
322
+ if (inside(listed.vaults)) return { ok: false, reason: 'vault-inside-another-vault' }
323
+ const asked = await attempt(() => registry.registerThroughApp({ vaultRoot }))
324
+ if (asked?.answered !== true && asked?.reason === 'no-vault-open') return { ok: false, reason: 'no-vault-open' }
325
+ if (asked?.answered === true && asked.result !== true) return { ok: false, reason: 'app-refused-registration' }
326
+ // An addition that was not answered (a call that timed out, say) may still have been made: the list tells.
327
+ for (let attempts = 1; ; attempts += 1) {
328
+ const again = await attempt(() => registry.listThroughApp())
329
+ const added = again?.answered === true ? findVaultEntry(again.vaults, vaultRoot) : null
330
+ if (added) return { ok: true, path: added.path, how: 'added-through-app', open: true, vaults: again.vaults }
331
+ if (attempts >= VERIFY_ATTEMPTS) return { ok: false, reason: asked?.answered === true ? 'registration-not-verified' : 'addition-not-answered' }
332
+ await sleep(pollMs)
333
+ }
334
+ }
335
+ const written = await attempt(() => registry.registerInSettings({ vaultRoot, keepBackups }))
336
+ if (written === null) return { ok: false, reason: 'obsidian-settings-unwritable' }
337
+ if (written.ok !== true) return { ok: false, reason: typeof written.code === 'string' ? written.code : 'obsidian-settings-unwritable' }
338
+ const settings = settingsWritten(written)
339
+ if (written.confirmed !== true) return { ok: false, reason: written.reason === 'registration-not-read-back' ? 'registration-not-read-back' : 'app-started-during-registration', ...(settings === null ? {} : { settings }) }
340
+ const how = written.registered === 'created' ? 'created-settings' : written.registered === 'already' ? 'listed' : 'added-to-settings'
341
+ // A vault already listed keeps its own open flag; one written here is written flagged open.
342
+ return { ok: true, path: written.entry.path, how, open: written.registered === 'already' ? written.entry.open === true : true, vaults: written.vaults, ...(settings === null ? {} : { settings }) }
343
+ }
344
+
345
+ // What was written to Obsidian's settings file, for the answer to show: null when nothing was.
346
+ function settingsWritten(written) {
347
+ const created = written.registered === 'created'
348
+ const cliTurnedOn = written.cliTurnedOn === true
349
+ if (!created && written.registered !== 'written' && !cliTurnedOn) return null
350
+ return {
351
+ ...(typeof written.file === 'string' ? { file: written.file } : {}), created, ...(created ? { directoryCreated: written.created?.directory === true } : {}),
352
+ vaultAdded: written.registered !== 'already', cliTurnedOn, ...(typeof written.backupPath === 'string' ? { backupPath: written.backupPath } : {}),
353
+ }
354
+ }
186
355
 
187
356
  // Starts or reconnects the owned service, asks it for a tick, reads the view
188
- // back, qualifies the app, has the vault opened and reports one outcome.
357
+ // back, qualifies the app, makes the app know the vault, has it opened, asks
358
+ // again for a view the app kept from being published, and reports one outcome.
189
359
  export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIMITIVES, lifecycleRules = LIFECYCLE_PRIMITIVES) {
190
360
  const {
191
- loadProject, dataRoot, scopeId: requestedScope, appProbe, launcher, service = {}, consent, allowStale = false, extensions = null,
361
+ loadProject, dataRoot, scopeId: requestedScope, appProbe, launcher, registry, quitter = null, restartApp = false, service = {}, consent, allowStale = false, extensions = null,
192
362
  tickTimeoutMs = 120 * 1000, appWaitMs = 30 * 1000, appPollMs = 500, sleep = defaultSleep, monotonic = () => Date.now(),
193
363
  env = process.env, platform = process.platform, probeTimeoutMs,
194
364
  } = options
@@ -196,19 +366,30 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
196
366
  // No defaults: reaching an app or the operating system is a decision of the entry that composes this.
197
367
  if (typeof appProbe?.inspect !== 'function' || typeof appProbe?.vaultState !== 'function') throw new TypeError('open needs an injected appProbe')
198
368
  if (typeof launcher?.open !== 'function') throw new TypeError('open needs an injected launcher')
369
+ if (!REGISTRY_OPERATIONS.every((name) => typeof registry?.[name] === 'function')) throw new TypeError('open needs an injected registry')
199
370
  if (typeof service.entryPath !== 'string') throw new TypeError('open needs the service entry it may start')
200
371
 
201
372
  const project = loadProject()
202
373
  const enablement = readObsidianEnablement(project)
203
374
  const applyAvailable = extensions !== null && extensions.applyOperation() !== UNAVAILABLE_APPLY_OPERATION
204
- const finish = (outcome, extra = {}) => ({ ok: outcome === 'current', ...describeOutcome(outcome), next: nextStep(outcome, extra.reason, { published: extra.readBack?.readable !== false }), launched: false, ...extra })
375
+ // A restart this run made, and what it wrote to Obsidian's settings: every answer after them shows both.
376
+ let restart = null
377
+ let settingsShown = null
378
+ const finish = (outcome, extra = {}) => {
379
+ const { afterOpen = false, ...shown } = extra
380
+ const next = restart?.quit === true && restart.startedAgain === false ? REASON_NEXT['app-not-started-again'] : nextStep(outcome, shown.reason, { afterOpen })
381
+ return { ok: outcome === 'current', ...describeOutcome(outcome), next, launched: false, ...shown, ...(restart === null ? {} : { restart }), ...(settingsShown === null ? {} : { obsidianSettings: settingsShown }) }
382
+ }
205
383
  if (enablement.state === 'disabled') return finish('disabled', { reason: enablement.reason, scopeId: requestedScope ?? null })
206
384
  const scopeId = resolveScope(enablement, requestedScope)
207
385
  const lifecycle = { loadProject, dataRoot, env, platform, ...(probeTimeoutMs === undefined ? {} : { probeTimeoutMs }) }
208
386
 
209
- // 1. The owned service: reconnect, or start. Never adopt, never replace.
387
+ // 1. The owned service: reconnect, or start. Never adopt; a runtime of ours is replaced only when it runs an earlier
388
+ // release, under the consent already recorded, when a tick is asked of it (requestServiceTick).
210
389
  let status = await serviceStatus(lifecycle, lifecycleRules)
211
390
  if (status.state === 'busy') return finish('busy', { scopeId, reason: status.reason, service: { state: status.state } })
391
+ // How a login item took part in starting the service (started through it, refreshed, or not used and why).
392
+ let loginItem = null
212
393
  if (status.state !== 'healthy') {
213
394
  let started
214
395
  try {
@@ -217,61 +398,186 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
217
398
  if (!(error instanceof ObsidianMaintenanceRefusal)) throw error
218
399
  return finish('service-unavailable', { scopeId, reason: error.code, service: { state: status.state } })
219
400
  }
220
- if (started.state === 'busy') return finish('busy', { scopeId, reason: started.reason, service: { state: 'busy', started: started.started } })
221
- if (started.state !== 'healthy') return finish('service-unavailable', { scopeId, reason: started.reason ?? started.state, service: { state: started.state } })
401
+ loginItem = started.loginItem ?? null
402
+ const item = loginItem === null ? {} : { loginItem }
403
+ if (started.state === 'busy') return finish('busy', { scopeId, reason: started.reason, service: { state: 'busy', started: started.started, ...item } })
404
+ if (started.state !== 'healthy') return finish('service-unavailable', { scopeId, reason: started.reason ?? started.state, service: { state: started.state, ...item } })
222
405
  status = started
223
406
  }
224
-
225
- // 2. A tick that starts after this request, bounded.
226
- const asked = await requestServiceTick({ ...lifecycle, tickTimeoutMs }, lifecycleRules)
227
- const serviceShown = { state: asked.state, runtimeId: status.record?.runtimeId ?? null }
228
- if (asked.state === 'busy') return finish('busy', { scopeId, reason: asked.reason, service: serviceShown })
229
- if (!asked.requested) return finish('service-unavailable', { scopeId, reason: asked.reason, service: serviceShown })
407
+ let runtimeId = status.record?.runtimeId ?? null
408
+ let restarted = null
230
409
  const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
231
410
  const report = (freshnessOverride) => scopeReport({ workspace, scopeId, repositoryRoots: protectedRoots(project), serviceState: 'healthy', applyAvailable, ...(freshnessOverride === undefined ? {} : { freshness: freshnessOverride }) }, rules)
232
- let view = report()
233
- const tick = asked.pending ? null : asked.tick
234
- if (asked.pending && view.outcome === 'current') view = { ...view, ...describeOutcome('updating'), reason: 'tick-still-running' }
235
- else if (!asked.pending && tick?.state === 'busy') return finish('busy', { scopeId, reason: 'engine-lock-held', service: serviceShown })
236
- else if (!asked.pending && (tick === null || tick.ok !== true || tick.state !== 'ticked') && view.outcome === 'current') {
237
- // The tick after this request did not complete: what was current before is not proven current now.
238
- view = { ...view, ...describeOutcome('stale-readable'), reason: `tick-${tick?.state ?? tick?.error?.code ?? 'unanswered'}` }
411
+
412
+ // A tick that starts after this request, bounded, which prepares and publishes this view once more, and the view as
413
+ // it left it. { view, serviceShown } or { answer }.
414
+ const tickAndReport = async () => {
415
+ const asked = await requestServiceTick({ ...lifecycle, tickTimeoutMs, scopeId, service }, lifecycleRules)
416
+ if (asked.restarted) restarted = asked.restarted
417
+ if (typeof asked.runtimeId === 'string') runtimeId = asked.runtimeId
418
+ const serviceShown = { state: asked.state, runtimeId, ...(restarted === null ? {} : { restarted }), ...(loginItem === null ? {} : { loginItem }) }
419
+ if (asked.state === 'busy') return { answer: { outcome: 'busy', extra: { scopeId, reason: asked.reason, service: serviceShown } } }
420
+ if (!asked.requested) return { answer: { outcome: 'service-unavailable', extra: { scopeId, reason: asked.reason, service: serviceShown } } }
421
+ let view = report()
422
+ const tick = asked.pending ? null : asked.tick
423
+ if (asked.pending && view.outcome === 'current') view = { ...view, ...describeOutcome('updating'), reason: 'tick-still-running' }
424
+ else if (!asked.pending && tick?.state === 'busy') return { answer: { outcome: 'busy', extra: { scopeId, reason: 'engine-lock-held', service: serviceShown } } }
425
+ else if (!asked.pending && (tick === null || tick.ok !== true || tick.state !== 'ticked') && view.outcome === 'current') {
426
+ // The tick after this request did not complete: what was current before is not proven current now.
427
+ view = { ...view, ...describeOutcome('stale-readable'), reason: `tick-${tick?.state ?? tick?.error?.code ?? 'unanswered'}` }
428
+ }
429
+ return { view, serviceShown }
239
430
  }
240
- const common = { scopeId, reason: view.reason, service: serviceShown, freshness: view.freshness, readBack: view.readBack, pendingEdits: view.pendingEdits }
241
- if (view.outcome !== 'current' && !(allowStale && READABLE.has(view.outcome) && view.readBack.readable)) return finish(view.outcome, common)
431
+ const shownOf = (view, serviceShown) => ({ scopeId, reason: view.reason, service: serviceShown, freshness: view.freshness, readBack: view.readBack, pendingEdits: view.pendingEdits })
432
+
433
+ // 2. The view, after a tick.
434
+ const first = await tickAndReport()
435
+ if (first.answer) return finish(first.answer.outcome, first.answer.extra)
436
+ let { view } = first
437
+ let common = shownOf(view, first.serviceShown)
438
+ // A view the app kept from being published goes on: the app is made to hold the vault, and the view asked for again.
439
+ const readable = allowStale && READABLE.has(view.outcome) && view.readBack.readable
440
+ const keptByApp = view.outcome !== 'current' && rules.keptByApp(view)
441
+ if (view.outcome !== 'current' && !readable && !keptByApp) return finish(view.outcome, common)
242
442
 
243
- // 3. The app, before anything is launched.
244
- const before = qualifyApp(await inspectApp(appProbe), { requireVersion: false })
443
+ // 3. The app, before anything is launched. With no vault open it answers nothing; a vault it already lists can still be opened by path.
444
+ let before = qualifyApp(await inspectApp(appProbe), { requireVersion: false })
245
445
  const app = (qualification) => ({ outcome: qualification.outcome, reason: qualification.reason, version: qualification.version, floor: qualification.floor })
246
- if (!rules.appQualifies(before)) return finish(before.outcome, { ...common, reason: before.reason, app: app(before) })
247
446
 
248
- // 4. Launch, then wait, bounded, for the app to answer for exactly this vault.
447
+ // A running app that cannot be reached (`state`: its command line is off, or it has no vault open and does not list
448
+ // this one) is quit, only when the person asked for it. { quit: true } with the app gone, or { answer } to give.
449
+ const QUIT_APP = { installed: true, cli: true, running: false, version: null }
450
+ const quitToRestart = async (state, stateOutcome) => {
451
+ if (!restartApp) return { answer: finish(stateOutcome, { ...common, reason: state, app: app(before) }) }
452
+ if (typeof quitter?.quit !== 'function' || typeof launcher.startPlain !== 'function' || typeof registry.backupSettings !== 'function') return { answer: finish(stateOutcome, { ...common, reason: 'restart-unavailable', app: app(before) }) }
453
+ // Read-only, before anything is sent: the write the restart is for can succeed. What it would refuse is answered
454
+ // now, with the app left running.
455
+ const notRestarted = (outcome, reason) => { restart = { asked: true, state, quit: false, signalled: false, signals: 0, reason }; return { answer: finish(outcome, { ...common, reason, app: app(before) }) } }
456
+ if (typeof view.vaultRoot !== 'string') return notRestarted('not-prepared', 'no-vault-folder')
457
+ const outlook = settingsWriteOutlook({ settings: await attempt(() => registry.readSettings()), vaultRoot: view.vaultRoot })
458
+ if (!outlook.ok) return notRestarted('launch-failed', outlook.code)
459
+ // A copy of the settings as they are, kept beside them once the main process is proven, right before the first
460
+ // signal; a copy that cannot be kept sends nothing. An app that has shown only its starter window has no file yet:
461
+ // there is nothing to copy, and the write creates it.
462
+ const quit = await attempt(() => quitter.quit({ beforeSignal: outlook.create === true ? null : () => registry.backupSettings() }))
463
+ restart = {
464
+ asked: true, state, quit: quit?.quit === true, signalled: quit?.signalled === true, ...(Number.isSafeInteger(quit?.pid) ? { pid: quit.pid } : {}),
465
+ signals: Number.isSafeInteger(quit?.signals) ? quit.signals : 0, ...(typeof quit?.settingsCopy === 'string' ? { settingsCopy: quit.settingsCopy } : {}),
466
+ }
467
+ if (quit?.quit === true) { before = qualifyApp(QUIT_APP, { requireVersion: false }); return { quit: true } }
468
+ restart.reason = typeof quit?.reason === 'string' ? quit.reason : 'app-did-not-quit'
469
+ if (typeof quit?.detail === 'string') restart.detail = quit.detail
470
+ // An app that was signalled and has not gone is neither started again nor written for: the person decides.
471
+ return { answer: finish(quit?.signalled === true ? 'launch-failed' : stateOutcome, { ...common, reason: restart.reason, app: app(before) }) }
472
+ }
473
+ // After a restart, every answer given before the launch starts the app again as it was: plainly, so it reopens the
474
+ // vaults it had open. Nothing was added for this view.
475
+ const giveBack = async () => {
476
+ if (restart?.quit !== true || restart.startedAgain !== undefined) return
477
+ restart.startedAgain = (await attempt(() => launcher.startPlain())) === true
478
+ }
479
+ if (before.reason === 'cli-turned-off') {
480
+ const stopped = await quitToRestart('cli-turned-off', before.outcome)
481
+ if (stopped.answer) return stopped.answer
482
+ }
483
+ const noVaultOpen = before.reason === 'no-vault-open'
484
+ if (!rules.appQualifies(before) && !noVaultOpen) {
485
+ // No app answered and none is known to run (a process table that could not be read, on Linux any app on a system
486
+ // Electron): for a view that app kept, the view's own reason says what to do.
487
+ if (keptByApp && !readable && before.running !== true && before.version === null) return finish(view.outcome, { ...common, app: app(before), afterOpen: true })
488
+ return finish(before.outcome, { ...common, reason: before.reason, app: app(before) })
489
+ }
490
+
491
+ // 4. The app knows this vault as one of its vaults: the folder the view is published into, wherever that is. Every
492
+ // call about it then reaches it and no other vault: in its folder, or by its id when a vault listed at a folder
493
+ // above it would take a call run there.
494
+ const { vaultRoot } = view
495
+ if (typeof vaultRoot !== 'string') { await giveBack(); return finish('not-prepared', { ...common, reason: 'no-vault-folder', app: app(before) }) }
496
+ // A version Atelier's plugin reported is not the command line answering (withPluginReportedVersion).
497
+ const fromPlugin = before.versionSource === 'plugin'
498
+ const knowVault = () => ensureAppKnowsVault({ registry, observation: { answering: typeof before.version === 'string' && !fromPlugin, noVaultOpen: before.reason === 'no-vault-open' || fromPlugin, fromPlugin }, vaultRoot, sleep, pollMs: appPollMs, keepBackups: typeof restart?.settingsCopy === 'string' ? [restart.settingsCopy] : [] })
499
+ let known = await knowVault()
500
+ // With no vault open, a vault the app does not list cannot be added while it runs: a restart the person asked for.
501
+ if (!known.ok && known.reason === 'no-vault-open' && restart === null) {
502
+ const stopped = await quitToRestart('no-vault-open', 'app-version-unsupported')
503
+ if (stopped.answer) return stopped.answer
504
+ known = await knowVault()
505
+ }
506
+ if (known.settings) settingsShown = known.settings
507
+ // An app that answered its version and then closed its last vault window is one with no vault open; one that holds
508
+ // this vault open but whose command line does not answer has no usable command line.
509
+ if (!known.ok) { await giveBack(); return finish(known.reason === 'no-vault-open' ? 'app-version-unsupported' : known.reason === 'vault-open-cli-silent' ? 'app-cli-unavailable' : 'launch-failed', { ...common, reason: known.reason, app: app(before) }) }
510
+ const registration = { how: known.how }
511
+ const route = vaultRoute({ vaults: known.vaults, vaultRoot })
512
+ // Open in several windows, one per entry of the list that names its folder: each holds the vault, and a publication
513
+ // coordinates with one only. Nothing is launched; the entries are named.
514
+ if (route.how === 'duplicated') { await giveBack(); return finish('publisher-conflict', { ...common, reason: 'vault-open-in-several-windows', duplicates: route.entries, app: app(before), registration }) }
515
+ if (route.how !== 'folder' && route.how !== 'id') { await giveBack(); return finish('launch-failed', { ...common, reason: route.how === 'ambiguous' ? 'vault-inside-another-vault' : 'registration-not-verified', app: app(before), registration }) }
516
+
517
+ // 5. Launch, then wait, bounded, for the app to answer for exactly this vault.
249
518
  let launch
250
- try { launch = await launcher.open({ vaultRoot: view.vaultRoot }) } catch { launch = { launched: false, reason: 'launcher-threw' } }
251
- if (launch?.launched !== true) return finish('launch-failed', { ...common, reason: typeof launch?.reason === 'string' ? launch.reason : 'launcher-refused', app: app(before) })
519
+ // A quit app is started plainly and handed the vault once it answers, so the vaults its list flags open come back
520
+ // (launch-plan.mjs).
521
+ // The id only where the route found it reaches this vault first (another vault's folder name may equal an id);
522
+ // otherwise the vault's own path, which the app matches exactly since the folder is listed.
523
+ const target = route.how === 'id' ? { vaultId: route.id, vaultPath: known.path } : { vaultPath: known.path }
524
+ try { launch = await launcher.open({ vaultRoot: known.path, ...target, appRunning: before.running === true }) } catch { launch = { launched: false, reason: 'launcher-threw' } }
525
+ // A restart ends here: the app was started again, or could not be (the next step then says to start it).
526
+ if (restart?.quit === true) restart.startedAgain = launch?.launched === true
527
+ if (launch?.launched !== true) return finish('launch-failed', { ...common, reason: typeof launch?.reason === 'string' ? launch.reason : 'launcher-refused', app: app(before), registration })
252
528
  const deadline = monotonic() + appWaitMs
253
529
  let after = before
254
530
  let vault = { answered: false, indexReady: false }
531
+ // Rounds in a row in which only Atelier's plugin gave the version and the command line did not answer for the vault.
532
+ let silentRounds = 0
533
+ // A quit app started plainly reopens only the vaults its list flags open. A listed vault that is closed opens through
534
+ // the link alone, so a link the started app did not take (it answered its tool, then refused the link) is handed
535
+ // once more, to the app that now runs, one round later, before the vault counts as not answering.
536
+ let linkRetry = launch.reason === 'app-started-link-not-taken' && known.open !== true ? 'due' : 'none'
537
+ let rounds = 0
255
538
  for (;;) {
539
+ rounds += 1
256
540
  after = qualifyApp(await inspectApp(appProbe), { requireVersion: true })
257
541
  // A running app below the floor is final; an app that has not come up yet, or not yet opened a vault, is asked again.
258
- if (!rules.appQualifies(after) && !NOT_UP_YET.has(after.reason)) return finish(after.outcome, { ...common, launched: true, reason: after.reason, app: app(after) })
542
+ if (!rules.appQualifies(after) && !NOT_UP_YET.has(after.reason)) return finish(after.outcome, { ...common, launched: true, reason: after.reason, app: app(after), registration })
259
543
  if (rules.appQualifies(after)) {
260
- try { vault = await appProbe.vaultState({ vaultRoot: view.vaultRoot }) } catch { vault = { answered: false, indexReady: false } }
544
+ try { vault = await appProbe.vaultState({ vaultRoot, route }) } catch { vault = { answered: false, indexReady: false } }
261
545
  if (vault?.answered === true && vault.indexReady === true) break
262
- }
546
+ // Only the command line answers for a vault. An app whose version still only Atelier's plugin reports gives no
547
+ // answer there, and waiting changes nothing: its command line is what is missing, not a launch. One such round
548
+ // is not enough: a tool call that timed out while the app was busy opening the window looks the same once, so
549
+ // the answer is given at the second round in a row.
550
+ silentRounds = vault?.answered !== true && after.versionSource === 'plugin' ? silentRounds + 1 : 0
551
+ if (silentRounds >= SILENT_ROUNDS) return finish('app-cli-unavailable', { ...common, launched: true, reason: 'vault-open-cli-silent', app: app(after), registration })
552
+ if (linkRetry === 'due' && rounds >= 2 && vault?.answered !== true) {
553
+ linkRetry = 'done'
554
+ // Through the app's tool only: this `open` started the app, so the link never goes to the operating system.
555
+ try { await launcher.open({ vaultRoot: known.path, ...target, appRunning: true, startedByThisOpen: true }) } catch { /* the wait decides */ }
556
+ }
557
+ } else silentRounds = 0
263
558
  if (monotonic() >= deadline) break
264
559
  await sleep(appPollMs)
265
560
  }
266
- if (!rules.appQualifies(after)) return finish(after.outcome, { ...common, launched: true, reason: after.reason, app: app(after) })
267
- if (vault?.answered !== true) return finish('launch-failed', { ...common, launched: true, reason: 'app-did-not-answer-for-this-vault', app: app(after) })
268
- if (vault.indexReady !== true) return finish('indexing', { ...common, launched: true, reason: 'metadata-cache-not-ready', app: app(after) })
561
+ if (!rules.appQualifies(after)) return finish(after.outcome, { ...common, launched: true, reason: after.reason, app: app(after), registration })
562
+ // The wait ended on a silent round with only the plugin giving the version: the command line is what did not answer.
563
+ if (vault?.answered !== true && silentRounds > 0) return finish('app-cli-unavailable', { ...common, launched: true, reason: 'vault-open-cli-silent', app: app(after), registration })
564
+ if (vault?.answered !== true) return finish('launch-failed', { ...common, launched: true, reason: 'app-did-not-answer-for-this-vault', app: app(after), registration })
565
+ if (vault.indexReady !== true) return finish('indexing', { ...common, launched: true, reason: 'metadata-cache-not-ready', app: app(after), registration })
566
+
567
+ // 6. The app holds the vault now: a view it kept from being published is asked for again, and published through it.
568
+ if (keptByApp) {
569
+ const second = await tickAndReport()
570
+ if (second.answer) return finish(second.answer.outcome, { ...second.answer.extra, launched: true, app: app(after), registration })
571
+ view = second.view
572
+ common = shownOf(view, second.serviceShown)
573
+ if (view.outcome !== 'current') return finish(view.outcome, { ...common, launched: true, app: app(after), registration, afterOpen: true })
574
+ }
269
575
 
270
- // 5. Still what was verified? The launch took time.
271
- if (view.outcome !== 'current') return finish(view.outcome, { ...common, launched: true, app: app(after) })
576
+ // 7. Still what was verified? The launch took time.
577
+ if (view.outcome !== 'current') return finish(view.outcome, { ...common, launched: true, app: app(after), registration })
272
578
  const again = report()
273
- if (again.outcome !== 'current') return finish(again.outcome, { ...common, launched: true, reason: again.reason, freshness: again.freshness, readBack: again.readBack, app: app(after) })
274
- return finish('current', { ...common, launched: true, app: app(after) })
579
+ if (again.outcome !== 'current') return finish(again.outcome, { ...common, launched: true, reason: again.reason, freshness: again.freshness, readBack: again.readBack, app: app(after), registration })
580
+ return finish('current', { ...common, launched: true, app: app(after), registration })
275
581
  }
276
582
 
277
583
  // Production entry point: the production decisions, always.