@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.14

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 (531) hide show
  1. package/CHANGELOG.md +894 -0
  2. package/README.md +159 -6
  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-personal-workspace-manifest.v1.schema.json +87 -0
  21. package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
  22. package/contracts/atelier-template-adoption.v1.schema.json +223 -0
  23. package/contracts/atelier-template-profile.v1.schema.json +1168 -0
  24. package/contracts/atelier-trackable.v1.schema.json +781 -0
  25. package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
  26. package/docs/architecture.md +89 -0
  27. package/docs/assurance-controls.md +2 -2
  28. package/docs/blocks/capability-stewardship.md +17 -0
  29. package/docs/blocks/claims.md +16 -3
  30. package/docs/blocks/inquiry-harnesses.md +19 -0
  31. package/docs/blocks/learning-harnesses.md +17 -0
  32. package/docs/capability-stewardship.md +267 -0
  33. package/docs/coordination.md +51 -0
  34. package/docs/decision-practices.md +111 -0
  35. package/docs/decisions.md +29 -0
  36. package/docs/evidence-navigation.md +237 -0
  37. package/docs/exact-upgrades.md +3 -2
  38. package/docs/ingestion.md +181 -0
  39. package/docs/inquiry-harnesses.md +210 -0
  40. package/docs/install.md +80 -3
  41. package/docs/intake-and-guides.md +5 -0
  42. package/docs/integration-contract-decisions.md +24 -2
  43. package/docs/interaction-and-reflection.md +72 -0
  44. package/docs/knowledge-graph.md +20 -0
  45. package/docs/knowledge-health/api.md +30 -0
  46. package/docs/knowledge-health/assessment/api.md +96 -0
  47. package/docs/knowledge-health/assessment/capabilities.md +40 -0
  48. package/docs/knowledge-health/assessment/evaluation.md +27 -0
  49. package/docs/knowledge-health/assessment/facilitator.md +30 -0
  50. package/docs/knowledge-health/assessment/quickstart.md +61 -0
  51. package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
  52. package/docs/knowledge-health/devday/facilitator.md +16 -0
  53. package/docs/knowledge-health/devday/participant-response.md +22 -0
  54. package/docs/knowledge-health/devday/public-default.md +107 -0
  55. package/docs/knowledge-health/devday/quickstart.md +18 -0
  56. package/docs/knowledge-health/devday/support.md +22 -0
  57. package/docs/knowledge-health/exercise.md +17 -0
  58. package/docs/knowledge-health/facilitator.md +20 -0
  59. package/docs/knowledge-health/quickstart.md +94 -0
  60. package/docs/knowledge-health/readme.md +15 -0
  61. package/docs/knowledge-health/support.md +34 -0
  62. package/docs/knowledge-setup.md +248 -0
  63. package/docs/knowledge-workspace.md +264 -0
  64. package/docs/learning-harnesses.md +237 -0
  65. package/docs/learning.md +215 -0
  66. package/docs/local-services.md +223 -19
  67. package/docs/obsidian-contract.md +246 -15
  68. package/docs/obsidian-plugin.md +252 -7
  69. package/docs/obsidian.md +874 -52
  70. package/docs/optional-ingestion.md +83 -0
  71. package/docs/personal-workspace.md +266 -0
  72. package/docs/practical-judgment.md +92 -0
  73. package/docs/progressive-ingestion-acceptance.md +89 -0
  74. package/docs/progressive-ingestion.md +235 -0
  75. package/docs/project-command-inventory.md +1 -0
  76. package/docs/release-engineering.md +50 -0
  77. package/docs/repo-boundary-guard.md +120 -3
  78. package/docs/research-starter-quickstart.md +66 -0
  79. package/docs/research-starter.md +114 -0
  80. package/docs/semantic-ingestion.md +198 -0
  81. package/docs/semantic-operation.md +318 -0
  82. package/docs/skill-steward.md +6 -0
  83. package/docs/template-profile.md +164 -0
  84. package/docs/template-upgrades.md +114 -0
  85. package/docs/trackables.md +111 -0
  86. package/docs/upgrade.md +219 -2
  87. package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
  88. package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
  89. package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
  90. package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
  91. package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
  92. package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
  93. package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
  94. package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
  95. package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
  96. package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
  97. package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
  98. package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
  99. package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
  100. package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
  101. package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
  102. package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
  103. package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
  104. package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
  105. package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
  106. package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
  107. package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
  108. package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
  109. package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
  110. package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
  111. package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
  112. package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
  113. package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
  114. package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
  115. package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
  116. package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
  117. package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
  118. package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
  119. package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
  120. package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
  121. package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
  122. package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
  123. package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
  124. package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
  125. package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
  126. package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
  127. package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
  128. package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
  129. package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
  130. package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
  131. package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
  132. package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
  133. package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
  134. package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
  135. package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
  136. package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
  137. package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
  138. package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
  139. package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
  140. package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
  141. package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
  142. package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
  143. package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
  144. package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
  145. package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
  146. package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
  147. package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
  148. package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
  149. package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
  150. package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
  151. package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
  152. package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
  153. package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
  154. package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
  155. package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
  156. package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
  157. package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
  158. package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
  159. package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
  160. package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
  161. package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
  162. package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
  163. package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
  164. package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
  165. package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
  166. package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
  167. package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
  168. package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
  169. package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
  170. package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
  171. package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
  172. package/fixtures/atelier-interaction/act/valid/document.json +24 -0
  173. package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
  174. package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
  175. package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
  176. package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
  177. package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
  178. package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
  179. package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
  180. package/fixtures/atelier-judgment/case/valid/document.json +75 -0
  181. package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
  182. package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
  183. package/fixtures/atelier-judgment/native-decision.md +3 -0
  184. package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
  185. package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
  186. package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
  187. package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
  188. package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
  189. package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
  190. package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
  191. package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
  192. package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
  193. package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
  194. package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
  195. package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
  196. package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
  197. package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
  198. package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
  199. package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
  200. package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
  201. package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
  202. package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
  203. package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
  204. package/fixtures/atelier-learning/invalid/authority.json +14 -0
  205. package/fixtures/atelier-learning/valid/capture.json +13 -0
  206. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
  207. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
  208. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
  209. package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
  210. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
  211. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
  212. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
  213. package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
  214. package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
  215. package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
  216. package/fixtures/atelier-trackable/request/valid/document.json +22 -0
  217. package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
  218. package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
  219. package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
  220. package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
  221. package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
  222. package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
  223. package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
  224. package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
  225. package/fixtures/capability-packages/adoption.example.json +70 -0
  226. package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
  227. package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
  228. package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
  229. package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
  230. package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
  231. package/fixtures/capability-packages/research/capability-package.json +88 -0
  232. package/fixtures/capability-packages/research/capability-release.json +134 -0
  233. package/fixtures/capability-packages/research/evaluation.md +15 -0
  234. package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
  235. package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
  236. package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
  237. package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
  238. package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
  239. package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
  240. package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
  241. package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
  242. package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
  243. package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
  244. package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
  245. package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
  246. package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
  247. package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
  248. package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
  249. package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
  250. package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
  251. package/fixtures/evidence-navigation/valid/claim.json +36 -0
  252. package/fixtures/evidence-navigation/valid/host.json +16 -0
  253. package/fixtures/evidence-navigation/valid/profile.json +16 -0
  254. package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
  255. package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
  256. package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
  257. package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
  258. package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
  259. package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
  260. package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
  261. package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
  262. package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
  263. package/fixtures/harnesses/learning-cycle.json +780 -0
  264. package/fixtures/inquiry/workshop.json +675 -0
  265. package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
  266. package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
  267. package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
  268. package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
  269. package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
  270. package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
  271. package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
  272. package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
  273. package/fixtures/knowledge-health/calibration-suite.json +26 -0
  274. package/fixtures/knowledge-health/calibration-trial.json +37 -0
  275. package/fixtures/knowledge-health/corrected/checklist.md +18 -0
  276. package/fixtures/knowledge-health/devday/participant-record.json +19 -0
  277. package/fixtures/knowledge-health/expected-summary.json +27 -0
  278. package/fixtures/knowledge-health/workshop/README.md +20 -0
  279. package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
  280. package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
  281. package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
  282. package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
  283. package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
  284. package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
  285. package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
  286. package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
  287. package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
  288. package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
  289. package/fixtures/personal-workspace/overlay.json +24 -0
  290. package/fixtures/templates/local-library.v1.json +76 -0
  291. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
  292. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
  293. package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
  294. package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
  295. package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
  296. package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
  297. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
  298. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
  299. package/package.json +60 -5
  300. package/plugins/obsidian/main.js +57 -8
  301. package/plugins/obsidian/manifest.json +1 -1
  302. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  303. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  304. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  305. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  306. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  307. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  308. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  309. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  310. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  311. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  312. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  313. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  314. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  315. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  316. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  317. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  318. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  319. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  320. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  321. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  322. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  323. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  324. package/src/architecture/catalog.mjs +355 -0
  325. package/src/architecture/index.mjs +93 -0
  326. package/src/boundary/content-rules.mjs +536 -69
  327. package/src/boundary/delegation-command.mjs +158 -0
  328. package/src/boundary/delegation.mjs +396 -0
  329. package/src/boundary/policy.mjs +185 -64
  330. package/src/build/git.mjs +18 -0
  331. package/src/build/index.mjs +3 -0
  332. package/src/build/ledger.mjs +88 -0
  333. package/src/capabilities/adoption.mjs +381 -0
  334. package/src/capabilities/evidence.mjs +140 -0
  335. package/src/capabilities/files.mjs +124 -0
  336. package/src/capabilities/index.mjs +4 -0
  337. package/src/capabilities/instructions.mjs +250 -0
  338. package/src/capabilities/package.mjs +136 -0
  339. package/src/cli/command-failure.mjs +36 -0
  340. package/src/cli/execute-command.mjs +16 -2
  341. package/src/cli/run.mjs +66 -9
  342. package/src/coauthor/store.mjs +43 -2
  343. package/src/collaboration/proposals.mjs +189 -0
  344. package/src/commands/architecture.mjs +27 -0
  345. package/src/commands/boundary.mjs +3 -0
  346. package/src/commands/capability.mjs +73 -0
  347. package/src/commands/enroll.mjs +3 -0
  348. package/src/commands/harness.mjs +79 -0
  349. package/src/commands/ingest.mjs +39 -0
  350. package/src/commands/init.mjs +4 -1
  351. package/src/commands/inquiry.mjs +66 -0
  352. package/src/commands/knowledge-semantic.mjs +128 -0
  353. package/src/commands/knowledge.mjs +119 -0
  354. package/src/commands/learn.mjs +61 -0
  355. package/src/commands/obsidian.mjs +772 -65
  356. package/src/commands/practice.mjs +24 -0
  357. package/src/commands/setup.mjs +22 -2
  358. package/src/commands/trackable.mjs +19 -0
  359. package/src/contracts/corpus.mjs +30 -0
  360. package/src/coordination/index.mjs +110 -0
  361. package/src/evidence-navigation/contracts.mjs +60 -0
  362. package/src/evidence-navigation/evaluate.mjs +69 -0
  363. package/src/evidence-navigation/generation.mjs +22 -0
  364. package/src/evidence-navigation/index.mjs +2 -0
  365. package/src/evidence-navigation/local.mjs +167 -0
  366. package/src/evidence-navigation/schema.generated.mjs +604 -0
  367. package/src/evidence-navigation/types.mjs +12 -0
  368. package/src/export/atelier-export-contract.mjs +35 -8
  369. package/src/graph/enroll.mjs +248 -0
  370. package/src/graph/graph.mjs +30 -6
  371. package/src/graph/knowledge-graph.mjs +77 -9
  372. package/src/harnesses/contracts.mjs +41 -0
  373. package/src/harnesses/exchange.mjs +154 -0
  374. package/src/harnesses/feedback.mjs +16 -0
  375. package/src/harnesses/history.mjs +37 -0
  376. package/src/harnesses/index.mjs +4 -0
  377. package/src/harnesses/store.mjs +29 -0
  378. package/src/ingestion/contracts.mjs +41 -0
  379. package/src/ingestion/evaluation.mjs +119 -0
  380. package/src/ingestion/optional/composition/index.mjs +171 -0
  381. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  382. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  383. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  384. package/src/ingestion/optional/graphify/graphify_supplier.py +938 -0
  385. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  386. package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
  387. package/src/ingestion/optional/graphify/native-pins.json +306 -0
  388. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  389. package/src/ingestion/optional/index.mjs +9 -0
  390. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  391. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  392. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  393. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  394. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  395. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  396. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  397. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  398. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  399. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  400. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  401. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  402. package/src/ingestion/processors.mjs +218 -0
  403. package/src/ingestion/semantic.mjs +203 -0
  404. package/src/ingestion/store.mjs +372 -0
  405. package/src/inquiry/beliefs.mjs +32 -0
  406. package/src/inquiry/contracts.mjs +23 -0
  407. package/src/inquiry/index.mjs +5 -0
  408. package/src/inquiry/ledger.mjs +135 -0
  409. package/src/inquiry/lenses.mjs +15 -0
  410. package/src/inquiry/projection.mjs +77 -0
  411. package/src/inquiry/store.mjs +35 -0
  412. package/src/intake/read-scope.mjs +12 -0
  413. package/src/intake/store.mjs +69 -6
  414. package/src/interaction/index.mjs +91 -0
  415. package/src/judgment/index.mjs +82 -0
  416. package/src/judgment/practice-evaluation.mjs +70 -0
  417. package/src/judgment/practice.mjs +144 -0
  418. package/src/knowledge/context.mjs +217 -0
  419. package/src/knowledge/index.mjs +5 -0
  420. package/src/knowledge/ingestion.mjs +79 -0
  421. package/src/knowledge/intake.mjs +33 -0
  422. package/src/knowledge/ledger.mjs +110 -0
  423. package/src/knowledge/participatory/guidance.mjs +36 -0
  424. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  425. package/src/knowledge/participatory/index.mjs +2 -0
  426. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  427. package/src/knowledge/plan.mjs +67 -0
  428. package/src/knowledge/projection.mjs +43 -0
  429. package/src/knowledge/semantic-operation.mjs +567 -0
  430. package/src/knowledge/sessions.mjs +467 -0
  431. package/src/knowledge/workspace.mjs +295 -0
  432. package/src/knowledge-health/assessment.mjs +165 -0
  433. package/src/knowledge-health/capabilities.mjs +350 -0
  434. package/src/knowledge-health/context.mjs +62 -0
  435. package/src/knowledge-health/example.mjs +47 -0
  436. package/src/knowledge-health/index.mjs +10 -0
  437. package/src/knowledge-health/owner-join.mjs +88 -0
  438. package/src/knowledge-health/proof/common.mjs +24 -0
  439. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  440. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  441. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  442. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  443. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  444. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  445. package/src/knowledge-health/rule-identity.mjs +49 -0
  446. package/src/knowledge-health/workshop-composition.mjs +19 -0
  447. package/src/knowledge-health/workshop-profile.mjs +176 -0
  448. package/src/knowledge-health/workshop.mjs +99 -0
  449. package/src/learning/adapters.mjs +68 -0
  450. package/src/learning/assistance.mjs +127 -0
  451. package/src/learning/contracts.mjs +65 -0
  452. package/src/learning/store.mjs +272 -0
  453. package/src/personal-workspace/index.mjs +391 -0
  454. package/src/personal-workspace/selection.mjs +401 -0
  455. package/src/project/config.mjs +8 -0
  456. package/src/project/durable-state.mjs +3 -1
  457. package/src/project/file-class.mjs +1 -1
  458. package/src/project/path-match.mjs +2 -1
  459. package/src/projection/obsidian/edits/apply.mjs +55 -16
  460. package/src/projection/obsidian/materialize/prepare-view.mjs +87 -5
  461. package/src/projection/obsidian/personal-workspace.mjs +519 -0
  462. package/src/projection/obsidian/proposals/adapter.mjs +79 -17
  463. package/src/projection/obsidian/proposals/observation.mjs +50 -4
  464. package/src/projection/obsidian/publication/publisher.mjs +214 -18
  465. package/src/projection/obsidian/publication/vault-list.mjs +4 -3
  466. package/src/projection/obsidian/recovery/index.mjs +4 -1
  467. package/src/projection/obsidian/recovery/store.mjs +160 -7
  468. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  469. package/src/projection/project.mjs +3 -2
  470. package/src/readiness/readiness.mjs +10 -3
  471. package/src/reflection/index.mjs +56 -0
  472. package/src/runtime/obsidian/app-production-seams.mjs +48 -16
  473. package/src/runtime/obsidian/app-registration.mjs +175 -28
  474. package/src/runtime/obsidian/app-restart.mjs +154 -0
  475. package/src/runtime/obsidian/engine.mjs +437 -56
  476. package/src/runtime/obsidian/extension-points.mjs +1 -1
  477. package/src/runtime/obsidian/index.mjs +10 -7
  478. package/src/runtime/obsidian/launch-plan.mjs +7 -4
  479. package/src/runtime/obsidian/lifecycle.mjs +108 -23
  480. package/src/runtime/obsidian/login-item.mjs +335 -0
  481. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  482. package/src/runtime/obsidian/observation.mjs +36 -4
  483. package/src/runtime/obsidian/opening.mjs +173 -38
  484. package/src/runtime/obsidian/personal-selection.mjs +125 -0
  485. package/src/runtime/obsidian/pipeline.mjs +115 -6
  486. package/src/runtime/obsidian/plugin-channel.mjs +11 -3
  487. package/src/runtime/obsidian/plugin-choice.mjs +16 -4
  488. package/src/runtime/obsidian/plugin-drift.mjs +59 -11
  489. package/src/runtime/obsidian/plugin-presence.mjs +14 -1
  490. package/src/runtime/obsidian/private-lock.mjs +39 -4
  491. package/src/runtime/obsidian/process-identity.mjs +20 -10
  492. package/src/runtime/obsidian/project-views.mjs +282 -0
  493. package/src/runtime/obsidian/questions.mjs +48 -0
  494. package/src/runtime/obsidian/release-watch.mjs +56 -0
  495. package/src/runtime/obsidian/service-main.mjs +148 -19
  496. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  497. package/src/runtime/obsidian/service-managers.mjs +275 -0
  498. package/src/runtime/obsidian/service-record.mjs +87 -23
  499. package/src/runtime/obsidian/service.mjs +102 -9
  500. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  501. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  502. package/src/runtime/obsidian/vault-location.mjs +300 -0
  503. package/src/runtime/obsidian/view-counts.mjs +34 -0
  504. package/src/runtime/obsidian/view-permission.mjs +334 -0
  505. package/src/server/local-sidecar.mjs +169 -2
  506. package/src/server/server.mjs +28 -2
  507. package/src/template-bindings/project.mjs +92 -0
  508. package/src/templates/profile.mjs +225 -0
  509. package/src/trackables/domain.mjs +175 -0
  510. package/src/trackables/store.mjs +114 -0
  511. package/src/ui/control-system.mjs +201 -0
  512. package/src/ui/html-primitives.mjs +3 -0
  513. package/src/ui/knowledge-page.mjs +1388 -0
  514. package/src/upgrade/closure-diagnostics.mjs +135 -0
  515. package/src/upgrade/closure.mjs +649 -0
  516. package/src/upgrade/explanation.mjs +1 -1
  517. package/src/upgrade/template-participant.mjs +171 -0
  518. package/src/upgrade/transaction.mjs +72 -25
  519. package/src/upgrade/upgrade.mjs +20 -7
  520. package/templates/external-project-workspace/pack-lifecycle.json +7 -1
  521. package/templates/knowledge-workspace/.gitattributes +2 -0
  522. package/templates/knowledge-workspace/AGENTS.md +27 -0
  523. package/templates/knowledge-workspace/README.md +44 -0
  524. package/templates/knowledge-workspace/atelier.project.json +22 -0
  525. package/templates/knowledge-workspace/gitignore +6 -0
  526. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  527. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  528. package/templates/knowledge-workspace/records/forecast.md +22 -0
  529. package/templates/knowledge-workspace/records/inspection.md +25 -0
  530. package/templates/knowledge-workspace/records/telescope.md +24 -0
  531. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { enclosingVaults, findVaultEntry, vaultRoute } from '../../projection/obsidian/publication/vault-list.mjs'
4
- import { createRecoveryStore as createStore, readFileBytes, sha256Digest } from '../../projection/obsidian/recovery/store.mjs'
4
+ import { createRecoveryStore as createStore, readFileBytes, sha256Digest, vaultRootFor } from '../../projection/obsidian/recovery/store.mjs'
5
5
  import { inspectApp, qualifyApp } from './app-capability.mjs'
6
+ import { settingsWriteOutlook } from './app-registration.mjs'
6
7
  import { readObsidianEnablement } from './enablement.mjs'
7
8
  import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
8
9
  import { UNAVAILABLE_APPLY_OPERATION } from './extension-points.mjs'
@@ -30,20 +31,32 @@ import { OPEN_EDIT_STATES, createMaintenanceStateStore } from './state-store.mjs
30
31
  //
31
32
  // Nothing a person does by hand is needed on the way. The app is made to know
32
33
  // the vault: through the app itself while it runs and answers, or, while no
33
- // Obsidian runs, in the app's own vault list (app-registration.mjs). A view
34
- // that the app kept from being published (an app that runs without the vault,
35
- // or that could not be qualified) is published again once the app holds the
36
- // vault, through the app, before the answer is given.
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.
37
49
  //
38
50
  // Nothing here talks to an app or an operating system itself: `appProbe`,
39
- // `registry` and `launcher` are injected, and there is no default for any.
51
+ // `registry`, `launcher` and `quitter` are injected, and there is no default for
52
+ // any.
40
53
 
41
54
  export const OPENING_OUTCOMES = Object.freeze({
42
55
  current: { summary: 'the vault is the present generation, verified by read-back, and the app has it open', next: 'nothing to do' },
43
56
  updating: { summary: 'maintenance is publishing or has not finished; the vault is not confirmed current', next: 'run `obsidian open` again in a moment' },
44
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' },
45
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' },
46
- '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' },
47
60
  'app-missing': { summary: 'no Obsidian installation was found', next: 'install Obsidian, then open again' },
48
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' },
49
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' },
@@ -80,11 +93,16 @@ const QUIT = 'quit Obsidian (on Linux, also any app that runs on a system Electr
80
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.
81
94
  const THROUGH_THE_APP = 'start Obsidian with any vault open, then open again: the vault is then added through the app'
82
95
  export const REASON_NEXT = Object.freeze({
83
- 'cli-turned-off': 'Obsidian\'s command line is turned off (the default of a new installation): turn it on in Obsidian under Settings > General > Advanced > Command line interface, then open again',
84
- 'no-vault-open': 'open any vault in Obsidian, or quit Obsidian, then open again; open then adds this view\'s vault to Obsidian itself',
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',
85
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',
86
104
  'editor-uncoordinated': `${UNCOORDINATED}; \`atelier obsidian open\` adds this view's vault to Obsidian and publishes through it, or ${QUIT}; it is retried automatically`,
87
- 'obsidian-settings-missing': 'Obsidian has not run on this account yet: start it once (it creates its settings), then open again with it running or quit',
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',
88
106
  'obsidian-settings-location-unknown': THROUGH_THE_APP,
89
107
  'obsidian-settings-unsafe': `Obsidian's settings file is a link or not a regular file, so it is not written; ${THROUGH_THE_APP}`,
90
108
  'obsidian-settings-not-owned': `Obsidian's settings file belongs to another user, so it is not written; ${THROUGH_THE_APP}`,
@@ -105,6 +123,18 @@ export const REASON_NEXT = Object.freeze({
105
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',
106
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',
107
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
+ 'selection-history-shortened': 'the selection history of the personal home ends at an earlier record than the running maintenance service has read: records were removed by something other than a confirmation, and the service publishes nothing while the history is shorter. Put the removed records back, and it goes on as before. Or accept the history as it now stands: a service started again (`atelier obsidian service stop`, then open again) reads the history as it is and follows the selection it then ends with, publishing it if its generation still holds (`atelier obsidian status` reports that selection, or why it cannot be used); a confirmation made after the records were removed is followed without a restart once the history is as long as it was',
108
138
  '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',
109
139
  })
110
140
 
@@ -136,11 +166,13 @@ export const OPENING_PRIMITIVES = Object.freeze({
136
166
  // Whether a view that is not current was kept from publication by the app: then making the app hold the vault
137
167
  // and asking for the view again can make it current. A vault open in several windows is asked for again once one
138
168
  // window is left; while more are open, open names them instead.
139
- keptByApp: (view) => ['publisher-conflict', 'stale-readable', 'not-prepared'].includes(view.outcome) && ['editor-uncoordinated', 'vault-open-in-several-windows', 'app-version-unsupported'].includes(view.reason),
169
+ // A view the service did not publish because the app's command line is turned off is one of them: once the app runs
170
+ // with it on and holds the vault, it is published through it.
171
+ 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),
140
172
  })
141
173
 
142
174
  const segment = (identifier) => identifier.replaceAll(':', '_')
143
- const STORE_AREAS = (scopeId) => [['vaults', segment(scopeId)], ['state', 'manifests', segment(scopeId)], ['state', 'journals', segment(scopeId)], ['state', 'locks'], ['recovery', 'objects'], ['staging']]
175
+ const STORE_AREAS = (scopeId) => [['state', 'manifests', segment(scopeId)], ['state', 'journals', segment(scopeId)], ['state', 'locks'], ['recovery', 'objects'], ['staging']]
144
176
 
145
177
  // Reads the trusted pointer of a view and every note of its vault. Read-only:
146
178
  // the store is only constructed when everything it would create already exists.
@@ -148,7 +180,13 @@ const STORE_AREAS = (scopeId) => [['vaults', segment(scopeId)], ['state', 'manif
148
180
  // store could be constructed, even before a first generation.
149
181
  export function readBackTrustedGeneration({ workspaceRoot, workspaceId, scopeId, repositoryRoots, createRecoveryStore = createStore }) {
150
182
  const unreadable = (reason) => ({ readable: false, reason, generationId: null, intact: false, noteCount: 0, differing: 0, missing: 0 })
151
- if (!STORE_AREAS(scopeId).every((parts) => fs.existsSync(path.join(workspaceRoot, ...parts)))) return unreadable('no-published-vault')
183
+ // The view's vault wherever it is: allocated for the view, or under the data root.
184
+ let vault
185
+ try { vault = vaultRootFor({ workspaceRoot, workspaceId, scopeId }) } catch (error) {
186
+ if (typeof error?.code === 'string') return unreadable(error.code)
187
+ throw error
188
+ }
189
+ if (!fs.existsSync(vault.path) || !STORE_AREAS(scopeId).every((parts) => fs.existsSync(path.join(workspaceRoot, ...parts)))) return unreadable('no-published-vault')
152
190
  let store
153
191
  let trusted
154
192
  try {
@@ -204,8 +242,14 @@ export function scopeReport({ workspace, scopeId, repositoryRoots, serviceState,
204
242
  reason = edited ? 'vault-differs-from-trusted-generation' : verification.readable ? 'trusted-generation-differs' : verification.reason
205
243
  } else if (serviceState !== 'healthy') { outcome = 'stale-readable'; reason = serviceState === 'busy' ? 'maintenance-busy-not-rechecked' : 'maintenance-not-running' }
206
244
  else { outcome = 'current'; reason = entry.reason }
245
+ // Where the view's vault is, published or not: allocated for the view, or under the data root.
246
+ let vault
247
+ try { const found = vaultRootFor({ ...workspace, scopeId }); vault = { path: found.path, origin: found.origin } } catch (error) {
248
+ if (typeof error?.code !== 'string') throw error
249
+ vault = { path: null, origin: 'unreadable', reason: error.code }
250
+ }
207
251
  return {
208
- scopeId, ...describeOutcome(outcome), next: nextStep(outcome, reason), reason,
252
+ scopeId, ...describeOutcome(outcome), next: nextStep(outcome, reason), reason, vault,
209
253
  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 },
210
254
  readBack: { readable: verification.readable, reason: verification.reason, generationId: verification.generationId, intact: verification.intact, noteCount: verification.noteCount, differing: verification.differing, missing: verification.missing },
211
255
  pendingEdits: pendingSummary(stateStore, scopeId, applyAvailable),
@@ -226,6 +270,9 @@ const defaultSleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms)
226
270
  const READABLE = new Set(['stale-readable', 'held-for-your-edit', 'updating', 'publisher-conflict'])
227
271
  // After a launch: the app is still starting, or still opening the vault it was asked for.
228
272
  const NOT_UP_YET = new Set(['version-unknown', 'no-vault-open'])
273
+ // After a launch: how many rounds in a row the command line must be silent, with only the plugin giving the version,
274
+ // before `open` says the command line is what is missing.
275
+ const SILENT_ROUNDS = 2
229
276
  const REGISTRY_OPERATIONS = ['listThroughApp', 'registerThroughApp', 'readSettings', 'registerInSettings']
230
277
 
231
278
  const attempt = async (operation) => { try { return await operation() } catch { return null } }
@@ -250,28 +297,29 @@ const attempt = async (operation) => { try { return await operation() } catch {
250
297
  // (`vault-inside-another-vault`): the app would show its notes in that vault
251
298
  // too, and a call run in its folder would reach that vault.
252
299
  //
253
- // { ok: true, path, how, vaults } with the path the app knows the vault by and
254
- // the list it was found in, or { ok: false, reason }.
300
+ // { ok: true, path, how, open, vaults } with the path the app knows the vault
301
+ // by, whether its list flags it open (an app started plainly reopens it then),
302
+ // and the list it was found in, or { ok: false, reason }.
255
303
  //
256
304
  // The window the app opens for an added vault takes the next command-line
257
305
  // call while it may still be loading, and can then answer that a command
258
306
  // does not exist yet: the list is asked again, a bounded number of times,
259
307
  // before an addition counts as not verified.
260
308
  const VERIFY_ATTEMPTS = 10
261
- async function ensureAppKnowsVault({ registry, observation, vaultRoot, sleep, pollMs }) {
309
+ async function ensureAppKnowsVault({ registry, observation, vaultRoot, sleep, pollMs, keepBackups = [] }) {
262
310
  const inside = (vaults) => enclosingVaults({ vaults, vaultRoot }).length > 0
263
311
  if (observation.noVaultOpen === true) {
264
312
  const settings = await attempt(() => registry.readSettings())
265
313
  const vaults = settings?.ok === true ? settings.vaults : null
266
314
  const entry = findVaultEntry(vaults, vaultRoot)
267
- if (entry) return { ok: true, path: entry.path, how: 'listed', vaults }
315
+ if (entry) return { ok: true, path: entry.path, how: 'listed', open: entry.open === true, vaults }
268
316
  return { ok: false, reason: inside(vaults) ? 'vault-inside-another-vault' : observation.fromPlugin === true ? 'vault-open-cli-silent' : 'no-vault-open' }
269
317
  }
270
318
  if (observation.answering === true) {
271
319
  const listed = await attempt(() => registry.listThroughApp())
272
320
  if (listed?.answered !== true) return { ok: false, reason: listed?.reason === 'no-vault-open' ? 'no-vault-open' : 'app-did-not-list-its-vaults' }
273
321
  const known = findVaultEntry(listed.vaults, vaultRoot)
274
- if (known) return { ok: true, path: known.path, how: 'listed', vaults: listed.vaults }
322
+ if (known) return { ok: true, path: known.path, how: 'listed', open: known.open === true, vaults: listed.vaults }
275
323
  if (inside(listed.vaults)) return { ok: false, reason: 'vault-inside-another-vault' }
276
324
  const asked = await attempt(() => registry.registerThroughApp({ vaultRoot }))
277
325
  if (asked?.answered !== true && asked?.reason === 'no-vault-open') return { ok: false, reason: 'no-vault-open' }
@@ -280,16 +328,30 @@ async function ensureAppKnowsVault({ registry, observation, vaultRoot, sleep, po
280
328
  for (let attempts = 1; ; attempts += 1) {
281
329
  const again = await attempt(() => registry.listThroughApp())
282
330
  const added = again?.answered === true ? findVaultEntry(again.vaults, vaultRoot) : null
283
- if (added) return { ok: true, path: added.path, how: 'added-through-app', vaults: again.vaults }
331
+ if (added) return { ok: true, path: added.path, how: 'added-through-app', open: true, vaults: again.vaults }
284
332
  if (attempts >= VERIFY_ATTEMPTS) return { ok: false, reason: asked?.answered === true ? 'registration-not-verified' : 'addition-not-answered' }
285
333
  await sleep(pollMs)
286
334
  }
287
335
  }
288
- const written = await attempt(() => registry.registerInSettings({ vaultRoot }))
336
+ const written = await attempt(() => registry.registerInSettings({ vaultRoot, keepBackups }))
289
337
  if (written === null) return { ok: false, reason: 'obsidian-settings-unwritable' }
290
338
  if (written.ok !== true) return { ok: false, reason: typeof written.code === 'string' ? written.code : 'obsidian-settings-unwritable' }
291
- if (written.confirmed !== true) return { ok: false, reason: written.reason === 'registration-not-read-back' ? 'registration-not-read-back' : 'app-started-during-registration' }
292
- return { ok: true, path: written.entry.path, how: written.registered === 'already' ? 'listed' : 'added-to-settings', vaults: written.vaults }
339
+ const settings = settingsWritten(written)
340
+ 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 }) }
341
+ const how = written.registered === 'created' ? 'created-settings' : written.registered === 'already' ? 'listed' : 'added-to-settings'
342
+ // A vault already listed keeps its own open flag; one written here is written flagged open.
343
+ return { ok: true, path: written.entry.path, how, open: written.registered === 'already' ? written.entry.open === true : true, vaults: written.vaults, ...(settings === null ? {} : { settings }) }
344
+ }
345
+
346
+ // What was written to Obsidian's settings file, for the answer to show: null when nothing was.
347
+ function settingsWritten(written) {
348
+ const created = written.registered === 'created'
349
+ const cliTurnedOn = written.cliTurnedOn === true
350
+ if (!created && written.registered !== 'written' && !cliTurnedOn) return null
351
+ return {
352
+ ...(typeof written.file === 'string' ? { file: written.file } : {}), created, ...(created ? { directoryCreated: written.created?.directory === true } : {}),
353
+ vaultAdded: written.registered !== 'already', cliTurnedOn, ...(typeof written.backupPath === 'string' ? { backupPath: written.backupPath } : {}),
354
+ }
293
355
  }
294
356
 
295
357
  // Starts or reconnects the owned service, asks it for a tick, reads the view
@@ -297,7 +359,7 @@ async function ensureAppKnowsVault({ registry, observation, vaultRoot, sleep, po
297
359
  // again for a view the app kept from being published, and reports one outcome.
298
360
  export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIMITIVES, lifecycleRules = LIFECYCLE_PRIMITIVES) {
299
361
  const {
300
- loadProject, dataRoot, scopeId: requestedScope, appProbe, launcher, registry, service = {}, consent, allowStale = false, extensions = null,
362
+ loadProject, locateProject, dataRoot, scopeId: requestedScope, appProbe, launcher, registry, quitter = null, restartApp = false, service = {}, consent, allowStale = false, extensions = null,
301
363
  tickTimeoutMs = 120 * 1000, appWaitMs = 30 * 1000, appPollMs = 500, sleep = defaultSleep, monotonic = () => Date.now(),
302
364
  env = process.env, platform = process.platform, probeTimeoutMs,
303
365
  } = options
@@ -311,18 +373,24 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
311
373
  const project = loadProject()
312
374
  const enablement = readObsidianEnablement(project)
313
375
  const applyAvailable = extensions !== null && extensions.applyOperation() !== UNAVAILABLE_APPLY_OPERATION
376
+ // A restart this run made, and what it wrote to Obsidian's settings: every answer after them shows both.
377
+ let restart = null
378
+ let settingsShown = null
314
379
  const finish = (outcome, extra = {}) => {
315
380
  const { afterOpen = false, ...shown } = extra
316
- return { ok: outcome === 'current', ...describeOutcome(outcome), next: nextStep(outcome, shown.reason, { afterOpen }), launched: false, ...shown }
381
+ const next = restart?.quit === true && restart.startedAgain === false ? REASON_NEXT['app-not-started-again'] : nextStep(outcome, shown.reason, { afterOpen })
382
+ return { ok: outcome === 'current', ...describeOutcome(outcome), next, launched: false, ...shown, ...(restart === null ? {} : { restart }), ...(settingsShown === null ? {} : { obsidianSettings: settingsShown }) }
317
383
  }
318
384
  if (enablement.state === 'disabled') return finish('disabled', { reason: enablement.reason, scopeId: requestedScope ?? null })
319
385
  const scopeId = resolveScope(enablement, requestedScope)
320
- const lifecycle = { loadProject, dataRoot, env, platform, ...(probeTimeoutMs === undefined ? {} : { probeTimeoutMs }) }
386
+ const lifecycle = { loadProject, ...(locateProject === undefined ? {} : { locateProject }), dataRoot, env, platform, ...(probeTimeoutMs === undefined ? {} : { probeTimeoutMs }) }
321
387
 
322
388
  // 1. The owned service: reconnect, or start. Never adopt; a runtime of ours is replaced only when it runs an earlier
323
389
  // release, under the consent already recorded, when a tick is asked of it (requestServiceTick).
324
390
  let status = await serviceStatus(lifecycle, lifecycleRules)
325
391
  if (status.state === 'busy') return finish('busy', { scopeId, reason: status.reason, service: { state: status.state } })
392
+ // How a login item took part in starting the service (started through it, refreshed, or not used and why).
393
+ let loginItem = null
326
394
  if (status.state !== 'healthy') {
327
395
  let started
328
396
  try {
@@ -331,8 +399,10 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
331
399
  if (!(error instanceof ObsidianMaintenanceRefusal)) throw error
332
400
  return finish('service-unavailable', { scopeId, reason: error.code, service: { state: status.state } })
333
401
  }
334
- if (started.state === 'busy') return finish('busy', { scopeId, reason: started.reason, service: { state: 'busy', started: started.started } })
335
- if (started.state !== 'healthy') return finish('service-unavailable', { scopeId, reason: started.reason ?? started.state, service: { state: started.state } })
402
+ loginItem = started.loginItem ?? null
403
+ const item = loginItem === null ? {} : { loginItem }
404
+ if (started.state === 'busy') return finish('busy', { scopeId, reason: started.reason, service: { state: 'busy', started: started.started, ...item } })
405
+ if (started.state !== 'healthy') return finish('service-unavailable', { scopeId, reason: started.reason ?? started.state, service: { state: started.state, ...item } })
336
406
  status = started
337
407
  }
338
408
  let runtimeId = status.record?.runtimeId ?? null
@@ -346,7 +416,7 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
346
416
  const asked = await requestServiceTick({ ...lifecycle, tickTimeoutMs, scopeId, service }, lifecycleRules)
347
417
  if (asked.restarted) restarted = asked.restarted
348
418
  if (typeof asked.runtimeId === 'string') runtimeId = asked.runtimeId
349
- const serviceShown = { state: asked.state, runtimeId, ...(restarted === null ? {} : { restarted }) }
419
+ const serviceShown = { state: asked.state, runtimeId, ...(restarted === null ? {} : { restarted }), ...(loginItem === null ? {} : { loginItem }) }
350
420
  if (asked.state === 'busy') return { answer: { outcome: 'busy', extra: { scopeId, reason: asked.reason, service: serviceShown } } }
351
421
  if (!asked.requested) return { answer: { outcome: 'service-unavailable', extra: { scopeId, reason: asked.reason, service: serviceShown } } }
352
422
  let view = report()
@@ -372,8 +442,45 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
372
442
  if (view.outcome !== 'current' && !readable && !keptByApp) return finish(view.outcome, common)
373
443
 
374
444
  // 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.
375
- const before = qualifyApp(await inspectApp(appProbe), { requireVersion: false })
445
+ let before = qualifyApp(await inspectApp(appProbe), { requireVersion: false })
376
446
  const app = (qualification) => ({ outcome: qualification.outcome, reason: qualification.reason, version: qualification.version, floor: qualification.floor })
447
+
448
+ // A running app that cannot be reached (`state`: its command line is off, or it has no vault open and does not list
449
+ // this one) is quit, only when the person asked for it. { quit: true } with the app gone, or { answer } to give.
450
+ const QUIT_APP = { installed: true, cli: true, running: false, version: null }
451
+ const quitToRestart = async (state, stateOutcome) => {
452
+ if (!restartApp) return { answer: finish(stateOutcome, { ...common, reason: state, app: app(before) }) }
453
+ if (typeof quitter?.quit !== 'function' || typeof launcher.startPlain !== 'function' || typeof registry.backupSettings !== 'function') return { answer: finish(stateOutcome, { ...common, reason: 'restart-unavailable', app: app(before) }) }
454
+ // Read-only, before anything is sent: the write the restart is for can succeed. What it would refuse is answered
455
+ // now, with the app left running.
456
+ const notRestarted = (outcome, reason) => { restart = { asked: true, state, quit: false, signalled: false, signals: 0, reason }; return { answer: finish(outcome, { ...common, reason, app: app(before) }) } }
457
+ if (typeof view.vaultRoot !== 'string') return notRestarted('not-prepared', 'no-vault-folder')
458
+ const outlook = settingsWriteOutlook({ settings: await attempt(() => registry.readSettings()), vaultRoot: view.vaultRoot })
459
+ if (!outlook.ok) return notRestarted('launch-failed', outlook.code)
460
+ // A copy of the settings as they are, kept beside them once the main process is proven, right before the first
461
+ // signal; a copy that cannot be kept sends nothing. An app that has shown only its starter window has no file yet:
462
+ // there is nothing to copy, and the write creates it.
463
+ const quit = await attempt(() => quitter.quit({ beforeSignal: outlook.create === true ? null : () => registry.backupSettings() }))
464
+ restart = {
465
+ asked: true, state, quit: quit?.quit === true, signalled: quit?.signalled === true, ...(Number.isSafeInteger(quit?.pid) ? { pid: quit.pid } : {}),
466
+ signals: Number.isSafeInteger(quit?.signals) ? quit.signals : 0, ...(typeof quit?.settingsCopy === 'string' ? { settingsCopy: quit.settingsCopy } : {}),
467
+ }
468
+ if (quit?.quit === true) { before = qualifyApp(QUIT_APP, { requireVersion: false }); return { quit: true } }
469
+ restart.reason = typeof quit?.reason === 'string' ? quit.reason : 'app-did-not-quit'
470
+ if (typeof quit?.detail === 'string') restart.detail = quit.detail
471
+ // An app that was signalled and has not gone is neither started again nor written for: the person decides.
472
+ return { answer: finish(quit?.signalled === true ? 'launch-failed' : stateOutcome, { ...common, reason: restart.reason, app: app(before) }) }
473
+ }
474
+ // After a restart, every answer given before the launch starts the app again as it was: plainly, so it reopens the
475
+ // vaults it had open. Nothing was added for this view.
476
+ const giveBack = async () => {
477
+ if (restart?.quit !== true || restart.startedAgain !== undefined) return
478
+ restart.startedAgain = (await attempt(() => launcher.startPlain())) === true
479
+ }
480
+ if (before.reason === 'cli-turned-off') {
481
+ const stopped = await quitToRestart('cli-turned-off', before.outcome)
482
+ if (stopped.answer) return stopped.answer
483
+ }
377
484
  const noVaultOpen = before.reason === 'no-vault-open'
378
485
  if (!rules.appQualifies(before) && !noVaultOpen) {
379
486
  // No app answered and none is known to run (a process table that could not be read, on Linux any app on a system
@@ -386,19 +493,27 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
386
493
  // call about it then reaches it and no other vault: in its folder, or by its id when a vault listed at a folder
387
494
  // above it would take a call run there.
388
495
  const { vaultRoot } = view
389
- if (typeof vaultRoot !== 'string') return finish('not-prepared', { ...common, reason: 'no-vault-folder', app: app(before) })
496
+ if (typeof vaultRoot !== 'string') { await giveBack(); return finish('not-prepared', { ...common, reason: 'no-vault-folder', app: app(before) }) }
390
497
  // A version Atelier's plugin reported is not the command line answering (withPluginReportedVersion).
391
498
  const fromPlugin = before.versionSource === 'plugin'
392
- const known = await ensureAppKnowsVault({ registry, observation: { answering: typeof before.version === 'string' && !fromPlugin, noVaultOpen: noVaultOpen || fromPlugin, fromPlugin }, vaultRoot, sleep, pollMs: appPollMs })
499
+ 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] : [] })
500
+ let known = await knowVault()
501
+ // With no vault open, a vault the app does not list cannot be added while it runs: a restart the person asked for.
502
+ if (!known.ok && known.reason === 'no-vault-open' && restart === null) {
503
+ const stopped = await quitToRestart('no-vault-open', 'app-version-unsupported')
504
+ if (stopped.answer) return stopped.answer
505
+ known = await knowVault()
506
+ }
507
+ if (known.settings) settingsShown = known.settings
393
508
  // An app that answered its version and then closed its last vault window is one with no vault open; one that holds
394
509
  // this vault open but whose command line does not answer has no usable command line.
395
- if (!known.ok) 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
+ 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) }) }
396
511
  const registration = { how: known.how }
397
512
  const route = vaultRoute({ vaults: known.vaults, vaultRoot })
398
513
  // Open in several windows, one per entry of the list that names its folder: each holds the vault, and a publication
399
514
  // coordinates with one only. Nothing is launched; the entries are named.
400
- if (route.how === 'duplicated') return finish('publisher-conflict', { ...common, reason: 'vault-open-in-several-windows', duplicates: route.entries, app: app(before), registration })
401
- if (route.how !== 'folder' && route.how !== 'id') return finish('launch-failed', { ...common, reason: route.how === 'ambiguous' ? 'vault-inside-another-vault' : 'registration-not-verified', app: app(before), registration })
515
+ if (route.how === 'duplicated') { await giveBack(); return finish('publisher-conflict', { ...common, reason: 'vault-open-in-several-windows', duplicates: route.entries, app: app(before), registration }) }
516
+ 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 }) }
402
517
 
403
518
  // 5. Launch, then wait, bounded, for the app to answer for exactly this vault.
404
519
  let launch
@@ -408,11 +523,21 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
408
523
  // otherwise the vault's own path, which the app matches exactly since the folder is listed.
409
524
  const target = route.how === 'id' ? { vaultId: route.id, vaultPath: known.path } : { vaultPath: known.path }
410
525
  try { launch = await launcher.open({ vaultRoot: known.path, ...target, appRunning: before.running === true }) } catch { launch = { launched: false, reason: 'launcher-threw' } }
526
+ // A restart ends here: the app was started again, or could not be (the next step then says to start it).
527
+ if (restart?.quit === true) restart.startedAgain = launch?.launched === true
411
528
  if (launch?.launched !== true) return finish('launch-failed', { ...common, reason: typeof launch?.reason === 'string' ? launch.reason : 'launcher-refused', app: app(before), registration })
412
529
  const deadline = monotonic() + appWaitMs
413
530
  let after = before
414
531
  let vault = { answered: false, indexReady: false }
532
+ // Rounds in a row in which only Atelier's plugin gave the version and the command line did not answer for the vault.
533
+ let silentRounds = 0
534
+ // A quit app started plainly reopens only the vaults its list flags open. A listed vault that is closed opens through
535
+ // the link alone, so a link the started app did not take (it answered its tool, then refused the link) is handed
536
+ // once more, to the app that now runs, one round later, before the vault counts as not answering.
537
+ let linkRetry = launch.reason === 'app-started-link-not-taken' && known.open !== true ? 'due' : 'none'
538
+ let rounds = 0
415
539
  for (;;) {
540
+ rounds += 1
416
541
  after = qualifyApp(await inspectApp(appProbe), { requireVersion: true })
417
542
  // 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.
418
543
  if (!rules.appQualifies(after) && !NOT_UP_YET.has(after.reason)) return finish(after.outcome, { ...common, launched: true, reason: after.reason, app: app(after), registration })
@@ -420,13 +545,23 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
420
545
  try { vault = await appProbe.vaultState({ vaultRoot, route }) } catch { vault = { answered: false, indexReady: false } }
421
546
  if (vault?.answered === true && vault.indexReady === true) break
422
547
  // Only the command line answers for a vault. An app whose version still only Atelier's plugin reports gives no
423
- // answer there, and waiting changes nothing: its command line is what is missing, not a launch.
424
- if (vault?.answered !== true && after.versionSource === 'plugin') return finish('app-cli-unavailable', { ...common, launched: true, reason: 'vault-open-cli-silent', app: app(after), registration })
425
- }
548
+ // answer there, and waiting changes nothing: its command line is what is missing, not a launch. One such round
549
+ // is not enough: a tool call that timed out while the app was busy opening the window looks the same once, so
550
+ // the answer is given at the second round in a row.
551
+ silentRounds = vault?.answered !== true && after.versionSource === 'plugin' ? silentRounds + 1 : 0
552
+ if (silentRounds >= SILENT_ROUNDS) return finish('app-cli-unavailable', { ...common, launched: true, reason: 'vault-open-cli-silent', app: app(after), registration })
553
+ if (linkRetry === 'due' && rounds >= 2 && vault?.answered !== true) {
554
+ linkRetry = 'done'
555
+ // Through the app's tool only: this `open` started the app, so the link never goes to the operating system.
556
+ try { await launcher.open({ vaultRoot: known.path, ...target, appRunning: true, startedByThisOpen: true }) } catch { /* the wait decides */ }
557
+ }
558
+ } else silentRounds = 0
426
559
  if (monotonic() >= deadline) break
427
560
  await sleep(appPollMs)
428
561
  }
429
562
  if (!rules.appQualifies(after)) return finish(after.outcome, { ...common, launched: true, reason: after.reason, app: app(after), registration })
563
+ // The wait ended on a silent round with only the plugin giving the version: the command line is what did not answer.
564
+ if (vault?.answered !== true && silentRounds > 0) return finish('app-cli-unavailable', { ...common, launched: true, reason: 'vault-open-cli-silent', app: app(after), registration })
430
565
  if (vault?.answered !== true) return finish('launch-failed', { ...common, launched: true, reason: 'app-did-not-answer-for-this-vault', app: app(after), registration })
431
566
  if (vault.indexReady !== true) return finish('indexing', { ...common, launched: true, reason: 'metadata-cache-not-ready', app: app(after), registration })
432
567
 
@@ -0,0 +1,125 @@
1
+ import path from 'node:path'
2
+ import { PersonalWorkspaceRefusal, readPersonalSelectionHead } from '@mnstry/atelier/personal-workspace'
3
+ import { ensureLocalState } from '@mnstry/atelier/project'
4
+ import { assertPersonalSelection, loadBoundProject, loadBoundProjectOffThread, notePersonalSelectionHead, personalSelectionOf, pinPersonalSelection } from '../../projection/obsidian/personal-workspace.mjs'
5
+ import { refuse } from './errors.mjs'
6
+
7
+ // A person's confirmed selection, bound to the maintenance runtime.
8
+ //
9
+ // A personal home keeps a history of the generations its person confirmed
10
+ // (`selectPersonalGeneration` of the personal-workspace module). The loaders
11
+ // here follow that history instead of naming a generation themselves:
12
+ //
13
+ // read the head -> load the generation it names -> read the head again
14
+ //
15
+ // The head is read from the history alone, in the caller's thread: one
16
+ // directory listing and one bounded read per record. Whether the generation
17
+ // may be used is composition's to decide, as for any binding: in a worker for
18
+ // a service, in the calling thread for a command run once. A head that moved
19
+ // while the generation was loaded is another selection, and refuses
20
+ // `personal-selection-changed`.
21
+ //
22
+ // Nothing is confirmed here and nothing is chosen for the person. A history
23
+ // with no record, one that does not hold, a home that cannot be read and a
24
+ // generation its composition refuses each refuse as a maintenance refusal
25
+ // with the module's own code (`nothing-selected`, `selection-history-corrupt`,
26
+ // `personal-home-unavailable`, `root-missing`, `stale-generation` and the
27
+ // rest). There is no project to fall back to: an engine whose loader refuses
28
+ // publishes nothing, and every view it knows says why.
29
+ //
30
+ // A loaded project is pinned to the record it was loaded under
31
+ // (pinPersonalSelection), so the engine observes that record and the place of
32
+ // the next one, and loads again at the tick after a confirmation or after the
33
+ // history was cut short. At every full reconciliation the history is read
34
+ // again, and a project whose record it no longer ends with is refused.
35
+ //
36
+ // A history never gets shorter through the module: selections are appended and
37
+ // nothing removes one. So a history that ends at a lower record than this
38
+ // process has read it ending at was cut short by something else, and following
39
+ // it back could publish what nobody confirmed after the records removed. Every
40
+ // read of the history's end is noted (notePersonalSelectionHead): the first
41
+ // read here, the read once the generation is loaded, the recheck of a project
42
+ // already loaded, and a read the engine asks for when its guard stops a view
43
+ // because an observed file changed or could not be read (once for views
44
+ // stopped one after another). A
45
+ // history that ends earlier than the highest record noted refuses
46
+ // `selection-history-shortened` at a load and at a recheck, and nothing is
47
+ // loaded or used from it while it is shorter; the guard's read only notes, and
48
+ // its view says `mixed-read`. An engine whose project is pinned below the
49
+ // highest record noted loads it again at its next tick
50
+ // (personalSelectionBehind). A record added and removed between two of those
51
+ // reads was never read, and does not count.
52
+ //
53
+ // Only the record number is kept, and only in this process. A record in a
54
+ // place already read, with other content, is read as a confirmation. A process
55
+ // started after the cut reads the history as it then stands and publishes what
56
+ // it ends with: starting the service again accepts the shorter history, and so
57
+ // does any start of it (a login, a restart after an upgrade or a failure).
58
+ //
59
+ // Who may see a vault is not decided here: that stays the machine's setting.
60
+ //
61
+ // Where the workspace is does not wait for a load: a bound project's folder is
62
+ // its private home, so the home alone locates it (locatePersonalHome). A
63
+ // service resolves its workspace from that at once, and starts, whether or not
64
+ // a selection is confirmed; its engine then awaits the loader at each tick.
65
+
66
+ // The decisions the oracles in test/obsidian-personal-selection.test.mjs are
67
+ // sensitive to. Production always uses these; a test hands a loader a broken
68
+ // one (`rules`) to prove that its oracle can fail.
69
+ export const SELECTION_PRIMITIVES = Object.freeze({
70
+ // A history that ends at a lower record than this process has read is refused, at every read of it.
71
+ refusesShortened: true,
72
+ // The head is read again once the generation is loaded.
73
+ readsHeadAgain: true,
74
+ // What the loaded project is pinned for (pinPersonalSelection).
75
+ pin: Object.freeze({ observe: true, recheck: true }),
76
+ })
77
+
78
+ const SOURCE = Object.freeze({ source: 'personal-workspace' })
79
+ const LOCATED = Symbol('atelier.obsidian.located-personal-home')
80
+
81
+ // The record the history ends with. A history without one refuses, and so does one shorter than this process read it.
82
+ function confirmedHead(personalHome, rules) {
83
+ let head
84
+ try { head = readPersonalSelectionHead({ personalHome }) } catch (error) {
85
+ if (!(error instanceof PersonalWorkspaceRefusal)) throw error
86
+ refuse(error.code, 'the selection history of the personal workspace could not be read; nothing is loaded from it', SOURCE)
87
+ }
88
+ notePersonalSelectionHead(personalHome, head, rules)
89
+ if (head.selected === null) refuse('nothing-selected', 'no generation of the personal workspace is confirmed; nothing is loaded until one is', SOURCE)
90
+ return head
91
+ }
92
+
93
+ function pinned(project, head, rules) {
94
+ pinPersonalSelection(project, head, { ...rules.pin, refusesShortened: rules.refusesShortened })
95
+ if (rules.readsHeadAgain) assertPersonalSelection(project)
96
+ return project
97
+ }
98
+
99
+ // What the workspace of a bound personal home is resolved from, without loading anything: a project that holds the home
100
+ // as its folder and nothing else, as a bound project's folder is. Its pointer is under the home, the data root is the
101
+ // pointer's or the caller's, and a home is outside every Git worktree. Synchronous; nothing is composed.
102
+ export function locatePersonalHome({ personalHome } = {}) {
103
+ if (typeof personalHome !== 'string' || !path.isAbsolute(personalHome) || path.resolve(personalHome) !== personalHome) refuse('path-not-absolute', 'a personal home is named by its absolute path', SOURCE)
104
+ const located = { configDir: personalHome, [LOCATED]: personalHome }
105
+ located.localState = ensureLocalState(located, { write: false, env: { PATH: process.env.PATH } })
106
+ return located
107
+ }
108
+
109
+ // The personal home a located or a loaded project follows, which a service it starts or a login item it installs is
110
+ // given instead of a project (--personal-home); null for any other project.
111
+ export const personalHomeOf = (project) => project?.[LOCATED] ?? personalSelectionOf(project)?.personalHome ?? null
112
+ export { personalSelectionOf }
113
+
114
+ // The project of the confirmed selection, for a loader that may block (a command run once). `bind` and `rules` are
115
+ // test seams.
116
+ export function loadSelectedProject({ personalHome } = {}, { bind, rules = SELECTION_PRIMITIVES } = {}) {
117
+ const head = confirmedHead(personalHome, rules)
118
+ return pinned(loadBoundProject({ personalHome, generationId: head.selected }, { bind }), head, rules)
119
+ }
120
+
121
+ // The same, for a loader on a service's event loop: the composition runs in a worker, and the engine awaits it.
122
+ export async function loadSelectedProjectOffThread({ personalHome } = {}, { bind, rules = SELECTION_PRIMITIVES } = {}) {
123
+ const head = confirmedHead(personalHome, rules)
124
+ return pinned(await loadBoundProjectOffThread({ personalHome, generationId: head.selected }, { bind }), head, rules)
125
+ }