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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (560) hide show
  1. package/CHANGELOG.md +1005 -0
  2. package/README.md +149 -9
  3. package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
  4. package/contracts/atelier-architecture.v1.schema.json +258 -0
  5. package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
  6. package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
  7. package/contracts/atelier-build.v1.schema.json +1076 -0
  8. package/contracts/atelier-capability.v1.schema.json +1252 -0
  9. package/contracts/atelier-coordination.v1.schema.json +1050 -0
  10. package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
  11. package/contracts/atelier-harness.v1.schema.json +234 -0
  12. package/contracts/atelier-ingestion.v1.schema.json +806 -0
  13. package/contracts/atelier-inquiry.v1.schema.json +1772 -0
  14. package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
  15. package/contracts/atelier-interaction.v1.schema.json +553 -0
  16. package/contracts/atelier-judgment.v1.schema.json +690 -0
  17. package/contracts/atelier-knowledge.v1.schema.json +1512 -0
  18. package/contracts/atelier-learning.v1.schema.json +1095 -0
  19. package/contracts/atelier-migration.v3.schema.json +64 -0
  20. package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
  21. package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
  22. package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
  23. package/contracts/atelier-template-adoption.v1.schema.json +223 -0
  24. package/contracts/atelier-template-profile.v1.schema.json +1168 -0
  25. package/contracts/atelier-trackable.v1.schema.json +781 -0
  26. package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
  27. package/contracts/compat-baseline.json +1 -1
  28. package/docs/architecture.md +89 -0
  29. package/docs/assurance-controls.md +4 -3
  30. package/docs/blocks/capability-stewardship.md +17 -0
  31. package/docs/blocks/claims.md +16 -3
  32. package/docs/blocks/inquiry-harnesses.md +19 -0
  33. package/docs/blocks/learning-harnesses.md +17 -0
  34. package/docs/capability-stewardship.md +269 -0
  35. package/docs/coordination.md +51 -0
  36. package/docs/decision-practices.md +111 -0
  37. package/docs/decisions.md +29 -0
  38. package/docs/evidence-navigation.md +237 -0
  39. package/docs/exact-upgrades.md +3 -2
  40. package/docs/ingestion.md +181 -0
  41. package/docs/inquiry-harnesses.md +211 -0
  42. package/docs/install.md +80 -3
  43. package/docs/intake-and-guides.md +5 -0
  44. package/docs/integration-contract-decisions.md +24 -2
  45. package/docs/interaction-and-reflection.md +72 -0
  46. package/docs/knowledge-graph.md +20 -0
  47. package/docs/knowledge-health/api.md +30 -0
  48. package/docs/knowledge-health/assessment/api.md +96 -0
  49. package/docs/knowledge-health/assessment/capabilities.md +40 -0
  50. package/docs/knowledge-health/assessment/evaluation.md +27 -0
  51. package/docs/knowledge-health/assessment/facilitator.md +30 -0
  52. package/docs/knowledge-health/assessment/quickstart.md +61 -0
  53. package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
  54. package/docs/knowledge-health/devday/facilitator.md +16 -0
  55. package/docs/knowledge-health/devday/participant-response.md +22 -0
  56. package/docs/knowledge-health/devday/public-default.md +107 -0
  57. package/docs/knowledge-health/devday/quickstart.md +18 -0
  58. package/docs/knowledge-health/devday/support.md +22 -0
  59. package/docs/knowledge-health/exercise.md +17 -0
  60. package/docs/knowledge-health/facilitator.md +20 -0
  61. package/docs/knowledge-health/quickstart.md +94 -0
  62. package/docs/knowledge-health/readme.md +15 -0
  63. package/docs/knowledge-health/support.md +34 -0
  64. package/docs/knowledge-setup.md +248 -0
  65. package/docs/knowledge-workspace.md +264 -0
  66. package/docs/learning-harnesses.md +238 -0
  67. package/docs/learning.md +215 -0
  68. package/docs/local-services.md +242 -26
  69. package/docs/obsidian-contract.md +878 -36
  70. package/docs/obsidian-plugin.md +619 -0
  71. package/docs/obsidian.md +809 -64
  72. package/docs/optional-ingestion.md +83 -0
  73. package/docs/personal-workspace.md +257 -0
  74. package/docs/practical-judgment.md +92 -0
  75. package/docs/progressive-ingestion-acceptance.md +89 -0
  76. package/docs/progressive-ingestion.md +235 -0
  77. package/docs/project-command-inventory.md +1 -0
  78. package/docs/release-engineering.md +56 -1
  79. package/docs/repo-boundary-guard.md +94 -0
  80. package/docs/research-starter-quickstart.md +66 -0
  81. package/docs/research-starter.md +114 -0
  82. package/docs/semantic-ingestion.md +198 -0
  83. package/docs/semantic-operation.md +317 -0
  84. package/docs/skill-steward.md +6 -0
  85. package/docs/template-profile.md +164 -0
  86. package/docs/template-upgrades.md +114 -0
  87. package/docs/trackables.md +111 -0
  88. package/docs/upgrade.md +68 -3
  89. package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
  90. package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
  91. package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
  92. package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
  93. package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
  94. package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
  95. package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
  96. package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
  97. package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
  98. package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
  99. package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
  100. package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
  101. package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
  102. package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
  103. package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
  104. package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
  105. package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
  106. package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
  107. package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
  108. package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
  109. package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
  110. package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
  111. package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
  112. package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
  113. package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
  114. package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
  115. package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
  116. package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
  117. package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
  118. package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
  119. package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
  120. package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
  121. package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
  122. package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
  123. package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
  124. package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
  125. package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
  126. package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
  127. package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
  128. package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
  129. package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
  130. package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
  131. package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
  132. package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
  133. package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
  134. package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
  135. package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
  136. package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
  137. package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
  138. package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
  139. package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
  140. package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
  141. package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
  142. package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
  143. package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
  144. package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
  145. package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
  146. package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
  147. package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
  148. package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
  149. package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
  150. package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
  151. package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
  152. package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
  153. package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
  154. package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
  155. package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
  156. package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
  157. package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
  158. package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
  159. package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
  160. package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
  161. package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
  162. package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
  163. package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
  164. package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
  165. package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
  166. package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
  167. package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
  168. package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
  169. package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
  170. package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
  171. package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
  172. package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
  173. package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
  174. package/fixtures/atelier-interaction/act/valid/document.json +24 -0
  175. package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
  176. package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
  177. package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
  178. package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
  179. package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
  180. package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
  181. package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
  182. package/fixtures/atelier-judgment/case/valid/document.json +75 -0
  183. package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
  184. package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
  185. package/fixtures/atelier-judgment/native-decision.md +3 -0
  186. package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
  187. package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
  188. package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
  189. package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
  190. package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
  191. package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
  192. package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
  193. package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
  194. package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
  195. package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
  196. package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
  197. package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
  198. package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
  199. package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
  200. package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
  201. package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
  202. package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
  203. package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
  204. package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
  205. package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
  206. package/fixtures/atelier-learning/invalid/authority.json +14 -0
  207. package/fixtures/atelier-learning/valid/capture.json +13 -0
  208. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
  209. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
  210. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
  211. package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
  212. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
  213. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
  214. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
  215. package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
  216. package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
  217. package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
  218. package/fixtures/atelier-trackable/request/valid/document.json +22 -0
  219. package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
  220. package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
  221. package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
  222. package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
  223. package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
  224. package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
  225. package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
  226. package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
  227. package/fixtures/capability-packages/adoption.example.json +70 -0
  228. package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
  229. package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
  230. package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
  231. package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
  232. package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
  233. package/fixtures/capability-packages/research/capability-package.json +88 -0
  234. package/fixtures/capability-packages/research/capability-release.json +134 -0
  235. package/fixtures/capability-packages/research/evaluation.md +15 -0
  236. package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
  237. package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
  238. package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
  239. package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
  240. package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
  241. package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
  242. package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
  243. package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
  244. package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
  245. package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
  246. package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
  247. package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
  248. package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
  249. package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
  250. package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
  251. package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
  252. package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
  253. package/fixtures/evidence-navigation/valid/claim.json +36 -0
  254. package/fixtures/evidence-navigation/valid/host.json +16 -0
  255. package/fixtures/evidence-navigation/valid/profile.json +16 -0
  256. package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
  257. package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
  258. package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
  259. package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
  260. package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
  261. package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
  262. package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
  263. package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
  264. package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
  265. package/fixtures/harnesses/learning-cycle.json +780 -0
  266. package/fixtures/inquiry/workshop.json +675 -0
  267. package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
  268. package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
  269. package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
  270. package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
  271. package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
  272. package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
  273. package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
  274. package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
  275. package/fixtures/knowledge-health/calibration-suite.json +26 -0
  276. package/fixtures/knowledge-health/calibration-trial.json +37 -0
  277. package/fixtures/knowledge-health/corrected/checklist.md +18 -0
  278. package/fixtures/knowledge-health/devday/participant-record.json +19 -0
  279. package/fixtures/knowledge-health/expected-summary.json +27 -0
  280. package/fixtures/knowledge-health/workshop/README.md +20 -0
  281. package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
  282. package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
  283. package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
  284. package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
  285. package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
  286. package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
  287. package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
  288. package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
  289. package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
  290. package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
  291. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
  292. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
  293. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
  294. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
  295. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
  296. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
  297. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
  298. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
  299. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
  300. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
  301. package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
  302. package/fixtures/obsidian/edits/workspace.json +1 -1
  303. package/fixtures/obsidian/materialization/expected-full.json +21 -21
  304. package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
  305. package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
  306. package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
  307. package/fixtures/personal-workspace/overlay.json +24 -0
  308. package/fixtures/templates/local-library.v1.json +75 -0
  309. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
  310. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
  311. package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
  312. package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
  313. package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
  314. package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
  315. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
  316. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
  317. package/package.json +64 -6
  318. package/plugins/obsidian/main.js +444 -0
  319. package/plugins/obsidian/manifest.json +9 -0
  320. package/plugins/obsidian/styles.css +22 -0
  321. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  322. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  323. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  324. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  325. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  326. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  327. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  328. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  329. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  330. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  331. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  332. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  333. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  334. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  335. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  336. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  337. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  338. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  339. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  340. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  341. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  342. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  343. package/src/architecture/catalog.mjs +355 -0
  344. package/src/architecture/index.mjs +93 -0
  345. package/src/boundary/content-rules.mjs +4 -2
  346. package/src/boundary/delegation-command.mjs +158 -0
  347. package/src/boundary/delegation.mjs +396 -0
  348. package/src/boundary/policy.mjs +52 -10
  349. package/src/build/git.mjs +18 -0
  350. package/src/build/index.mjs +3 -0
  351. package/src/build/ledger.mjs +88 -0
  352. package/src/capabilities/adoption.mjs +381 -0
  353. package/src/capabilities/evidence.mjs +140 -0
  354. package/src/capabilities/files.mjs +124 -0
  355. package/src/capabilities/index.mjs +4 -0
  356. package/src/capabilities/instructions.mjs +250 -0
  357. package/src/capabilities/package.mjs +136 -0
  358. package/src/cli/command-failure.mjs +36 -0
  359. package/src/cli/execute-command.mjs +16 -2
  360. package/src/cli/run.mjs +65 -9
  361. package/src/coauthor/store.mjs +43 -2
  362. package/src/collaboration/proposals.mjs +189 -0
  363. package/src/commands/architecture.mjs +27 -0
  364. package/src/commands/boundary.mjs +3 -0
  365. package/src/commands/capability.mjs +73 -0
  366. package/src/commands/enroll.mjs +3 -0
  367. package/src/commands/harness.mjs +79 -0
  368. package/src/commands/ingest.mjs +39 -0
  369. package/src/commands/init.mjs +4 -1
  370. package/src/commands/inquiry.mjs +66 -0
  371. package/src/commands/knowledge-semantic.mjs +128 -0
  372. package/src/commands/knowledge.mjs +119 -0
  373. package/src/commands/learn.mjs +61 -0
  374. package/src/commands/obsidian.mjs +832 -62
  375. package/src/commands/practice.mjs +24 -0
  376. package/src/commands/setup.mjs +22 -2
  377. package/src/commands/trackable.mjs +19 -0
  378. package/src/contracts/corpus.mjs +36 -0
  379. package/src/coordination/index.mjs +110 -0
  380. package/src/egress/forbidden-egress.mjs +2 -0
  381. package/src/evidence-navigation/contracts.mjs +60 -0
  382. package/src/evidence-navigation/evaluate.mjs +69 -0
  383. package/src/evidence-navigation/generation.mjs +22 -0
  384. package/src/evidence-navigation/index.mjs +2 -0
  385. package/src/evidence-navigation/local.mjs +167 -0
  386. package/src/evidence-navigation/schema.generated.mjs +604 -0
  387. package/src/evidence-navigation/types.mjs +12 -0
  388. package/src/export/atelier-export-contract.mjs +35 -8
  389. package/src/graph/enroll.mjs +248 -0
  390. package/src/graph/graph.mjs +17 -1
  391. package/src/graph/knowledge-graph.mjs +58 -8
  392. package/src/harnesses/contracts.mjs +41 -0
  393. package/src/harnesses/exchange.mjs +154 -0
  394. package/src/harnesses/feedback.mjs +16 -0
  395. package/src/harnesses/history.mjs +37 -0
  396. package/src/harnesses/index.mjs +4 -0
  397. package/src/harnesses/store.mjs +29 -0
  398. package/src/ingestion/contracts.mjs +41 -0
  399. package/src/ingestion/evaluation.mjs +119 -0
  400. package/src/ingestion/optional/composition/index.mjs +171 -0
  401. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  402. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  403. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  404. package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
  405. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  406. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  407. package/src/ingestion/optional/index.mjs +9 -0
  408. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  409. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  410. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  411. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  412. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  413. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  414. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  415. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  416. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  417. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  418. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  419. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  420. package/src/ingestion/processors.mjs +218 -0
  421. package/src/ingestion/semantic.mjs +203 -0
  422. package/src/ingestion/store.mjs +372 -0
  423. package/src/inquiry/beliefs.mjs +32 -0
  424. package/src/inquiry/contracts.mjs +23 -0
  425. package/src/inquiry/index.mjs +5 -0
  426. package/src/inquiry/ledger.mjs +135 -0
  427. package/src/inquiry/lenses.mjs +15 -0
  428. package/src/inquiry/projection.mjs +77 -0
  429. package/src/inquiry/store.mjs +35 -0
  430. package/src/intake/read-scope.mjs +12 -0
  431. package/src/intake/store.mjs +69 -6
  432. package/src/interaction/index.mjs +91 -0
  433. package/src/judgment/index.mjs +82 -0
  434. package/src/judgment/practice-evaluation.mjs +70 -0
  435. package/src/judgment/practice.mjs +144 -0
  436. package/src/knowledge/context.mjs +217 -0
  437. package/src/knowledge/index.mjs +5 -0
  438. package/src/knowledge/ingestion.mjs +79 -0
  439. package/src/knowledge/intake.mjs +33 -0
  440. package/src/knowledge/ledger.mjs +110 -0
  441. package/src/knowledge/participatory/guidance.mjs +36 -0
  442. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  443. package/src/knowledge/participatory/index.mjs +2 -0
  444. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  445. package/src/knowledge/plan.mjs +67 -0
  446. package/src/knowledge/projection.mjs +43 -0
  447. package/src/knowledge/semantic-operation.mjs +567 -0
  448. package/src/knowledge/sessions.mjs +467 -0
  449. package/src/knowledge/workspace.mjs +295 -0
  450. package/src/knowledge-health/assessment.mjs +165 -0
  451. package/src/knowledge-health/capabilities.mjs +350 -0
  452. package/src/knowledge-health/context.mjs +62 -0
  453. package/src/knowledge-health/example.mjs +47 -0
  454. package/src/knowledge-health/index.mjs +10 -0
  455. package/src/knowledge-health/owner-join.mjs +88 -0
  456. package/src/knowledge-health/proof/common.mjs +24 -0
  457. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  458. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  459. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  460. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  461. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  462. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  463. package/src/knowledge-health/rule-identity.mjs +44 -0
  464. package/src/knowledge-health/workshop-composition.mjs +19 -0
  465. package/src/knowledge-health/workshop-profile.mjs +176 -0
  466. package/src/knowledge-health/workshop.mjs +99 -0
  467. package/src/learning/adapters.mjs +68 -0
  468. package/src/learning/assistance.mjs +127 -0
  469. package/src/learning/contracts.mjs +65 -0
  470. package/src/learning/store.mjs +272 -0
  471. package/src/personal-workspace/index.mjs +391 -0
  472. package/src/personal-workspace/selection.mjs +385 -0
  473. package/src/project/config.mjs +8 -0
  474. package/src/project/durable-state.mjs +3 -1
  475. package/src/project/file-class.mjs +1 -1
  476. package/src/project/private-state.mjs +10 -1
  477. package/src/projection/obsidian/contracts.mjs +224 -18
  478. package/src/projection/obsidian/edits/apply.mjs +63 -19
  479. package/src/projection/obsidian/edits/regions.mjs +137 -42
  480. package/src/projection/obsidian/materialize/index.mjs +6 -3
  481. package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
  482. package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
  483. package/src/projection/obsidian/materialize/redaction.mjs +327 -0
  484. package/src/projection/obsidian/materialize/settings.mjs +113 -34
  485. package/src/projection/obsidian/personal-workspace.mjs +436 -0
  486. package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
  487. package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
  488. package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
  489. package/src/projection/obsidian/proposals/adapter.mjs +47 -8
  490. package/src/projection/obsidian/proposals/observation.mjs +20 -7
  491. package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
  492. package/src/projection/obsidian/publication/index.mjs +2 -1
  493. package/src/projection/obsidian/publication/publisher.mjs +312 -36
  494. package/src/projection/obsidian/publication/transport.mjs +63 -8
  495. package/src/projection/obsidian/publication/vault-list.mjs +200 -0
  496. package/src/projection/obsidian/recovery/index.mjs +4 -1
  497. package/src/projection/obsidian/recovery/restart.mjs +3 -1
  498. package/src/projection/obsidian/recovery/store.mjs +168 -8
  499. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  500. package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
  501. package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
  502. package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
  503. package/src/readiness/readiness.mjs +10 -3
  504. package/src/reflection/index.mjs +56 -0
  505. package/src/runtime/obsidian/app-capability.mjs +104 -12
  506. package/src/runtime/obsidian/app-production-seams.mjs +161 -42
  507. package/src/runtime/obsidian/app-registration.mjs +313 -0
  508. package/src/runtime/obsidian/app-restart.mjs +154 -0
  509. package/src/runtime/obsidian/engine.mjs +288 -29
  510. package/src/runtime/obsidian/extension-points.mjs +1 -1
  511. package/src/runtime/obsidian/index.mjs +15 -8
  512. package/src/runtime/obsidian/launch-plan.mjs +91 -0
  513. package/src/runtime/obsidian/lifecycle.mjs +193 -22
  514. package/src/runtime/obsidian/login-item.mjs +332 -0
  515. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  516. package/src/runtime/obsidian/opening.mjs +369 -63
  517. package/src/runtime/obsidian/pending-edits.mjs +10 -0
  518. package/src/runtime/obsidian/pipeline.mjs +71 -3
  519. package/src/runtime/obsidian/plugin-channel.mjs +289 -0
  520. package/src/runtime/obsidian/plugin-choice.mjs +182 -0
  521. package/src/runtime/obsidian/plugin-drift.mjs +124 -0
  522. package/src/runtime/obsidian/plugin-presence.mjs +61 -0
  523. package/src/runtime/obsidian/process-identity.mjs +20 -10
  524. package/src/runtime/obsidian/project-views.mjs +282 -0
  525. package/src/runtime/obsidian/questions.mjs +48 -0
  526. package/src/runtime/obsidian/release-watch.mjs +56 -0
  527. package/src/runtime/obsidian/service-main.mjs +113 -17
  528. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  529. package/src/runtime/obsidian/service-managers.mjs +275 -0
  530. package/src/runtime/obsidian/service-record.mjs +105 -5
  531. package/src/runtime/obsidian/service-server.mjs +50 -12
  532. package/src/runtime/obsidian/service.mjs +134 -7
  533. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  534. package/src/runtime/obsidian/state-store.mjs +13 -1
  535. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  536. package/src/runtime/obsidian/vault-location.mjs +300 -0
  537. package/src/runtime/obsidian/view-counts.mjs +34 -0
  538. package/src/server/local-sidecar.mjs +193 -7
  539. package/src/server/server.mjs +45 -6
  540. package/src/template-bindings/project.mjs +92 -0
  541. package/src/templates/profile.mjs +225 -0
  542. package/src/trackables/domain.mjs +175 -0
  543. package/src/trackables/store.mjs +114 -0
  544. package/src/ui/knowledge-page.mjs +1388 -0
  545. package/src/upgrade/explanation.mjs +1 -1
  546. package/src/upgrade/template-participant.mjs +171 -0
  547. package/src/upgrade/transaction.mjs +72 -25
  548. package/src/upgrade/upgrade.mjs +2 -1
  549. package/templates/external-project-workspace/pack-lifecycle.json +6 -1
  550. package/templates/knowledge-workspace/.gitattributes +2 -0
  551. package/templates/knowledge-workspace/AGENTS.md +27 -0
  552. package/templates/knowledge-workspace/README.md +44 -0
  553. package/templates/knowledge-workspace/atelier.project.json +22 -0
  554. package/templates/knowledge-workspace/gitignore +6 -0
  555. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  556. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  557. package/templates/knowledge-workspace/records/forecast.md +22 -0
  558. package/templates/knowledge-workspace/records/inspection.md +25 -0
  559. package/templates/knowledge-workspace/records/telescope.md +24 -0
  560. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -0,0 +1,332 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../project/private-state.mjs'
6
+ import { canonicalJson, closedObject, isoTime } from './documents.mjs'
7
+ import { refuse } from './errors.mjs'
8
+ import { ensureServiceSettings } from './lifecycle.mjs'
9
+ import { ensureWorkspaceIdentity, readLocalPointer, resolveDataRoot } from './machine-settings.mjs'
10
+ import { SERVICE_MANAGER_KINDS } from './service-managers.mjs'
11
+ import { readLastStartup, readServiceSettings, serviceNameFor, servicePaths, writeServiceSettings } from './service-record.mjs'
12
+ import { resolveServiceWorkspace } from './service.mjs'
13
+ import { STARTUP_PLATFORMS, buildStartupAdapter, startupSearchPath } from './startup-adapters.mjs'
14
+
15
+ // A login item: the operating system starts this workspace's maintenance
16
+ // service when the person logs in, so a vault is fresh after a restart
17
+ // without anything being run.
18
+ //
19
+ // macOS a launchd user agent: ~/Library/LaunchAgents/ai.mnstry.atelier.<project>.<workspace-id>.plist
20
+ // Linux a systemd user unit: ~/.config/systemd/user/atelier-obsidian-<workspace-id>.service
21
+ // Windows not offered (startup-platform-unqualified)
22
+ //
23
+ // One per workspace, each with its own label, port, log and consent, each
24
+ // running its own project's installed release. The unit runs the service
25
+ // entry of the package installed in the project, named by its path, so an
26
+ // upgrade in the project is what the next start runs; the Node that ran the
27
+ // installing command, by its real path, so a per-shell link of a version
28
+ // manager is never named; the search path the person had then, absolute
29
+ // entries only; the data root the workspace was resolved under and the
30
+ // workspace, so the service at login finds the same workspace whatever its
31
+ // manager's environment carries (XDG_DATA_HOME, the overlay's variable), and
32
+ // records a refusal there even when the project no longer leads to it; and
33
+ // `--startup`, under which the service refuses to run without a consent that
34
+ // covers startup.
35
+ //
36
+ // Installing records that consent first, since the manager starts the
37
+ // service as soon as it loads the unit, then hands the text to the injected
38
+ // service manager (service-managers.mjs) and keeps
39
+ //
40
+ // state/service/login-item.json atelier-obsidian-login-item/v1
41
+ //
42
+ // { label, file, digest of the text, program: { node, entry }, searchPath }.
43
+ // The label is allocated once and kept. Once installed, the service is
44
+ // started through the manager (loginItemStarter), never as a child beside
45
+ // it; a unit that differs from what would be written now (a Node that was
46
+ // removed, another entry, an earlier release's format) is written again on
47
+ // that way and reloaded, keeping the search path recorded at installation.
48
+ // Removing lowers the consent to the service alone first, so a unit left
49
+ // behind by a failed removal could only refuse, then removes the unit and
50
+ // the record. A person who switched the item off (System Settings, or
51
+ // `systemctl --user disable`) is not overridden: the manager is asked first,
52
+ // the unit is neither written again nor started through it, and the service
53
+ // is started as a child for that command only.
54
+
55
+ export const LOGIN_ITEM_SCHEMA = 'atelier-obsidian-login-item/v1'
56
+ export const LOGIN_ITEM_PACKAGE = '@mnstry/atelier'
57
+ const ENTRY_IN_PACKAGE = Object.freeze(['src', 'runtime', 'obsidian', 'service-main.mjs'])
58
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
59
+ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
60
+ const DIGEST = /^sha256:[0-9a-f]{64}$/
61
+ const CONTROL = /[\u0000-\u001f\u007f]/
62
+ // A package runner's cache: npx, pnpm dlx, bunx. A package there can be removed by the runner at any time.
63
+ const RUNNER_CACHES = Object.freeze([/[\\/]_npx[\\/]/, /[\\/]pnpm[\\/]dlx[\\/]/, /[\\/]\.bun[\\/]install[\\/]cache[\\/]/])
64
+
65
+ const sha256 = (text) => `sha256:${createHash('sha256').update(text).digest('hex')}`
66
+ const isAbsolutePlain = (value) => typeof value === 'string' && path.isAbsolute(value) && !CONTROL.test(value)
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // The record
70
+ // ---------------------------------------------------------------------------
71
+
72
+ function validateLoginItem(document, workspaceId) {
73
+ const code = 'invalid-login-item'
74
+ closedObject(document, { required: ['schema', 'workspaceId', 'kind', 'label', 'file', 'digest', 'program', 'searchPath', 'installedAt', 'updatedAt'] }, code, 'the login item record')
75
+ closedObject(document.program, { required: ['node', 'entry'] }, code, 'the program of the login item')
76
+ const ok = document.schema === LOGIN_ITEM_SCHEMA && document.workspaceId === workspaceId && SERVICE_MANAGER_KINDS.includes(document.kind)
77
+ && typeof document.label === 'string' && IDENTIFIER.test(document.label) && isAbsolutePlain(document.file) && path.basename(document.file).startsWith(document.label)
78
+ && typeof document.digest === 'string' && DIGEST.test(document.digest) && isAbsolutePlain(document.program.node) && isAbsolutePlain(document.program.entry)
79
+ && (document.searchPath === null || (typeof document.searchPath === 'string' && document.searchPath.split(':').every(isAbsolutePlain)))
80
+ && TIMESTAMP.test(document.installedAt) && TIMESTAMP.test(document.updatedAt)
81
+ if (!ok) refuse(code, 'the login item record is malformed')
82
+ return document
83
+ }
84
+
85
+ export function readLoginItemRecord({ workspaceRoot, workspaceId }) {
86
+ let text
87
+ try { text = readRegularTextNoFollow(servicePaths(workspaceRoot).loginItem) } catch (error) {
88
+ if (error.code === 'ENOENT') return null
89
+ return refuse('invalid-login-item', 'the login item record cannot be read', { cause: error.code ?? 'unreadable' })
90
+ }
91
+ let document
92
+ try { document = JSON.parse(text) } catch { return refuse('invalid-login-item', 'the login item record is not JSON') }
93
+ return validateLoginItem(document, workspaceId)
94
+ }
95
+
96
+ function writeLoginItemRecord({ workspaceRoot, workspaceId, record }) {
97
+ validateLoginItem(record, workspaceId)
98
+ const directory = ensureContainedPrivateDirectory({ workspaceRoot, directory: servicePaths(workspaceRoot).directory, label: 'Obsidian service state' })
99
+ atomicReplacePrivateText(path.join(directory, 'login-item.json'), canonicalJson(record))
100
+ return record
101
+ }
102
+
103
+ function removeLoginItemRecord({ workspaceRoot }) {
104
+ try { fs.unlinkSync(servicePaths(workspaceRoot).loginItem); return true } catch (error) { if (error.code === 'ENOENT') return false; throw error }
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // What a unit is made of
109
+ // ---------------------------------------------------------------------------
110
+
111
+ // The part of a launchd label that stands for the project: lower-case letters, digits and dashes, at most 40, from
112
+ // its name, so a person who looks in ~/Library/LaunchAgents can tell whose it is. Pure.
113
+ export function labelSlug(name) {
114
+ const slug = String(name ?? '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40).replace(/-+$/, '')
115
+ return slug === '' ? 'project' : slug
116
+ }
117
+
118
+ // The label of this workspace's login item, as it is allocated the first time. Pure.
119
+ export function loginItemLabel({ platform, projectName, workspaceId }) {
120
+ const id = String(workspaceId).replaceAll(':', '_')
121
+ return platform === 'darwin' ? `ai.mnstry.atelier.${labelSlug(projectName)}.${id}` : serviceNameFor(workspaceId).replaceAll(':', '_')
122
+ }
123
+
124
+ // The name people read for a project: its configured `name`, else the name of its folder.
125
+ export const projectNameOf = (project) => (typeof project?.config?.name === 'string' && project.config.name.trim() !== '' ? project.config.name : path.basename(project?.configDir ?? 'project'))
126
+
127
+ // Absolute folders whose contents do not outlive a restart, or not for long.
128
+ export function temporaryRoots({ tmpdir = os.tmpdir() } = {}) {
129
+ const roots = new Set(['/tmp', '/private/tmp', '/var/tmp', '/private/var/tmp'])
130
+ if (isAbsolutePlain(tmpdir)) { roots.add(path.resolve(tmpdir)); try { roots.add(fs.realpathSync(tmpdir)) } catch { /* only as given */ } }
131
+ return [...roots]
132
+ }
133
+
134
+ const inside = (file, root) => file === root || file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)
135
+ const isFile = (file) => { try { return fs.statSync(file).isFile() } catch { return false } }
136
+
137
+ // The entry a login item runs: the service entry of the package installed for this project, found as Node finds a
138
+ // package from the project's folder (in its node_modules or a folder's above), named by that path and not its real
139
+ // path, so an upgrade in place is what the next start runs; else `ownEntry`, the entry of the package this command
140
+ // runs from, unless that one is in a package runner's cache (npx) or a temporary folder, which are gone after a
141
+ // restart (`login-item-needs-installed-package`). { entryPath, source: 'project' | 'command' }.
142
+ export function resolveLoginItemEntry({ project, ownEntry, temporary = temporaryRoots(), exists = isFile } = {}) {
143
+ if (typeof project?.configDir === 'string' && path.isAbsolute(project.configDir)) {
144
+ for (let directory = path.resolve(project.configDir); ; directory = path.dirname(directory)) {
145
+ const candidate = path.join(directory, 'node_modules', ...LOGIN_ITEM_PACKAGE.split('/'), ...ENTRY_IN_PACKAGE)
146
+ if (exists(candidate)) return { entryPath: candidate, source: 'project' }
147
+ if (path.dirname(directory) === directory) break
148
+ }
149
+ }
150
+ const transient = typeof ownEntry !== 'string' || !path.isAbsolute(ownEntry) || RUNNER_CACHES.some((pattern) => pattern.test(ownEntry)) || temporary.some((root) => inside(path.resolve(ownEntry), root))
151
+ if (transient) refuse('login-item-needs-installed-package', 'a login item runs the package installed in the project, and this project has none; the command runs from a package runner\'s cache or a temporary folder, which may be gone after a restart')
152
+ return { entryPath: ownEntry, source: 'command' }
153
+ }
154
+
155
+ // The Node a unit names: the one running this command, by its real path.
156
+ export const realNodePath = (execPath = process.execPath) => { try { return fs.realpathSync(execPath) } catch { return execPath } }
157
+
158
+ // The data root this workspace is resolved under for this command (the flag, the pointer, the overlay's preference or
159
+ // the platform default), which a unit names.
160
+ export const loginItemDataRoot = ({ project, dataRoot, env = process.env, platform = process.platform }) => resolveDataRoot({ dataRoot, pointer: readLocalPointer(project), project, env, platform })
161
+
162
+ // The unit this workspace's login item would have now. `dataRoot` is the resolved one (loginItemDataRoot), always named;
163
+ // `entryArgs` follow `--project`, `--data-root` and `--workspace-id`. Pure but for the builder's refusals.
164
+ export function planLoginItem({ platform, project, workspaceRoot, workspaceId, dataRoot, label, entryPath, entryArgs = [], nodePath, searchPath = null }) {
165
+ if (!isAbsolutePlain(dataRoot)) throw new TypeError('a login item names the absolute data root of its workspace')
166
+ if (typeof workspaceId !== 'string' || workspaceId === '') throw new TypeError('a login item names its workspace')
167
+ const args = [`--project=${project.configPath}`, `--data-root=${dataRoot}`, `--workspace-id=${workspaceId}`, ...entryArgs]
168
+ const unit = buildStartupAdapter({ platform, label, nodePath, entryPath, args, logPath: servicePaths(workspaceRoot).loginItemLog, searchPath })
169
+ return { ...unit, label, digest: sha256(unit.text), program: { node: nodePath, entry: entryPath }, searchPath }
170
+ }
171
+
172
+ function checkManager(manager, platform) {
173
+ if (!STARTUP_PLATFORMS.includes(platform)) buildStartupAdapter({ platform })
174
+ const operations = ['readUnit', 'install', 'start', 'remove', 'inspect']
175
+ if (manager === null || typeof manager !== 'object' || !operations.every((name) => typeof manager[name] === 'function') || !SERVICE_MANAGER_KINDS.includes(manager.kind)) {
176
+ throw new TypeError('a login item needs an injected service manager')
177
+ }
178
+ if (manager.platform !== platform) throw new TypeError('the service manager is not this platform\'s')
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Install, remove, start through it, and what it looks like
183
+ // ---------------------------------------------------------------------------
184
+
185
+ // Installs, or installs again, this workspace's login item. `consent` ({ actor }) is the person's consent to the
186
+ // service running at login; without it, a recorded consent must already cover startup (`startup-consent-required`).
187
+ // `ownEntry` and `entryArgs` are the entry this command would start and its arguments (`--adapter=...`).
188
+ // `beforeInstall({ entryPath })`, when given, runs once the consent is recorded and before the manager loads the unit,
189
+ // which starts the service at once: the command stops a service of an earlier release there, so that start runs the
190
+ // entry the unit names instead of finding the earlier one running.
191
+ // { installed: true, label, file, kind, entry: { path, source }, node, searchPath } or, when the manager did not take
192
+ // the unit, { installed: false, reason, label } with the consent recorded before, or, when none was, the one just
193
+ // recorded covering the service alone.
194
+ export async function installLoginItem(options = {}) {
195
+ const { loadProject, dataRoot, env = process.env, platform = process.platform, manager, consent, ownEntry, entryArgs = [], nodePath = realNodePath(), pathValue = env.PATH, clock = () => new Date(), temporary = temporaryRoots(), beforeInstall = null } = options
196
+ checkManager(manager, platform)
197
+ const project = loadProject()
198
+ ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }) })
199
+ const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform, create: true })
200
+ const { entryPath, source } = resolveLoginItemEntry({ project, ownEntry, temporary })
201
+ const recorded = readLoginItemRecord(workspace)
202
+ const label = recorded?.label ?? loginItemLabel({ platform, projectName: projectNameOf(project), workspaceId: workspace.workspaceId })
203
+ const plan = planLoginItem({
204
+ platform, project, workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId, dataRoot: loginItemDataRoot({ project, dataRoot, env, platform }), label, entryPath, entryArgs, nodePath,
205
+ searchPath: startupSearchPath(pathValue, { temporary }),
206
+ })
207
+
208
+ // The consent first: the manager starts the service as soon as it loads the unit.
209
+ const now = isoTime(clock)
210
+ const before = readServiceSettings(workspace)
211
+ if (consent === undefined && before?.consent.coverage !== 'service-and-startup') refuse('startup-consent-required', 'a login item needs a consent that covers the service starting at login; name who gives it')
212
+ if (consent !== undefined && (typeof consent?.actor !== 'string' || consent.actor === '')) refuse('startup-consent-required', 'a consent names its actor')
213
+ await ensureServiceSettings(workspace, { consent: consent === undefined ? undefined : { actor: consent.actor, coverage: 'service-and-startup' }, now })
214
+ if (typeof beforeInstall === 'function') await beforeInstall({ entryPath })
215
+
216
+ const installed = await manager.install({ label, fileName: plan.fileName, text: plan.text })
217
+ if (!installed.ok) {
218
+ restoreConsent(workspace, before, now)
219
+ return { installed: false, reason: installed.code, label, message: installed.message ?? null }
220
+ }
221
+ writeLoginItemRecord({
222
+ ...workspace,
223
+ record: { schema: LOGIN_ITEM_SCHEMA, workspaceId: workspace.workspaceId, kind: manager.kind, label, file: installed.file, digest: plan.digest, program: plan.program, searchPath: plan.searchPath, installedAt: recorded?.installedAt ?? now, updatedAt: now },
224
+ })
225
+ return { installed: true, label, file: installed.file, kind: manager.kind, entry: { path: entryPath, source }, node: nodePath, searchPath: plan.searchPath }
226
+ }
227
+
228
+ // Back to the consent that was recorded before an installation the manager refused: the service may still run, as a
229
+ // child, under the consent it had.
230
+ function restoreConsent(workspace, before, now) {
231
+ const current = readServiceSettings(workspace)
232
+ if (current === null) return
233
+ const consent = before?.consent ?? { ...current.consent, coverage: 'service' }
234
+ if (JSON.stringify(consent) !== JSON.stringify(current.consent)) writeServiceSettings({ ...workspace, settings: { ...current, consent, updatedAt: now } })
235
+ }
236
+
237
+ // Removes this workspace's login item: the consent is lowered to the service alone first, then the manager unloads
238
+ // and deletes the unit (a service it runs is stopped as it stops it), then the record goes. { removed, label, file }
239
+ // or { removed: false, reason } when the manager kept it.
240
+ export async function removeLoginItem(options = {}) {
241
+ const { loadProject, dataRoot, env = process.env, platform = process.platform, manager, clock = () => new Date() } = options
242
+ checkManager(manager, platform)
243
+ const project = loadProject()
244
+ const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
245
+ if (!workspace?.workspaceRoot) return { removed: false, reason: 'workspace-not-prepared' }
246
+ const record = readLoginItemRecord(workspace)
247
+ const label = record?.label ?? loginItemLabel({ platform, projectName: projectNameOf(project), workspaceId: workspace.workspaceId })
248
+ const fileName = record === null ? `${label}${manager.kind === 'launchd-user-agent' ? '.plist' : '.service'}` : path.basename(record.file)
249
+ const now = isoTime(clock)
250
+ const settings = readServiceSettings(workspace)
251
+ if (settings?.consent.coverage === 'service-and-startup') await ensureServiceSettings(workspace, { consent: { actor: settings.consent.actor, coverage: 'service' }, now })
252
+ const removed = await manager.remove({ label, fileName })
253
+ if (!removed.ok) return { removed: false, reason: removed.code, label, message: removed.message ?? null }
254
+ removeLoginItemRecord(workspace)
255
+ return { removed: removed.removed === true || record !== null, label, file: manager.fileFor(fileName) }
256
+ }
257
+
258
+ // The unit this workspace's installed login item would have now, keeping its label and the search path recorded when
259
+ // it was installed; null when no entry for it can be found from here (the command runs from a package runner and the
260
+ // project has no package installed): what is installed then stays as it is.
261
+ export function currentLoginItemPlan({ record, project, workspaceRoot, dataRoot, env = process.env, platform, ownEntry, entryArgs = [], nodePath = realNodePath(), temporary = temporaryRoots() }) {
262
+ try {
263
+ const { entryPath } = resolveLoginItemEntry({ project, ownEntry, temporary })
264
+ return planLoginItem({
265
+ platform, project, workspaceRoot, workspaceId: record.workspaceId, dataRoot: loginItemDataRoot({ project, dataRoot, env, platform }), label: record.label, entryPath, entryArgs, nodePath,
266
+ searchPath: record.searchPath,
267
+ })
268
+ } catch (error) {
269
+ if (error?.code === 'login-item-needs-installed-package' || error?.code === 'startup-adapter-input-invalid') return null
270
+ throw error
271
+ }
272
+ }
273
+
274
+ // { entryPath, start() } for startService: the entry the item runs, and a start through its manager. The manager is
275
+ // asked first whether the person switched the item off (System Settings, `systemctl --user disable`): one that is off
276
+ // is neither written again, reloaded, enabled nor started (`login-item-switched-off`), and startService starts a child
277
+ // for that command only. A unit that differs from `plan` is written again and reloaded first (the answer then carries
278
+ // `refreshed: true`); a unit whose file is gone was removed by somebody and is not put back.
279
+ export function loginItemStarter({ workspace, manager, record, plan = null, clock = () => new Date() }) {
280
+ const fileName = path.basename(record.file)
281
+ let entryPath = record.program.entry
282
+ return {
283
+ get entryPath() { return entryPath },
284
+ async start() {
285
+ let refreshed = false
286
+ const text = manager.readUnit({ label: record.label, fileName })
287
+ if (text === null) return { ok: false, code: 'login-item-file-missing', refreshed }
288
+ let seen = null
289
+ try { seen = await manager.inspect({ label: record.label, fileName }) } catch { seen = null }
290
+ if (seen?.ok === true && seen.disabled === true) return { ok: false, code: 'login-item-switched-off', refreshed }
291
+ if (plan !== null && plan.fileName === fileName && text !== plan.text) {
292
+ const installed = await manager.install({ label: record.label, fileName, text: plan.text })
293
+ if (!installed.ok) return { ok: false, code: installed.code, refreshed }
294
+ writeLoginItemRecord({ ...workspace, record: { ...record, file: installed.file, digest: plan.digest, program: plan.program, updatedAt: isoTime(clock) } })
295
+ entryPath = plan.program.entry
296
+ refreshed = true
297
+ }
298
+ const started = await manager.start({ label: record.label, fileName })
299
+ return { ...started, refreshed, entryPath }
300
+ },
301
+ }
302
+ }
303
+
304
+ // What this workspace's login item looks like: the record, what the manager says about it, and how the last start by
305
+ // it ended. `manager` is a manager, or a function that answers one (asked only when an item is recorded), or null
306
+ // (it cannot be asked from here): the record alone is reported then.
307
+ // state loaded | not-loaded | switched-off | file-missing | unknown
308
+ export async function loginItemStatus({ workspace, manager: given = null }) {
309
+ const lastStartup = (() => { try { return readLastStartup(workspace) } catch { return null } })()
310
+ let record
311
+ try { record = readLoginItemRecord(workspace) } catch (error) { return { installed: null, reason: error.code ?? 'invalid-login-item', lastStartup } }
312
+ if (record === null) return { installed: false, lastStartup }
313
+ // A unit whose entry or Node is gone cannot start: its manager tries again every minute until it is removed or installed again.
314
+ const present = (file) => { try { return fs.statSync(file).isFile() } catch { return false } }
315
+ const shown = {
316
+ installed: true, label: record.label, file: record.file, kind: record.kind, entry: record.program.entry, node: record.program.node, installedAt: record.installedAt,
317
+ programPresent: present(record.program.entry) && present(record.program.node), lastStartup,
318
+ }
319
+ const manager = typeof given === 'function' ? await given() : given
320
+ if (manager === null) return { ...shown, state: 'unknown', inspected: false }
321
+ const fileName = path.basename(record.file)
322
+ let seen = null
323
+ try { seen = await manager.inspect({ label: record.label, fileName }) } catch { seen = null }
324
+ let text = null
325
+ try { text = manager.readUnit({ label: record.label, fileName }) } catch { text = null }
326
+ const state = seen === null || seen.ok !== true ? 'unknown'
327
+ : seen.present === false ? 'file-missing'
328
+ : seen.disabled === true ? 'switched-off'
329
+ : seen.loaded === true ? 'loaded'
330
+ : seen.loaded === false ? 'not-loaded' : 'unknown'
331
+ return { ...shown, state, inspected: true, running: seen?.running ?? null, pid: seen?.pid ?? null, lastExit: seen?.lastExit ?? null, changedOnDisk: typeof text === 'string' && sha256(text) !== record.digest }
332
+ }
@@ -6,12 +6,13 @@ import { LOCAL_STATE_DIR, localStateRoot } from '../../project/config.mjs'
6
6
  import { checkManagedRoots } from '../../project/file-class.mjs'
7
7
  import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../project/private-state.mjs'
8
8
  import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, assertObsidianContract } from '../../projection/obsidian/contracts.mjs'
9
+ import { LOCAL_AUDIENCES } from '../../projection/policy.mjs'
9
10
  import { refuse } from './errors.mjs'
10
11
  import { canonicalJson, closedObject, isPlainObject } from './documents.mjs'
11
12
 
12
13
  // Machine-private settings of the Obsidian maintenance runtime.
13
14
  //
14
- // <data>/obsidian/<workspace-id>/state/settings/machine.json mode, audiences, policy reference
15
+ // <data>/obsidian/<workspace-id>/state/settings/machine.json mode, audiences, policy reference, remembered decisions
15
16
  // <data>/obsidian/<workspace-id>/state/settings/apply-policy.json the installed apply policy
16
17
  // <project>/.atelier-local/obsidian.json pointer: workspace id, optional data root
17
18
  //
@@ -22,14 +23,33 @@ import { canonicalJson, closedObject, isPlainObject } from './documents.mjs'
22
23
  // checkout keeps its vaults) and, optionally, where this machine keeps the
23
24
  // data directory.
24
25
 
25
- export const MACHINE_SETTINGS_SCHEMA = 'atelier-obsidian-machine-settings/v1'
26
+ export const MACHINE_SETTINGS_SCHEMA = 'atelier-obsidian-machine-settings/v2'
27
+ // What releases up to 0.2.0-alpha.12 wrote. It is still read, as the v2 document it stands for, and never written:
28
+ // the next write writes v2. A release that knows only v1 refuses a v2 document, so a downgrade stops, loudly, rather
29
+ // than working from settings it cannot read.
30
+ export const MACHINE_SETTINGS_SCHEMA_V1 = 'atelier-obsidian-machine-settings/v1'
26
31
  export const LOCAL_POINTER_SCHEMA = 'atelier-obsidian-local-pointer/v1'
27
32
  export const LOCAL_POINTER_FILE = 'obsidian.json'
28
33
  export const MAINTENANCE_MODES = Object.freeze(['manual', 'automatic'])
29
34
 
35
+ // What a person decided once for this workspace on this machine, so that no later run has to ask again: who may see
36
+ // its vaults, where they live, whether maintenance starts at login, and that the installed app may be reached. Each is
37
+ // null until it is decided.
38
+ export const DECISIONS = Object.freeze(['audience', 'location', 'loginItem', 'adapter'])
39
+ // How a decision was made: answered to a question at a terminal, given on the command line (an operation or a flag),
40
+ // taken as the defaults, or carried over from a v1 document.
41
+ export const DECISION_SOURCES = Object.freeze(['question', 'command', 'defaults', 'v1'])
42
+ // "Only you": every audience of a note, except `sensitive`, which a vault takes only when it is named. A vault is a
43
+ // folder in which the app's community plugins run with full access to its files.
44
+ export const ONLY_YOU_AUDIENCES = Object.freeze(LOCAL_AUDIENCES.filter((audience) => audience !== 'sensitive').sort())
45
+ // Whether the notes that carry no classification enter this workspace's vaults. They may only for "only you": a vault
46
+ // of any other audience never shows them. Withheld unless a person decided otherwise.
47
+ export const UNCLASSIFIED_CHOICES = Object.freeze(['shown', 'withheld'])
48
+
30
49
  const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
31
50
  const DIGEST = /^sha256:[0-9a-f]{64}$/
32
51
  const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/
52
+ const CONTROL = /[\u0000-\u001f\u007f]/
33
53
 
34
54
  const segment = (identifier) => identifier.replaceAll(':', '_')
35
55
 
@@ -138,9 +158,9 @@ export function ensureWorkspaceIdentity({ project, dataRoot, randomBytes = crypt
138
158
  // Machine settings
139
159
  // ---------------------------------------------------------------------------
140
160
 
141
- function validateMachineSettings(document, workspaceId) {
142
- closedObject(document, { required: ['schema', 'workspaceId', 'maintenanceMode', 'audienceAllow', 'applyPolicy', 'updatedAt'] }, 'invalid-machine-settings', 'machine settings')
143
- if (document.schema !== MACHINE_SETTINGS_SCHEMA) refuse('invalid-machine-settings', 'machine settings name an unknown schema')
161
+ const MACHINE_FIELDS = Object.freeze(['schema', 'workspaceId', 'maintenanceMode', 'audienceAllow', 'applyPolicy', 'updatedAt'])
162
+
163
+ function validateCommonFields(document, workspaceId) {
144
164
  if (document.workspaceId !== workspaceId) refuse('invalid-machine-settings', 'machine settings belong to another workspace')
145
165
  if (!MAINTENANCE_MODES.includes(document.maintenanceMode)) refuse('invalid-machine-settings', 'maintenanceMode must be manual or automatic')
146
166
  if (!Array.isArray(document.audienceAllow) || document.audienceAllow.length > 64 || document.audienceAllow.some((item) => typeof item !== 'string' || !IDENTIFIER.test(item))
@@ -155,29 +175,116 @@ function validateMachineSettings(document, workspaceId) {
155
175
  }
156
176
  }
157
177
  if (typeof document.updatedAt !== 'string' || !TIMESTAMP.test(document.updatedAt)) refuse('invalid-machine-settings', 'updatedAt must be a UTC timestamp')
178
+ }
179
+
180
+ const isOneOf = (values) => (value) => values.includes(value)
181
+ const isAbsoluteDirectory = (value) => typeof value === 'string' && value.length <= 4096 && !CONTROL.test(value) && path.isAbsolute(value) && path.resolve(value) === value
182
+ // The members of each decision besides its stamp, and what each may hold. A decision gains a member here, and a
183
+ // document that carries it is refused by a release that does not know it, loudly.
184
+ const DECISION_MEMBERS = Object.freeze({
185
+ audience: Object.freeze({ choice: isOneOf(['only-you', 'custom']), unclassified: isOneOf(UNCLASSIFIED_CHOICES) }),
186
+ location: Object.freeze({ parent: isAbsoluteDirectory }),
187
+ loginItem: Object.freeze({ choice: isOneOf(['on', 'off']) }),
188
+ adapter: Object.freeze({ choice: isOneOf(['obsidian-cli']) }),
189
+ })
190
+ function sameMembers(left, right) {
191
+ const [sortedLeft, sortedRight] = [[...left].sort(), [...right].sort()]
192
+ return sortedLeft.length === sortedRight.length && sortedLeft.every((item, index) => item === sortedRight[index])
193
+ }
194
+
195
+ // `deciding` is true when the audience decision is being recorded (withDecision). Only then must "only you" be exactly
196
+ // the set this release stands for. Read, or written back by any other change, an "only you" list is the one recorded
197
+ // when it was decided: any part of this release's set, never `sensitive` and never an audience this release does not
198
+ // know. So a later release that adds an audience to "only you" still reads what an earlier one recorded, and
199
+ // `audience set me` records the current set again.
200
+ function validateDecisions(document, { deciding = false } = {}) {
201
+ const code = 'invalid-machine-settings'
202
+ closedObject(document.decisions, { required: DECISIONS }, code, 'the remembered decisions')
203
+ for (const name of DECISIONS) {
204
+ const decision = document.decisions[name]
205
+ if (decision === null) continue
206
+ const members = DECISION_MEMBERS[name]
207
+ closedObject(decision, { required: [...Object.keys(members), 'decidedAt', 'decidedBy', 'via'] }, code, `the ${name} decision`)
208
+ for (const [member, holds] of Object.entries(members)) if (!holds(decision[member])) refuse(code, `the ${name} decision carries an unknown ${member}`)
209
+ if (typeof decision.decidedAt !== 'string' || !TIMESTAMP.test(decision.decidedAt)) refuse(code, `the ${name} decision needs a UTC time`)
210
+ // Who decided, when it is known: an identifier, or null.
211
+ if (decision.decidedBy !== null && (typeof decision.decidedBy !== 'string' || !IDENTIFIER.test(decision.decidedBy))) refuse(code, `the ${name} decision names who decided as an identifier or null`)
212
+ if (!DECISION_SOURCES.includes(decision.via)) refuse(code, `the ${name} decision says how it was made`)
213
+ }
214
+ // "Only you" is a set of audiences, and the list the engine reads is exactly that set when it is decided.
215
+ if (document.decisions.audience?.choice === 'only-you') {
216
+ const recorded = document.audienceAllow.length > 0 && document.audienceAllow.every((audience) => ONLY_YOU_AUDIENCES.includes(audience))
217
+ if (deciding ? !sameMembers(document.audienceAllow, ONLY_YOU_AUDIENCES) : !recorded) refuse(code, 'the audiences allowed are not the ones "only you" stands for')
218
+ }
219
+ if (document.decisions.audience?.unclassified === 'shown' && document.decisions.audience.choice !== 'only-you') {
220
+ refuse(code, 'notes without a classification are shown only in a vault that is only yours')
221
+ }
222
+ }
223
+
224
+ function validateV1(document, workspaceId) {
225
+ closedObject(document, { required: MACHINE_FIELDS }, 'invalid-machine-settings', 'machine settings')
226
+ validateCommonFields(document, workspaceId)
158
227
  return document
159
228
  }
160
229
 
161
- // The defaults fail closed: edits are only queued, no audience is visible and
162
- // no apply policy is installed until the person says otherwise.
230
+ function validateV2(document, workspaceId, { deciding = false } = {}) {
231
+ closedObject(document, { required: [...MACHINE_FIELDS, 'decisions'] }, 'invalid-machine-settings', 'machine settings')
232
+ if (document.schema !== MACHINE_SETTINGS_SCHEMA) refuse('invalid-machine-settings', 'machine settings name an unknown schema')
233
+ validateCommonFields(document, workspaceId)
234
+ validateDecisions(document, { deciding })
235
+ return document
236
+ }
237
+
238
+ // A v1 document as the v2 document it stands for. Nothing was remembered then, except that a list of audiences a
239
+ // person set (`audience set`) is their decision, and is carried over as one, with the notes that carry no
240
+ // classification withheld as they were; who made it is not known.
241
+ function fromV1(document) {
242
+ const { schema: _v1, ...fields } = document
243
+ const audience = fields.audienceAllow.length === 0 ? null : { choice: 'custom', unclassified: 'withheld', decidedAt: fields.updatedAt, decidedBy: null, via: 'v1' }
244
+ return { ...fields, schema: MACHINE_SETTINGS_SCHEMA, decisions: { audience, location: null, loginItem: null, adapter: null } }
245
+ }
246
+
247
+ // Every document this module reads or is handed, as a validated v2 document.
248
+ function normalizeMachineSettings(document, workspaceId) {
249
+ if (isPlainObject(document) && document.schema === MACHINE_SETTINGS_SCHEMA_V1) return validateV2(fromV1(validateV1(document, workspaceId)), workspaceId)
250
+ return validateV2(document, workspaceId)
251
+ }
252
+
253
+ // The defaults fail closed: edits are only queued, no audience is visible,
254
+ // no apply policy is installed and nothing is decided until the person says
255
+ // otherwise.
163
256
  export function defaultMachineSettings({ workspaceId, updatedAt }) {
164
- return { schema: MACHINE_SETTINGS_SCHEMA, workspaceId, maintenanceMode: 'manual', audienceAllow: [], applyPolicy: null, updatedAt }
257
+ return {
258
+ schema: MACHINE_SETTINGS_SCHEMA, workspaceId, maintenanceMode: 'manual', audienceAllow: [], applyPolicy: null,
259
+ decisions: Object.fromEntries(DECISIONS.map((name) => [name, null])), updatedAt,
260
+ }
261
+ }
262
+
263
+ // The settings with one decision recorded, validated; nothing is written. `decision` holds the decision's own members
264
+ // (`{ choice, unclassified }`, `{ parent }`, `{ choice }`); the stamp says when, by whom (an identifier, or null when not
265
+ // known) and how.
266
+ export function withDecision(settings, name, decision, { decidedAt, decidedBy = null, via }) {
267
+ if (!DECISIONS.includes(name)) throw new TypeError(`unknown decision: ${String(name)}`)
268
+ const next = { ...settings, decisions: { ...settings.decisions, [name]: { ...decision, decidedAt, decidedBy, via } } }
269
+ return validateV2(next, settings.workspaceId, { deciding: name === 'audience' })
165
270
  }
166
271
 
167
272
  const settingsDirectory = (workspaceRoot) => ensureContainedPrivateDirectory({ workspaceRoot, directory: path.join(workspaceRoot, 'state', 'settings'), label: 'Obsidian machine settings' })
168
273
  const settingsFile = (workspaceRoot, name) => path.join(workspaceRoot, 'state', 'settings', name)
169
274
 
275
+ // A v1 document is returned as v2 (see fromV1); the file keeps its v1 bytes until the next write.
170
276
  export function readMachineSettings({ workspaceRoot, workspaceId }) {
171
277
  const document = readJsonFile(settingsFile(workspaceRoot, 'machine.json'), 'invalid-machine-settings', 'machine settings')
172
- return document === null ? null : validateMachineSettings(document, workspaceId)
278
+ return document === null ? null : normalizeMachineSettings(document, workspaceId)
173
279
  }
174
280
 
281
+ // Writes v2, whatever version it is handed, and returns what it wrote.
175
282
  export function writeMachineSettings({ workspaceRoot, workspaceId, settings, repositoryRoots }) {
176
- validateMachineSettings(settings, workspaceId)
283
+ const document = normalizeMachineSettings(settings, workspaceId)
177
284
  assertOutsideRepositories({ managedRoot: workspaceRoot, repositoryRoots })
178
285
  fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 })
179
- atomicReplacePrivateText(path.join(settingsDirectory(workspaceRoot), 'machine.json'), canonicalJson(settings))
180
- return settings
286
+ atomicReplacePrivateText(path.join(settingsDirectory(workspaceRoot), 'machine.json'), canonicalJson(document))
287
+ return document
181
288
  }
182
289
 
183
290
  // ---------------------------------------------------------------------------