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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (516) hide show
  1. package/CHANGELOG.md +661 -0
  2. package/README.md +142 -6
  3. package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
  4. package/contracts/atelier-architecture.v1.schema.json +258 -0
  5. package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
  6. package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
  7. package/contracts/atelier-build.v1.schema.json +1076 -0
  8. package/contracts/atelier-capability.v1.schema.json +1252 -0
  9. package/contracts/atelier-coordination.v1.schema.json +1050 -0
  10. package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
  11. package/contracts/atelier-harness.v1.schema.json +234 -0
  12. package/contracts/atelier-ingestion.v1.schema.json +806 -0
  13. package/contracts/atelier-inquiry.v1.schema.json +1772 -0
  14. package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
  15. package/contracts/atelier-interaction.v1.schema.json +553 -0
  16. package/contracts/atelier-judgment.v1.schema.json +690 -0
  17. package/contracts/atelier-knowledge.v1.schema.json +1512 -0
  18. package/contracts/atelier-learning.v1.schema.json +1095 -0
  19. package/contracts/atelier-migration.v3.schema.json +64 -0
  20. package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
  21. package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
  22. package/contracts/atelier-template-adoption.v1.schema.json +223 -0
  23. package/contracts/atelier-template-profile.v1.schema.json +1168 -0
  24. package/contracts/atelier-trackable.v1.schema.json +781 -0
  25. package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
  26. package/docs/architecture.md +89 -0
  27. package/docs/assurance-controls.md +2 -2
  28. package/docs/blocks/capability-stewardship.md +17 -0
  29. package/docs/blocks/claims.md +16 -3
  30. package/docs/blocks/inquiry-harnesses.md +19 -0
  31. package/docs/blocks/learning-harnesses.md +17 -0
  32. package/docs/capability-stewardship.md +269 -0
  33. package/docs/coordination.md +51 -0
  34. package/docs/decision-practices.md +111 -0
  35. package/docs/decisions.md +29 -0
  36. package/docs/evidence-navigation.md +237 -0
  37. package/docs/exact-upgrades.md +3 -2
  38. package/docs/ingestion.md +181 -0
  39. package/docs/inquiry-harnesses.md +211 -0
  40. package/docs/install.md +80 -3
  41. package/docs/intake-and-guides.md +5 -0
  42. package/docs/integration-contract-decisions.md +24 -2
  43. package/docs/interaction-and-reflection.md +72 -0
  44. package/docs/knowledge-graph.md +20 -0
  45. package/docs/knowledge-health/api.md +30 -0
  46. package/docs/knowledge-health/assessment/api.md +96 -0
  47. package/docs/knowledge-health/assessment/capabilities.md +40 -0
  48. package/docs/knowledge-health/assessment/evaluation.md +27 -0
  49. package/docs/knowledge-health/assessment/facilitator.md +30 -0
  50. package/docs/knowledge-health/assessment/quickstart.md +61 -0
  51. package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
  52. package/docs/knowledge-health/devday/facilitator.md +16 -0
  53. package/docs/knowledge-health/devday/participant-response.md +22 -0
  54. package/docs/knowledge-health/devday/public-default.md +107 -0
  55. package/docs/knowledge-health/devday/quickstart.md +18 -0
  56. package/docs/knowledge-health/devday/support.md +22 -0
  57. package/docs/knowledge-health/exercise.md +17 -0
  58. package/docs/knowledge-health/facilitator.md +20 -0
  59. package/docs/knowledge-health/quickstart.md +94 -0
  60. package/docs/knowledge-health/readme.md +15 -0
  61. package/docs/knowledge-health/support.md +34 -0
  62. package/docs/knowledge-setup.md +248 -0
  63. package/docs/knowledge-workspace.md +264 -0
  64. package/docs/learning-harnesses.md +238 -0
  65. package/docs/learning.md +215 -0
  66. package/docs/local-services.md +181 -16
  67. package/docs/obsidian-contract.md +223 -15
  68. package/docs/obsidian-plugin.md +38 -4
  69. package/docs/obsidian.md +613 -51
  70. package/docs/optional-ingestion.md +83 -0
  71. package/docs/personal-workspace.md +257 -0
  72. package/docs/practical-judgment.md +92 -0
  73. package/docs/progressive-ingestion-acceptance.md +89 -0
  74. package/docs/progressive-ingestion.md +235 -0
  75. package/docs/project-command-inventory.md +1 -0
  76. package/docs/release-engineering.md +49 -0
  77. package/docs/repo-boundary-guard.md +94 -0
  78. package/docs/research-starter-quickstart.md +66 -0
  79. package/docs/research-starter.md +114 -0
  80. package/docs/semantic-ingestion.md +198 -0
  81. package/docs/semantic-operation.md +317 -0
  82. package/docs/skill-steward.md +6 -0
  83. package/docs/template-profile.md +164 -0
  84. package/docs/template-upgrades.md +114 -0
  85. package/docs/trackables.md +111 -0
  86. package/docs/upgrade.md +47 -2
  87. package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
  88. package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
  89. package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
  90. package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
  91. package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
  92. package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
  93. package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
  94. package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
  95. package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
  96. package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
  97. package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
  98. package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
  99. package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
  100. package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
  101. package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
  102. package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
  103. package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
  104. package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
  105. package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
  106. package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
  107. package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
  108. package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
  109. package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
  110. package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
  111. package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
  112. package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
  113. package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
  114. package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
  115. package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
  116. package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
  117. package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
  118. package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
  119. package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
  120. package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
  121. package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
  122. package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
  123. package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
  124. package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
  125. package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
  126. package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
  127. package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
  128. package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
  129. package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
  130. package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
  131. package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
  132. package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
  133. package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
  134. package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
  135. package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
  136. package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
  137. package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
  138. package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
  139. package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
  140. package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
  141. package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
  142. package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
  143. package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
  144. package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
  145. package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
  146. package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
  147. package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
  148. package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
  149. package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
  150. package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
  151. package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
  152. package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
  153. package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
  154. package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
  155. package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
  156. package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
  157. package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
  158. package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
  159. package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
  160. package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
  161. package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
  162. package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
  163. package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
  164. package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
  165. package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
  166. package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
  167. package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
  168. package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
  169. package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
  170. package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
  171. package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
  172. package/fixtures/atelier-interaction/act/valid/document.json +24 -0
  173. package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
  174. package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
  175. package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
  176. package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
  177. package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
  178. package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
  179. package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
  180. package/fixtures/atelier-judgment/case/valid/document.json +75 -0
  181. package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
  182. package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
  183. package/fixtures/atelier-judgment/native-decision.md +3 -0
  184. package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
  185. package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
  186. package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
  187. package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
  188. package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
  189. package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
  190. package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
  191. package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
  192. package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
  193. package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
  194. package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
  195. package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
  196. package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
  197. package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
  198. package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
  199. package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
  200. package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
  201. package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
  202. package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
  203. package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
  204. package/fixtures/atelier-learning/invalid/authority.json +14 -0
  205. package/fixtures/atelier-learning/valid/capture.json +13 -0
  206. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
  207. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
  208. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
  209. package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
  210. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
  211. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
  212. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
  213. package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
  214. package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
  215. package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
  216. package/fixtures/atelier-trackable/request/valid/document.json +22 -0
  217. package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
  218. package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
  219. package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
  220. package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
  221. package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
  222. package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
  223. package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
  224. package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
  225. package/fixtures/capability-packages/adoption.example.json +70 -0
  226. package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
  227. package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
  228. package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
  229. package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
  230. package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
  231. package/fixtures/capability-packages/research/capability-package.json +88 -0
  232. package/fixtures/capability-packages/research/capability-release.json +134 -0
  233. package/fixtures/capability-packages/research/evaluation.md +15 -0
  234. package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
  235. package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
  236. package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
  237. package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
  238. package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
  239. package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
  240. package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
  241. package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
  242. package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
  243. package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
  244. package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
  245. package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
  246. package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
  247. package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
  248. package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
  249. package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
  250. package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
  251. package/fixtures/evidence-navigation/valid/claim.json +36 -0
  252. package/fixtures/evidence-navigation/valid/host.json +16 -0
  253. package/fixtures/evidence-navigation/valid/profile.json +16 -0
  254. package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
  255. package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
  256. package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
  257. package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
  258. package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
  259. package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
  260. package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
  261. package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
  262. package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
  263. package/fixtures/harnesses/learning-cycle.json +780 -0
  264. package/fixtures/inquiry/workshop.json +675 -0
  265. package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
  266. package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
  267. package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
  268. package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
  269. package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
  270. package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
  271. package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
  272. package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
  273. package/fixtures/knowledge-health/calibration-suite.json +26 -0
  274. package/fixtures/knowledge-health/calibration-trial.json +37 -0
  275. package/fixtures/knowledge-health/corrected/checklist.md +18 -0
  276. package/fixtures/knowledge-health/devday/participant-record.json +19 -0
  277. package/fixtures/knowledge-health/expected-summary.json +27 -0
  278. package/fixtures/knowledge-health/workshop/README.md +20 -0
  279. package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
  280. package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
  281. package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
  282. package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
  283. package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
  284. package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
  285. package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
  286. package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
  287. package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
  288. package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
  289. package/fixtures/personal-workspace/overlay.json +24 -0
  290. package/fixtures/templates/local-library.v1.json +75 -0
  291. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
  292. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
  293. package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
  294. package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
  295. package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
  296. package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
  297. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
  298. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
  299. package/package.json +59 -5
  300. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  301. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  302. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  303. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  304. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  305. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  306. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  307. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  308. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  309. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  310. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  311. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  312. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  313. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  314. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  315. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  316. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  317. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  318. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  319. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  320. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  321. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  322. package/src/architecture/catalog.mjs +355 -0
  323. package/src/architecture/index.mjs +93 -0
  324. package/src/boundary/content-rules.mjs +4 -2
  325. package/src/boundary/delegation-command.mjs +158 -0
  326. package/src/boundary/delegation.mjs +396 -0
  327. package/src/boundary/policy.mjs +52 -10
  328. package/src/build/git.mjs +18 -0
  329. package/src/build/index.mjs +3 -0
  330. package/src/build/ledger.mjs +88 -0
  331. package/src/capabilities/adoption.mjs +381 -0
  332. package/src/capabilities/evidence.mjs +140 -0
  333. package/src/capabilities/files.mjs +124 -0
  334. package/src/capabilities/index.mjs +4 -0
  335. package/src/capabilities/instructions.mjs +250 -0
  336. package/src/capabilities/package.mjs +136 -0
  337. package/src/cli/command-failure.mjs +36 -0
  338. package/src/cli/execute-command.mjs +16 -2
  339. package/src/cli/run.mjs +65 -9
  340. package/src/coauthor/store.mjs +43 -2
  341. package/src/collaboration/proposals.mjs +189 -0
  342. package/src/commands/architecture.mjs +27 -0
  343. package/src/commands/boundary.mjs +3 -0
  344. package/src/commands/capability.mjs +73 -0
  345. package/src/commands/enroll.mjs +3 -0
  346. package/src/commands/harness.mjs +79 -0
  347. package/src/commands/ingest.mjs +39 -0
  348. package/src/commands/init.mjs +4 -1
  349. package/src/commands/inquiry.mjs +66 -0
  350. package/src/commands/knowledge-semantic.mjs +128 -0
  351. package/src/commands/knowledge.mjs +119 -0
  352. package/src/commands/learn.mjs +61 -0
  353. package/src/commands/obsidian.mjs +754 -63
  354. package/src/commands/practice.mjs +24 -0
  355. package/src/commands/setup.mjs +22 -2
  356. package/src/commands/trackable.mjs +19 -0
  357. package/src/contracts/corpus.mjs +30 -0
  358. package/src/coordination/index.mjs +110 -0
  359. package/src/evidence-navigation/contracts.mjs +60 -0
  360. package/src/evidence-navigation/evaluate.mjs +69 -0
  361. package/src/evidence-navigation/generation.mjs +22 -0
  362. package/src/evidence-navigation/index.mjs +2 -0
  363. package/src/evidence-navigation/local.mjs +167 -0
  364. package/src/evidence-navigation/schema.generated.mjs +604 -0
  365. package/src/evidence-navigation/types.mjs +12 -0
  366. package/src/export/atelier-export-contract.mjs +35 -8
  367. package/src/graph/enroll.mjs +248 -0
  368. package/src/graph/graph.mjs +17 -1
  369. package/src/graph/knowledge-graph.mjs +58 -8
  370. package/src/harnesses/contracts.mjs +41 -0
  371. package/src/harnesses/exchange.mjs +154 -0
  372. package/src/harnesses/feedback.mjs +16 -0
  373. package/src/harnesses/history.mjs +37 -0
  374. package/src/harnesses/index.mjs +4 -0
  375. package/src/harnesses/store.mjs +29 -0
  376. package/src/ingestion/contracts.mjs +41 -0
  377. package/src/ingestion/evaluation.mjs +119 -0
  378. package/src/ingestion/optional/composition/index.mjs +171 -0
  379. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  380. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  381. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  382. package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
  383. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  384. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  385. package/src/ingestion/optional/index.mjs +9 -0
  386. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  387. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  388. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  389. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  390. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  391. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  392. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  393. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  394. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  395. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  396. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  397. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  398. package/src/ingestion/processors.mjs +218 -0
  399. package/src/ingestion/semantic.mjs +203 -0
  400. package/src/ingestion/store.mjs +372 -0
  401. package/src/inquiry/beliefs.mjs +32 -0
  402. package/src/inquiry/contracts.mjs +23 -0
  403. package/src/inquiry/index.mjs +5 -0
  404. package/src/inquiry/ledger.mjs +135 -0
  405. package/src/inquiry/lenses.mjs +15 -0
  406. package/src/inquiry/projection.mjs +77 -0
  407. package/src/inquiry/store.mjs +35 -0
  408. package/src/intake/read-scope.mjs +12 -0
  409. package/src/intake/store.mjs +69 -6
  410. package/src/interaction/index.mjs +91 -0
  411. package/src/judgment/index.mjs +82 -0
  412. package/src/judgment/practice-evaluation.mjs +70 -0
  413. package/src/judgment/practice.mjs +144 -0
  414. package/src/knowledge/context.mjs +217 -0
  415. package/src/knowledge/index.mjs +5 -0
  416. package/src/knowledge/ingestion.mjs +79 -0
  417. package/src/knowledge/intake.mjs +33 -0
  418. package/src/knowledge/ledger.mjs +110 -0
  419. package/src/knowledge/participatory/guidance.mjs +36 -0
  420. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  421. package/src/knowledge/participatory/index.mjs +2 -0
  422. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  423. package/src/knowledge/plan.mjs +67 -0
  424. package/src/knowledge/projection.mjs +43 -0
  425. package/src/knowledge/semantic-operation.mjs +567 -0
  426. package/src/knowledge/sessions.mjs +467 -0
  427. package/src/knowledge/workspace.mjs +295 -0
  428. package/src/knowledge-health/assessment.mjs +165 -0
  429. package/src/knowledge-health/capabilities.mjs +350 -0
  430. package/src/knowledge-health/context.mjs +62 -0
  431. package/src/knowledge-health/example.mjs +47 -0
  432. package/src/knowledge-health/index.mjs +10 -0
  433. package/src/knowledge-health/owner-join.mjs +88 -0
  434. package/src/knowledge-health/proof/common.mjs +24 -0
  435. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  436. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  437. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  438. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  439. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  440. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  441. package/src/knowledge-health/rule-identity.mjs +44 -0
  442. package/src/knowledge-health/workshop-composition.mjs +19 -0
  443. package/src/knowledge-health/workshop-profile.mjs +176 -0
  444. package/src/knowledge-health/workshop.mjs +99 -0
  445. package/src/learning/adapters.mjs +68 -0
  446. package/src/learning/assistance.mjs +127 -0
  447. package/src/learning/contracts.mjs +65 -0
  448. package/src/learning/store.mjs +272 -0
  449. package/src/personal-workspace/index.mjs +391 -0
  450. package/src/personal-workspace/selection.mjs +385 -0
  451. package/src/project/config.mjs +8 -0
  452. package/src/project/durable-state.mjs +3 -1
  453. package/src/project/file-class.mjs +1 -1
  454. package/src/projection/obsidian/edits/apply.mjs +55 -16
  455. package/src/projection/obsidian/personal-workspace.mjs +436 -0
  456. package/src/projection/obsidian/proposals/adapter.mjs +47 -8
  457. package/src/projection/obsidian/proposals/observation.mjs +10 -2
  458. package/src/projection/obsidian/publication/publisher.mjs +142 -13
  459. package/src/projection/obsidian/publication/vault-list.mjs +4 -3
  460. package/src/projection/obsidian/recovery/index.mjs +4 -1
  461. package/src/projection/obsidian/recovery/store.mjs +160 -7
  462. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  463. package/src/readiness/readiness.mjs +10 -3
  464. package/src/reflection/index.mjs +56 -0
  465. package/src/runtime/obsidian/app-production-seams.mjs +48 -16
  466. package/src/runtime/obsidian/app-registration.mjs +175 -28
  467. package/src/runtime/obsidian/app-restart.mjs +154 -0
  468. package/src/runtime/obsidian/engine.mjs +163 -20
  469. package/src/runtime/obsidian/extension-points.mjs +1 -1
  470. package/src/runtime/obsidian/index.mjs +10 -7
  471. package/src/runtime/obsidian/launch-plan.mjs +7 -4
  472. package/src/runtime/obsidian/lifecycle.mjs +92 -12
  473. package/src/runtime/obsidian/login-item.mjs +332 -0
  474. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  475. package/src/runtime/obsidian/opening.mjs +171 -37
  476. package/src/runtime/obsidian/pipeline.mjs +71 -3
  477. package/src/runtime/obsidian/plugin-channel.mjs +11 -3
  478. package/src/runtime/obsidian/plugin-choice.mjs +16 -4
  479. package/src/runtime/obsidian/plugin-drift.mjs +59 -11
  480. package/src/runtime/obsidian/plugin-presence.mjs +14 -1
  481. package/src/runtime/obsidian/process-identity.mjs +20 -10
  482. package/src/runtime/obsidian/project-views.mjs +282 -0
  483. package/src/runtime/obsidian/questions.mjs +48 -0
  484. package/src/runtime/obsidian/release-watch.mjs +56 -0
  485. package/src/runtime/obsidian/service-main.mjs +99 -16
  486. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  487. package/src/runtime/obsidian/service-managers.mjs +275 -0
  488. package/src/runtime/obsidian/service-record.mjs +87 -23
  489. package/src/runtime/obsidian/service.mjs +36 -3
  490. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  491. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  492. package/src/runtime/obsidian/vault-location.mjs +300 -0
  493. package/src/runtime/obsidian/view-counts.mjs +34 -0
  494. package/src/server/local-sidecar.mjs +169 -2
  495. package/src/server/server.mjs +28 -2
  496. package/src/template-bindings/project.mjs +92 -0
  497. package/src/templates/profile.mjs +225 -0
  498. package/src/trackables/domain.mjs +175 -0
  499. package/src/trackables/store.mjs +114 -0
  500. package/src/ui/knowledge-page.mjs +1388 -0
  501. package/src/upgrade/explanation.mjs +1 -1
  502. package/src/upgrade/template-participant.mjs +171 -0
  503. package/src/upgrade/transaction.mjs +72 -25
  504. package/src/upgrade/upgrade.mjs +1 -0
  505. package/templates/external-project-workspace/pack-lifecycle.json +6 -1
  506. package/templates/knowledge-workspace/.gitattributes +2 -0
  507. package/templates/knowledge-workspace/AGENTS.md +27 -0
  508. package/templates/knowledge-workspace/README.md +44 -0
  509. package/templates/knowledge-workspace/atelier.project.json +22 -0
  510. package/templates/knowledge-workspace/gitignore +6 -0
  511. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  512. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  513. package/templates/knowledge-workspace/records/forecast.md +22 -0
  514. package/templates/knowledge-workspace/records/inspection.md +25 -0
  515. package/templates/knowledge-workspace/records/telescope.md +24 -0
  516. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -0,0 +1,436 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads'
5
+ import { PersonalWorkspaceRefusal, composePersonalWorkspace } from '@mnstry/atelier/personal-workspace'
6
+ import { ensureLocalState, resolveProjectConfig, validateProjectConfigDoc } from '@mnstry/atelier/project'
7
+ import { buildCanonicalGraph } from '../../graph/graph.mjs'
8
+ import { refuse } from '../../runtime/obsidian/errors.mjs'
9
+ import { OBSIDIAN_EXT_KEY } from './contracts.mjs'
10
+
11
+ // The one personal-workspace route of the Obsidian projection.
12
+ //
13
+ // A person's private home holds the authored manifest and overlay, and the
14
+ // generations the personal-workspace module materialized from them. A binding
15
+ // names one home and one generation, both explicitly: nothing here discovers
16
+ // either from the environment or the working directory. The module's
17
+ // `composePersonalWorkspace` is the only authority on whether that generation
18
+ // may be used.
19
+ //
20
+ // One graph. Every caller (the engine, view counts, source apply, the proposal
21
+ // adapter and the selection operation) builds through the pipeline's
22
+ // `buildGraph`, which builds a bound project here: the canonical graph of the
23
+ // generation's configuration, with the composition's own rule that no private
24
+ // note is a link target, and the caller's file cache. A plain build would
25
+ // differ wherever a shared note's link names a private note.
26
+ //
27
+ // Validity. A graph is returned only under a validity key the composition has
28
+ // confirmed: the key names the inputs a stat or a bounded read can see (see
29
+ // inputsDigest) and a digest of the built graph's facts. The inputs are read
30
+ // before composing, so a key is confirmed only when the composition accepted
31
+ // those inputs and composed a graph with exactly those facts, and a returned
32
+ // graph is always one the composition built too (a change and its exact undo
33
+ // during one composition is caught at the next full reconciliation). A key not yet confirmed is
34
+ // confirmed by composing: synchronously for a caller that builds once, and off
35
+ // the event loop, in a worker with a deadline, for the engine and the proposal
36
+ // adapter it runs (`validatePersonalWorkspace`). What no key names is asked of
37
+ // the composition at every full reconciliation.
38
+ //
39
+ // What a binding returns:
40
+ // project the generation's atelier.project.json, resolved, with the
41
+ // Obsidian settings member built in memory from the overlay's
42
+ // saved views (each a scope) plus an `everything` scope, and the
43
+ // frozen binding under one symbol key. No generation file is
44
+ // ever written.
45
+ // binding { personalHome, generationId, overlayRepoId }, frozen.
46
+ // preferences the overlay's preferences, read only. A theme is reported,
47
+ // never written: the vault's appearance is the person's.
48
+ // coverage passed through as the module returned it. Nothing here reads
49
+ // it: it describes what is not enforced, and is never permission.
50
+ //
51
+ // The project's own folder is the private home rather than the generation:
52
+ // the projection keeps its workspace pointer in `<folder>/.atelier-local/`,
53
+ // and a generation is a closed, verified inventory that any extra file
54
+ // invalidates. A home is outside every Git worktree, so its local state is
55
+ // ignored by construction.
56
+
57
+ const BINDING = Symbol('atelier.obsidian.personal-workspace-binding')
58
+ // What each binding was made with: the composition that admitted it, the build that serves its graph, and whether the
59
+ // composition can run in a worker (only the module's own can).
60
+ const ROUTES = new WeakMap()
61
+ // The last validity key the composition confirmed for each binding: { inputs, facts }.
62
+ const CONFIRMED = new WeakMap()
63
+ export const EVERYTHING_SCOPE_ID = 'everything'
64
+ const EXT_SCHEMA = 'atelier-obsidian-ext-settings/v1'
65
+ // The settings member of a bound project names its generation here. It survives any copy of the project (a structured
66
+ // clone, a JSON round trip) that drops the symbol-keyed binding, so such a copy is refused instead of being built as an
67
+ // ordinary project; and a rebound generation changes the member, which invalidates every view. An ordinary project's
68
+ // settings may not use this key.
69
+ export const PERSONAL_MEMBER_KEY = 'mnstry.atelier.personal-workspace'
70
+ // The Obsidian settings contract's limits: at most 256 scopes, one of them `everything`, and at most 256 members in a
71
+ // selector's union.
72
+ const MAX_SAVED_VIEWS = 255
73
+ const MAX_VIEW_REPOSITORIES = 256
74
+ // A build that found its validity key unconfirmed, for a caller that confirms it off the event loop and builds again.
75
+ export const PERSONAL_VALIDATION_PENDING = 'personal-validation-pending'
76
+ const WORKER_KIND = 'atelier-obsidian-personal-composition'
77
+
78
+ const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex')
79
+ const deepFreeze = (value) => { if (value && typeof value === 'object') { Object.values(value).forEach(deepFreeze); Object.freeze(value) } return value }
80
+ const sortKeys = (value) => (Array.isArray(value) ? value.map(sortKeys)
81
+ : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])])) : value)
82
+ const digestOf = (value) => sha256(JSON.stringify(sortKeys(value)))
83
+ const generationRoot = ({ personalHome, generationId }) => path.join(personalHome, 'generations', generationId)
84
+ const linkTargetRule = (overlayRepoId) => (node) => node.repo !== overlayRepoId
85
+
86
+ // The facts of a graph every caller reads: nodes, edges, embeds, assets, resolved links and every finding.
87
+ const graphFactsDigest = (graph) => digestOf([graph.nodes, graph.edges, graph.embeds ?? [], graph.assets ?? [], graph.links ?? [], graph.linkDiagnostics ?? [], graph.diagnostics ?? []])
88
+
89
+ // The personal-workspace module's read limits: an authored input, and a generation file.
90
+ const AUTHORED_LIMIT = 1024 * 1024
91
+ const GENERATION_LIMIT = 8 * 1024 * 1024
92
+ const NONBLOCK = fs.constants.O_NONBLOCK ?? 0
93
+
94
+ // One regular file, opened without following a link and without waiting on it (a FIFO or a device never blocks the
95
+ // caller), and read up to `limit`. Anything else refuses `code`, as the composition would.
96
+ function regularFileBytes(file, limit, code, { follow = false } = {}) {
97
+ let fd
98
+ try { fd = fs.openSync(file, fs.constants.O_RDONLY | (follow ? 0 : fs.constants.O_NOFOLLOW) | NONBLOCK) } catch (error) {
99
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null
100
+ throw new PersonalWorkspaceRefusal(code)
101
+ }
102
+ try {
103
+ const stat = fs.fstatSync(fd)
104
+ if (!stat.isFile() || stat.size > limit) throw new PersonalWorkspaceRefusal(code)
105
+ return fs.readFileSync(fd)
106
+ } finally { fs.closeSync(fd) }
107
+ }
108
+ const regularFileDigest = (file, limit, code) => { const bytes = regularFileBytes(file, limit, code); return bytes === null ? 'missing' : sha256(bytes) }
109
+ // A Git configuration file, which Git itself reads through a link (a managed dotfile): followed, but still only a
110
+ // regular file, bounded, and never waited on.
111
+ const configDigest = (file) => { try { const bytes = regularFileBytes(file, AUTHORED_LIMIT, 'malformed-input', { follow: true }); return bytes === null ? 'missing' : sha256(bytes) } catch { return 'not-regular' } }
112
+ // The configuration files of an enrolled repository: `.git/config` (through `.git` when it is a link to a directory),
113
+ // or, where `.git` is a file (a separate Git directory, a linked worktree, a submodule), the configuration of the
114
+ // directory it names and of its common directory. A root that cannot be looked at (a file, a link loop, no permission)
115
+ // is recorded as such, so the composition answers with its own code.
116
+ function repositoryConfigDigests(repoRoot) {
117
+ const dotGit = path.join(repoRoot, '.git')
118
+ let kind
119
+ try { kind = fs.lstatSync(dotGit, { throwIfNoEntry: false }) } catch (error) { return [`unreadable:${error.code ?? 'error'}`] }
120
+ if (kind === undefined) return ['missing']
121
+ if (kind.isDirectory()) return [configDigest(path.join(dotGit, 'config'))]
122
+ if (kind.isSymbolicLink()) {
123
+ try { if (fs.statSync(dotGit).isDirectory()) return ['linked', fs.realpathSync(dotGit), configDigest(path.join(dotGit, 'config'))] } catch (error) { return [`unreadable:${error.code ?? 'error'}`] }
124
+ }
125
+ let pointer
126
+ try { pointer = regularFileBytes(dotGit, AUTHORED_LIMIT, 'malformed-input', { follow: true }) } catch { return ['not-regular'] }
127
+ const named = /^gitdir:\s*(.+?)\s*$/m.exec(pointer?.toString('utf8') ?? '')
128
+ if (named === null) return ['unnamed']
129
+ const gitDir = path.resolve(repoRoot, named[1])
130
+ let commonDir = null
131
+ try { const common = regularFileBytes(path.join(gitDir, 'commondir'), AUTHORED_LIMIT, 'malformed-input', { follow: true }); if (common !== null) commonDir = path.resolve(gitDir, common.toString('utf8').trim()) } catch { commonDir = 'not-regular' }
132
+ return [gitDir, configDigest(path.join(gitDir, 'config')), commonDir, commonDir === null || commonDir === 'not-regular' ? null : configDigest(path.join(commonDir, 'config'))]
133
+ }
134
+
135
+ // Everything outside the graph that a composition's answer depends on and a stat or a bounded read can see: the authored
136
+ // inputs (regular files only), every file of the generation (an extra, missing or irregular one too), the private home's
137
+ // own state and any `.git` above it, each enrolled root's link status and Git configuration (its remote and its helpers),
138
+ // the user's Git configuration, and any ambient Git variable. A file that is not regular refuses here with the code the
139
+ // composition gives it, before anything waits on it.
140
+ function inputsDigest({ personalHome, generationId }) {
141
+ const stat = (file) => { try { const s = fs.lstatSync(file); return [s.mode, s.uid, s.ino, s.isSymbolicLink()] } catch (error) { return `missing:${error.code ?? 'error'}` } }
142
+ const authoredBytes = ['atelier.personal.json', 'atelier.overlay.json'].map((name) => regularFileBytes(path.join(personalHome, name), AUTHORED_LIMIT, 'malformed-input'))
143
+ const authored = ['atelier.personal.json', 'atelier.overlay.json'].map((name, index) => [stat(path.join(personalHome, name)), authoredBytes[index] === null ? 'missing' : sha256(authoredBytes[index])])
144
+ const generation = []
145
+ const walk = (directory, prefix) => {
146
+ let entries
147
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }) } catch (error) { generation.push([prefix, `unreadable:${error.code ?? 'error'}`]); return }
148
+ for (const entry of entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))) {
149
+ const relative = prefix === '' ? entry.name : `${prefix}/${entry.name}`
150
+ const absolute = path.join(directory, entry.name)
151
+ if (entry.isDirectory()) { generation.push([relative, stat(absolute)]); walk(absolute, relative) } else if (entry.isFile()) generation.push([relative, regularFileDigest(absolute, GENERATION_LIMIT, 'generation-corrupt')])
152
+ else throw new PersonalWorkspaceRefusal('generation-corrupt')
153
+ }
154
+ }
155
+ const root = generationRoot({ personalHome, generationId })
156
+ walk(root, '')
157
+ let realHome
158
+ try { realHome = fs.realpathSync(personalHome) === personalHome } catch { realHome = false }
159
+ const ancestorsWithGit = []
160
+ for (let ancestor = personalHome; ; ancestor = path.dirname(ancestor)) {
161
+ if (fs.lstatSync(path.join(ancestor, '.git'), { throwIfNoEntry: false }) !== undefined) ancestorsWithGit.push(ancestor)
162
+ if (ancestor === path.dirname(ancestor)) break
163
+ }
164
+ let repos = []
165
+ // The bytes read above, never the file again.
166
+ try { repos = JSON.parse(authoredBytes[0]?.toString('utf8') ?? '{}').repos.filter((repo) => repo.enrolled).map((repo) => repo.root) } catch { repos = [] }
167
+ const enrolled = repos.map((repoRoot) => {
168
+ let real
169
+ try { real = fs.realpathSync(repoRoot) === repoRoot } catch { real = false }
170
+ return [repoRoot, stat(repoRoot), real, repositoryConfigDigests(repoRoot)]
171
+ })
172
+ const home = process.env.HOME
173
+ const userGit = typeof home === 'string' && path.isAbsolute(home)
174
+ ? [path.join(home, '.gitconfig'), path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'git', 'config')].map(configDigest)
175
+ : ['no-home']
176
+ return digestOf({
177
+ authored,
178
+ generation,
179
+ roots: [stat(personalHome), realHome, stat(path.join(personalHome, 'generations')), stat(path.join(personalHome, 'generations', '.git')), stat(root), ancestorsWithGit],
180
+ enrolled,
181
+ userGit,
182
+ ambientGit: Object.keys(process.env).filter((name) => /^GIT_/i.test(name)).sort(),
183
+ })
184
+ }
185
+
186
+ // One generation file, read without following a link, and checked against the
187
+ // digest the generation records for it (and compose verified a moment ago).
188
+ function generationFile(generation, final, relative) {
189
+ const listed = generation.files.find((file) => file.path === relative)
190
+ if (!listed) throw new PersonalWorkspaceRefusal('generation-corrupt')
191
+ let bytes
192
+ try {
193
+ const fd = fs.openSync(path.join(final, ...relative.split('/')), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
194
+ try { bytes = fs.readFileSync(fd) } finally { fs.closeSync(fd) }
195
+ } catch { throw new PersonalWorkspaceRefusal('generation-corrupt') }
196
+ if (sha256(bytes) !== listed.sha256) throw new PersonalWorkspaceRefusal('generation-corrupt')
197
+ return bytes
198
+ }
199
+
200
+ function selectorFor(repoIds) {
201
+ const repos = [...new Set(repoIds)]
202
+ if (repos.length === 0) return { ids: [] }
203
+ if (repos.length === 1) return { repo: repos[0] }
204
+ return { union: repos.map((repo) => ({ repo })) }
205
+ }
206
+
207
+ // The Obsidian settings member of a personal binding: one scoped view per saved
208
+ // selection of repositories, in the overlay's order, and one full view of the
209
+ // whole composed graph. A preferred view that names a saved one is the default.
210
+ // Refused, typed, where the overlay allows more than a projection can declare.
211
+ export function personalWorkspaceScopes(overlay, generationId) {
212
+ if (overlay.views.some((view) => view.id === EVERYTHING_SCOPE_ID)) {
213
+ refuse('personal-view-id-reserved', `a saved view may not be named "${EVERYTHING_SCOPE_ID}"; that view is the whole personal workspace`)
214
+ }
215
+ if (overlay.views.length > MAX_SAVED_VIEWS) refuse('personal-views-exceed-settings-limit', `a personal workspace can show at most ${MAX_SAVED_VIEWS} saved views; nothing is bound`, { limit: MAX_SAVED_VIEWS, kind: 'views' })
216
+ if (overlay.views.some((view) => new Set(view.repoIds).size > MAX_VIEW_REPOSITORIES)) {
217
+ refuse('personal-views-exceed-settings-limit', `a saved view can name at most ${MAX_VIEW_REPOSITORIES} repositories; nothing is bound`, { limit: MAX_VIEW_REPOSITORIES, kind: 'repositories' })
218
+ }
219
+ const scopes = [
220
+ ...overlay.views.map((view) => ({ scopeId: view.id, mode: 'scoped', selector: selectorFor(view.repoIds) })),
221
+ { scopeId: EVERYTHING_SCOPE_ID, mode: 'full', selector: { all: true } },
222
+ ]
223
+ const preferred = overlay.preferences?.defaultView
224
+ const defaultScopeId = typeof preferred === 'string' && overlay.views.some((view) => view.id === preferred) ? preferred : EVERYTHING_SCOPE_ID
225
+ return { schema: EXT_SCHEMA, enabled: true, defaultScopeId, scopes, ext: { [PERSONAL_MEMBER_KEY]: { generationId } } }
226
+ }
227
+
228
+ // The bound project, from a composition the caller just made: `composed` carries the generation's record, the
229
+ // preferences and the coverage, and the validity key that composition confirmed.
230
+ function assemble({ personalHome, generationId }, composed, route) {
231
+ const final = generationRoot({ personalHome, generationId })
232
+ const projectBytes = generationFile(composed.generation, final, 'atelier.project.json')
233
+ const inputs = JSON.parse(generationFile(composed.generation, final, 'inputs.json').toString('utf8'))
234
+ const configPath = path.join(final, 'atelier.project.json')
235
+ const resolved = resolveProjectConfig({ argv: [`--project-config=${configPath}`], cwd: final, env: { PATH: process.env.PATH }, writeLocalState: false })
236
+ // The same pinned resolution compose made; anything else means the generation changed between the two reads.
237
+ if (JSON.stringify(resolved.config) !== JSON.stringify(JSON.parse(projectBytes.toString('utf8')))) throw new PersonalWorkspaceRefusal('generation-corrupt')
238
+ if (validateProjectConfigDoc(resolved.config).length > 0) throw new PersonalWorkspaceRefusal('generation-corrupt')
239
+ if (resolved.localOverlay.paths.length > 0 || resolved.repos.some((repo) => repo.pathSource !== 'tracked-config')) throw new PersonalWorkspaceRefusal('ambient-overlay-present')
240
+ const overlayRepoId = `personal-${inputs.manifest.workspaceId}`
241
+ const overlayRepo = resolved.repos.find((repo) => repo.name === overlayRepoId)
242
+ if (!overlayRepo || overlayRepo.path !== path.join(final, 'overlay')) throw new PersonalWorkspaceRefusal('generation-corrupt')
243
+
244
+ const binding = Object.freeze({ personalHome, generationId, overlayRepoId })
245
+ ROUTES.set(binding, route)
246
+ CONFIRMED.set(binding, composed.key)
247
+ const member = deepFreeze(personalWorkspaceScopes(inputs.overlay, generationId))
248
+ const project = {
249
+ ...resolved,
250
+ config: { ...resolved.config, ext: { ...(resolved.config.ext ?? {}), [OBSIDIAN_EXT_KEY]: member } },
251
+ configDir: personalHome,
252
+ [BINDING]: binding,
253
+ }
254
+ project.localState = ensureLocalState(project, { write: false, env: { PATH: process.env.PATH } })
255
+ return Object.freeze({ project, binding, preferences: deepFreeze(structuredClone(composed.preferences ?? {})), coverage: composed.coverage })
256
+ }
257
+
258
+ // A composition in this thread, as the summary a worker sends back.
259
+ // The inputs are digested BEFORE composing: a single change that lands while the composition runs then either is seen by
260
+ // it, or leaves the confirmed key behind, so the next build composes again. A change and its exact undo within one
261
+ // composition (A, then B, then A again) is not seen this way: it is caught at the next full reconciliation.
262
+ function composeHere(compose, binding) {
263
+ const inputs = inputsDigest(binding)
264
+ const composed = compose({ personalHome: binding.personalHome, generationId: binding.generationId })
265
+ return { generation: composed.generation, preferences: composed.preferences, coverage: composed.coverage, key: { inputs, facts: graphFactsDigest(composed.graph) } }
266
+ }
267
+
268
+ // A composition in a worker thread, so the event loop of the caller stays free while it runs. Resolves to the summary, or
269
+ // to `{ code }` for a refusal; a worker that cannot run answers `personal-composition-unavailable`.
270
+ // A worker that does not answer within the deadline is terminated and let go of, and the composition refuses
271
+ // `personal-composition-unavailable`: the tick does not wait on it. A worker blocked in a system call (a FIFO opened
272
+ // for reading, say) cannot be stopped until that call returns: until then it keeps the process from exiting, and each
273
+ // such expiry leaves one thread behind. A service stopped in that state needs a kill.
274
+ export const WORKER_DEADLINE_MS = 120 * 1000
275
+ function composeInWorker({ personalHome, generationId }, { deadlineMs = WORKER_DEADLINE_MS, stallMs = 0 } = {}) {
276
+ return new Promise((resolve) => {
277
+ let settled = false
278
+ let worker = null
279
+ let timer = null
280
+ const settle = (value) => {
281
+ if (settled) return
282
+ settled = true
283
+ clearTimeout(timer)
284
+ if (value.code === 'personal-composition-unavailable' && worker !== null) { worker.unref(); worker.terminate().catch(() => {}) }
285
+ resolve(value)
286
+ }
287
+ try { worker = new Worker(new URL(import.meta.url), { workerData: { kind: WORKER_KIND, personalHome, generationId, stallMs } }) } catch { settle({ code: 'personal-composition-unavailable' }); return }
288
+ timer = setTimeout(() => settle({ code: 'personal-composition-unavailable' }), deadlineMs)
289
+ timer.unref?.()
290
+ worker.once('message', settle)
291
+ worker.once('error', () => settle({ code: 'personal-composition-unavailable' }))
292
+ worker.once('exit', () => settle({ code: 'personal-composition-unavailable' }))
293
+ })
294
+ }
295
+
296
+ // Test seam only: `compose` replaces the module's composePersonalWorkspace for the binding and every validation of
297
+ // it, `build` the canonical build that serves its graph, and `worker` the worker's deadline (and a stall, so a worker that
298
+ // never answers can be shown). Production code uses bindPersonalWorkspace.
299
+ export function createPersonalWorkspaceBinderForOracleTests({ compose = composePersonalWorkspace, build = null, worker = {} } = {}) {
300
+ const route = { compose, offThread: compose === composePersonalWorkspace, build, worker }
301
+ const bind = ({ personalHome, generationId } = {}) => {
302
+ // The validity authority first: a refusal propagates as the module's own typed refusal, and nothing is read or
303
+ // written after it.
304
+ const composed = composeHere(compose, { personalHome, generationId })
305
+ return assemble({ personalHome, generationId }, composed, route)
306
+ }
307
+ // The same binding, with the composition off the event loop.
308
+ bind.offThread = async ({ personalHome, generationId } = {}) => {
309
+ const composed = route.offThread ? await composeInWorker({ personalHome, generationId }, route.worker) : composeHere(compose, { personalHome, generationId })
310
+ if (typeof composed.code === 'string') {
311
+ if (composed.code === 'personal-composition-unavailable') refuse(composed.code, 'the personal workspace could not be composed off the event loop; nothing is bound')
312
+ throw new PersonalWorkspaceRefusal(composed.code)
313
+ }
314
+ return assemble({ personalHome, generationId }, composed, route)
315
+ }
316
+ return bind
317
+ }
318
+
319
+ export const bindPersonalWorkspace = createPersonalWorkspaceBinderForOracleTests()
320
+
321
+ // A refusal of the personal-workspace module becomes the maintenance refusal with the same code, as at every build, so
322
+ // the engine and every command report it typed and no view stays current.
323
+ const asMaintenanceRefusal = (error, message) => {
324
+ if (error instanceof PersonalWorkspaceRefusal) refuse(error.code, message, { source: 'personal-workspace' })
325
+ throw error
326
+ }
327
+
328
+ // The project of a binding, for a loader that may block (a command run once).
329
+ export function loadBoundProject({ personalHome, generationId } = {}, { bind = bindPersonalWorkspace } = {}) {
330
+ try { return bind({ personalHome, generationId }).project } catch (error) { return asMaintenanceRefusal(error, 'the personal workspace refused this generation; nothing is loaded from it') }
331
+ }
332
+
333
+ // The project of a binding, for a loader on a service's event loop: the composition runs in a worker. The engine
334
+ // awaits its loader, so this is the loader a service uses.
335
+ export async function loadBoundProjectOffThread({ personalHome, generationId } = {}, { bind = bindPersonalWorkspace } = {}) {
336
+ try { return (await bind.offThread({ personalHome, generationId })).project } catch (error) { return asMaintenanceRefusal(error, 'the personal workspace refused this generation; nothing is loaded from it') }
337
+ }
338
+
339
+ // The binding a project carries, or null for every other project.
340
+ export function personalWorkspaceBindingOf(project) {
341
+ return project !== null && typeof project === 'object' && Object.hasOwn(project, BINDING) ? project[BINDING] : null
342
+ }
343
+
344
+ // The graph of a bound project, or null when the project carries no binding. `fileCache` and `observedDigest` are the
345
+ // caller's, as for any build. With `defer`, a key the composition has not confirmed is not confirmed here: the build
346
+ // refuses `personal-validation-pending`, and the caller confirms it off the event loop (validatePersonalWorkspace) and
347
+ // builds again.
348
+ export function personalWorkspaceGraph(project, { fileCache = null, observedDigest = null, defer = false } = {}) {
349
+ const binding = personalWorkspaceBindingOf(project)
350
+ if (binding === null) {
351
+ if (project?.config?.ext?.[OBSIDIAN_EXT_KEY]?.ext?.[PERSONAL_MEMBER_KEY] !== undefined) {
352
+ refuse('personal-binding-lost', `the project's Obsidian settings use the reserved key ext["${PERSONAL_MEMBER_KEY}"], which only a personal-workspace binding sets, and the project carries no binding; nothing is prepared from it`)
353
+ }
354
+ return null
355
+ }
356
+ const route = ROUTES.get(binding)
357
+ if (route === undefined) refuse('personal-binding-unrecognized', 'the project carries a personal-workspace binding this release did not make; nothing is prepared from it')
358
+ if (project.configPath !== path.join(generationRoot(binding), 'atelier.project.json')) refuse('personal-binding-mismatch', 'the project is not the configuration of the generation it is bound to; nothing is prepared from it')
359
+ const pending = () => refuse(PERSONAL_VALIDATION_PENDING, 'the personal workspace has changed since it was last composed; it is composed again before a graph is used', { source: 'personal-workspace-pending' })
360
+ const confirmed = CONFIRMED.get(binding)
361
+ // Inputs that changed are composed before anything is built from them.
362
+ let inputs
363
+ try { inputs = inputsDigest(binding) } catch (error) { CONFIRMED.delete(binding); asMaintenanceRefusal(error, 'the personal workspace refused this generation; nothing is prepared from it') }
364
+ if (confirmed?.inputs !== inputs) {
365
+ if (defer) pending()
366
+ confirmHere(route, binding)
367
+ }
368
+ const build = route.build ?? ((target, options) => buildCanonicalGraph(target, { ...options, isLinkTargetEligible: linkTargetRule(binding.overlayRepoId) }))
369
+ const graph = build(project, { fileCache, observedDigest })
370
+ if (CONFIRMED.get(binding)?.facts === graphFactsDigest(graph)) return graph
371
+ if (defer) pending()
372
+ confirmHere(route, binding)
373
+ if (CONFIRMED.get(binding)?.facts === graphFactsDigest(graph)) return graph
374
+ // Composed just now, and still another graph: the sources moved between the two reads, or the build is not the
375
+ // composition's. Nothing is prepared from it.
376
+ return 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' })
377
+ }
378
+
379
+ function confirmHere(route, binding) {
380
+ try { CONFIRMED.set(binding, composeHere(route.compose, binding).key) } catch (error) { CONFIRMED.delete(binding); asMaintenanceRefusal(error, 'the personal workspace refused this generation; nothing is prepared from it') }
381
+ }
382
+
383
+ // Asks the composition again, off the event loop when it can: `{ ok: true, changed }` when it accepts the generation
384
+ // (`changed` when its key differs from the one last confirmed), `{ ok: false, code }` when it refuses. Null for a project
385
+ // without a binding. The engine asks this before a build that found its key unconfirmed, and at every full
386
+ // reconciliation of a bound project.
387
+ export async function validatePersonalWorkspace(project) {
388
+ const binding = personalWorkspaceBindingOf(project)
389
+ if (binding === null) return null
390
+ const route = ROUTES.get(binding)
391
+ if (route === undefined) return { ok: false, code: 'personal-binding-unrecognized' }
392
+ let composed
393
+ if (route.offThread) composed = await composeInWorker(binding, route.worker)
394
+ else {
395
+ try { composed = composeHere(route.compose, binding) } catch (error) { if (!(error instanceof PersonalWorkspaceRefusal)) throw error; composed = { code: error.code } }
396
+ }
397
+ if (typeof composed.code === 'string') { CONFIRMED.delete(binding); return { ok: false, code: composed.code } }
398
+ const previous = CONFIRMED.get(binding)
399
+ CONFIRMED.set(binding, composed.key)
400
+ return { ok: true, changed: previous?.inputs !== composed.key.inputs || previous?.facts !== composed.key.facts }
401
+ }
402
+
403
+ // The authored and generation files whose change means the binding must be asked again: the manifest, the overlay and
404
+ // the bound generation's record. Observed as configuration by the engine. Empty for any other project.
405
+ export function personalWorkspaceInputs(project) {
406
+ const binding = personalWorkspaceBindingOf(project)
407
+ if (binding === null) return []
408
+ return [path.join(binding.personalHome, 'atelier.personal.json'), path.join(binding.personalHome, 'atelier.overlay.json'), path.join(generationRoot(binding), 'generation.json')]
409
+ }
410
+
411
+ // The repository of a bound project's private notes, or null. A vault edit to one of them is never applied into a
412
+ // module-owned generation, and is not proposed yet: it stays held in the vault.
413
+ export function personalOverlayRepoOf(project) {
414
+ return personalWorkspaceBindingOf(project)?.overlayRepoId ?? null
415
+ }
416
+
417
+ // Settings that a personal binding takes from the overlay or does not have yet. A saved view is authored in the
418
+ // overlay, so `view add` would write a module-owned generation file; who may see and where vaults live wait for a
419
+ // stable per-person projection configuration. Refused before anything is resolved or written.
420
+ export function refuseOnPersonalBinding(project, operation) {
421
+ if (personalWorkspaceBindingOf(project) === null) return
422
+ if (operation === 'view add') refuse('views-from-personal-overlay', 'the views of a personal workspace come from its overlay; nothing was written', { operation })
423
+ refuse('personal-binding-decision-unavailable', `${operation} is not available for a personal workspace yet; nothing was written`, { operation })
424
+ }
425
+
426
+ // The worker side of composeInWorker: one composition, its summary, and exit.
427
+ if (!isMainThread && workerData?.kind === WORKER_KIND) {
428
+ const binding = { personalHome: workerData.personalHome, generationId: workerData.generationId }
429
+ // A worker that never answers, for the deadline test only: blocked as a system call would block it.
430
+ if (workerData.stallMs > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, workerData.stallMs)
431
+ try {
432
+ parentPort.postMessage(composeHere(composePersonalWorkspace, binding))
433
+ } catch (error) {
434
+ parentPort.postMessage({ code: error instanceof PersonalWorkspaceRefusal ? error.code : 'personal-composition-unavailable' })
435
+ }
436
+ }
@@ -6,7 +6,7 @@ import { AtelierDiagnosticError } from '../../../project/config.mjs'
6
6
  import { compareText, isoTime } from '../../../runtime/obsidian/documents.mjs'
7
7
  import { ObsidianMaintenanceRefusal } from '../../../runtime/obsidian/errors.mjs'
8
8
  import { readMachineSettings } from '../../../runtime/obsidian/machine-settings.mjs'
9
- import { DEFAULT_ELIGIBILITY, createProductionSeams } from '../../../runtime/obsidian/pipeline.mjs'
9
+ import { createProductionSeams, eligibilityFor } from '../../../runtime/obsidian/pipeline.mjs'
10
10
  import { acquirePrivateGenerationLock, createAbandonmentProof } from '../../../runtime/obsidian/private-lock.mjs'
11
11
  import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, assertObsidianContract } from '../contracts.mjs'
12
12
  import { SOURCE_APPLY_PRIMITIVES } from '../edits/apply.mjs'
@@ -15,6 +15,7 @@ import { isIdentifier } from '../edits/object-identity.mjs'
15
15
  import { openObjectStore } from '../edits/object-store.mjs'
16
16
  import { decideApply } from '../edits/policy.mjs'
17
17
  import { applyEditLens } from '../edits/regions.mjs'
18
+ import { PERSONAL_VALIDATION_PENDING, personalOverlayRepoOf } from '../personal-workspace.mjs'
18
19
  import { PublicationRefusal } from '../recovery/store.mjs'
19
20
  import {
20
21
  PROPOSAL_BACKPRESSURE, PROPOSAL_LEDGER_LIMITS, classifyLedgerRead, classifyStoreRefusal, estimateEventLineBytes, isDue, isExhausted, nextAttemptAt, preflightAppend,
@@ -168,7 +169,7 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
168
169
  // two questions asked of the machine; `proveAbandoned` is the proof a held lock is passed over with.
169
170
  return function createProposalAdapter(options = {}) {
170
171
  const {
171
- crash = () => {}, bounds: boundsInput = {}, limits = PROPOSAL_LEDGER_LIMITS, eligibility = DEFAULT_ELIGIBILITY,
172
+ crash = () => {}, bounds: boundsInput = {}, limits = PROPOSAL_LEDGER_LIMITS, eligibility: fixedEligibility = null,
172
173
  openStore = createProposalStore, objectStore = openObjectStore, isGitIgnored = SOURCE_APPLY_PRIMITIVES.isGitIgnored, proveAbandoned = createAbandonmentProof(), queueCrash = () => {},
173
174
  } = options
174
175
  const bounds = { ...PROPOSAL_BACKPRESSURE, ...boundsInput }
@@ -184,6 +185,9 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
184
185
  let observation = { key: null, run: null }
185
186
  // Edits whose operation is known not to be a proposal. A cache: losing it costs one read of an object.
186
187
  const notProposals = new Set()
188
+ // Set when an observation found the personal workspace's graph pending: the edits it could not look at are offered
189
+ // again on the next observation, not parked until a full reconciliation.
190
+ let observePendingAgain = false
187
191
 
188
192
  // -- the workspace of one call -------------------------------------------
189
193
 
@@ -198,12 +202,31 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
198
202
  if (!stores.has(scopeId)) stores.set(scopeId, seams.createRecoveryStore({ workspaceRoot, workspaceId, scopeId, repositoryRoots }))
199
203
  return stores.get(scopeId)
200
204
  }
201
- // The canonical graph as it is now and the corpus profile of this machine, built once per call. Throws typed.
205
+ // The canonical graph as it is now and the corpus profile of this machine, built once per call with the notes the
206
+ // machine settings let into a view, as the engine builds it (eligibilityFor). Throws typed.
202
207
  let corpus = null
203
- const currentCorpus = () => (corpus ??= {
204
- graph: seams.buildGraph({ project, eligibility }),
205
- profile: seams.profileFor({ project, workspaceId, audienceAllow: readMachineSettings({ workspaceRoot, workspaceId })?.audienceAllow ?? [] }),
206
- })
208
+ // A pending answer is kept for the call too, so a call that looks at several edits builds at most once.
209
+ let pendingCorpus = null
210
+ const currentCorpus = () => {
211
+ if (corpus !== null) return corpus
212
+ if (pendingCorpus !== null) throw pendingCorpus
213
+ const machine = readMachineSettings({ workspaceRoot, workspaceId })
214
+ corpus = {
215
+ // The engine hands its deferral over: a personal workspace is then never composed on its event loop here.
216
+ graph: (() => {
217
+ try { return seams.buildGraph({ project, eligibility: fixedEligibility ?? eligibilityFor({ machine, project }), deferPersonalValidation: context.deferPersonalValidation === true }) } catch (error) {
218
+ if (error?.code === PERSONAL_VALIDATION_PENDING) pendingCorpus = error
219
+ throw error
220
+ }
221
+ })(),
222
+ profile: seams.profileFor({ project, workspaceId, audienceAllow: machine?.audienceAllow ?? [] }),
223
+ }
224
+ return corpus
225
+ }
226
+ // Whether the personal workspace's graph is pending on this call. Asked only where a graph would be built anyway.
227
+ const personalPending = () => {
228
+ try { currentCorpus(); return false } catch (error) { if (error?.code === PERSONAL_VALIDATION_PENDING) return true; if (isTyped(error)) return false; throw error }
229
+ }
207
230
  // true, false, or null when the canonical graph cannot be read now. The rule is the one source apply asks.
208
231
  const visible = options.isVisible ? (identity) => options.isVisible(identity, context) : ({ repoId, nodeId }) => {
209
232
  let built
@@ -223,6 +246,7 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
223
246
  return {
224
247
  ...context, recoveryOf, visible, clock, env: env ?? context.env ?? process.env,
225
248
  corpus: currentCorpus,
249
+ personalPending,
226
250
  objects: () => (objects ??= objectStore({ stateRoot: workspaceRoot, workspaceId, repositoryRoots, clock })),
227
251
  queue,
228
252
  managedRoots: () => [workspaceRoot, ...[...stores.values()].map((store) => store.vaultRoot)],
@@ -304,6 +328,8 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
304
328
  if (requireOpenEdit && editState !== 'open') return known ? refused(`edit-${editState}`) : outcome('skipped', null, { code: `edit-${editState}` })
305
329
  // The object store no longer says this is a proposed structural edit, so there is nothing to describe.
306
330
  if (item.observed === null) return refused('operation-not-proposed')
331
+ // A personal workspace whose graph is pending leaves the operation as it is: no attempt is spent on this tick.
332
+ if (workspace.deferPersonalValidation === true && personalOverlayRepoOf(workspace.project) !== null && workspace.personalPending()) return outcome('deferred', history?.head ?? null, { code: PERSONAL_VALIDATION_PENDING })
307
333
  const resolved = resolveProposalRoute({ project: workspace.project, workspaceId: workspace.workspaceId, identity: item, sourcePath: item.sourcePath, managedRoots: workspace.managedRoots(), isVisible: workspace.visible, isGitIgnored, env: env ?? workspace.env ?? process.env })
308
334
  if (!resolved.ok && !resolved.transient) return refused(resolved.code)
309
335
  // Recorded before anything is asked of the store.
@@ -432,7 +458,20 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
432
458
  async observe(context, { retryRefused = false } = {}) {
433
459
  const workspace = open(context)
434
460
  const edits = Array.isArray(context.edits) ? context.edits : []
435
- return { adapterId: PROPOSAL_ADAPTER_ID, observed: observation.run(workspace, { edits, retryRefused }) }
461
+ // A private note of a personal workspace lives in a generation the personal-workspace module owns: its edit is
462
+ // not observed, recorded or proposed, and stays held in the vault. Answered by name, with nothing written.
463
+ const overlayRepoId = personalOverlayRepoOf(workspace.project)
464
+ const isPrivate = (edit) => overlayRepoId !== null && edit?.identity?.repoId === overlayRepoId
465
+ const privateNotes = edits.filter((edit) => isPrivate(edit) && edit.closedAt === null)
466
+ .map((edit) => ({ editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId, status: 'refused', code: 'personal-overlay-not-proposed' }))
467
+ const shared = edits.filter((edit) => !isPrivate(edit))
468
+ // The graph is built only when an edit is looked at, as for any project. When the personal workspace's graph is
469
+ // pending, the edits it reached are not reported or recorded, and are offered again on the next observation.
470
+ const retry = retryRefused || observePendingAgain
471
+ const looked = observation.run(workspace, { edits: shared, retryRefused: retry })
472
+ const pending = looked.filter((item) => item.code === PERSONAL_VALIDATION_PENDING)
473
+ observePendingAgain = pending.length > 0
474
+ return { adapterId: PROPOSAL_ADAPTER_ID, observed: [...privateNotes, ...looked.filter((item) => item.code !== PERSONAL_VALIDATION_PENDING)], ...(pending.length > 0 ? { deferred: PERSONAL_VALIDATION_PENDING } : {}) }
436
475
  },
437
476
 
438
477
  // The tick. `context` is { project, workspaceRoot, workspaceId, repositoryRoots, edits }: the pending edit
@@ -126,6 +126,11 @@ export function createTickObservation(options = {}) {
126
126
  // The note exactly as it was published, which the lens needs to tell generated bytes from authored ones. The
127
127
  // publisher keeps no copy of a note it created, so the note is prepared again from the sources as they are now and
128
128
  // used only when it has the digest the manifest recorded; a retained object of that digest serves as well.
129
+ //
130
+ // A source replaced between the look at its path and its open (ELEAFCHANGED) is another program writing it, as it
131
+ // is to the publisher and to source apply: the preparation answers null, and the caller reads the source again and
132
+ // refuses stale-source when it changed. Only a preparation that succeeded is kept for its scope and generation: one
133
+ // failure tells of one moment and is never the answer for the other edits of that generation in the tick.
129
134
  function publishedNoteOf(workspace, { scope, manifest, noteEntry }, now) {
130
135
  const store = workspace.recoveryOf(scope.scopeId)
131
136
  const kept = retained(store, noteEntry.noteDigest)
@@ -134,7 +139,7 @@ export function createTickObservation(options = {}) {
134
139
  // Prepared in the vault layout of the edit's generation, once per generation.
135
140
  const preparedKey = `${scope.scopeId}\u0000${manifest.generationId}`
136
141
  if (!workspace.prepared.has(preparedKey)) {
137
- let files = []
142
+ let files
138
143
  try {
139
144
  const { graph, profile } = workspace.corpus()
140
145
  const snapshot = seams.captureSnapshot({ project: workspace.project, graph, workspaceId: workspace.workspaceId, index: new Map(), configDigest: manifest.ext?.[EXT]?.configDigest ?? `sha256:${'0'.repeat(64)}`, capturedAt: now })
@@ -143,7 +148,10 @@ export function createTickObservation(options = {}) {
143
148
  snapshot, profile, scope, persistentPathRegistry, priorManifest: manifest, existingSettings: null, clock: workspace.clock,
144
149
  vaultRootBytes: Buffer.byteLength(store.vaultRoot, 'utf8'), layout: manifestLayoutVersion(manifest),
145
150
  }).files
146
- } catch (error) { if (!isTyped(error) && !GONE.has(error?.code)) throw error }
151
+ } catch (error) {
152
+ if (!isTyped(error) && error?.code !== 'ELEAFCHANGED' && !GONE.has(error?.code)) throw error
153
+ return null
154
+ }
147
155
  workspace.prepared.set(preparedKey, files)
148
156
  }
149
157
  const file = workspace.prepared.get(preparedKey).find((item) => item.path === noteEntry.path && item.digest === noteEntry.noteDigest)