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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (560) hide show
  1. package/CHANGELOG.md +1005 -0
  2. package/README.md +149 -9
  3. package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
  4. package/contracts/atelier-architecture.v1.schema.json +258 -0
  5. package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
  6. package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
  7. package/contracts/atelier-build.v1.schema.json +1076 -0
  8. package/contracts/atelier-capability.v1.schema.json +1252 -0
  9. package/contracts/atelier-coordination.v1.schema.json +1050 -0
  10. package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
  11. package/contracts/atelier-harness.v1.schema.json +234 -0
  12. package/contracts/atelier-ingestion.v1.schema.json +806 -0
  13. package/contracts/atelier-inquiry.v1.schema.json +1772 -0
  14. package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
  15. package/contracts/atelier-interaction.v1.schema.json +553 -0
  16. package/contracts/atelier-judgment.v1.schema.json +690 -0
  17. package/contracts/atelier-knowledge.v1.schema.json +1512 -0
  18. package/contracts/atelier-learning.v1.schema.json +1095 -0
  19. package/contracts/atelier-migration.v3.schema.json +64 -0
  20. package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
  21. package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
  22. package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
  23. package/contracts/atelier-template-adoption.v1.schema.json +223 -0
  24. package/contracts/atelier-template-profile.v1.schema.json +1168 -0
  25. package/contracts/atelier-trackable.v1.schema.json +781 -0
  26. package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
  27. package/contracts/compat-baseline.json +1 -1
  28. package/docs/architecture.md +89 -0
  29. package/docs/assurance-controls.md +4 -3
  30. package/docs/blocks/capability-stewardship.md +17 -0
  31. package/docs/blocks/claims.md +16 -3
  32. package/docs/blocks/inquiry-harnesses.md +19 -0
  33. package/docs/blocks/learning-harnesses.md +17 -0
  34. package/docs/capability-stewardship.md +269 -0
  35. package/docs/coordination.md +51 -0
  36. package/docs/decision-practices.md +111 -0
  37. package/docs/decisions.md +29 -0
  38. package/docs/evidence-navigation.md +237 -0
  39. package/docs/exact-upgrades.md +3 -2
  40. package/docs/ingestion.md +181 -0
  41. package/docs/inquiry-harnesses.md +211 -0
  42. package/docs/install.md +80 -3
  43. package/docs/intake-and-guides.md +5 -0
  44. package/docs/integration-contract-decisions.md +24 -2
  45. package/docs/interaction-and-reflection.md +72 -0
  46. package/docs/knowledge-graph.md +20 -0
  47. package/docs/knowledge-health/api.md +30 -0
  48. package/docs/knowledge-health/assessment/api.md +96 -0
  49. package/docs/knowledge-health/assessment/capabilities.md +40 -0
  50. package/docs/knowledge-health/assessment/evaluation.md +27 -0
  51. package/docs/knowledge-health/assessment/facilitator.md +30 -0
  52. package/docs/knowledge-health/assessment/quickstart.md +61 -0
  53. package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
  54. package/docs/knowledge-health/devday/facilitator.md +16 -0
  55. package/docs/knowledge-health/devday/participant-response.md +22 -0
  56. package/docs/knowledge-health/devday/public-default.md +107 -0
  57. package/docs/knowledge-health/devday/quickstart.md +18 -0
  58. package/docs/knowledge-health/devday/support.md +22 -0
  59. package/docs/knowledge-health/exercise.md +17 -0
  60. package/docs/knowledge-health/facilitator.md +20 -0
  61. package/docs/knowledge-health/quickstart.md +94 -0
  62. package/docs/knowledge-health/readme.md +15 -0
  63. package/docs/knowledge-health/support.md +34 -0
  64. package/docs/knowledge-setup.md +248 -0
  65. package/docs/knowledge-workspace.md +264 -0
  66. package/docs/learning-harnesses.md +238 -0
  67. package/docs/learning.md +215 -0
  68. package/docs/local-services.md +242 -26
  69. package/docs/obsidian-contract.md +878 -36
  70. package/docs/obsidian-plugin.md +619 -0
  71. package/docs/obsidian.md +809 -64
  72. package/docs/optional-ingestion.md +83 -0
  73. package/docs/personal-workspace.md +257 -0
  74. package/docs/practical-judgment.md +92 -0
  75. package/docs/progressive-ingestion-acceptance.md +89 -0
  76. package/docs/progressive-ingestion.md +235 -0
  77. package/docs/project-command-inventory.md +1 -0
  78. package/docs/release-engineering.md +56 -1
  79. package/docs/repo-boundary-guard.md +94 -0
  80. package/docs/research-starter-quickstart.md +66 -0
  81. package/docs/research-starter.md +114 -0
  82. package/docs/semantic-ingestion.md +198 -0
  83. package/docs/semantic-operation.md +317 -0
  84. package/docs/skill-steward.md +6 -0
  85. package/docs/template-profile.md +164 -0
  86. package/docs/template-upgrades.md +114 -0
  87. package/docs/trackables.md +111 -0
  88. package/docs/upgrade.md +68 -3
  89. package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
  90. package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
  91. package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
  92. package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
  93. package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
  94. package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
  95. package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
  96. package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
  97. package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
  98. package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
  99. package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
  100. package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
  101. package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
  102. package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
  103. package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
  104. package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
  105. package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
  106. package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
  107. package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
  108. package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
  109. package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
  110. package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
  111. package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
  112. package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
  113. package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
  114. package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
  115. package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
  116. package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
  117. package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
  118. package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
  119. package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
  120. package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
  121. package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
  122. package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
  123. package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
  124. package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
  125. package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
  126. package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
  127. package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
  128. package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
  129. package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
  130. package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
  131. package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
  132. package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
  133. package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
  134. package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
  135. package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
  136. package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
  137. package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
  138. package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
  139. package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
  140. package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
  141. package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
  142. package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
  143. package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
  144. package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
  145. package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
  146. package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
  147. package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
  148. package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
  149. package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
  150. package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
  151. package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
  152. package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
  153. package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
  154. package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
  155. package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
  156. package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
  157. package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
  158. package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
  159. package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
  160. package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
  161. package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
  162. package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
  163. package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
  164. package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
  165. package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
  166. package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
  167. package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
  168. package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
  169. package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
  170. package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
  171. package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
  172. package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
  173. package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
  174. package/fixtures/atelier-interaction/act/valid/document.json +24 -0
  175. package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
  176. package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
  177. package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
  178. package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
  179. package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
  180. package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
  181. package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
  182. package/fixtures/atelier-judgment/case/valid/document.json +75 -0
  183. package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
  184. package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
  185. package/fixtures/atelier-judgment/native-decision.md +3 -0
  186. package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
  187. package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
  188. package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
  189. package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
  190. package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
  191. package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
  192. package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
  193. package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
  194. package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
  195. package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
  196. package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
  197. package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
  198. package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
  199. package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
  200. package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
  201. package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
  202. package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
  203. package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
  204. package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
  205. package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
  206. package/fixtures/atelier-learning/invalid/authority.json +14 -0
  207. package/fixtures/atelier-learning/valid/capture.json +13 -0
  208. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
  209. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
  210. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
  211. package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
  212. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
  213. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
  214. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
  215. package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
  216. package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
  217. package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
  218. package/fixtures/atelier-trackable/request/valid/document.json +22 -0
  219. package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
  220. package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
  221. package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
  222. package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
  223. package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
  224. package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
  225. package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
  226. package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
  227. package/fixtures/capability-packages/adoption.example.json +70 -0
  228. package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
  229. package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
  230. package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
  231. package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
  232. package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
  233. package/fixtures/capability-packages/research/capability-package.json +88 -0
  234. package/fixtures/capability-packages/research/capability-release.json +134 -0
  235. package/fixtures/capability-packages/research/evaluation.md +15 -0
  236. package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
  237. package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
  238. package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
  239. package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
  240. package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
  241. package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
  242. package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
  243. package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
  244. package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
  245. package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
  246. package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
  247. package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
  248. package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
  249. package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
  250. package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
  251. package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
  252. package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
  253. package/fixtures/evidence-navigation/valid/claim.json +36 -0
  254. package/fixtures/evidence-navigation/valid/host.json +16 -0
  255. package/fixtures/evidence-navigation/valid/profile.json +16 -0
  256. package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
  257. package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
  258. package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
  259. package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
  260. package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
  261. package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
  262. package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
  263. package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
  264. package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
  265. package/fixtures/harnesses/learning-cycle.json +780 -0
  266. package/fixtures/inquiry/workshop.json +675 -0
  267. package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
  268. package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
  269. package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
  270. package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
  271. package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
  272. package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
  273. package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
  274. package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
  275. package/fixtures/knowledge-health/calibration-suite.json +26 -0
  276. package/fixtures/knowledge-health/calibration-trial.json +37 -0
  277. package/fixtures/knowledge-health/corrected/checklist.md +18 -0
  278. package/fixtures/knowledge-health/devday/participant-record.json +19 -0
  279. package/fixtures/knowledge-health/expected-summary.json +27 -0
  280. package/fixtures/knowledge-health/workshop/README.md +20 -0
  281. package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
  282. package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
  283. package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
  284. package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
  285. package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
  286. package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
  287. package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
  288. package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
  289. package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
  290. package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
  291. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
  292. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
  293. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
  294. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
  295. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
  296. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
  297. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
  298. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
  299. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
  300. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
  301. package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
  302. package/fixtures/obsidian/edits/workspace.json +1 -1
  303. package/fixtures/obsidian/materialization/expected-full.json +21 -21
  304. package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
  305. package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
  306. package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
  307. package/fixtures/personal-workspace/overlay.json +24 -0
  308. package/fixtures/templates/local-library.v1.json +75 -0
  309. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
  310. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
  311. package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
  312. package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
  313. package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
  314. package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
  315. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
  316. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
  317. package/package.json +64 -6
  318. package/plugins/obsidian/main.js +444 -0
  319. package/plugins/obsidian/manifest.json +9 -0
  320. package/plugins/obsidian/styles.css +22 -0
  321. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  322. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  323. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  324. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  325. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  326. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  327. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  328. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  329. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  330. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  331. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  332. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  333. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  334. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  335. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  336. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  337. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  338. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  339. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  340. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  341. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  342. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  343. package/src/architecture/catalog.mjs +355 -0
  344. package/src/architecture/index.mjs +93 -0
  345. package/src/boundary/content-rules.mjs +4 -2
  346. package/src/boundary/delegation-command.mjs +158 -0
  347. package/src/boundary/delegation.mjs +396 -0
  348. package/src/boundary/policy.mjs +52 -10
  349. package/src/build/git.mjs +18 -0
  350. package/src/build/index.mjs +3 -0
  351. package/src/build/ledger.mjs +88 -0
  352. package/src/capabilities/adoption.mjs +381 -0
  353. package/src/capabilities/evidence.mjs +140 -0
  354. package/src/capabilities/files.mjs +124 -0
  355. package/src/capabilities/index.mjs +4 -0
  356. package/src/capabilities/instructions.mjs +250 -0
  357. package/src/capabilities/package.mjs +136 -0
  358. package/src/cli/command-failure.mjs +36 -0
  359. package/src/cli/execute-command.mjs +16 -2
  360. package/src/cli/run.mjs +65 -9
  361. package/src/coauthor/store.mjs +43 -2
  362. package/src/collaboration/proposals.mjs +189 -0
  363. package/src/commands/architecture.mjs +27 -0
  364. package/src/commands/boundary.mjs +3 -0
  365. package/src/commands/capability.mjs +73 -0
  366. package/src/commands/enroll.mjs +3 -0
  367. package/src/commands/harness.mjs +79 -0
  368. package/src/commands/ingest.mjs +39 -0
  369. package/src/commands/init.mjs +4 -1
  370. package/src/commands/inquiry.mjs +66 -0
  371. package/src/commands/knowledge-semantic.mjs +128 -0
  372. package/src/commands/knowledge.mjs +119 -0
  373. package/src/commands/learn.mjs +61 -0
  374. package/src/commands/obsidian.mjs +832 -62
  375. package/src/commands/practice.mjs +24 -0
  376. package/src/commands/setup.mjs +22 -2
  377. package/src/commands/trackable.mjs +19 -0
  378. package/src/contracts/corpus.mjs +36 -0
  379. package/src/coordination/index.mjs +110 -0
  380. package/src/egress/forbidden-egress.mjs +2 -0
  381. package/src/evidence-navigation/contracts.mjs +60 -0
  382. package/src/evidence-navigation/evaluate.mjs +69 -0
  383. package/src/evidence-navigation/generation.mjs +22 -0
  384. package/src/evidence-navigation/index.mjs +2 -0
  385. package/src/evidence-navigation/local.mjs +167 -0
  386. package/src/evidence-navigation/schema.generated.mjs +604 -0
  387. package/src/evidence-navigation/types.mjs +12 -0
  388. package/src/export/atelier-export-contract.mjs +35 -8
  389. package/src/graph/enroll.mjs +248 -0
  390. package/src/graph/graph.mjs +17 -1
  391. package/src/graph/knowledge-graph.mjs +58 -8
  392. package/src/harnesses/contracts.mjs +41 -0
  393. package/src/harnesses/exchange.mjs +154 -0
  394. package/src/harnesses/feedback.mjs +16 -0
  395. package/src/harnesses/history.mjs +37 -0
  396. package/src/harnesses/index.mjs +4 -0
  397. package/src/harnesses/store.mjs +29 -0
  398. package/src/ingestion/contracts.mjs +41 -0
  399. package/src/ingestion/evaluation.mjs +119 -0
  400. package/src/ingestion/optional/composition/index.mjs +171 -0
  401. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  402. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  403. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  404. package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
  405. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  406. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  407. package/src/ingestion/optional/index.mjs +9 -0
  408. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  409. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  410. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  411. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  412. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  413. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  414. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  415. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  416. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  417. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  418. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  419. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  420. package/src/ingestion/processors.mjs +218 -0
  421. package/src/ingestion/semantic.mjs +203 -0
  422. package/src/ingestion/store.mjs +372 -0
  423. package/src/inquiry/beliefs.mjs +32 -0
  424. package/src/inquiry/contracts.mjs +23 -0
  425. package/src/inquiry/index.mjs +5 -0
  426. package/src/inquiry/ledger.mjs +135 -0
  427. package/src/inquiry/lenses.mjs +15 -0
  428. package/src/inquiry/projection.mjs +77 -0
  429. package/src/inquiry/store.mjs +35 -0
  430. package/src/intake/read-scope.mjs +12 -0
  431. package/src/intake/store.mjs +69 -6
  432. package/src/interaction/index.mjs +91 -0
  433. package/src/judgment/index.mjs +82 -0
  434. package/src/judgment/practice-evaluation.mjs +70 -0
  435. package/src/judgment/practice.mjs +144 -0
  436. package/src/knowledge/context.mjs +217 -0
  437. package/src/knowledge/index.mjs +5 -0
  438. package/src/knowledge/ingestion.mjs +79 -0
  439. package/src/knowledge/intake.mjs +33 -0
  440. package/src/knowledge/ledger.mjs +110 -0
  441. package/src/knowledge/participatory/guidance.mjs +36 -0
  442. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  443. package/src/knowledge/participatory/index.mjs +2 -0
  444. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  445. package/src/knowledge/plan.mjs +67 -0
  446. package/src/knowledge/projection.mjs +43 -0
  447. package/src/knowledge/semantic-operation.mjs +567 -0
  448. package/src/knowledge/sessions.mjs +467 -0
  449. package/src/knowledge/workspace.mjs +295 -0
  450. package/src/knowledge-health/assessment.mjs +165 -0
  451. package/src/knowledge-health/capabilities.mjs +350 -0
  452. package/src/knowledge-health/context.mjs +62 -0
  453. package/src/knowledge-health/example.mjs +47 -0
  454. package/src/knowledge-health/index.mjs +10 -0
  455. package/src/knowledge-health/owner-join.mjs +88 -0
  456. package/src/knowledge-health/proof/common.mjs +24 -0
  457. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  458. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  459. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  460. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  461. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  462. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  463. package/src/knowledge-health/rule-identity.mjs +44 -0
  464. package/src/knowledge-health/workshop-composition.mjs +19 -0
  465. package/src/knowledge-health/workshop-profile.mjs +176 -0
  466. package/src/knowledge-health/workshop.mjs +99 -0
  467. package/src/learning/adapters.mjs +68 -0
  468. package/src/learning/assistance.mjs +127 -0
  469. package/src/learning/contracts.mjs +65 -0
  470. package/src/learning/store.mjs +272 -0
  471. package/src/personal-workspace/index.mjs +391 -0
  472. package/src/personal-workspace/selection.mjs +385 -0
  473. package/src/project/config.mjs +8 -0
  474. package/src/project/durable-state.mjs +3 -1
  475. package/src/project/file-class.mjs +1 -1
  476. package/src/project/private-state.mjs +10 -1
  477. package/src/projection/obsidian/contracts.mjs +224 -18
  478. package/src/projection/obsidian/edits/apply.mjs +63 -19
  479. package/src/projection/obsidian/edits/regions.mjs +137 -42
  480. package/src/projection/obsidian/materialize/index.mjs +6 -3
  481. package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
  482. package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
  483. package/src/projection/obsidian/materialize/redaction.mjs +327 -0
  484. package/src/projection/obsidian/materialize/settings.mjs +113 -34
  485. package/src/projection/obsidian/personal-workspace.mjs +436 -0
  486. package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
  487. package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
  488. package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
  489. package/src/projection/obsidian/proposals/adapter.mjs +47 -8
  490. package/src/projection/obsidian/proposals/observation.mjs +20 -7
  491. package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
  492. package/src/projection/obsidian/publication/index.mjs +2 -1
  493. package/src/projection/obsidian/publication/publisher.mjs +312 -36
  494. package/src/projection/obsidian/publication/transport.mjs +63 -8
  495. package/src/projection/obsidian/publication/vault-list.mjs +200 -0
  496. package/src/projection/obsidian/recovery/index.mjs +4 -1
  497. package/src/projection/obsidian/recovery/restart.mjs +3 -1
  498. package/src/projection/obsidian/recovery/store.mjs +168 -8
  499. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  500. package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
  501. package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
  502. package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
  503. package/src/readiness/readiness.mjs +10 -3
  504. package/src/reflection/index.mjs +56 -0
  505. package/src/runtime/obsidian/app-capability.mjs +104 -12
  506. package/src/runtime/obsidian/app-production-seams.mjs +161 -42
  507. package/src/runtime/obsidian/app-registration.mjs +313 -0
  508. package/src/runtime/obsidian/app-restart.mjs +154 -0
  509. package/src/runtime/obsidian/engine.mjs +288 -29
  510. package/src/runtime/obsidian/extension-points.mjs +1 -1
  511. package/src/runtime/obsidian/index.mjs +15 -8
  512. package/src/runtime/obsidian/launch-plan.mjs +91 -0
  513. package/src/runtime/obsidian/lifecycle.mjs +193 -22
  514. package/src/runtime/obsidian/login-item.mjs +332 -0
  515. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  516. package/src/runtime/obsidian/opening.mjs +369 -63
  517. package/src/runtime/obsidian/pending-edits.mjs +10 -0
  518. package/src/runtime/obsidian/pipeline.mjs +71 -3
  519. package/src/runtime/obsidian/plugin-channel.mjs +289 -0
  520. package/src/runtime/obsidian/plugin-choice.mjs +182 -0
  521. package/src/runtime/obsidian/plugin-drift.mjs +124 -0
  522. package/src/runtime/obsidian/plugin-presence.mjs +61 -0
  523. package/src/runtime/obsidian/process-identity.mjs +20 -10
  524. package/src/runtime/obsidian/project-views.mjs +282 -0
  525. package/src/runtime/obsidian/questions.mjs +48 -0
  526. package/src/runtime/obsidian/release-watch.mjs +56 -0
  527. package/src/runtime/obsidian/service-main.mjs +113 -17
  528. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  529. package/src/runtime/obsidian/service-managers.mjs +275 -0
  530. package/src/runtime/obsidian/service-record.mjs +105 -5
  531. package/src/runtime/obsidian/service-server.mjs +50 -12
  532. package/src/runtime/obsidian/service.mjs +134 -7
  533. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  534. package/src/runtime/obsidian/state-store.mjs +13 -1
  535. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  536. package/src/runtime/obsidian/vault-location.mjs +300 -0
  537. package/src/runtime/obsidian/view-counts.mjs +34 -0
  538. package/src/server/local-sidecar.mjs +193 -7
  539. package/src/server/server.mjs +45 -6
  540. package/src/template-bindings/project.mjs +92 -0
  541. package/src/templates/profile.mjs +225 -0
  542. package/src/trackables/domain.mjs +175 -0
  543. package/src/trackables/store.mjs +114 -0
  544. package/src/ui/knowledge-page.mjs +1388 -0
  545. package/src/upgrade/explanation.mjs +1 -1
  546. package/src/upgrade/template-participant.mjs +171 -0
  547. package/src/upgrade/transaction.mjs +72 -25
  548. package/src/upgrade/upgrade.mjs +2 -1
  549. package/templates/external-project-workspace/pack-lifecycle.json +6 -1
  550. package/templates/knowledge-workspace/.gitattributes +2 -0
  551. package/templates/knowledge-workspace/AGENTS.md +27 -0
  552. package/templates/knowledge-workspace/README.md +44 -0
  553. package/templates/knowledge-workspace/atelier.project.json +22 -0
  554. package/templates/knowledge-workspace/gitignore +6 -0
  555. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  556. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  557. package/templates/knowledge-workspace/records/forecast.md +22 -0
  558. package/templates/knowledge-workspace/records/inspection.md +25 -0
  559. package/templates/knowledge-workspace/records/telescope.md +24 -0
  560. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -1,23 +1,26 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { AtelierDiagnosticError } from '../../project/config.mjs'
4
- import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
4
+ import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, manifestLayoutVersion } from '../../projection/obsidian/contracts.mjs'
5
+ import { PERSONAL_VALIDATION_PENDING, personalWorkspaceBindingOf, personalWorkspaceInputs, validatePersonalWorkspace } from '../../projection/obsidian/personal-workspace.mjs'
5
6
  import { PROTOCOL_ID } from '../../projection/obsidian/publication/bridge-script.mjs'
6
- import { PublicationRefusal } from '../../projection/obsidian/recovery/store.mjs'
7
+ import { PublicationRefusal, allocatedFolderState, hasCommittedGeneration, readVaultAllocation } from '../../projection/obsidian/recovery/store.mjs'
7
8
  import { canonicalJson, compareText, isoTime } from './documents.mjs'
8
9
  import { readObsidianEnablement } from './enablement.mjs'
9
- import { ObsidianMaintenanceRefusal } from './errors.mjs'
10
+ import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
10
11
  import { createMaintenanceExtensions } from './extension-points.mjs'
11
12
  import {
12
13
  assertOutsideRepositories, authorizeAutomaticApply, defaultMachineSettings, ensureWorkspaceIdentity, protectedRoots, readLocalPointer,
13
14
  readMachineSettings, resolveDataRoot, workspaceStateRoot, writeMachineSettings,
14
15
  } from './machine-settings.mjs'
15
16
  import { configKey, listConfigFiles, listSourceFiles, listVaultNotes, readFileFacts, reconcile, sha256Digest, sourceKey, vaultKey } from './observation.mjs'
16
- import { dispatchAutomaticApply, heldPaths, observeVaultEdits, preserveInRecoveryStore, trustedNoteBases } from './pending-edits.mjs'
17
- import { DEFAULT_ELIGIBILITY, createProductionSeams } from './pipeline.mjs'
17
+ import { dispatchAutomaticApply, heldPaths, layoutHeldPaths, observeVaultEdits, preserveInRecoveryStore, trustedNoteBases } from './pending-edits.mjs'
18
+ import { createProductionSeams, eligibilityFor } from './pipeline.mjs'
19
+ import { PLUGIN_FILES_WAIT_FOR_APP } from './plugin-presence.mjs'
18
20
  import { ENGINE_LOCK_DIRECTORY, acquirePrivateGenerationLock, createAbandonmentProof } from './private-lock.mjs'
19
21
  import { probeHealth } from './service-client.mjs'
20
22
  import { FRESHNESS_SCHEMA, LATE_WRITERS_SCHEMA, createMaintenanceStateStore } from './state-store.mjs'
23
+ import { ensureVaultAllocation, projectDisplayName } from './vault-location.mjs'
21
24
  import { createNullWatcherFactory } from './watchers.mjs'
22
25
 
23
26
  // The maintenance engine. One explicit `tick()`; no timer, no process, no
@@ -38,6 +41,18 @@ import { createNullWatcherFactory } from './watchers.mjs'
38
41
  // prepareView -> publishView, unless that would replace a held note
39
42
  // 7. persist the freshness of every view
40
43
  //
44
+ // A view is prepared and published once more, whatever its state, on the
45
+ // next tick after `requestPreparation(scopeId)`: a one-shot request per view
46
+ // the project declared at the last tick (at most 64 wait before the first),
47
+ // which the service makes for a tick requested for that view over its
48
+ // listener (`open` requests its own). A view whose last publication did not
49
+ // settle (stale, updating, or refused as a publisher conflict) is also tried
50
+ // again without a request: at the full reconciliation; as soon as the app
51
+ // looks different from when it was last tried (`observeApp`, when given: an
52
+ // app that quit, started, or opened or closed a vault); and otherwise after a
53
+ // delay that doubles per attempt from `publicationRetryMs` up to the full
54
+ // reconciliation interval.
55
+ //
41
56
  // From the moment a workspace is resolved until the tick ends, the tick holds
42
57
  // the workspace's private engine lock, so two engines (a service and a
43
58
  // foreground command, or two services) never tick one workspace together. An
@@ -50,12 +65,13 @@ import { createNullWatcherFactory } from './watchers.mjs'
50
65
 
51
66
  export const DEFAULT_FULL_RECONCILIATION_INTERVAL_MS = 5 * 60 * 1000
52
67
  export const DEFAULT_RETRY_INTERVAL_MS = 60 * 1000
68
+ export const DEFAULT_PUBLICATION_RETRY_MS = 30 * 1000
53
69
  export const DEFAULT_LATE_WRITER_WINDOW_MS = 7 * 24 * 60 * 60 * 1000
54
70
  export const LATE_WRITER_EVERY_TICK_WINDOW_MS = 60 * 60 * 1000
55
71
 
56
72
  // A refusal that means "someone else is publishing or editing here" rather
57
73
  // than "this machine cannot publish".
58
- const CONFLICT_REFUSALS = new Set(['publication-in-progress', 'generation-mismatch', 'editor-uncoordinated', 'state-mismatch'])
74
+ const CONFLICT_REFUSALS = new Set(['publication-in-progress', 'generation-mismatch', 'editor-uncoordinated', 'vault-open-in-several-windows', 'state-mismatch'])
59
75
  const EDIT_OUTCOMES = new Set(['disk-changed', 'editor-edit'])
60
76
  const RETRIED_STATES = new Set(['stale', 'updating', 'publisher-conflict'])
61
77
  // A file changed or went away under a tick: everything is hashed again on the next one.
@@ -76,6 +92,15 @@ export const ENGINE_PRIMITIVES = Object.freeze({
76
92
  dispatchAllowed: (maintenanceMode) => maintenanceMode === 'automatic',
77
93
  // Every file is hashed, whatever its stat says, at least this often.
78
94
  isFullReconciliationDue: ({ nowMs, lastFullMs, intervalMs }) => lastFullMs === null || nowMs - lastFullMs >= intervalMs,
95
+ // A view whose last attempt did not settle, between full reconciliations: again once a delay has passed that doubles
96
+ // per attempt in a row, from `retryMs` up to `maxMs`, and at once when the app looks different from when it was last
97
+ // tried. `appState()` answers what it looks like now, or null when that is not known.
98
+ isRetryDue({ nowMs, unsettled, appState, retryMs, maxMs }) {
99
+ if (unsettled === undefined) return false
100
+ if (nowMs - unsettled.lastAttemptMs >= Math.min(maxMs, retryMs * 2 ** Math.min(unsettled.attempts - 1, 20))) return true
101
+ const now = appState()
102
+ return now !== null && now !== unsettled.appState
103
+ },
79
104
  // The embedded assets observed beside the walk: those the last built graph lets a view copy, and no withheld one.
80
105
  observedAssetsOf: (graph) => (graph.assets ?? []).filter((asset) => asset.eligible === true).map(({ repo, path: assetPath }) => ({ repo, path: assetPath })),
81
106
  // One engine per workspace at a time, across processes.
@@ -104,7 +129,37 @@ export const ENGINE_PRIMITIVES = Object.freeze({
104
129
  })
105
130
 
106
131
  const isTypedRefusal = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof AtelierDiagnosticError || error instanceof ObsidianContractRefusal || error instanceof PublicationRefusal
132
+ // The code a view is refused with when its own vault cannot be placed: a typed refusal's, or `vault-location-unusable`
133
+ // for a file-system error there (a file in the way, no permission, a read-only volume). Null for anything else, which
134
+ // is a fault of the engine and stops the tick.
135
+ const viewRefusalCode = (error) => (isTypedRefusal(error) ? error.code : typeof error?.code === 'string' && /^E[A-Z]+$/.test(error.code) ? 'vault-location-unusable' : null)
107
136
  const digestOfJson = (value) => sha256Digest(Buffer.from(canonicalJson(value)))
137
+ // The configuration a tick observes. A project bound to a personal workspace adds the person's authored manifest and
138
+ // overlay and its generation's record: a change to any of them is a configuration change, so every view is prepared
139
+ // again, asks the composition, and is refused at once when the generation no longer holds.
140
+ const configFilesOf = (project) => [...listConfigFiles(project), ...personalWorkspaceInputs(project).map((absolute) => ({ key: configKey(absolute), changeClass: 'config', absolute }))]
141
+
142
+ // An editor adapter that `build` makes, synchronously or not, the first time the publisher calls it; a refusal of
143
+ // `build` is thrown from that call, and `refusal()` answers it afterwards.
144
+ function builtOnFirstUse(build) {
145
+ let adapter = null
146
+ let refused = null
147
+ const built = () => (adapter ??= Promise.resolve().then(build).catch((error) => { refused = error; throw error }))
148
+ return {
149
+ refusal: () => refused,
150
+ probe: async (input) => (await built()).probe(input),
151
+ inspect: async (payload) => (await built()).inspect(payload),
152
+ collect: async (payload) => (await built()).collect(payload),
153
+ publish: async (payload) => (await built()).publish(payload),
154
+ }
155
+ }
156
+ // The adapter a publication gets when the factory refused the app: it coordinates with nothing and never calls it.
157
+ function uncoordinatedAdapter(refusal) {
158
+ const never = async () => { throw refusal }
159
+ return { probe: async () => ({ state: 'uncoordinated', reason: `${refusal.code}: the installed Obsidian does not qualify, and nothing is published through it` }), inspect: never, collect: never, publish: never }
160
+ }
161
+ // Views a tick may be asked to prepare again, at most, before it runs.
162
+ const MAX_PREPARATION_REQUESTS = 64
108
163
 
109
164
  function journalTime(journalId) {
110
165
  const match = /^journal-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{3})-/.exec(journalId)
@@ -127,8 +182,19 @@ function journalsToRecheck(store, { nowMs, full, windowMs }) {
127
182
  export function createMaintenanceEngineForOracleTests(options = {}, primitives = ENGINE_PRIMITIVES) {
128
183
  const {
129
184
  loadProject, dataRoot, adapterFactory, clock,
130
- watcherFactory = createNullWatcherFactory(), extensions = createMaintenanceExtensions(), eligibility = DEFAULT_ELIGIBILITY,
185
+ // Which notes may enter a view: what the machine settings decide at each tick (eligibilityFor), unless a test fixes it.
186
+ watcherFactory = createNullWatcherFactory(), extensions = createMaintenanceExtensions(), eligibility: fixedEligibility = null,
131
187
  fullReconciliationIntervalMs = DEFAULT_FULL_RECONCILIATION_INTERVAL_MS, retryIntervalMs = DEFAULT_RETRY_INTERVAL_MS, lateWriterWindowMs = DEFAULT_LATE_WRITER_WINDOW_MS,
188
+ publicationRetryMs = DEFAULT_PUBLICATION_RETRY_MS, observeApp = null,
189
+ // Reads the app's own vault list without asking the app (readAppVaultListForAllocation): { ok: true, vaults } with
190
+ // the map id -> { path }, or { ok: false, code }. A folder for a new vault is never allocated inside a vault it
191
+ // lists, nor under a name a vault it lists already has, nor while the list cannot be read.
192
+ readAppVaultList = null,
193
+ // Reads the app's own vault list from its file, without asking the app (unheld-evidence.mjs): { unlisted: true } only
194
+ // when no list names a view's vault, a folder above it or one inside it. A view never published into its allocated
195
+ // folder is then published even while an app runs that cannot be coordinated with, creating files only (the
196
+ // publisher's `direct-unheld` path). An engine given no reader never takes that path.
197
+ readUnheldEvidence = null,
132
198
  quietPeriodMs, lstat = fs.lstatSync, randomBytes, env = process.env, platform = process.platform,
133
199
  // A service names where it answers health, so a lock it leaves behind can be proven abandoned.
134
200
  lockOwner = null, lockProbe = probeHealth,
@@ -163,8 +229,26 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
163
229
  let watching = { signature: null, handle: null }
164
230
  let known = null // { stateStore, maintenanceMode } once a workspace has been resolved
165
231
  let heldLock = null
232
+ // Views somebody asked to have prepared and published once more, at the next tick.
233
+ const preparationRequests = new Set()
234
+ // The views the project declared at the last tick; null before the first.
235
+ let declaredScopes = null
236
+ // Views whose last attempt did not settle, by scope: { attempts in a row, lastAttemptMs, appState then }. Kept in
237
+ // memory only: a new engine attempts every view on its first tick.
238
+ const unsettled = new Map()
239
+ // Views kept current although publishing their committed generation again (a drifted plugin file) needed an app
240
+ // that did not qualify: tried again on the same schedule as an unsettled view, and forgotten once one settles.
241
+ const waitingForApp = new Map()
166
242
  const proveAbandoned = createAbandonmentProof({ probe: lockProbe })
167
243
 
244
+ // Whether a view kept from publication by an app that does not qualify may still be published on the publisher's
245
+ // `direct-unheld` path: never committed, its vault the folder allocated for it, and the app's list naming no folder
246
+ // on the way. The publisher checks every condition again, under its locks.
247
+ async function unheldMayApply(store, pointer) {
248
+ if (typeof readUnheldEvidence !== 'function' || pointer !== null || store.vaultOrigin !== 'allocated') return false
249
+ try { return (await readUnheldEvidence({ vaultRoot: store.vaultRoot }))?.unlisted === true } catch { return false }
250
+ }
251
+
168
252
  async function takeLock(workspaceRoot, workspaceId) {
169
253
  if (heldLock) return { acquired: true }
170
254
  const lock = await rules.acquireEngineLock({
@@ -199,9 +283,18 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
199
283
  }
200
284
 
201
285
  function storeFor(scope, workspaceRoot, workspaceId, repositoryRoots) {
202
- const signature = JSON.stringify([workspaceRoot, workspaceId, repositoryRoots])
286
+ // A view's vault can be allocated, made again or moved while the engine runs: the store follows its record. A
287
+ // store is used again only while its allocated folder is still the one the record names; otherwise it is dropped,
288
+ // and the store made in its place refuses, typed (vault-allocation-replaced, say), so a folder replaced while the
289
+ // service runs, or a link put where it was, is never published into.
290
+ const signature = JSON.stringify([workspaceRoot, workspaceId, repositoryRoots, readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })])
203
291
  const cached = stores.get(scope.scopeId)
204
- if (cached?.signature === signature) return cached.store
292
+ // A vault root that is gone (a folder under the data root the person removed, say) is looked for again too, so the
293
+ // store made in its place says why rather than failing on the folder that is not there.
294
+ if (cached?.signature === signature && fs.lstatSync(cached.store.vaultRoot, { throwIfNoEntry: false }) !== undefined) {
295
+ try { cached.store.checkAllocatedVault?.(); return cached.store } catch (error) { if (!isTypedRefusal(error)) throw error }
296
+ }
297
+ stores.delete(scope.scopeId)
205
298
  const store = seams.createRecoveryStore({ workspaceRoot, workspaceId, scopeId: scope.scopeId, repositoryRoots })
206
299
  stores.set(scope.scopeId, { signature, store })
207
300
  return store
@@ -246,6 +339,17 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
246
339
  async function runTick() {
247
340
  const now = isoTime(clock)
248
341
  const nowMs = Date.parse(now)
342
+ // A request that arrives while this tick runs belongs to the next one.
343
+ const requested = new Set(preparationRequests)
344
+ preparationRequests.clear()
345
+ // What the app looked like, asked at most once per tick and only when a decision needs it.
346
+ let appState
347
+ const appNow = () => {
348
+ if (appState === undefined) { try { appState = typeof observeApp === 'function' ? observeApp() ?? null : null } catch { appState = null } }
349
+ return appState
350
+ }
351
+ // An adapter built on a tick somebody asked for asks the app again instead of reusing an answer from before it changed.
352
+ if (requested.size > 0) try { adapterFactory.forget?.() } catch { /* a factory that remembers nothing */ }
249
353
  const full = firstTick || forceFull || rules.isFullReconciliationDue({ nowMs, lastFullMs, intervalMs: fullReconciliationIntervalMs })
250
354
  const changes = []
251
355
  // The hints collected so far belong to this tick; one that arrives while it runs belongs to the next.
@@ -257,20 +361,25 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
257
361
 
258
362
  // 1. Project and enablement. The digests are seeded before the project is
259
363
  // loaded for good, so the loaded project is never older than the index.
364
+ // A loader may answer a promise (one that composes a personal workspace off
365
+ // the event loop). A project bound to a personal workspace is loaded once:
366
+ // what a generation holds is fixed, and an input that changed after the load
367
+ // changes the validity key every build is checked against.
260
368
  if (project === null) {
261
- project = loadProject()
262
- reconcile({ index, files: listConfigFiles(project), prefix: CONFIG_PREFIX, full: true, lstat })
263
- project = loadProject()
369
+ project = await loadProject()
370
+ reconcile({ index, files: configFilesOf(project), prefix: CONFIG_PREFIX, full: true, lstat })
371
+ if (personalWorkspaceBindingOf(project) === null) project = await loadProject()
264
372
  } else {
265
- const config = reconcile({ index, files: listConfigFiles(project), prefix: CONFIG_PREFIX, full, hinted, lstat })
373
+ const config = reconcile({ index, files: configFilesOf(project), prefix: CONFIG_PREFIX, full, hinted, lstat })
266
374
  if (config.changes.length > 0) {
267
- project = loadProject()
268
- reconcile({ index, files: listConfigFiles(project), prefix: CONFIG_PREFIX, full: true, lstat })
375
+ project = await loadProject()
376
+ reconcile({ index, files: configFilesOf(project), prefix: CONFIG_PREFIX, full: true, lstat })
269
377
  }
270
378
  }
271
379
  const enablement = readObsidianEnablement(project)
272
380
  if (enablement.state === 'disabled') {
273
381
  stopWatching()
382
+ declaredScopes = new Set()
274
383
  await persistOutcome({ enablement: 'disabled', state: 'disabled', reason: enablement.reason, now, scopeIds: enablement.scopes.map((scope) => scope.scopeId) })
275
384
  semantic = null
276
385
  firstTick = true
@@ -289,14 +398,63 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
289
398
  if (!lock.acquired) return { state: 'busy', reason: 'engine-lock-held', lock: { reason: lock.reason, holder: lock.holder ?? null }, changes: [], scopes: [], pendingEdits: [], dispatched: [], lateWriters: [] }
290
399
  const machine = readMachineSettings({ workspaceRoot, workspaceId })
291
400
  ?? writeMachineSettings({ workspaceRoot, workspaceId, repositoryRoots, settings: defaultMachineSettings({ workspaceId, updatedAt: now }) })
401
+ const eligibility = fixedEligibility ?? eligibilityFor({ machine, project })
292
402
  const stateStore = createMaintenanceStateStore({ workspaceRoot, workspaceId })
293
403
  known = { stateStore, maintenanceMode: machine.maintenanceMode }
294
- const scopes = enablement.scopes.map((scope) => ({ scope, store: storeFor(scope, workspaceRoot, workspaceId, repositoryRoots) }))
404
+ // Each view's vault: allocated now where this workspace decided its vaults live, when it has none and was never
405
+ // published under the data root (vault-location.mjs). A view whose folder cannot be allocated is not prepared on
406
+ // this tick, and its freshness says why; the other views go on.
407
+ const allocationRefusals = new Map()
408
+ if (machine.decisions.location !== null) {
409
+ // The app's list, read once per tick and only when a view is about to be allocated. An engine given no reader
410
+ // coordinates with no app, and knows no list. A list that cannot be read allocates nothing: the view says why,
411
+ // and its folder is allocated at a later tick, once the list reads.
412
+ let list = null
413
+ const appList = async () => {
414
+ if (typeof readAppVaultList !== 'function') return { ok: true, vaults: null }
415
+ if (list === null) {
416
+ let answer
417
+ try { answer = await readAppVaultList() } catch { answer = null }
418
+ list = answer?.ok === true ? { ok: true, vaults: answer.vaults ?? {} } : { ok: false, code: typeof answer?.code === 'string' ? answer.code : 'obsidian-settings-unreadable' }
419
+ }
420
+ return list
421
+ }
422
+ // The folders allocated to the views; a record that cannot be read refuses only its own view, below.
423
+ const allocatedPaths = enablement.scopes.map((scope) => { try { return readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })?.path } catch { return undefined } }).filter((item) => typeof item === 'string')
424
+ for (const scope of enablement.scopes) {
425
+ try {
426
+ let vaults = null
427
+ // The list is read where a folder is about to be made: a first allocation, or an allocated folder that has gone.
428
+ const record = readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })
429
+ if (record === null ? !hasCommittedGeneration({ workspaceRoot, scopeId: scope.scopeId }) : allocatedFolderState(record) === 'missing') {
430
+ const known = await appList()
431
+ if (!known.ok) refuse('app-vault-list-unreadable', 'Obsidian\'s vault list could not be read, so no folder is allocated for this view yet; it is tried again at the next tick', { cause: known.code })
432
+ vaults = known.vaults
433
+ }
434
+ ensureVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId, location: machine.decisions.location, projectName: projectDisplayName(project), repositoryRoots, vaults, allocatedPaths, now })
435
+ } catch (error) {
436
+ const code = viewRefusalCode(error)
437
+ if (code === null) throw error
438
+ allocationRefusals.set(scope.scopeId, code)
439
+ }
440
+ }
441
+ }
442
+ // Each view's store, on its own: one whose vault cannot be placed (a record naming a folder inside a repository,
443
+ // say) is refused alone, and the other views go on.
444
+ const scopes = []
445
+ for (const scope of enablement.scopes) {
446
+ if (allocationRefusals.has(scope.scopeId)) continue
447
+ try { scopes.push({ scope, store: storeFor(scope, workspaceRoot, workspaceId, repositoryRoots) }) } catch (error) {
448
+ const code = viewRefusalCode(error)
449
+ if (code === null) throw error
450
+ allocationRefusals.set(scope.scopeId, code)
451
+ }
452
+ }
295
453
 
296
454
  watch([
297
455
  ...(project.repos ?? []).filter((repo) => !repo.external && typeof repo.path === 'string').map((repo) => ({ id: `repo:${repo.name}`, path: repo.path, recursive: true, keyPrefix: sourceKey(repo.name, ''), keyOf: (relative) => sourceKey(repo.name, relative) })),
298
456
  ...scopes.map(({ scope, store }) => ({ id: `vault:${scope.scopeId}`, path: store.vaultRoot, recursive: true, keyPrefix: vaultKey(scope.scopeId, ''), keyOf: (relative) => vaultKey(scope.scopeId, relative) })),
299
- ...[...new Set(listConfigFiles(project).map((file) => path.dirname(file.absolute)))].filter((directory) => fs.existsSync(directory))
457
+ ...[...new Set(configFilesOf(project).map((file) => path.dirname(file.absolute)))].filter((directory) => fs.existsSync(directory))
300
458
  .map((directory) => ({ id: `config:${directory}`, path: directory, recursive: false, keyPrefix: CONFIG_PREFIX, keyOf: (relative) => configKey(path.join(directory, relative)) })),
301
459
  ])
302
460
 
@@ -305,10 +463,12 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
305
463
  const pending = stateStore.readPendingEdits()
306
464
  let edits = pending.edits
307
465
  const basesOf = new Map()
466
+ const earlierLayout = new Set()
308
467
  for (const { scope, store } of scopes) {
309
468
  const { manifest, bases } = trustedNoteBases(store)
310
469
  basesOf.set(scope.scopeId, bases)
311
470
  if (!manifest) continue
471
+ if (manifestLayoutVersion(manifest) === 1) earlierLayout.add(scope.scopeId)
312
472
  const vault = reconcile({ index, files: listVaultNotes({ scopeId: scope.scopeId, vaultRoot: store.vaultRoot, manifest }), prefix: vaultKey(scope.scopeId, ''), full, hinted, lstat })
313
473
  const digestOf = (notePath) => index.get(vaultKey(scope.scopeId, notePath))?.digest ?? null
314
474
  const observed = observeVaultEdits({ store, workspaceId, scopeId: scope.scopeId, manifest, bases, digestOf, edits, now, preserve: rules.preserve })
@@ -321,9 +481,12 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
321
481
  // Observation. The registered proposal adapter is handed a copy of the pending edits and observes the open ones
322
482
  // the object store does not know yet, a bounded number per tick: each is classified from its preserved bytes
323
483
  // against the source as it is now and recorded as what it is, in manual and in automatic mode alike, and no
324
- // source is written. What refused before anything was recorded is offered again on a full reconciliation only.
484
+ // source is written. What refused before anything was recorded is offered again on a full reconciliation only;
485
+ // for a personal workspace, also on the tick after one whose graph was pending, which offers every earlier
486
+ // refusal again.
325
487
  const proposalAdapter = extensions.get('proposal-adapter')
326
- const adapterContext = () => ({ project, workspaceRoot, workspaceId, repositoryRoots, edits: structuredClone(edits), clock, env })
488
+ // A personal workspace is never composed on this event loop by the adapter: its builds defer, as the engine's do.
489
+ const adapterContext = () => ({ project, workspaceRoot, workspaceId, repositoryRoots, edits: structuredClone(edits), clock, env, deferPersonalValidation: true })
327
490
  let observed = null
328
491
  if (proposalAdapter !== null && rules.observeEdits(proposalAdapter)) {
329
492
  try { observed = await proposalAdapter.observe(adapterContext(), { retryRefused: full }) } catch { observed = { adapterId: proposalAdapter.id, failed: 'proposal-adapter-threw' } }
@@ -391,6 +554,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
391
554
  if (full) { lastFullMs = nowMs; forceFull = false }
392
555
 
393
556
  // 7. Which views are invalid.
557
+ declaredScopes = new Set(enablement.scopes.map((scope) => scope.scopeId))
394
558
  const previous = readPreviousFreshness(stateStore)
395
559
  const entries = new Map()
396
560
  const unseen = new Set()
@@ -401,15 +565,34 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
401
565
  }
402
566
  const attempt = new Map()
403
567
  const invalidate = (scopeId, changeClass) => attempt.set(scopeId, new Set([...(attempt.get(scopeId) ?? []), ...(changeClass ? [changeClass] : [])]))
568
+ // The notes that keep a view in the vault layout they were published in (see layoutHeldPaths).
569
+ const layoutHeldOf = (scopeId) => layoutHeldPaths(edits, scopeId, now)
404
570
  for (const { scope } of scopes) {
405
571
  const entry = entries.get(scope.scopeId)
406
- // A view that did not settle is tried again at the full reconciliation cadence, not on every tick.
407
- if (firstTick || unseen.has(scope.scopeId) || (full && RETRIED_STATES.has(entry.state))) invalidate(scope.scopeId, null)
572
+ // A view that did not settle is tried again, not on every tick: see the head of this file.
573
+ const retried = (RETRIED_STATES.has(entry.state)
574
+ && (full || rules.isRetryDue({ nowMs, unsettled: unsettled.get(scope.scopeId), appState: appNow, retryMs: publicationRetryMs, maxMs: fullReconciliationIntervalMs })))
575
+ || (waitingForApp.has(scope.scopeId) && rules.isRetryDue({ nowMs, unsettled: waitingForApp.get(scope.scopeId), appState: appNow, retryMs: publicationRetryMs, maxMs: fullReconciliationIntervalMs }))
576
+ if (firstTick || unseen.has(scope.scopeId) || requested.has(scope.scopeId) || retried) invalidate(scope.scopeId, null)
577
+ // A settled view still in the earlier vault layout is laid out again at the first tick where no note holds it.
578
+ if (earlierLayout.has(scope.scopeId) && entry.state === 'current' && layoutHeldOf(scope.scopeId).length === 0) invalidate(scope.scopeId, null)
408
579
  }
409
580
  for (const change of changes) for (const { scope } of scopes) if (change.scopeId === undefined || change.scopeId === scope.scopeId) invalidate(scope.scopeId, change.changeClass)
581
+ // A bound personal workspace is composed again, off the event loop, at every full reconciliation, whatever else is
582
+ // being prepared: a cause outside its validity key can stop a generation holding without a file this engine
583
+ // observes. Its views are prepared again only when the composition refuses, or confirms another key.
584
+ let personalRefusal = null
585
+ if (full && personalWorkspaceBindingOf(project) !== null) {
586
+ const verdict = await validatePersonalWorkspace(project)
587
+ if (verdict.ok !== true) personalRefusal = verdict.code
588
+ if (verdict.ok !== true || verdict.changed) for (const { scope } of scopes) invalidate(scope.scopeId, null)
589
+ }
410
590
 
411
591
  // Invalidation is durable before any work: a view is not reported current while it is being rebuilt.
412
592
  for (const [scopeId, classes] of attempt) entries.set(scopeId, { ...demote(entries.get(scopeId), 'stale', 'invalidated', now), changeClasses: [...classes].sort() })
593
+ for (const [scopeId, code] of allocationRefusals) {
594
+ entries.set(scopeId, demote(previous?.scopes.find((entry) => entry.scopeId === scopeId && entry.state !== 'disabled') ?? blankEntry(scopeId, now), 'stale', code, now))
595
+ }
413
596
  for (const entry of previous?.scopes ?? []) if (!entries.has(entry.scopeId)) entries.set(entry.scopeId, demote(entry, 'disabled', 'scope-not-configured', now))
414
597
  const persist = () => stateStore.writeFreshness(freshnessDocument({ workspaceId, enablement: 'enabled', maintenanceMode: machine.maintenanceMode, now, entries }))
415
598
  persist()
@@ -418,7 +601,19 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
418
601
  if (attempt.size > 0) {
419
602
  let built = null
420
603
  try {
421
- const graph = seams.buildGraph({ project, eligibility, cache: graphCache, index })
604
+ if (personalRefusal !== null) refuse(personalRefusal, 'the personal workspace refused this generation; nothing is prepared from it', { source: 'personal-workspace' })
605
+ // A personal workspace whose validity key is not confirmed yet is composed off the event loop, then built again.
606
+ const build = () => seams.buildGraph({ project, eligibility, cache: graphCache, index, deferPersonalValidation: true })
607
+ let graph
608
+ try { graph = build() } catch (error) {
609
+ if (error?.code !== PERSONAL_VALIDATION_PENDING) throw error
610
+ const verdict = await validatePersonalWorkspace(project)
611
+ if (verdict?.ok !== true) refuse(verdict?.code ?? 'personal-binding-unrecognized', 'the personal workspace refused this generation; nothing is prepared from it', { source: 'personal-workspace' })
612
+ try { graph = build() } catch (again) {
613
+ if (again?.code !== PERSONAL_VALIDATION_PENDING) throw again
614
+ refuse('personal-graph-unconfirmed', 'the graph built for the personal workspace is not the one its composition built; nothing is prepared from it', { source: 'personal-workspace' })
615
+ }
616
+ }
422
617
  // The assets this graph lets a view copy are observed from now on, and hashed now so the snapshot pins
423
618
  // what observation saw. A withheld asset is not observed: its bytes can change no view.
424
619
  const formerAssetKeys = new Set(observedAssets.map((asset) => sourceKey(asset.repo, asset.path)))
@@ -436,20 +631,30 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
436
631
  if (!isTypedRefusal(error)) throw error
437
632
  for (const scopeId of attempt.keys()) entries.set(scopeId, demote(entries.get(scopeId), 'stale', error.code, now))
438
633
  if (REREAD_CODES.has(error.code)) forceFull = true
634
+ // The personal workspace refused the bound generation: the project is loaded again at the next tick, so a
635
+ // loader that now binds another generation is followed.
636
+ if (error.detail?.source === 'personal-workspace') project = null
439
637
  }
440
638
  for (const { scope, store } of built ? scopes.filter((item) => attempt.has(item.scope.scopeId)) : []) {
441
639
  const { scopeId } = scope
442
640
  const entry = entries.get(scopeId)
443
- const settle = (state, reason, extra = {}) => entries.set(scopeId, { ...entry, ...extra, state, reason, verified: extra.verified === true, checkedAt: now })
641
+ // What this attempt found worth naming: notes and rules (see "Notes that were laid out anyway" in docs/obsidian-contract.md).
642
+ let diagnostics = []
643
+ const settle = (state, reason, extra = {}) => {
644
+ const { diagnostics: _earlier, ...rest } = { ...entry, ...extra, state, reason, verified: extra.verified === true, checkedAt: now }
645
+ entries.set(scopeId, diagnostics.length > 0 ? { ...rest, diagnostics } : rest)
646
+ }
444
647
  try {
648
+ const held = heldPaths(edits, scopeId)
445
649
  const prepared = seams.prepareView({
446
650
  snapshot: built.snapshot, profile: built.profile, scope, persistentPathRegistry: stateStore.readPathRegistry(), priorManifest: store.readCurrentManifest(),
447
651
  existingSettings: null, clock, vaultRootBytes: Buffer.byteLength(store.vaultRoot, 'utf8'), cache: preparationCacheFor(scopeId),
652
+ heldNotePaths: held, layoutHeldNotePaths: layoutHeldOf(scopeId), viewScopeIds: scopes.map((item) => item.scope.scopeId),
448
653
  })
449
654
  stateStore.writePathRegistry(prepared.persistentPathRegistry)
655
+ diagnostics = (prepared.manifest.ext?.[OBSIDIAN_EXT_KEY]?.diagnostics ?? []).slice(0, 100)
450
656
  const preparedGenerationId = prepared.manifest.generationId
451
657
  const trusted = () => store.readCurrent()
452
- const held = heldPaths(edits, scopeId)
453
658
  const observed = new Map(held.map((notePath) => [notePath, rules.heldNoteDigest({ file: path.join(store.vaultRoot, notePath), indexed: index.get(vaultKey(scopeId, notePath))?.digest ?? null })]))
454
659
  const conflicts = rules.publicationConflicts({ prepared, held, bases: basesOf.get(scopeId) ?? new Map(), observed })
455
660
  if (conflicts.length > 0) {
@@ -459,10 +664,43 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
459
664
  }
460
665
  settle('updating', 'publishing', { generationId: trusted()?.generationId ?? null, preparedGenerationId, heldNotes: held })
461
666
  persist()
462
- const result = await seams.publishView({
463
- preparedView: prepared, protocolId: PROTOCOL_ID, expectedGeneration: trusted()?.generationId ?? null, recoveryStore: store, adapter: adapterFactory({ store, scope }), clock,
464
- ...(quietPeriodMs === undefined ? {} : { quietPeriodMs }),
465
- })
667
+ // A generation that is already the committed one needs no app: the publisher settles it after its own restart
668
+ // recovery, before it would probe. Its adapter is then built only if the publisher asks for one, so an app
669
+ // that cannot be qualified never makes a current view stale. Any other publication builds the adapter first,
670
+ // and an app that does not qualify keeps the publisher from being reached at all.
671
+ const lazy = trusted()?.generationId === preparedGenerationId ? builtOnFirstUse(() => adapterFactory({ store, scope })) : null
672
+ // A view never published into its allocated folder is not kept from its first publication by an app that
673
+ // does not qualify, while that app's list shows no entry for the folder: the publisher is reached with an
674
+ // adapter that coordinates with nothing, and may take its `direct-unheld` path. Any other outcome is the
675
+ // factory's refusal, as before.
676
+ let heldBackBy = null
677
+ let adapter = lazy
678
+ if (adapter === null) {
679
+ try { adapter = await adapterFactory({ store, scope }) } catch (error) {
680
+ if (!isTypedRefusal(error) || !(await unheldMayApply(store, trusted()))) throw error
681
+ heldBackBy = error
682
+ adapter = uncoordinatedAdapter(error)
683
+ }
684
+ }
685
+ let result
686
+ try {
687
+ result = await seams.publishView({
688
+ preparedView: prepared, protocolId: PROTOCOL_ID, expectedGeneration: trusted()?.generationId ?? null, recoveryStore: store, adapter, clock,
689
+ ...(quietPeriodMs === undefined ? {} : { quietPeriodMs }),
690
+ ...(typeof readUnheldEvidence === 'function' ? { unheldEvidence: readUnheldEvidence, platform } : {}),
691
+ })
692
+ // Stopped before its commit for want of an app that coordinates: the app's refusal is what the person has to
693
+ // act on, and `open` answers it before it would add the vault to the app.
694
+ if (heldBackBy !== null && (result.state === 'refused' || (result.state === 'updating' && result.notes.some((note) => note.blocking && note.outcome === 'editor-uncoordinated')))) throw heldBackBy
695
+ } catch (error) {
696
+ // The publisher asked for the app only to publish the committed generation again (a plugin file drifted),
697
+ // and the app did not qualify: nothing was written, and the committed generation is read back as it is.
698
+ // The file is written at a later attempt, once the app qualifies.
699
+ if (lazy === null || lazy.refusal() !== error || !isTypedRefusal(error) || trusted()?.generationId !== preparedGenerationId) throw error
700
+ result = { state: 'committed', alreadyCommitted: true, notes: [], waitsForApp: true }
701
+ }
702
+ if (result.waitsForApp === true) waitingForApp.set(scopeId, { attempts: (waitingForApp.get(scopeId)?.attempts ?? 0) + 1, lastAttemptMs: nowMs, appState: appNow() })
703
+ else waitingForApp.delete(scopeId)
466
704
  const pointerNow = trusted()
467
705
  const common = { generationId: pointerNow?.generationId ?? null, preparedGenerationId, retainedEdits: pointerNow?.retained?.length ?? 0 }
468
706
  if (result.state === 'refused') {
@@ -483,16 +721,27 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
483
721
  // Somebody wrote during the publication. The next tick preserves and queues it.
484
722
  else if (differing.length > 0 || editedUnderPublisher) settle('updating', 'read-back-differs', common)
485
723
  else if (common.generationId !== preparedGenerationId) settle('stale', 'committed-generation-differs', common)
486
- else settle('current', result.alreadyCommitted ? 'verified-by-read-back' : 'published-and-verified', { ...common, heldNotes: [], verified: true })
724
+ // Kept current while a plugin file waits for the app: the reason says so, and status shows it on the plugin line.
725
+ else settle('current', result.waitsForApp === true ? PLUGIN_FILES_WAIT_FOR_APP : result.alreadyCommitted ? 'verified-by-read-back' : 'published-and-verified', { ...common, heldNotes: [], verified: true })
487
726
  }
488
727
  } catch (error) {
489
728
  if (!isTypedRefusal(error)) { settle('stale', 'publisher-error'); persist(); throw error }
729
+ // A redaction refusal names the note and the rule it concerns, never the value.
730
+ const where = error.code === 'redaction-failure' ? error.detail ?? {} : {}
731
+ diagnostics = typeof where.rule === 'string' ? [{ code: error.code, rule: where.rule, ...(typeof where.notePath === 'string' ? { notePath: where.notePath } : {}), ...(typeof where.filePath === 'string' ? { filePath: where.filePath } : {}) }] : []
490
732
  settle('stale', error.code)
491
733
  if (REREAD_CODES.has(error.code)) forceFull = true
492
734
  }
493
735
  }
494
736
  }
495
737
  persist()
738
+ for (const scopeId of attempt.keys()) {
739
+ const settled = !RETRIED_STATES.has(entries.get(scopeId).state)
740
+ if (settled) unsettled.delete(scopeId)
741
+ else unsettled.set(scopeId, { attempts: (unsettled.get(scopeId)?.attempts ?? 0) + 1, lastAttemptMs: nowMs, appState: appNow() })
742
+ }
743
+ for (const scopeId of [...unsettled.keys()]) if (!entries.has(scopeId) || entries.get(scopeId).state === 'disabled') unsettled.delete(scopeId)
744
+ for (const scopeId of [...waitingForApp.keys()]) if (entries.get(scopeId)?.state !== 'current') waitingForApp.delete(scopeId)
496
745
  semantic = nextSemantic
497
746
  firstTick = false
498
747
  return {
@@ -505,6 +754,16 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
505
754
 
506
755
  return {
507
756
  extensions,
757
+ // The next tick that starts prepares and publishes this view once more, whatever its state. A view this
758
+ // project does not declare is ignored then.
759
+ // A view the project declared at the last tick (before the first, any view), and at most MAX_PREPARATION_REQUESTS
760
+ // of them; whether the request was taken.
761
+ requestPreparation(scopeId) {
762
+ if (typeof scopeId !== 'string' || scopeId === '' || (declaredScopes !== null && !declaredScopes.has(scopeId))) return false
763
+ if (!preparationRequests.has(scopeId) && preparationRequests.size >= MAX_PREPARATION_REQUESTS) return false
764
+ preparationRequests.add(scopeId)
765
+ return true
766
+ },
508
767
  async tick() {
509
768
  if (running) return { state: 'busy' }
510
769
  running = true
@@ -65,7 +65,7 @@ export function normalizeApplyResult(value) {
65
65
  // an apply operation replaces it.
66
66
  const OPERATION_NAME = /^[a-z][a-z0-9-]{0,31}$/
67
67
  // The operations the command ships. The service composes the same registry, so both refuse the same contributions.
68
- export const BUILT_IN_OPERATIONS = Object.freeze(['status', 'scope', 'audience', 'mode', 'policy', 'service', 'open', 'apply', 'help'])
68
+ export const BUILT_IN_OPERATIONS = Object.freeze(['status', 'settings', 'location', 'scope', 'view', 'audience', 'mode', 'policy', 'service', 'open', 'uninstall', 'plugin', 'apply', 'help'])
69
69
  export const REPLACEABLE_OPERATIONS = Object.freeze(['apply'])
70
70
 
71
71
  export function createCommandOperations({ reserved = [] } = {}) {
@@ -6,29 +6,36 @@
6
6
  export { ObsidianMaintenanceRefusal } from './errors.mjs'
7
7
  export { DISABLED_REASONS, ENABLEMENT_STATES, readObsidianEnablement } from './enablement.mjs'
8
8
  export {
9
- LOCAL_POINTER_SCHEMA, MACHINE_SETTINGS_SCHEMA, MAINTENANCE_MODES, authorizeAutomaticApply, defaultDataRoot, defaultMachineSettings, ensureWorkspaceIdentity,
10
- installApplyPolicy, localPointerPath, protectedRoots, readInstalledApplyPolicy, readLocalPointer, readMachineSettings, resolveDataRoot, revokeApplyPolicy, workspaceStateRoot, writeLocalPointer, writeMachineSettings,
9
+ DECISIONS, DECISION_SOURCES, LOCAL_POINTER_SCHEMA, MACHINE_SETTINGS_SCHEMA, MACHINE_SETTINGS_SCHEMA_V1, MAINTENANCE_MODES, ONLY_YOU_AUDIENCES, authorizeAutomaticApply,
10
+ defaultDataRoot, defaultMachineSettings, ensureWorkspaceIdentity, installApplyPolicy, localPointerPath, protectedRoots, readInstalledApplyPolicy, readLocalPointer,
11
+ readMachineSettings, resolveDataRoot, revokeApplyPolicy, withDecision, workspaceStateRoot, writeLocalPointer, writeMachineSettings,
11
12
  } from './machine-settings.mjs'
12
13
  export { APPLY_RESULT_STATUSES, BUILT_IN_OPERATIONS, EXTENSION_KINDS, REPLACEABLE_OPERATIONS, UNAVAILABLE_APPLY_OPERATION, createCommandOperations, createMaintenanceExtensions, createObsidianRegistry } from './extension-points.mjs'
13
14
  export { CONTRIBUTIONS_DIRECTORY, loadContributions } from './contributions.mjs'
14
15
  export { APP_OUTCOMES, MINIMUM_APP_VERSION, compareAppVersions, createQualifiedAdapterFactory, meetsMinimumAppVersion, parseAppVersion, qualifyApp } from './app-capability.mjs'
15
16
  export { OPENING_OUTCOMES, describeOutcome, openScope, scopeReport } from './opening.mjs'
17
+ export { appStateSignature, registerVaultInObsidianSettings } from './app-registration.mjs'
16
18
  export {
17
19
  CHANGE_CLASSES, CLOSED_EDIT_STATES, EDIT_STATES, FRESHNESS_SCHEMA, FRESHNESS_STATES, LATE_WRITERS_SCHEMA, OPEN_EDIT_STATES, PENDING_EDITS_SCHEMA,
18
20
  createMaintenanceStateStore, validateFreshness, validateLateWriters, validatePendingEdits,
19
21
  } from './state-store.mjs'
20
22
  export { listConfigFiles, listSourceFiles, listVaultNotes, reconcile } from './observation.mjs'
21
23
  export { createFsWatcherFactory, createNullWatcherFactory } from './watchers.mjs'
22
- export { DEFAULT_ELIGIBILITY, assetEligibilityFor, createProductionSeams } from './pipeline.mjs'
23
- export { DEFAULT_FULL_RECONCILIATION_INTERVAL_MS, DEFAULT_LATE_WRITER_WINDOW_MS, DEFAULT_RETRY_INTERVAL_MS, createMaintenanceEngine } from './engine.mjs'
24
+ export { DEFAULT_ELIGIBILITY, assetEligibilityFor, createProductionSeams, eligibilityFor, onlyYouEligibility } from './pipeline.mjs'
25
+ export { DEFAULT_FULL_RECONCILIATION_INTERVAL_MS, DEFAULT_LATE_WRITER_WINDOW_MS, DEFAULT_PUBLICATION_RETRY_MS, DEFAULT_RETRY_INTERVAL_MS, createMaintenanceEngine } from './engine.mjs'
24
26
  export { ENGINE_LOCK_DIRECTORY, LOCK_TICKET_SCHEMA, acquirePrivateGenerationLock, createAbandonmentProof, inspectPrivateGenerationLock } from './private-lock.mjs'
25
27
  export { DEFAULT_MAX_BACKOFF_MS, DEFAULT_TICK_INTERVAL_MS, createTickLoop } from './tick-loop.mjs'
26
28
  export { HEALTH_SCHEMA, LOOPBACK_HOSTS, probeHealth, requestLoopback } from './service-client.mjs'
27
29
  export {
28
- CONSENT_COVERAGES, SERVICE_ERROR_SCHEMA, SERVICE_SETTINGS_SCHEMA, executableIdentity, publicRecord, readLastServiceError, readServiceRecord, readServiceSettings,
29
- removeServiceRecord, serviceNameFor, servicePaths, validateServiceRecord, writeServiceRecord, writeServiceSettings,
30
+ CONSENT_COVERAGES, LAST_STARTUP_SCHEMA, SERVICE_ERROR_SCHEMA, SERVICE_SETTINGS_SCHEMA, STARTUP_OUTCOMES, executableIdentity, publicRecord, readLastServiceError, readLastStartup,
31
+ readServiceRecord, readServiceSettings, removeServiceRecord, serviceNameFor, servicePaths, validateServiceRecord, writeServiceRecord, writeServiceSettings,
30
32
  } from './service-record.mjs'
31
33
  export { MAX_REQUEST_BYTES, SERVICE_OPERATIONS, createServiceServer } from './service-server.mjs'
32
- export { DEFAULT_SHUTDOWN_GRACE_MS, SERVICE_STATUS_SCHEMA, resolveServiceWorkspace, runMaintenanceService } from './service.mjs'
34
+ export { PLUGIN_BEARER_SCHEMA, PLUGIN_PRESENCE_SCHEMA, createPluginChannel, createPluginSessions, ensurePluginBearer, pluginBearerDirectory, pluginPresence, readPluginBearers } from './plugin-channel.mjs'
35
+ export { PLUGIN_PRESENCE_REASONS, pluginPresenceOf, turnPluginOnNext, withPluginReportedVersion } from './plugin-presence.mjs'
36
+ export { PLUGIN_CHOICE_SCHEMA, PLUGIN_CHOICE_STATES, confirmPluginEntry, confirmPluginSeen, currentPluginChoice, decidePluginChoice, pluginChoiceDirectory, readCommunityEntry, readPluginChoice, viewVaultRoot, writePluginChoice } from './plugin-choice.mjs'
37
+ export { DEFAULT_SHUTDOWN_GRACE_MS, RELEASE_CHANGED, SERVICE_STATUS_SCHEMA, resolveServiceWorkspace, runMaintenanceService } from './service.mjs'
33
38
  export { DEFAULT_START_TIMEOUT_MS, DEFAULT_STOP_TIMEOUT_MS, SERVICE_STATES, readServiceStatusDocument, requestServiceTick, serviceStatus, startService, stopService } from './lifecycle.mjs'
34
- export { STARTUP_PLATFORMS, buildStartupAdapter } from './startup-adapters.mjs'
39
+ export { STARTUP_PLATFORMS, buildStartupAdapter, startupSearchPath } from './startup-adapters.mjs'
40
+ export { LOGIN_ITEM_SCHEMA, installLoginItem, loginItemStatus, removeLoginItem, resolveLoginItemEntry } from './login-item.mjs'
41
+ export { SERVICE_MANAGER_KINDS, createLaunchdManager, createSystemdManager } from './service-managers.mjs'