@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,7 +1,9 @@
1
+ import { prepareViewCooperatively } from '../../projection/obsidian/materialize/prepare-view.mjs'
1
2
  import { randomBytes as cryptoRandomBytes } from 'node:crypto'
2
3
  import fs from 'node:fs'
4
+ import path from 'node:path'
3
5
  import { AtelierDiagnosticError } from '../../project/config.mjs'
4
- import { ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
6
+ import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
5
7
  import { prepareView as productionPrepareView } from '../../projection/obsidian/materialize/index.mjs'
6
8
  import { preparePluginFiles } from '../../projection/obsidian/plugin-bridge/bundle.mjs'
7
9
  import { publishView as productionPublishView } from '../../projection/obsidian/publication/publisher.mjs'
@@ -21,6 +23,7 @@ import {
21
23
  import { createServiceServer } from './service-server.mjs'
22
24
  import { OPEN_EDIT_STATES, createMaintenanceStateStore } from './state-store.mjs'
23
25
  import { DEFAULT_MAX_BACKOFF_MS, DEFAULT_TICK_INTERVAL_MS, createTickLoop } from './tick-loop.mjs'
26
+ import { createLoadReport, createViewPermission } from './view-permission.mjs'
24
27
  import { createFsWatcherFactory } from './watchers.mjs'
25
28
 
26
29
  // The maintenance service of one workspace, inside one process.
@@ -48,12 +51,33 @@ import { createFsWatcherFactory } from './watchers.mjs'
48
51
  //
49
52
  // `adapterFactory` has no default here either: whoever starts the service
50
53
  // decides whether it may reach a running app.
54
+ //
55
+ // Started by a login item (`startup`), the service also asks `releaseWatch`
56
+ // after every tick whether the package it runs from changed on disk; when it
57
+ // did, it finishes that tick and shuts down with the reason RELEASE_CHANGED,
58
+ // which its process turns into an exit code the service manager restarts, on
59
+ // the new release (release-watch.mjs, service-main.mjs).
51
60
 
52
61
  export const SERVICE_STATUS_SCHEMA = 'atelier-obsidian-service-status/v1'
53
62
  export const DEFAULT_SHUTDOWN_GRACE_MS = 30 * 1000
63
+ export const RELEASE_CHANGED = 'release-changed'
64
+
65
+ // What of a project its workspace is resolved from, as a project that holds nothing else: the folder its pointer is
66
+ // under (readLocalPointer), the data root its local overlay prefers (resolveDataRoot), and the repositories private
67
+ // state may not overlap (protectedRoots). resolveServiceWorkspace reads these and nothing else of a project, so two
68
+ // projects with the same inputs resolve alike, and the plugin's status read compares exactly them (view-permission.mjs).
69
+ export function serviceWorkspaceInputs(project) {
70
+ const preference = project?.localOverlay?.overlay?.preferences?.[OBSIDIAN_EXT_KEY]
71
+ return {
72
+ ...(typeof project?.configDir === 'string' ? { configDir: project.configDir } : {}),
73
+ repos: (Array.isArray(project?.repos) ? project.repos : []).map((repo) => ({ path: repo?.path, external: Boolean(repo?.external) })),
74
+ localOverlay: { overlay: { preferences: preference === undefined ? {} : { [OBSIDIAN_EXT_KEY]: preference } } },
75
+ }
76
+ }
54
77
 
55
78
  // Where a workspace keeps its private state, without creating an identity.
56
- export function resolveServiceWorkspace({ project, dataRoot, env = process.env, platform = process.platform, create = false }) {
79
+ export function resolveServiceWorkspace({ project: whole, dataRoot, env = process.env, platform = process.platform, create = false }) {
80
+ const project = serviceWorkspaceInputs(whole)
57
81
  const pointer = readLocalPointer(project)
58
82
  if (pointer === null) return null
59
83
  const requested = workspaceStateRoot(resolveDataRoot({ dataRoot, pointer, project, env, platform }), pointer.workspaceId)
@@ -79,18 +103,37 @@ async function writeRecordPatiently(input) {
79
103
 
80
104
  export async function runMaintenanceService(options = {}) {
81
105
  const {
82
- loadProject, dataRoot, adapterFactory, entryPath, runtimeId = `rt-${cryptoRandomBytes(16).toString('hex')}`, startup = false,
106
+ loadProject, dataRoot,
107
+ // What the workspace is resolved from at once, where the loader may answer a promise (locatePersonalHome): the service
108
+ // starts from it, and the engine awaits the loader at its ticks. Without it, the loader's own first answer is used.
109
+ locateProject = null, adapterFactory, entryPath, runtimeId = `rt-${cryptoRandomBytes(16).toString('hex')}`, startup = false,
83
110
  intervalMs = DEFAULT_TICK_INTERVAL_MS, maxBackoffMs = Math.max(DEFAULT_MAX_BACKOFF_MS, intervalMs), shutdownGraceMs = DEFAULT_SHUTDOWN_GRACE_MS,
84
111
  clock = () => new Date(), log = () => {}, pid = process.pid, randomBytes = cryptoRandomBytes, env = process.env, platform = process.platform,
85
112
  engineOptions = {}, createEngine = createMaintenanceEngine,
86
113
  // What the adapter factory last learned about the installed app, when it qualifies one. Codes and versions only.
87
114
  appStatus = null,
115
+ // Under `startup`: { changed() } (createReleaseWatch), asked after every tick.
116
+ releaseWatch = null,
117
+ // The entry as this process was started by it (`process.argv[1]`), when that is not its real path: a login item
118
+ // names the installed entry by its path. It is recorded, so the process table can still prove this process ours.
119
+ invokedAs = null,
88
120
  } = options
89
121
  if (typeof loadProject !== 'function') throw new TypeError('the service needs loadProject')
90
122
  if (typeof adapterFactory !== 'function') throw new TypeError('the service needs an adapterFactory')
91
123
  if (typeof entryPath !== 'string') throw new TypeError('the service needs the path of its entry module')
92
124
 
93
- const workspace = resolveServiceWorkspace({ project: loadProject(), dataRoot, env, platform })
125
+ // The workspace comes from the project the loader answers now, or the locator. A loader without a locator, or a locator,
126
+ // that answers a promise is refused, typed, before anything is read from the promise (it has no pointer, no overlay and
127
+ // no repositories); its answer is let go unread. A located project names its folder, or is refused.
128
+ const project = locateProject === null ? loadProject() : locateProject()
129
+ if (typeof project?.then === 'function') {
130
+ // Let go without calling a `then` of its own, which a promise whose `constructor` reads as this realm's `Promise`
131
+ // may carry and which may throw; any other thenable is adopted, its `then` called once.
132
+ Promise.prototype.then.call(Promise.resolve(project), undefined, () => {})
133
+ refuse('service-loader-not-synchronous', 'the service resolves its workspace from a project its loader answers at once; a loader that answers a promise is not supported when the service starts')
134
+ }
135
+ if (locateProject !== null && typeof project?.configDir !== 'string') refuse('service-locator-invalid', 'the locator of this service answered no folder to resolve its workspace from')
136
+ const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
94
137
  if (!workspace?.workspaceRoot) refuse('service-workspace-not-prepared', 'this workspace has no private state yet; `start` prepares it')
95
138
  const { workspaceId, workspaceRoot } = workspace
96
139
  const settings = readServiceSettings({ workspaceRoot, workspaceId })
@@ -107,6 +150,7 @@ export async function runMaintenanceService(options = {}) {
107
150
  }
108
151
 
109
152
  const executable = executableIdentity(entryPath)
153
+ const alias = typeof invokedAs === 'string' && path.isAbsolute(invokedAs) && path.resolve(invokedAs) !== executable.path ? path.resolve(invokedAs) : null
110
154
  const startedAt = isoTime(clock)
111
155
  const identity = { serviceName: serviceNameFor(workspaceId), workspaceId, runtimeId, pid, host, port, executableDigest: executable.digest, startedAt }
112
156
  const bearer = randomBytes(32).toString('base64url')
@@ -117,6 +161,10 @@ export async function runMaintenanceService(options = {}) {
117
161
  let stopping = false
118
162
  let consecutiveFailures = 0
119
163
  let lastTick = null
164
+ // What the last tick that did not fail named each refused view for (refusalCauses). A view the next tick names for the
165
+ // same reason is not written to the log again: a refusal that lasts (a folder that stays unwritable is tried again at
166
+ // every tick) is written once, and again when its reason changes or after a tick that failed or did not name it.
167
+ let refusedLastTick = new Map()
120
168
  let settleDone
121
169
  const done = new Promise((resolve) => { settleDone = resolve })
122
170
 
@@ -126,7 +174,7 @@ export async function runMaintenanceService(options = {}) {
126
174
  // view without the plugin for this tick; the view itself is prepared as ever.
127
175
  const pluginFor = (scopeId) => {
128
176
  try {
129
- const vaultRoot = viewVaultRoot(workspaceRoot, scopeId)
177
+ const vaultRoot = viewVaultRoot(workspaceRoot, scopeId, workspaceId)
130
178
  const { choice, community } = decidePluginChoice({ workspaceRoot, workspaceId, scopeId, vaultRoot, clock })
131
179
  const off = choice.state === 'off'
132
180
  const bearer = ensurePluginBearer({ workspaceRoot, workspaceId, scopeId, randomBytes, clock })
@@ -146,6 +194,14 @@ export async function runMaintenanceService(options = {}) {
146
194
  }
147
195
  }
148
196
  const prepareWithPlugin = (input) => (engineOptions.seams?.prepareView ?? productionPrepareView)({ ...input, plugin: pluginFor(input.scope.scopeId) })
197
+ // The engine's rule (engine.mjs): a prepareView that is not the one that ships, handed in with the cooperative
198
+ // preparation left as it ships or absent, prepares the view, synchronously; the engine then gets no cooperative seam.
199
+ // Nor does it when the cooperative preparation was handed in as null: that says there is none, as it does to the engine.
200
+ const handed = engineOptions.seams ?? {}
201
+ const replacedPrepare = typeof handed.prepareView === 'function' && handed.prepareView !== productionPrepareView
202
+ && (handed.prepareViewCooperatively === undefined || handed.prepareViewCooperatively === prepareViewCooperatively)
203
+ const cooperative = replacedPrepare || handed.prepareViewCooperatively === null ? null : handed.prepareViewCooperatively ?? prepareViewCooperatively
204
+ const prepareCooperativelyWithPlugin = cooperative === null ? null : (input) => cooperative({ ...input, plugin: pluginFor(input.scope.scopeId) })
149
205
  // An entry offered to a vault and now in place is confirmed: from then on, a list without it is the person's decision.
150
206
  const publishAndConfirm = async (input) => {
151
207
  const result = await (engineOptions.seams?.publishView ?? productionPublishView)(input)
@@ -160,8 +216,8 @@ export async function runMaintenanceService(options = {}) {
160
216
  // The engine also asks the factory itself (`forget`, on a tick somebody asked for): the wrapper keeps its methods.
161
217
  const pluginAwareAdapterFactory = Object.assign((input) => adapterFactory({ ...input, pluginReport: typeof input?.scope?.scopeId === 'string' ? pluginReportOf(input.scope.scopeId) : null }), adapterFactory)
162
218
  const engine = createEngine({
163
- watcherFactory: createFsWatcherFactory(), ...engineOptions, seams: { ...(engineOptions.seams ?? {}), prepareView: prepareWithPlugin, publishView: publishAndConfirm },
164
- loadProject, dataRoot, adapterFactory: pluginAwareAdapterFactory, clock, env, platform, lockOwner: { host, port, runtimeId },
219
+ watcherFactory: createFsWatcherFactory(), ...engineOptions, seams: { ...(engineOptions.seams ?? {}), prepareView: prepareWithPlugin, prepareViewCooperatively: prepareCooperativelyWithPlugin, publishView: publishAndConfirm },
220
+ loadProject, locateProject, dataRoot, adapterFactory: pluginAwareAdapterFactory, clock, env, platform, lockOwner: { host, port, runtimeId },
165
221
  })
166
222
 
167
223
  const recordIsOurs = () => {
@@ -173,11 +229,20 @@ export async function runMaintenanceService(options = {}) {
173
229
  consecutiveFailures = outcome.consecutiveFailures
174
230
  if (outcome.ok) {
175
231
  lastTick = { at, state: outcome.report.state, reason: outcome.report.reason ?? outcome.report.refusal?.code ?? null }
232
+ // A view the tick refused for a file-system error: the view's freshness names the refusal, and this line the error.
233
+ const refused = new Map()
234
+ for (const item of outcome.report.refusalCauses ?? []) {
235
+ const reason = JSON.stringify([item.code, item.cause ?? null, item.cleanupCause ?? null])
236
+ refused.set(item.scopeId, reason)
237
+ if (refusedLastTick.get(item.scopeId) !== reason) log({ at, event: 'view-refused', scopeId: item.scopeId, code: item.code, ...(item.cause ? { cause: item.cause } : {}), ...(item.cleanupCause ? { cleanupCause: item.cleanupCause } : {}) })
238
+ }
239
+ refusedLastTick = refused
176
240
  const previous = readLastServiceError({ workspaceRoot, workspaceId })
177
241
  if (previous && previous.resolvedAt === null) writeLastServiceError({ workspaceRoot, workspaceId, document: { ...previous, consecutiveFailures: 0, resolvedAt: at } })
178
242
  return
179
243
  }
180
244
  lastTick = { at, state: 'failed', reason: errorCode(outcome.error) }
245
+ refusedLastTick = new Map()
181
246
  // The message may carry a path or a title: it goes to the private log and nowhere else.
182
247
  log({ at, event: 'tick-failed', code: errorCode(outcome.error), name: errorName(outcome.error), message: String(outcome.error?.message ?? outcome.error), consecutiveFailures })
183
248
  let previous = null
@@ -193,6 +258,18 @@ export async function runMaintenanceService(options = {}) {
193
258
  log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
194
259
  }
195
260
  }
261
+ // What a publication of this tick could not change is no drift to ask about again (plugin-drift.mjs).
262
+ const settleDriftLooks = () => {
263
+ try { pluginDrift.settle([...pluginChannel.bearers().keys()]) } catch (error) {
264
+ log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
265
+ }
266
+ }
267
+
268
+ // A package that changed on disk: another release, loaded module by module into this process from then on.
269
+ const releaseChanged = () => {
270
+ if (!startup || releaseWatch === null) return false
271
+ try { return releaseWatch.changed() === true } catch { return false }
272
+ }
196
273
 
197
274
  const loop = createTickLoop({
198
275
  intervalMs, maxBackoffMs, onOutcome: recordOutcome,
@@ -200,7 +277,14 @@ export async function runMaintenanceService(options = {}) {
200
277
  // The service runs only while its record names it: a replaced or removed record ends it, cleanly.
201
278
  if (!recordIsOurs()) { void shutdown('record-no-longer-names-this-runtime'); return { state: 'stopping', reason: 'record-no-longer-names-this-runtime' } }
202
279
  askForDriftedViews()
203
- return engine.tick()
280
+ try {
281
+ const report = await engine.tick()
282
+ if (report?.state === 'ticked') settleDriftLooks()
283
+ return report
284
+ } finally {
285
+ // After the tick, whether it succeeded or not: the next one runs in a process of the release now on disk.
286
+ if (!stopping && releaseChanged()) { log({ at: isoTime(clock), event: RELEASE_CHANGED }); void shutdown(RELEASE_CHANGED) }
287
+ }
204
288
  },
205
289
  })
206
290
 
@@ -218,8 +302,16 @@ export async function runMaintenanceService(options = {}) {
218
302
 
219
303
  const healthStatus = () => (stopping ? 'stopped' : consecutiveFailures > 0 ? 'degraded' : 'healthy')
220
304
 
305
+ // What is stored about a view is not permission to tell it: the project is asked as it is now (view-permission.mjs).
306
+ // Its loads are counted for the status document; the log gets a line for those somebody has to see (createLoadReport).
307
+ const statusLoads = createLoadReport({ write: (entry) => log({ at: isoTime(clock), ...entry }) })
308
+ const viewPermitted = createViewPermission({
309
+ loadProject, resolveWorkspace: (loaded) => resolveServiceWorkspace({ project: loaded, dataRoot, env, platform }), workspaceInputsOf: serviceWorkspaceInputs,
310
+ workspaceId, workspaceRoot, onLoad: statusLoads.onLoad,
311
+ })
221
312
  // One view as the plugin is told about it: its freshness entry with held notes counted, and its open pending edits.
222
313
  function pluginStatusOf(scopeId) {
314
+ if (!viewPermitted(scopeId)) return { view: null, pendingEdits: null }
223
315
  let entry = null
224
316
  try { entry = stateStore.readFreshness()?.scopes.find((item) => item.scopeId === scopeId) ?? null } catch (error) { if (!isTyped(error)) throw error; return { view: null, pendingEdits: null } }
225
317
  let open = null
@@ -248,6 +340,7 @@ export async function runMaintenanceService(options = {}) {
248
340
  return {
249
341
  schema: SERVICE_STATUS_SCHEMA, service: { ...identity, status: healthStatus() }, loop: loop.state(), lastTick, lastError, freshness: freshnessSummary(),
250
342
  plugins: pluginPresence({ sessions: pluginSessions, scopeIds: [...pluginChannel.bearers().keys()], entryOf: (scopeId) => currentPluginChoice({ workspaceRoot, workspaceId, scopeId }).state }),
343
+ statusLoads: statusLoads.counts(),
251
344
  ...(typeof appStatus === 'function' ? { app: appStatus() } : {}),
252
345
  }
253
346
  },
@@ -296,7 +389,7 @@ export async function runMaintenanceService(options = {}) {
296
389
  workspaceRoot, workspaceId,
297
390
  record: {
298
391
  schema: 'atelier-obsidian-service-state/v1', contractVersion: '1.0.0', workspaceId, serviceName: identity.serviceName, host, port, runtimeId, pid,
299
- executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity() } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
392
+ executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity(), ...(alias === null ? {} : { invokedAs: alias }) } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
300
393
  health: { status: 'healthy', checkedAt: startedAt }, consent: settings.consent, ext: { bearer },
301
394
  },
302
395
  })
@@ -4,9 +4,8 @@ import { refuse } from './errors.mjs'
4
4
  // Builds the text of an operating-system startup unit for the maintenance
5
5
  // service. Pure: it returns text and touches nothing. It writes no file,
6
6
  // installs nothing and runs no service manager. Installing a unit is a
7
- // separate system change that needs a person's explicit authorization and a
8
- // recorded consent that covers startup; the service refuses `--startup`
9
- // without one.
7
+ // separate step (login-item.mjs) that a person asks for, and the service
8
+ // refuses `--startup` without a recorded consent that covers startup.
10
9
  //
11
10
  // Every machine-specific value is an input. Nothing is looked up here: no
12
11
  // home directory, no environment, no current directory, no installed path.
@@ -15,17 +14,30 @@ import { refuse } from './errors.mjs'
15
14
  // Linux a systemd user unit
16
15
  // Windows refused, typed: startup there has not been qualified
17
16
  //
18
- // The unit runs the service entry in the foreground with `--startup`; the
19
- // service writes its own record once it listens, exactly as under `start`.
17
+ // The unit runs the service entry in the foreground with `--startup`, in the
18
+ // root directory (never one that could be a vault), with the search path it
19
+ // is given, so the service finds the same programs the person uses. It is
20
+ // restarted after a crash and after an exit with a code the service uses to
21
+ // ask for it (a new release on disk), never after a clean stop and never in a
22
+ // tight loop; a refusal under `--startup` exits cleanly. The service writes
23
+ // its own bounded log; the unit's output file receives only what happens
24
+ // before it can, such as a module that cannot be loaded.
20
25
 
21
26
  export const STARTUP_PLATFORMS = Object.freeze(['darwin', 'linux'])
22
27
  const LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
28
+ const CONTROL = /[\u0000-\u001f\u007f]/
29
+ // Seconds: the least time between two starts, and how long a stop may take before the manager ends the process. The
30
+ // service gives a tick in flight 30 seconds to finish when it is asked to stop.
31
+ const THROTTLE_SECONDS = 60
32
+ const EXIT_TIMEOUT_SECONDS = 60
23
33
 
24
34
  const xml = (value) => value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&apos;')
25
35
  // systemd: quoted, with its specifier and variable characters doubled.
26
36
  const unitWord = (value) => `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('$', '$$$$')}"`
37
+ // systemd: one quoted assignment of Environment=, where `$` has no meaning and only specifiers are expanded.
38
+ const unitAssignment = (name, value) => `"${`${name}=${value}`.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%')}"`
27
39
 
28
- function launchdText({ label, words, logPath, workingDirectory }) {
40
+ function launchdText({ label, words, logPath, workingDirectory, searchPath }) {
29
41
  const lines = [
30
42
  '<?xml version="1.0" encoding="UTF-8"?>',
31
43
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
@@ -33,12 +45,15 @@ function launchdText({ label, words, logPath, workingDirectory }) {
33
45
  '<dict>',
34
46
  ' <key>Label</key>', ` <string>${xml(label)}</string>`,
35
47
  ' <key>ProgramArguments</key>', ' <array>', ...words.map((word) => ` <string>${xml(word)}</string>`), ' </array>',
48
+ ...(searchPath === null ? [] : [' <key>EnvironmentVariables</key>', ' <dict>', ' <key>PATH</key>', ` <string>${xml(searchPath)}</string>`, ' </dict>']),
49
+ ' <key>WorkingDirectory</key>', ` <string>${xml(workingDirectory)}</string>`,
36
50
  ' <key>RunAtLoad</key>', ' <true/>',
37
- // Restarted after a crash, not after a clean stop; a refusal is not retried in a tight loop.
51
+ // Restarted after a crash or a non-zero exit, not after a clean stop; a refusal exits cleanly and is not retried.
38
52
  ' <key>KeepAlive</key>', ' <dict>', ' <key>SuccessfulExit</key>', ' <false/>', ' </dict>',
39
- ' <key>ThrottleInterval</key>', ' <integer>60</integer>',
40
- ' <key>ProcessType</key>', ' <string>Background</string>',
41
- ...(workingDirectory === undefined ? [] : [' <key>WorkingDirectory</key>', ` <string>${xml(workingDirectory)}</string>`]),
53
+ ' <key>ThrottleInterval</key>', ` <integer>${THROTTLE_SECONDS}</integer>`,
54
+ ' <key>ExitTimeOut</key>', ` <integer>${EXIT_TIMEOUT_SECONDS}</integer>`,
55
+ // Standard, not Background: the service's budgets were measured without the I/O throttling of a background process.
56
+ ' <key>ProcessType</key>', ' <string>Standard</string>',
42
57
  ' <key>StandardOutPath</key>', ` <string>${xml(logPath)}</string>`,
43
58
  ' <key>StandardErrorPath</key>', ` <string>${xml(logPath)}</string>`,
44
59
  '</dict>',
@@ -47,7 +62,7 @@ function launchdText({ label, words, logPath, workingDirectory }) {
47
62
  return `${lines.join('\n')}\n`
48
63
  }
49
64
 
50
- function systemdText({ label, words, logPath, workingDirectory }) {
65
+ function systemdText({ label, words, logPath, workingDirectory, searchPath }) {
51
66
  const lines = [
52
67
  '[Unit]',
53
68
  `Description=Atelier Obsidian maintenance (${label.replaceAll(/[^A-Za-z0-9._-]/g, '')})`,
@@ -55,11 +70,13 @@ function systemdText({ label, words, logPath, workingDirectory }) {
55
70
  '[Service]',
56
71
  'Type=simple',
57
72
  `ExecStart=${words.map(unitWord).join(' ')}`,
58
- ...(workingDirectory === undefined ? [] : [`WorkingDirectory=${workingDirectory.replaceAll('%', '%%')}`]),
59
- // Restarted after a crash; not after a clean stop, and not after a refusal (exit status 2).
73
+ ...(searchPath === null ? [] : [`Environment=${unitAssignment('PATH', searchPath)}`]),
74
+ `WorkingDirectory=${workingDirectory.replaceAll('%', '%%')}`,
75
+ // Restarted after a crash or a non-zero exit; not after a clean stop, and not after a refusal (exit status 2).
60
76
  'Restart=on-failure',
61
- 'RestartSec=60',
77
+ `RestartSec=${THROTTLE_SECONDS}`,
62
78
  'RestartPreventExitStatus=2',
79
+ `TimeoutStopSec=${EXIT_TIMEOUT_SECONDS}`,
63
80
  `StandardOutput=append:${logPath.replaceAll('%', '%%')}`,
64
81
  `StandardError=append:${logPath.replaceAll('%', '%%')}`,
65
82
  '',
@@ -69,19 +86,40 @@ function systemdText({ label, words, logPath, workingDirectory }) {
69
86
  return `${lines.join('\n')}\n`
70
87
  }
71
88
 
72
- // { platform, kind, fileName, text }. `args` are the arguments of the entry after `--startup`.
73
- export function buildStartupAdapter({ platform, label, nodePath, entryPath, args = [], logPath, workingDirectory } = {}) {
89
+ // The search path a unit carries: the entries of `value` (a PATH as the environment holds it) that are absolute
90
+ // directories, in their order, each once, and none inside a temporary directory (`temporary`, absolute prefixes): a
91
+ // relative entry would be resolved in the unit's working directory, and a temporary one (an agent's session, say) is
92
+ // gone after a restart. Pure. Null when nothing is left.
93
+ export function startupSearchPath(value, { temporary = [] } = {}) {
94
+ if (typeof value !== 'string') return null
95
+ const inside = (entry) => temporary.some((prefix) => typeof prefix === 'string' && path.posix.isAbsolute(prefix) && (entry === prefix || entry.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`)))
96
+ const kept = []
97
+ for (const raw of value.split(':')) {
98
+ const entry = raw.length > 1 ? raw.replace(/\/+$/, '') : raw
99
+ if (!path.posix.isAbsolute(entry) || CONTROL.test(entry) || inside(entry) || kept.includes(entry)) continue
100
+ kept.push(entry)
101
+ }
102
+ return kept.length === 0 ? null : kept.join(':')
103
+ }
104
+
105
+ // { platform, kind, fileName, text }. `args` are the arguments of the entry after `--startup`; `searchPath` is the PATH
106
+ // the unit sets (absolute entries joined by `:`; see startupSearchPath), none when omitted.
107
+ export function buildStartupAdapter({ platform, label, nodePath, entryPath, args = [], logPath, workingDirectory = '/', searchPath = null } = {}) {
74
108
  if (platform === 'win32') refuse('startup-platform-unqualified', 'operating-system startup on Windows has not been qualified; no unit is generated for it', { platform })
75
109
  if (!STARTUP_PLATFORMS.includes(platform)) refuse('startup-platform-unsupported', 'no startup unit is known for this platform', { platform: String(platform) })
76
110
  if (typeof label !== 'string' || !LABEL.test(label)) refuse('startup-adapter-input-invalid', 'the unit label must be a plain identifier')
77
111
  const flavor = path.posix
78
- for (const [name, value] of [['nodePath', nodePath], ['entryPath', entryPath], ['logPath', logPath], ...(workingDirectory === undefined ? [] : [['workingDirectory', workingDirectory]])]) {
79
- if (typeof value !== 'string' || !flavor.isAbsolute(value) || /[\u0000-\u001f]/.test(value)) refuse('startup-adapter-input-invalid', `${name} must be an absolute path given by the caller`, { name })
112
+ for (const [name, value] of [['nodePath', nodePath], ['entryPath', entryPath], ['logPath', logPath], ['workingDirectory', workingDirectory]]) {
113
+ if (typeof value !== 'string' || !flavor.isAbsolute(value) || CONTROL.test(value)) refuse('startup-adapter-input-invalid', `${name} must be an absolute path given by the caller`, { name })
114
+ }
115
+ if (searchPath !== null && (typeof searchPath !== 'string' || searchPath.split(':').some((entry) => !flavor.isAbsolute(entry) || CONTROL.test(entry)))) {
116
+ refuse('startup-adapter-input-invalid', 'searchPath must be absolute directories joined by ":"', { name: 'searchPath' })
80
117
  }
81
- if (!Array.isArray(args) || args.some((word) => typeof word !== 'string' || word === '' || /[\u0000-\u001f]/.test(word))) refuse('startup-adapter-input-invalid', 'args must be a list of plain words')
118
+ if (!Array.isArray(args) || args.some((word) => typeof word !== 'string' || word === '' || CONTROL.test(word))) refuse('startup-adapter-input-invalid', 'args must be a list of plain words')
82
119
  if (args.includes('--startup')) refuse('startup-adapter-input-invalid', '--startup is added by the builder')
83
120
  const words = [nodePath, entryPath, '--startup', ...args]
121
+ const input = { label, words, logPath, workingDirectory, searchPath }
84
122
  return platform === 'darwin'
85
- ? { platform, kind: 'launchd-user-agent', fileName: `${label}.plist`, text: launchdText({ label, words, logPath, workingDirectory }) }
86
- : { platform, kind: 'systemd-user-unit', fileName: `${label}.service`, text: systemdText({ label, words, logPath, workingDirectory }) }
123
+ ? { platform, kind: 'launchd-user-agent', fileName: `${label}.plist`, text: launchdText(input) }
124
+ : { platform, kind: 'systemd-user-unit', fileName: `${label}.service`, text: systemdText(input) }
87
125
  }
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { setTimeout as delay } from 'node:timers/promises'
4
+ import { obsidianSandboxedBuild, obsidianUserDataDir, readObsidianSettings } from '../../projection/obsidian/publication/vault-list.mjs'
5
+ import { realPathOfLocation } from './vault-location.mjs'
6
+
7
+ // Whether the Obsidian this account runs lists a vault Atelier is filling for
8
+ // the first time, as the publisher's `direct-unheld` path needs to know
9
+ // (publisher.mjs; docs/obsidian-contract.md, "First publication into a vault
10
+ // no Obsidian lists"). Read from the app's settings file, never from the app:
11
+ //
12
+ // { unlisted: true } the file was read, and none of its entries is the vault root, a folder above
13
+ // it or a folder inside it
14
+ // { unlisted: false, reason, ... } anything else, which gives no evidence
15
+ //
16
+ // It errs towards "no evidence": a file that is missing (an app that has shown
17
+ // only its starter window writes none), that cannot be read after `attempts`
18
+ // reads `delayMs` apart (the app rewrites it with a plain write, so a reader
19
+ // can find it empty or cut short while it writes), that is not a JSON object,
20
+ // or that has an entry whose folder is not an absolute path or has a `..`
21
+ // segment (after a link, `..` leads where the text does not); and a Flatpak or
22
+ // snap build of the app anywhere on this account, whose list lives in its
23
+ // sandbox and not in this file. The root and every listed folder are compared
24
+ // as written and as their real paths, in any letter case, so a link on the way
25
+ // to either side, or another spelling, does not hide an entry.
26
+
27
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
28
+ const folded = (target) => target.normalize('NFC').toLowerCase()
29
+ // The real path of a folder that may not exist, and its lexical path when it cannot be resolved (a volume that is gone).
30
+ const realOrLexical = (target) => { try { return realPathOfLocation(target) } catch { return path.resolve(target) } }
31
+ const spellings = (target) => [...new Set([path.resolve(target), realOrLexical(target)].map(folded))]
32
+ // Strictly inside: `..x` is a folder name, not a way up.
33
+ const inside = (parent, child) => { const relative = path.relative(parent, child); return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) }
34
+
35
+ // How a listed folder stands to the vault root, or null when it is neither the root, above it, nor inside it.
36
+ function relationOf(entryPath, root) {
37
+ const listed = spellings(entryPath)
38
+ if (listed.some((folder) => root.includes(folder))) return 'vault-listed'
39
+ if (listed.some((folder) => root.some((candidate) => inside(folder, candidate)))) return 'vault-inside-listed-vault'
40
+ if (listed.some((folder) => root.some((candidate) => inside(candidate, folder)))) return 'listed-vault-inside-vault'
41
+ return null
42
+ }
43
+
44
+ const noEvidence = (reason, extra = {}) => ({ unlisted: false, reason, ...extra })
45
+
46
+ // `read` answers readObsidianSettings for the app's user-data directory; `sandboxed` answers whether a Flatpak or snap
47
+ // build is present. Either one that throws gives no evidence.
48
+ export async function readUnheldEvidence({ vaultRoot, read, sandboxed, attempts = 3, delayMs = 50, sleep = delay } = {}) {
49
+ if (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot)) return noEvidence('vault-root-unknown')
50
+ if (typeof read !== 'function' || typeof sandboxed !== 'function') return noEvidence('obsidian-settings-location-unknown')
51
+ let present = true
52
+ try { present = sandboxed() !== false } catch { present = true }
53
+ if (present) return noEvidence('obsidian-sandboxed')
54
+ let answer = null
55
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
56
+ try { answer = read() } catch { answer = { ok: false, code: 'obsidian-settings-unreadable' } }
57
+ if (answer?.ok === true || answer?.code !== 'obsidian-settings-unreadable') break
58
+ if (attempt < attempts) await sleep(delayMs)
59
+ }
60
+ if (answer?.ok !== true) return noEvidence(typeof answer?.code === 'string' ? answer.code : 'obsidian-settings-unreadable')
61
+ const vaults = answer.vaults ?? {}
62
+ if (!isPlainObject(vaults)) return noEvidence('obsidian-settings-not-object')
63
+ const root = spellings(vaultRoot)
64
+ for (const [id, entry] of Object.entries(vaults)) {
65
+ if (!isPlainObject(entry) || typeof entry.path !== 'string' || !path.isAbsolute(entry.path) || entry.path.split(/[\\/]/).includes('..')) return noEvidence('obsidian-settings-entry-unreadable', { id })
66
+ const relation = relationOf(entry.path, root)
67
+ if (relation !== null) return noEvidence(relation, { id, path: entry.path })
68
+ }
69
+ return { unlisted: true }
70
+ }
71
+
72
+ // The reader the maintenance service gives its engine: the settings file of the app for this account (HOME, and on
73
+ // Linux XDG_CONFIG_HOME), and any Flatpak or snap build, installed or having left its list, whichever wrote last.
74
+ // `exists` answers whether a path exists: the build's system-wide places (/var/lib/flatpak, /snap) are looked up through
75
+ // it too.
76
+ export function createProductionUnheldEvidence({ platform = process.platform, env = process.env, exists = fs.existsSync } = {}) {
77
+ const userDataDir = obsidianUserDataDir({ platform, env })
78
+ return ({ vaultRoot }) => readUnheldEvidence({
79
+ vaultRoot,
80
+ read: () => (userDataDir === null ? { ok: false, code: 'obsidian-settings-location-unknown' } : readObsidianSettings({ userDataDir })),
81
+ // Present at all, not only when its list was written last: with no list time given, any installation or list counts.
82
+ sandboxed: () => obsidianSandboxedBuild({ platform, env, exists, modified: () => null }) !== null,
83
+ })
84
+ }