@mnstry/atelier 0.2.0-alpha.12 → 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 (516) hide show
  1. package/CHANGELOG.md +661 -0
  2. package/README.md +142 -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 +269 -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 +211 -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 +238 -0
  65. package/docs/learning.md +215 -0
  66. package/docs/local-services.md +181 -16
  67. package/docs/obsidian-contract.md +223 -15
  68. package/docs/obsidian-plugin.md +38 -4
  69. package/docs/obsidian.md +613 -51
  70. package/docs/optional-ingestion.md +83 -0
  71. package/docs/personal-workspace.md +257 -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 +49 -0
  77. package/docs/repo-boundary-guard.md +94 -0
  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 +317 -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 +47 -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 +75 -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 +59 -5
  300. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  301. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  302. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  303. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  304. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  305. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  306. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  307. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  308. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  309. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  310. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  311. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  312. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  313. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  314. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  315. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  316. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  317. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  318. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  319. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  320. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  321. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  322. package/src/architecture/catalog.mjs +355 -0
  323. package/src/architecture/index.mjs +93 -0
  324. package/src/boundary/content-rules.mjs +4 -2
  325. package/src/boundary/delegation-command.mjs +158 -0
  326. package/src/boundary/delegation.mjs +396 -0
  327. package/src/boundary/policy.mjs +52 -10
  328. package/src/build/git.mjs +18 -0
  329. package/src/build/index.mjs +3 -0
  330. package/src/build/ledger.mjs +88 -0
  331. package/src/capabilities/adoption.mjs +381 -0
  332. package/src/capabilities/evidence.mjs +140 -0
  333. package/src/capabilities/files.mjs +124 -0
  334. package/src/capabilities/index.mjs +4 -0
  335. package/src/capabilities/instructions.mjs +250 -0
  336. package/src/capabilities/package.mjs +136 -0
  337. package/src/cli/command-failure.mjs +36 -0
  338. package/src/cli/execute-command.mjs +16 -2
  339. package/src/cli/run.mjs +65 -9
  340. package/src/coauthor/store.mjs +43 -2
  341. package/src/collaboration/proposals.mjs +189 -0
  342. package/src/commands/architecture.mjs +27 -0
  343. package/src/commands/boundary.mjs +3 -0
  344. package/src/commands/capability.mjs +73 -0
  345. package/src/commands/enroll.mjs +3 -0
  346. package/src/commands/harness.mjs +79 -0
  347. package/src/commands/ingest.mjs +39 -0
  348. package/src/commands/init.mjs +4 -1
  349. package/src/commands/inquiry.mjs +66 -0
  350. package/src/commands/knowledge-semantic.mjs +128 -0
  351. package/src/commands/knowledge.mjs +119 -0
  352. package/src/commands/learn.mjs +61 -0
  353. package/src/commands/obsidian.mjs +754 -63
  354. package/src/commands/practice.mjs +24 -0
  355. package/src/commands/setup.mjs +22 -2
  356. package/src/commands/trackable.mjs +19 -0
  357. package/src/contracts/corpus.mjs +30 -0
  358. package/src/coordination/index.mjs +110 -0
  359. package/src/evidence-navigation/contracts.mjs +60 -0
  360. package/src/evidence-navigation/evaluate.mjs +69 -0
  361. package/src/evidence-navigation/generation.mjs +22 -0
  362. package/src/evidence-navigation/index.mjs +2 -0
  363. package/src/evidence-navigation/local.mjs +167 -0
  364. package/src/evidence-navigation/schema.generated.mjs +604 -0
  365. package/src/evidence-navigation/types.mjs +12 -0
  366. package/src/export/atelier-export-contract.mjs +35 -8
  367. package/src/graph/enroll.mjs +248 -0
  368. package/src/graph/graph.mjs +17 -1
  369. package/src/graph/knowledge-graph.mjs +58 -8
  370. package/src/harnesses/contracts.mjs +41 -0
  371. package/src/harnesses/exchange.mjs +154 -0
  372. package/src/harnesses/feedback.mjs +16 -0
  373. package/src/harnesses/history.mjs +37 -0
  374. package/src/harnesses/index.mjs +4 -0
  375. package/src/harnesses/store.mjs +29 -0
  376. package/src/ingestion/contracts.mjs +41 -0
  377. package/src/ingestion/evaluation.mjs +119 -0
  378. package/src/ingestion/optional/composition/index.mjs +171 -0
  379. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  380. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  381. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  382. package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
  383. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  384. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  385. package/src/ingestion/optional/index.mjs +9 -0
  386. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  387. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  388. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  389. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  390. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  391. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  392. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  393. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  394. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  395. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  396. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  397. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  398. package/src/ingestion/processors.mjs +218 -0
  399. package/src/ingestion/semantic.mjs +203 -0
  400. package/src/ingestion/store.mjs +372 -0
  401. package/src/inquiry/beliefs.mjs +32 -0
  402. package/src/inquiry/contracts.mjs +23 -0
  403. package/src/inquiry/index.mjs +5 -0
  404. package/src/inquiry/ledger.mjs +135 -0
  405. package/src/inquiry/lenses.mjs +15 -0
  406. package/src/inquiry/projection.mjs +77 -0
  407. package/src/inquiry/store.mjs +35 -0
  408. package/src/intake/read-scope.mjs +12 -0
  409. package/src/intake/store.mjs +69 -6
  410. package/src/interaction/index.mjs +91 -0
  411. package/src/judgment/index.mjs +82 -0
  412. package/src/judgment/practice-evaluation.mjs +70 -0
  413. package/src/judgment/practice.mjs +144 -0
  414. package/src/knowledge/context.mjs +217 -0
  415. package/src/knowledge/index.mjs +5 -0
  416. package/src/knowledge/ingestion.mjs +79 -0
  417. package/src/knowledge/intake.mjs +33 -0
  418. package/src/knowledge/ledger.mjs +110 -0
  419. package/src/knowledge/participatory/guidance.mjs +36 -0
  420. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  421. package/src/knowledge/participatory/index.mjs +2 -0
  422. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  423. package/src/knowledge/plan.mjs +67 -0
  424. package/src/knowledge/projection.mjs +43 -0
  425. package/src/knowledge/semantic-operation.mjs +567 -0
  426. package/src/knowledge/sessions.mjs +467 -0
  427. package/src/knowledge/workspace.mjs +295 -0
  428. package/src/knowledge-health/assessment.mjs +165 -0
  429. package/src/knowledge-health/capabilities.mjs +350 -0
  430. package/src/knowledge-health/context.mjs +62 -0
  431. package/src/knowledge-health/example.mjs +47 -0
  432. package/src/knowledge-health/index.mjs +10 -0
  433. package/src/knowledge-health/owner-join.mjs +88 -0
  434. package/src/knowledge-health/proof/common.mjs +24 -0
  435. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  436. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  437. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  438. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  439. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  440. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  441. package/src/knowledge-health/rule-identity.mjs +44 -0
  442. package/src/knowledge-health/workshop-composition.mjs +19 -0
  443. package/src/knowledge-health/workshop-profile.mjs +176 -0
  444. package/src/knowledge-health/workshop.mjs +99 -0
  445. package/src/learning/adapters.mjs +68 -0
  446. package/src/learning/assistance.mjs +127 -0
  447. package/src/learning/contracts.mjs +65 -0
  448. package/src/learning/store.mjs +272 -0
  449. package/src/personal-workspace/index.mjs +391 -0
  450. package/src/personal-workspace/selection.mjs +385 -0
  451. package/src/project/config.mjs +8 -0
  452. package/src/project/durable-state.mjs +3 -1
  453. package/src/project/file-class.mjs +1 -1
  454. package/src/projection/obsidian/edits/apply.mjs +55 -16
  455. package/src/projection/obsidian/personal-workspace.mjs +436 -0
  456. package/src/projection/obsidian/proposals/adapter.mjs +47 -8
  457. package/src/projection/obsidian/proposals/observation.mjs +10 -2
  458. package/src/projection/obsidian/publication/publisher.mjs +142 -13
  459. package/src/projection/obsidian/publication/vault-list.mjs +4 -3
  460. package/src/projection/obsidian/recovery/index.mjs +4 -1
  461. package/src/projection/obsidian/recovery/store.mjs +160 -7
  462. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  463. package/src/readiness/readiness.mjs +10 -3
  464. package/src/reflection/index.mjs +56 -0
  465. package/src/runtime/obsidian/app-production-seams.mjs +48 -16
  466. package/src/runtime/obsidian/app-registration.mjs +175 -28
  467. package/src/runtime/obsidian/app-restart.mjs +154 -0
  468. package/src/runtime/obsidian/engine.mjs +163 -20
  469. package/src/runtime/obsidian/extension-points.mjs +1 -1
  470. package/src/runtime/obsidian/index.mjs +10 -7
  471. package/src/runtime/obsidian/launch-plan.mjs +7 -4
  472. package/src/runtime/obsidian/lifecycle.mjs +92 -12
  473. package/src/runtime/obsidian/login-item.mjs +332 -0
  474. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  475. package/src/runtime/obsidian/opening.mjs +171 -37
  476. package/src/runtime/obsidian/pipeline.mjs +71 -3
  477. package/src/runtime/obsidian/plugin-channel.mjs +11 -3
  478. package/src/runtime/obsidian/plugin-choice.mjs +16 -4
  479. package/src/runtime/obsidian/plugin-drift.mjs +59 -11
  480. package/src/runtime/obsidian/plugin-presence.mjs +14 -1
  481. package/src/runtime/obsidian/process-identity.mjs +20 -10
  482. package/src/runtime/obsidian/project-views.mjs +282 -0
  483. package/src/runtime/obsidian/questions.mjs +48 -0
  484. package/src/runtime/obsidian/release-watch.mjs +56 -0
  485. package/src/runtime/obsidian/service-main.mjs +99 -16
  486. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  487. package/src/runtime/obsidian/service-managers.mjs +275 -0
  488. package/src/runtime/obsidian/service-record.mjs +87 -23
  489. package/src/runtime/obsidian/service.mjs +36 -3
  490. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  491. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  492. package/src/runtime/obsidian/vault-location.mjs +300 -0
  493. package/src/runtime/obsidian/view-counts.mjs +34 -0
  494. package/src/server/local-sidecar.mjs +169 -2
  495. package/src/server/server.mjs +28 -2
  496. package/src/template-bindings/project.mjs +92 -0
  497. package/src/templates/profile.mjs +225 -0
  498. package/src/trackables/domain.mjs +175 -0
  499. package/src/trackables/store.mjs +114 -0
  500. package/src/ui/knowledge-page.mjs +1388 -0
  501. package/src/upgrade/explanation.mjs +1 -1
  502. package/src/upgrade/template-participant.mjs +171 -0
  503. package/src/upgrade/transaction.mjs +72 -25
  504. package/src/upgrade/upgrade.mjs +1 -0
  505. package/templates/external-project-workspace/pack-lifecycle.json +6 -1
  506. package/templates/knowledge-workspace/.gitattributes +2 -0
  507. package/templates/knowledge-workspace/AGENTS.md +27 -0
  508. package/templates/knowledge-workspace/README.md +44 -0
  509. package/templates/knowledge-workspace/atelier.project.json +22 -0
  510. package/templates/knowledge-workspace/gitignore +6 -0
  511. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  512. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  513. package/templates/knowledge-workspace/records/forecast.md +22 -0
  514. package/templates/knowledge-workspace/records/inspection.md +25 -0
  515. package/templates/knowledge-workspace/records/telescope.md +24 -0
  516. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -123,6 +123,13 @@ function buildPackageReadiness({
123
123
  }
124
124
  }
125
125
 
126
+ // Paths in the written artifact are relative to the project config directory
127
+ // in POSIX form, so `readiness --check` does not depend on where the project
128
+ // is checked out and the artifact names no machine-specific directory.
129
+ function configRelative(project, file) {
130
+ return path.relative(project.configDir, file).split(path.sep).join('/') || '.'
131
+ }
132
+
126
133
  function buildProjectReadiness({ project, graph = null } = {}) {
127
134
  const loadedGraph = graph || (fs.existsSync(project.graphPath) ? readJson(project.graphPath) : null)
128
135
  const projectionEntry = path.join(project.outputRoot, 'index.html')
@@ -142,14 +149,14 @@ function buildProjectReadiness({ project, graph = null } = {}) {
142
149
  blockers,
143
150
  warnings,
144
151
  graph: {
145
- path: project.graphPath,
152
+ path: configRelative(project, project.graphPath),
146
153
  nodes: loadedGraph?.counts?.nodes ?? loadedGraph?.nodes?.length ?? 0,
147
154
  edges: loadedGraph?.counts?.edges ?? loadedGraph?.edges?.length ?? 0,
148
155
  diagnostics: loadedGraph?.counts?.diagnostics ?? loadedGraph?.diagnostics?.length ?? 0,
149
156
  },
150
157
  projection: {
151
- outputRoot: project.outputRoot,
152
- entry: projectionEntry,
158
+ outputRoot: configRelative(project, project.outputRoot),
159
+ entry: configRelative(project, projectionEntry),
153
160
  },
154
161
  support: {
155
162
  telemetry: 'none',
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs'
2
+ import Ajv from 'ajv/dist/2020.js'
3
+ import addFormats from 'ajv-formats'
4
+ import { boundedLearningValue, learningDigest as digest } from '../learning/contracts.mjs'
5
+ const schema = JSON.parse(fs.readFileSync(new URL('../../contracts/atelier-interaction.v1.schema.json', import.meta.url)))
6
+ const ajv = new Ajv({ strict: true, allErrors: true }); addFormats(ajv)
7
+ const validators = new Map()
8
+ export function validateInteraction(value, shape) {
9
+ try { boundedLearningValue(value) } catch (error) { return [error.message] }
10
+ if (!['observation', 'assessment', 'act', 'policy'].includes(shape)) return ['unsupported interaction shape']
11
+ if (!validators.has(shape)) validators.set(shape, ajv.compile({ $schema: schema.$schema, $defs: schema.$defs, $ref: `#/$defs/${shape}` }))
12
+ const validate = validators.get(shape)
13
+ return validate(value) ? [] : validate.errors.map(e => `${e.instancePath || '/'}: ${e.message}`)
14
+ }
15
+ export function assertInteraction(value, shape) {
16
+ const errors = validateInteraction(value, shape)
17
+ if (errors.length) throw new Error(`invalid ${shape}: ${errors.join('; ')}`)
18
+ return value
19
+ }
20
+ export function reflectionReference(record) {
21
+ const kind = record?.schema === 'atelier-reflection-observation@v1' ? 'observation' : 'assessment'
22
+ assertInteraction(record, kind)
23
+ return { kind, id: record.id, digest: digest(record) }
24
+ }
25
+ export function qualifyReflection({ assessment, observations, scope, purpose }) {
26
+ assertInteraction(assessment, 'assessment'); boundedLearningValue(observations)
27
+ if (!Array.isArray(observations) || observations.length > 128) throw new Error('bounded observations required')
28
+ for (const o of observations) assertInteraction(o, 'observation')
29
+ if (new Set(observations.map(o => o.id)).size !== observations.length) throw new Error('ambiguous observation identity')
30
+ const reasons = []
31
+ if (assessment.scope !== scope || assessment.purpose !== purpose || assessment.status !== 'active') reasons.push('assessment-outside-active-purpose')
32
+ if (!assessment.evidence.length) reasons.push('assessment-without-evidence')
33
+ if (new Set(assessment.evidence.map(e => `${e.kind}:${e.id}`)).size !== assessment.evidence.length) reasons.push('duplicate-evidence')
34
+ const admitted = []
35
+ for (const pin of assessment.evidence) {
36
+ const o = observations.find(o => o.id === pin.id)
37
+ if (pin.kind !== 'observation' || !o || digest(o) !== pin.digest || o.status !== 'active' || o.scope !== scope || o.purpose !== purpose) reasons.push(`evidence-stale-or-outside-purpose:${pin.id}`)
38
+ else admitted.push(o)
39
+ }
40
+ if (assessment.claimKind === 'independent-capacity' && (assessment.subject.kind !== 'person' || admitted.some(o => o.assistance !== 'independent' || o.subject.kind !== 'person' || o.subject.id !== assessment.subject.id))) reasons.push('independent-capacity-not-supported-by-admitted-evidence')
41
+ if (assessment.claimKind === 'system-friction' && assessment.subject.kind === 'person') reasons.push('system-friction-cannot-be-a-personal-capacity-claim')
42
+ return { reference: reflectionReference(assessment), current: reasons.length === 0, reasons,
43
+ uncertainty: assessment.uncertainty, limits: assessment.limits, semanticTruthVerified: false, observationAuthorityTransferred: false }
44
+ }
45
+ // The caller supplies an attributed judgment. This function qualifies its exact
46
+ // evidence, never invents a developmental score or silently persists a profile.
47
+ export function prepareReflectiveAct({ assessment, observations, act }) {
48
+ assertInteraction(act, 'act')
49
+ const result = qualifyReflection({ assessment, observations, scope: act.scope, purpose: act.purpose })
50
+ if (!result.current) throw new Error(`reflection unavailable: ${result.reasons.join('; ')}`)
51
+ if (act.origin !== 'witness' || !['reflective', 'coaching'].includes(act.mode)) throw new Error('reflective contribution requires Witness origin and declared mode')
52
+ const required = [reflectionReference(assessment), ...assessment.evidence]
53
+ for (const pin of required) if (!act.dependencies.some(p => digest(p) === digest(pin))) throw new Error('reflective act must pin its assessment and evidence')
54
+ if (act.mode === 'coaching' && !act.dependencies.some(p => p.kind === 'intention')) throw new Error('coaching requires a current chosen intention')
55
+ return { act: structuredClone(act), assessment: result, deliverable: false, executionAuthorized: false }
56
+ }
@@ -1,25 +1,28 @@
1
- import { execFile, spawnSync } from 'node:child_process'
1
+ import { execFile, execFileSync, spawnSync } from 'node:child_process'
2
2
  import fs from 'node:fs'
3
+ import { performance } from 'node:perf_hooks'
3
4
  import path from 'node:path'
4
5
  import { NEUTRAL_DIRECTORY, defaultCliPath, defaultObsidianProcessProbe, routedCall } from '../../projection/obsidian/publication/transport.mjs'
5
6
  import { obsidianSandboxedBuild, obsidianUserDataDir, readObsidianSettings } from '../../projection/obsidian/publication/vault-list.mjs'
6
7
  import { realPathAsStored } from '../../project/private-state.mjs'
7
8
  import { appAnswered, readEvalAnswer, readVersionAnswer } from './app-capability.mjs'
8
- import { registerVaultInObsidianSettings } from './app-registration.mjs'
9
+ import { backupObsidianSettings, registerVaultInObsidianSettings } from './app-registration.mjs'
10
+ import { createAppQuitter } from './app-restart.mjs'
9
11
  import { launchPlan, runLaunchPlan, urlProcessed } from './launch-plan.mjs'
10
12
 
11
13
  // The production seams of `obsidian open` and of the service's adapter
12
- // factory: the only code that asks the installed Obsidian anything or asks the
13
- // operating system to open it.
14
+ // factory: the only code that asks the installed Obsidian anything, asks the
15
+ // operating system to open it, or quits it (for a restart a person asked for).
14
16
  //
15
17
  // This module is imported by exactly two places, both real command-line
16
18
  // entries and both behind an explicit `--adapter=obsidian-cli`: the service
17
19
  // entry (service-main.mjs) and the command entry (src/commands/obsidian.mjs,
18
20
  // `production: true`). No test of the default suite imports it in-process, and
19
- // importing it leaves a trace below that the test suite asserts is absent. One
20
- // test imports it in a child process, with a stand-in for the command-line
21
- // tool, to read the version answers it parses; the opt-in real-app suite
22
- // imports it to reach an isolated app through that app's private HOME.
21
+ // importing it leaves a trace below that the test suite asserts is absent.
22
+ // Tests import it in child processes only, with a private HOME: with a
23
+ // stand-in for the command-line tool, to read the answers it parses, and with
24
+ // a stand-in execFile, to check the launcher's executors; the opt-in real-app
25
+ // suite imports it to reach an isolated app through that app's private HOME.
23
26
  //
24
27
  // Status: exercised against an isolated Obsidian 1.13.7 (installer 1.12.7) on
25
28
  // macOS by the opt-in real-app suite in test/obsidian-first-open-real-app.test.mjs;
@@ -127,7 +130,8 @@ function vaultRegisterCode(vaultRoot) {
127
130
  // listThroughApp() -> { answered: true, vaults } | { answered: false, reason }
128
131
  // registerThroughApp({ vaultRoot }) -> { answered: true, result } | { answered: false, reason }
129
132
  // readSettings() -> readObsidianSettings answer (never writes)
130
- // registerInSettings({ vaultRoot }) -> registerVaultInObsidianSettings answer
133
+ // registerInSettings({ vaultRoot, keepBackups }) -> registerVaultInObsidianSettings answer
134
+ // backupSettings() -> backupObsidianSettings answer (a copy beside the file; the file is not written)
131
135
  //
132
136
  // Through the app only while it runs and answers; in its settings file only
133
137
  // while no Obsidian runs, which registerVaultInObsidianSettings checks itself.
@@ -155,9 +159,13 @@ export function createProductionAppRegistry({
155
159
  return answer.answered ? { answered: true, result: answer.value?.result ?? null } : answer
156
160
  },
157
161
  readSettings: () => (sandbox === null ? readObsidianSettings({ userDataDir }) : sandboxed),
158
- registerInSettings: ({ vaultRoot }) => (sandbox !== null ? sandboxed : userDataDir === null
162
+ // A copy of the settings file as it is, before a restart sends the app any signal (backupObsidianSettings).
163
+ backupSettings: () => (sandbox !== null ? sandboxed : userDataDir === null
159
164
  ? { ok: false, code: 'obsidian-settings-location-unknown', message: 'where Obsidian keeps its settings on this system is not known' }
160
- : registerVaultInObsidianSettings({ userDataDir, vaultRoot, processProbe })),
165
+ : backupObsidianSettings({ userDataDir })),
166
+ registerInSettings: ({ vaultRoot, keepBackups = [] }) => (sandbox !== null ? sandboxed : userDataDir === null
167
+ ? { ok: false, code: 'obsidian-settings-location-unknown', message: 'where Obsidian keeps its settings on this system is not known' }
168
+ : registerVaultInObsidianSettings({ userDataDir, vaultRoot, processProbe, keepBackups })),
161
169
  }
162
170
  }
163
171
 
@@ -167,18 +175,25 @@ export function createProductionAppRegistry({
167
175
  // the app itself answers the tool; a running app is handed the URL through its
168
176
  // tool, which takes a URL even with its command line turned off, and through the
169
177
  // operating system when the tool did not take it. A URL is never handed to the
170
- // operating system for an app this launch started. No shell.
178
+ // operating system for an app this launch started. No shell. `execFile` is
179
+ // node's, and a stand-in in the test of these executors, which runs in a child
180
+ // process (the default suite never imports this module in-process).
171
181
  export function createProductionLauncher({
172
- platform = process.platform, env = process.env, cliPath = defaultCliPath(platform), workingDirectory = NEUTRAL_DIRECTORY, waitMs, pollMs, sleep,
182
+ platform = process.platform, env = process.env, cliPath = defaultCliPath(platform), workingDirectory = NEUTRAL_DIRECTORY, waitMs, pollMs, sleep, execFile: runFile = execFile,
173
183
  } = {}) {
174
184
  const osCommand = platform === 'darwin' ? '/usr/bin/open' : platform === 'linux' ? 'xdg-open' : null
175
185
  const run = (file, args, timeout = 15_000) => new Promise((resolve) => {
176
- execFile(file, args, { env, cwd: workingDirectory, timeout, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => resolve({ failed: Boolean(error), stdout: String(stdout ?? ''), stderr: String(stderr ?? '') }))
186
+ runFile(file, args, { env, cwd: workingDirectory, timeout, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => resolve({ failed: Boolean(error), stdout: String(stdout ?? ''), stderr: String(stderr ?? '') }))
177
187
  })
178
188
  return {
179
- open({ vaultId = null, vaultPath = null, appRunning }) {
189
+ // Starts a quit app with no URL, so it reopens the vaults its list flags open: for giving back an app `open`
190
+ // quit to restart it, when the vault could not be added after all. macOS only, as the restart is.
191
+ startPlain: async () => platform === 'darwin' && !(await run(osCommand, ['-b', 'md.obsidian'])).failed,
192
+ // `startedByThisOpen`: this `open` started the app with an earlier launch; the link goes through its tool only.
193
+ open({ vaultId = null, vaultPath = null, appRunning, startedByThisOpen = false }) {
180
194
  const plan = launchPlan({ platform, appRunning, vaultId, vaultPath })
181
195
  return runLaunchPlan(plan, {
196
+ startedEarlier: startedByThisOpen === true,
182
197
  start: async () => !(await run(osCommand, ['-b', 'md.obsidian'])).failed,
183
198
  answered: async () => { const reply = await run(cliPath, ['version'], CLI_TIMEOUT_MS); return appAnswered({ stdout: reply.stdout, stderr: reply.stderr, exited: !reply.failed }) },
184
199
  handLink: async (uri) => urlProcessed((await run(cliPath, [uri], CLI_TIMEOUT_MS)).stdout),
@@ -189,6 +204,23 @@ export function createProductionLauncher({
189
204
  }
190
205
  }
191
206
 
207
+ // Quits the running app for a restart a person asked for (app-restart.mjs): SIGTERM to its main process only, proven
208
+ // from this machine's process table (`ps`, never a pattern search) as this account's, then waits until the process
209
+ // probe says no Obsidian runs. The table is `pid ppid uid executable`; one process is described again by its parent,
210
+ // user, start time and executable immediately before the signal.
211
+ export function createProductionAppQuitter({ platform = process.platform, processProbe = () => defaultObsidianProcessProbe({ platform }), waitMs } = {}) {
212
+ const ps = (args) => execFileSync('/bin/ps', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 16 * 1024 * 1024, timeout: 5000, killSignal: 'SIGKILL' })
213
+ return createAppQuitter({
214
+ platform, uid: typeof process.getuid === 'function' ? process.getuid() : null, processProbe,
215
+ readTable: () => ps(['-ww', '-A', '-o', 'pid=,ppid=,uid=,comm=']),
216
+ readProcess: (pid) => ps(['-ww', '-o', 'ppid=,uid=,lstart=,comm=', '-p', String(pid)]),
217
+ signal: (pid) => process.kill(pid, 'SIGTERM'),
218
+ // The wait and the linger are measured on a monotonic clock: a step of the wall clock changes neither.
219
+ now: () => performance.now(),
220
+ ...(waitMs === undefined ? {} : { waitMs }),
221
+ })
222
+ }
223
+
192
224
  export function createProductionAppSeams(options = {}) {
193
- return { appProbe: createProductionAppProbe(options), launcher: createProductionLauncher(options), registry: createProductionAppRegistry(options) }
225
+ return { appProbe: createProductionAppProbe(options), launcher: createProductionLauncher(options), registry: createProductionAppRegistry(options), quitter: createProductionAppQuitter(options) }
194
226
  }
@@ -5,7 +5,7 @@ import { MAX_OBSIDIAN_SETTINGS_BYTES, OBSIDIAN_SETTINGS_FILE, enclosingVaults, f
5
5
  import { openRegularFileNoFollow, realPathAsStored, syncPrivateDirectory } from '../../project/private-state.mjs'
6
6
 
7
7
  // Adding a view's vault to Obsidian's own vault list while Obsidian is not
8
- // running, so that `obsidian://open?path=` finds it when the app starts.
8
+ // running, so that the app opens it when it starts.
9
9
  //
10
10
  // Outside Atelier's own storage, it is the only file of another application
11
11
  // Atelier writes. The list is `obsidian.json` in the app's user-data directory
@@ -18,10 +18,11 @@ import { openRegularFileNoFollow, realPathAsStored, syncPrivateDirectory } from
18
18
  //
19
19
  // - never for a Flatpak or snap build, which reads its list inside its
20
20
  // sandbox and never this file (`sandbox`, see obsidianSandboxedBuild);
21
- // - only an existing file, in an existing user-data directory, both this
21
+ // - an existing file only in an existing user-data directory, both this
22
22
  // user's own, a real directory and a regular file (no link, and no second
23
- // name: a hard link would keep the old list), holding a JSON object: a file
24
- // Obsidian has not written is never created;
23
+ // name: a hard link would keep the old list), holding a JSON object;
24
+ // - a file that does not exist (Obsidian never started on this account) is
25
+ // created, and only then: see createObsidianSettings below;
25
26
  // - never for a vault inside a folder the list has as a vault already: the
26
27
  // app would show its notes in that vault too, and a call run in its folder
27
28
  // would reach that vault (see vault-list.mjs);
@@ -29,6 +30,12 @@ import { openRegularFileNoFollow, realPathAsStored, syncPrivateDirectory } from
29
30
  // it, as values: the app itself rewrites the file with JSON.stringify;
30
31
  // - the new entry is { path: the vault root's real path, ts: now, open:
31
32
  // true } under a fresh 16-hex id that no entry has;
33
+ // - the command-line switch: whenever the list is written, `cli` is set to
34
+ // true when it is not (the default of a new installation is off, and
35
+ // `open` needs the command line while the app holds the vault). A vault the
36
+ // list has already, with `cli` not true, is written for the switch alone.
37
+ // Only here, so only while no Obsidian runs: a running app keeps its
38
+ // switch in memory and would write it back;
32
39
  // - the new file is no larger than a settings file this module reads;
33
40
  // - the bytes as they were are kept beside it in
34
41
  // `obsidian.json.atelier-backup-<UTC time>`, fsynced, before the file is
@@ -46,13 +53,14 @@ const currentUid = () => (typeof process.getuid === 'function' ? process.getuid(
46
53
  const compactUtc = (ms) => new Date(ms).toISOString().replace(/[-:.]/g, '')
47
54
  const BACKUP = new RegExp(`^${OBSIDIAN_SETTINGS_FILE.replaceAll('.', '\\.')}\\.atelier-backup-\\d{8}T\\d{9}Z$`)
48
55
 
49
- // Keeps the first backup, the list as it was before Atelier ever wrote it, and `latest`; removes the ones between.
50
- // Their names sort as their times. A backup that cannot be removed stays.
51
- function pruneBackups(directory, latest) {
56
+ // Keeps the first backup, the list as it was before Atelier ever wrote it, `latest`, and the names in `keep` (a copy
57
+ // the same run took before it quit the app); removes the others between. Their names sort as their times. A backup
58
+ // that cannot be removed stays.
59
+ function pruneBackups(directory, latest, keep = []) {
52
60
  let names
53
61
  try { names = fs.readdirSync(directory).filter((name) => BACKUP.test(name)).sort() } catch { return }
54
62
  for (const name of names.slice(1)) {
55
- if (name === latest) continue
63
+ if (name === latest || keep.includes(name)) continue
56
64
  const file = path.join(directory, name)
57
65
  try { if (fs.lstatSync(file).isFile()) fs.unlinkSync(file) } catch { /* it stays */ }
58
66
  }
@@ -82,35 +90,113 @@ function readBytesNoFollow(file) {
82
90
  try { return fs.readFileSync(descriptor) } finally { try { fs.closeSync(descriptor) } catch { /* read already, or failed */ } }
83
91
  }
84
92
 
85
- // { ok: true, registered: 'already' | 'written', entry: { id, path, open }, confirmed, vaults, backupPath?, reason? }
86
- // or a typed refusal { ok: false, code, message }. `confirmed` is false when an
87
- // app appeared right after the rename (`app-started-during-registration`): it
88
- // may have read the list before it, so whoever asked verifies through the
89
- // app; and when the written file could not be read back to find the entry
90
- // (`registration-not-read-back`). `vaults` is the list as it was found or
91
- // written. `processProbe()` answers 'absent', 'running' or 'unknown'; only
92
- // 'absent' allows a write. `sandbox` names a Flatpak or snap build found for
93
+ // Creates the settings file of an Obsidian that never started on this account:
94
+ // the file does not exist, and the process table says, positively, that no
95
+ // Obsidian runs, read before and again immediately before the file appears.
96
+ // The app reads the file when it starts (a missing or unreadable one reads as
97
+ // no settings) and opens every vault flagged open, so a plain start opens this
98
+ // one, with its command line on. Only:
99
+ //
100
+ // - in the user-data directory as it is when that is this user's own real
101
+ // directory, or, when it does not exist, in one created here with mode
102
+ // 0700 inside an existing real parent directory (a missing parent is
103
+ // `obsidian-settings-missing`: nothing is created above the directory);
104
+ // - with exactly { "vaults": { "<16 hex>": { path, ts, open: true } },
105
+ // "cli": true }, mode 0600;
106
+ // - by an exclusive create: the complete bytes are written, fsynced, to a
107
+ // temporary file in the directory, which is then linked to the file's name
108
+ // (a link fails on any name there, a file the app wrote meanwhile
109
+ // included, which is kept: `obsidian-settings-changed`), and the
110
+ // temporary name removed. A reader never sees a part of the file.
111
+ //
112
+ // There is nothing to back up. A refusal removes what it created, the
113
+ // directory included.
114
+ function createObsidianSettings({ userDataDir, vaultRoot, absent, now, randomBytes, uid }) {
115
+ const owned = (stat) => uid === null || stat.uid === uid
116
+ let directory = null
117
+ try { directory = fs.lstatSync(userDataDir) } catch (error) { if (error.code !== 'ENOENT') return refused('obsidian-settings-unreadable', 'the Obsidian settings directory cannot be read') }
118
+ let folder
119
+ try { folder = realPathAsStored(vaultRoot) } catch { return refused('vault-root-missing', 'the view\'s vault folder does not exist') }
120
+ if (!fs.statSync(folder).isDirectory()) return refused('vault-root-missing', 'the view\'s vault folder is not a directory')
121
+ let madeDirectory = false
122
+ if (directory === null) {
123
+ const parent = path.dirname(userDataDir)
124
+ let above
125
+ try { above = fs.lstatSync(parent) } catch { above = null }
126
+ if (above === null || above.isSymbolicLink() || !above.isDirectory() || !owned(above)) return refused('obsidian-settings-missing', 'Obsidian has no settings directory on this account, and the folder it would be in is missing, a link or not this user\'s own')
127
+ try { fs.mkdirSync(userDataDir, { mode: 0o700 }) } catch (error) {
128
+ return refused(error.code === 'EEXIST' ? 'obsidian-settings-changed' : 'obsidian-settings-unwritable', 'the Obsidian settings directory cannot be created')
129
+ }
130
+ madeDirectory = true
131
+ directory = fs.lstatSync(userDataDir)
132
+ }
133
+ const removeDirectory = () => { if (madeDirectory) try { fs.rmdirSync(userDataDir) } catch { /* something else is in it now: it stays */ } }
134
+ if (directory.isSymbolicLink() || !directory.isDirectory()) return refused('obsidian-settings-unsafe', 'the Obsidian settings directory is a link or not a directory')
135
+ if (!owned(directory)) return refused('obsidian-settings-not-owned', 'the Obsidian settings directory belongs to another user')
136
+ const id = randomBytes(8).toString('hex')
137
+ const at = now()
138
+ const document = { vaults: { [id]: { path: folder, ts: at, open: true } }, cli: true }
139
+ const bytes = Buffer.from(JSON.stringify(document), 'utf8')
140
+ if (bytes.length > MAX_OBSIDIAN_SETTINGS_BYTES) { removeDirectory(); return refused('obsidian-settings-too-large', 'with this vault the Obsidian settings file would be larger than a settings file can be') }
141
+ const file = path.join(userDataDir, OBSIDIAN_SETTINGS_FILE)
142
+ const temporary = path.join(userDataDir, `.${OBSIDIAN_SETTINGS_FILE}.atelier-${randomBytes(6).toString('hex')}.tmp`)
143
+ const undo = () => { try { fs.unlinkSync(temporary) } catch { /* already gone */ } removeDirectory() }
144
+ try { writeNewFile(temporary, bytes, 0o600) } catch { undo(); return refused('obsidian-settings-unwritable', 'the Obsidian settings directory cannot be written') }
145
+ // Immediately before the file appears: still no app.
146
+ if (!absent()) { undo(); return refused('app-may-be-running', 'an Obsidian process appeared, and its settings belong to it') }
147
+ try { fs.linkSync(temporary, file) } catch (error) {
148
+ undo()
149
+ return refused(error.code === 'EEXIST' ? 'obsidian-settings-changed' : 'obsidian-settings-unwritable', error.code === 'EEXIST' ? 'an Obsidian settings file appeared while it was being created, and it was kept as it is' : 'the Obsidian settings file cannot be created')
150
+ }
151
+ try { fs.unlinkSync(temporary) } catch { /* the file is complete; a second name is refused by the next write */ }
152
+ try { syncPrivateDirectory(userDataDir); if (madeDirectory) syncPrivateDirectory(path.dirname(userDataDir)) } catch { /* the file exists; a directory that cannot be fsynced is not undone */ }
153
+ const stillAbsent = absent()
154
+ const written = readObsidianSettings({ userDataDir, uid })
155
+ const entry = written.ok ? findVaultEntry(written.vaults, vaultRoot) : null
156
+ const reason = !stillAbsent ? 'app-started-during-registration' : entry === null ? 'registration-not-read-back' : null
157
+ return {
158
+ ok: true, registered: 'created', entry: entry ?? { id, path: folder, open: true }, confirmed: reason === null, vaults: written.ok ? written.vaults : document.vaults,
159
+ file, created: { directory: madeDirectory }, cliTurnedOn: true, ...(reason === null ? {} : { reason }),
160
+ }
161
+ }
162
+
163
+ // { ok: true, registered: 'already' | 'written' | 'created', entry: { id, path, open }, confirmed, vaults, file, cliTurnedOn,
164
+ // backupPath?, created?, reason? } or a typed refusal { ok: false, code, message }. `registered` is 'already' when the
165
+ // list had the vault (then the file was written only to turn the command line on, `cliTurnedOn`), 'written' when this
166
+ // vault was added to an existing file, 'created' when the file was created (createObsidianSettings). `confirmed` is
167
+ // false when an app appeared right after the rename (`app-started-during-registration`): it may have read the list
168
+ // before it, so whoever asked verifies through the app; and when the written file could not be read back to find the
169
+ // entry (`registration-not-read-back`). `vaults` is the list as it was found or written. `processProbe()` answers
170
+ // 'absent', 'running' or 'unknown'; only 'absent' allows a write. `sandbox` names a Flatpak or snap build found for
93
171
  // this account, which refuses (`obsidian-sandboxed`).
94
- export function registerVaultInObsidianSettings({ userDataDir, vaultRoot, processProbe, sandbox = null, now = () => Date.now(), randomBytes = cryptoRandomBytes, uid = currentUid() } = {}) {
172
+ // `keepBackups`: backup names (in the file's directory) this write must not prune: the copy `open` kept before it
173
+ // quit the app for this write.
174
+ export function registerVaultInObsidianSettings({ userDataDir, vaultRoot, processProbe, sandbox = null, now = () => Date.now(), randomBytes = cryptoRandomBytes, uid = currentUid(), keepBackups = [] } = {}) {
95
175
  if (typeof processProbe !== 'function') throw new TypeError('registering a vault needs a process probe')
96
176
  if (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot) || vaultRoot.includes('\u0000')) throw new TypeError('vaultRoot must be an absolute path')
97
177
  if (sandbox !== null) return refused('obsidian-sandboxed', `this Obsidian is a ${sandbox} build, which reads its vault list inside its sandbox, where Atelier does not write`)
98
178
  const absent = () => { try { return processProbe() === 'absent' } catch { return false } }
99
179
  if (!absent()) return refused('app-may-be-running', 'an Obsidian process may be running, and its vault list belongs to it')
100
180
  const settings = readObsidianSettings({ userDataDir, uid })
181
+ // No file: Obsidian never started on this account. A missing directory is refused as missing when it has no parent.
182
+ if (!settings.ok && settings.code === 'obsidian-settings-missing' && typeof userDataDir === 'string' && path.isAbsolute(userDataDir)) return createObsidianSettings({ userDataDir, vaultRoot, absent, now, randomBytes, uid })
101
183
  if (!settings.ok) return settings
102
184
  const known = findVaultEntry(settings.vaults, vaultRoot)
103
- if (known) return { ok: true, registered: 'already', entry: known, confirmed: true, vaults: settings.vaults }
185
+ const switchOn = settings.document.cli !== true
186
+ if (known && !switchOn) return { ok: true, registered: 'already', entry: known, confirmed: true, vaults: settings.vaults, file: settings.file, cliTurnedOn: false }
104
187
  if (settings.links > 1) return refused('obsidian-settings-unsafe', 'the Obsidian settings file has a second name (a hard link), which a replacement would leave with the old list')
105
- if (enclosingVaults({ vaults: settings.vaults, vaultRoot }).length > 0) return refused('vault-inside-another-vault', 'Obsidian lists a vault at a folder that contains this vault\'s folder')
188
+ if (!known && enclosingVaults({ vaults: settings.vaults, vaultRoot }).length > 0) return refused('vault-inside-another-vault', 'Obsidian lists a vault at a folder that contains this vault\'s folder')
106
189
 
107
- let folder
108
- try { folder = realPathAsStored(vaultRoot) } catch { return refused('vault-root-missing', 'the view\'s vault folder does not exist') }
109
- if (!fs.statSync(folder).isDirectory()) return refused('vault-root-missing', 'the view\'s vault folder is not a directory')
110
- let id
111
- do { id = randomBytes(8).toString('hex') } while (Object.hasOwn(settings.vaults, id))
190
+ let folder = null
191
+ let id = null
192
+ if (!known) {
193
+ try { folder = realPathAsStored(vaultRoot) } catch { return refused('vault-root-missing', 'the view\'s vault folder does not exist') }
194
+ if (!fs.statSync(folder).isDirectory()) return refused('vault-root-missing', 'the view\'s vault folder is not a directory')
195
+ do { id = randomBytes(8).toString('hex') } while (Object.hasOwn(settings.vaults, id))
196
+ }
112
197
  const at = now()
113
- const document = { ...settings.document, vaults: { ...settings.vaults, [id]: { path: folder, ts: at, open: true } } }
198
+ // The switch keeps its place when the file has one; otherwise it is added last, as the app would.
199
+ const document = { ...settings.document, ...(known ? {} : { vaults: { ...settings.vaults, [id]: { path: folder, ts: at, open: true } } }), ...(switchOn ? { cli: true } : {}) }
114
200
  const bytes = Buffer.from(JSON.stringify(document), 'utf8')
115
201
  if (bytes.length > MAX_OBSIDIAN_SETTINGS_BYTES) return refused('obsidian-settings-too-large', 'with this vault the Obsidian settings file would be larger than a settings file can be')
116
202
  const directory = path.dirname(settings.file)
@@ -140,15 +226,76 @@ export function registerVaultInObsidianSettings({ userDataDir, vaultRoot, proces
140
226
  }
141
227
  created.splice(0)
142
228
  try { syncPrivateDirectory(directory) } catch { /* the rename is done; a directory that cannot be fsynced is not undone */ }
143
- pruneBackups(directory, path.basename(backupPath))
229
+ pruneBackups(directory, path.basename(backupPath), (Array.isArray(keepBackups) ? keepBackups : []).filter((name) => typeof name === 'string').map((name) => path.basename(name)))
144
230
  const stillAbsent = absent()
145
231
  const written = readObsidianSettings({ userDataDir, uid })
146
232
  const entry = written.ok ? findVaultEntry(written.vaults, vaultRoot) : null
147
233
  const reason = !stillAbsent ? 'app-started-during-registration' : entry === null ? 'registration-not-read-back' : null
148
234
  return {
149
- ok: true, registered: 'written', entry: entry ?? { id, path: folder, open: true }, backupPath, confirmed: reason === null, vaults: written.ok ? written.vaults : document.vaults,
150
- ...(reason === null ? {} : { reason }),
235
+ ok: true, registered: known ? 'already' : 'written', entry: entry ?? known ?? { id, path: folder, open: true }, backupPath, confirmed: reason === null, vaults: written.ok ? written.vaults : document.vaults,
236
+ file: settings.file, cliTurnedOn: switchOn, ...(reason === null ? {} : { reason }),
237
+ }
238
+ }
239
+
240
+ // Read-only, for `open` before it quits a running app to restart it: whether registerVaultInObsidianSettings would
241
+ // write this vault into the settings file `settings` (a readObsidianSettings answer, read while the app runs), as far
242
+ // as a read can tell. { ok: true, create } or { ok: false, code } with the refusal the write would give: an
243
+ // unreadable, linked, foreign, sandboxed or too large file, a vault inside another listed vault, a vault folder that
244
+ // is missing. A file that does not exist (an app that has shown only its starter window writes none) is the create
245
+ // case, `create: true`, checked as createObsidianSettings checks before it writes: the vault folder is a directory,
246
+ // and the user-data directory is this user's own real directory, or is absent inside this user's own real parent.
247
+ // What only the moment of the write can tell (an app that appears, a file that changes) is still checked then.
248
+ export function settingsWriteOutlook({ settings, vaultRoot, now = () => Date.now(), uid = currentUid() } = {}) {
249
+ const no = (code) => ({ ok: false, code })
250
+ if (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot)) return no('vault-root-missing')
251
+ if (settings?.ok !== true && settings?.code === 'obsidian-settings-missing') {
252
+ const userDataDir = settings.userDataDir
253
+ if (typeof userDataDir !== 'string' || !path.isAbsolute(userDataDir)) return no('obsidian-settings-location-unknown')
254
+ try { if (!fs.statSync(realPathAsStored(vaultRoot)).isDirectory()) return no('vault-root-missing') } catch { return no('vault-root-missing') }
255
+ const owned = (stat) => uid === null || stat.uid === uid
256
+ let directory = null
257
+ try { directory = fs.lstatSync(userDataDir) } catch (error) { if (error.code !== 'ENOENT') return no('obsidian-settings-unreadable') }
258
+ if (directory !== null) {
259
+ if (directory.isSymbolicLink() || !directory.isDirectory()) return no('obsidian-settings-unsafe')
260
+ return owned(directory) ? { ok: true, create: true } : no('obsidian-settings-not-owned')
261
+ }
262
+ let above = null
263
+ try { above = fs.lstatSync(path.dirname(userDataDir)) } catch { above = null }
264
+ return above !== null && !above.isSymbolicLink() && above.isDirectory() && owned(above) ? { ok: true, create: true } : no('obsidian-settings-missing')
265
+ }
266
+ if (settings?.ok !== true) return no(typeof settings?.code === 'string' ? settings.code : 'obsidian-settings-unreadable')
267
+ if (typeof settings.links === 'number' && settings.links > 1) return no('obsidian-settings-unsafe')
268
+ const size = Buffer.isBuffer(settings.bytes) ? settings.bytes.length : 0
269
+ const known = findVaultEntry(settings.vaults, vaultRoot)
270
+ if (known) return size + 16 > MAX_OBSIDIAN_SETTINGS_BYTES ? no('obsidian-settings-too-large') : { ok: true, create: false }
271
+ if (enclosingVaults({ vaults: settings.vaults, vaultRoot }).length > 0) return no('vault-inside-another-vault')
272
+ let folder
273
+ try { folder = realPathAsStored(vaultRoot); if (!fs.statSync(folder).isDirectory()) return no('vault-root-missing') } catch { return no('vault-root-missing') }
274
+ const entry = Buffer.byteLength(JSON.stringify({ '0123456789abcdef': { path: folder, ts: now(), open: true } }), 'utf8')
275
+ return size + entry + 16 > MAX_OBSIDIAN_SETTINGS_BYTES ? no('obsidian-settings-too-large') : { ok: true, create: false }
276
+ }
277
+
278
+ // Keeps a copy of the settings file as it is, beside it, under the backup name the write uses
279
+ // (`obsidian.json.atelier-backup-<UTC time>`), fsynced, with the file's mode. Read-only for the file itself. `open`
280
+ // takes it right before the first signal to a running app, so a list the app was writing when it ended can be
281
+ // restored. { ok: true, backupPath } or a typed refusal (readObsidianSettings's, or `obsidian-settings-unwritable`).
282
+ // The write that follows in the same run keeps it (`keepBackups`); a later write prunes it as any other backup.
283
+ export function backupObsidianSettings({ userDataDir, now = () => Date.now(), uid = currentUid() } = {}) {
284
+ const settings = readObsidianSettings({ userDataDir, uid })
285
+ if (!settings.ok) return settings
286
+ const directory = path.dirname(settings.file)
287
+ for (let offset = 0; offset < 3; offset += 1) {
288
+ const backupPath = path.join(directory, `${OBSIDIAN_SETTINGS_FILE}.atelier-backup-${compactUtc(now() + offset)}`)
289
+ try {
290
+ writeNewFile(backupPath, settings.bytes, settings.mode)
291
+ } catch (error) {
292
+ if (error.code === 'EEXIST') continue
293
+ return refused('obsidian-settings-unwritable', 'a copy of the Obsidian settings file cannot be kept beside it')
294
+ }
295
+ try { syncPrivateDirectory(directory) } catch { /* the copy is written */ }
296
+ return { ok: true, backupPath }
151
297
  }
298
+ return refused('obsidian-settings-unwritable', 'a copy of the Obsidian settings file cannot be kept beside it')
152
299
  }
153
300
 
154
301
  // What the app looks like, as one comparable text, from what can be seen