@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,385 @@
1
+ // Explicit selection, inventory and restore for a personal workspace's private
2
+ // home. Composition decides eligibility; this module records a person's choice,
3
+ // lists what the home retains, and restores authored inputs from an earlier
4
+ // generation without ever widening enrollment or bindings. It never deletes
5
+ // authored inputs, generations or records; it removes only its own temporary
6
+ // files. Every failure is a PersonalWorkspaceRefusal with a stable code and no path.
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { createHash, randomUUID } from 'node:crypto'
10
+ import { composePersonalWorkspace, loadPersonalManifest, loadPersonalOverlay, PersonalWorkspaceRefusal } from './index.mjs'
11
+
12
+ const MANIFEST_FILE = 'atelier.personal.json'
13
+ const OVERLAY_FILE = 'atelier.overlay.json'
14
+ const SELECTION_SCHEMA = 'atelier-personal-workspace-selection@v1'
15
+ const RESTORE_SCHEMA = 'atelier-personal-workspace-restore@v1'
16
+ const GENESIS = 'genesis'
17
+ const MAX_RECORD = 1024 * 1024
18
+ // The generation module bounds every generation file at eight times its input limit.
19
+ const MAX_GENERATION_FILE = 8 * 1024 * 1024
20
+ const MAX_RESTORE_RECORD = 4 * MAX_RECORD
21
+ const ID = /^[0-9a-f]{64}$/
22
+ // Every temporary name this module writes, including restore aside copies
23
+ // (.restore-<uuid>-aside-<key>.json).
24
+ const TEMPORARY = /^\.(selection|restore|validate)-[0-9a-f-]{36}(-[a-z]+)*\.json$/
25
+ const ASIDE = (key) => new RegExp(`^\\.restore-[0-9a-f-]{36}-aside-${key}\\.json$`)
26
+ const hash = (bytes) => createHash('sha256').update(bytes).digest('hex')
27
+ const sortObject = (v) => Array.isArray(v) ? v.map(sortObject) : v && typeof v === 'object'
28
+ ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortObject(v[k])])) : v
29
+ // The same canonical form the generation id is computed over.
30
+ const canonical = (v) => `${JSON.stringify(sortObject(v))}\n`
31
+ const refuse = (code, extra) => { throw Object.assign(new PersonalWorkspaceRefusal(code), extra ?? {}) }
32
+ const freeze = (v) => { if (v && typeof v === 'object') { Object.values(v).forEach(freeze); Object.freeze(v) } return v }
33
+ const restorePlans = new WeakSet()
34
+
35
+ // Maps any non-refusal failure to a stable code, so no raw error or path escapes.
36
+ function safe(fn, fallback) {
37
+ try { return fn() } catch (error) { if (error instanceof PersonalWorkspaceRefusal) throw error; refuse(fallback) }
38
+ }
39
+ function privateHome(personalHome) {
40
+ if (typeof personalHome !== 'string' || !path.isAbsolute(personalHome) || path.resolve(personalHome) !== personalHome) refuse('path-not-absolute')
41
+ if (process.platform === 'win32' || typeof process.getuid !== 'function') refuse('private-root-unverifiable')
42
+ let stat
43
+ try { stat = fs.lstatSync(personalHome) } catch { refuse('root-missing') }
44
+ if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(personalHome) !== personalHome) refuse('root-symlinked')
45
+ if (stat.uid !== process.getuid() || (stat.mode & 0o022) !== 0) refuse('not-private-location')
46
+ return personalHome
47
+ }
48
+ function readBytes(file, limit, tooLarge = 'malformed-input') {
49
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
50
+ try {
51
+ const stat = fs.fstatSync(fd)
52
+ if (!stat.isFile()) refuse('malformed-input')
53
+ if (stat.size > limit) refuse(tooLarge)
54
+ const bytes = fs.readFileSync(fd)
55
+ if (bytes.length > limit) refuse(tooLarge)
56
+ return bytes
57
+ } finally { fs.closeSync(fd) }
58
+ }
59
+ const bytesOrNull = (file, limit) => { try { return readBytes(file, limit) } catch (error) { if (error.code === 'ENOENT') return null; throw error } }
60
+ function syncDir(dir) { const fd = fs.openSync(dir, 'r'); try { fs.fsyncSync(fd) } finally { fs.closeSync(fd) } }
61
+ function checkPrivateDir(dir) {
62
+ const stat = fs.lstatSync(dir)
63
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 0o022) !== 0) refuse('not-private-location')
64
+ return dir
65
+ }
66
+ function privateDir(personalHome, name, create) {
67
+ const dir = path.join(personalHome, name)
68
+ if (create) {
69
+ try { fs.mkdirSync(dir, { mode: 0o700 }); syncDir(personalHome) } catch (error) { if (error.code !== 'EEXIST') throw error }
70
+ } else {
71
+ try { fs.lstatSync(dir) } catch (error) { if (error.code === 'ENOENT') return null; throw error }
72
+ }
73
+ return checkPrivateDir(dir)
74
+ }
75
+ function writeExclusive(file, content) {
76
+ const fd = fs.openSync(file, 'wx', 0o600)
77
+ try { fs.writeFileSync(fd, content); fs.fsyncSync(fd) }
78
+ catch (error) { fs.closeSync(fd); fs.rmSync(file, { force: true }); throw error }
79
+ fs.closeSync(fd)
80
+ }
81
+ // Writes complete bytes to a temporary name, then publishes them under `final`
82
+ // with link, which never replaces an existing name. A crash leaves only a
83
+ // temporary file, which readers ignore and the inventory lists.
84
+ function publishExclusive(dir, final, content, kind) {
85
+ const temporary = path.join(dir, `.${kind}-${randomUUID()}.json`)
86
+ writeExclusive(temporary, content)
87
+ try { fs.linkSync(temporary, path.join(dir, final)) } catch (error) { fs.rmSync(temporary, { force: true }); throw error }
88
+ // Published. A temporary that cannot be removed now stays listed by the inventory,
89
+ // and a failed directory sync does not turn a published record into a failure.
90
+ try { fs.unlinkSync(temporary) } catch {}
91
+ try { syncDir(dir) } catch {}
92
+ }
93
+ // An inventory entry for a file that may be missing, oversized, not regular or
94
+ // unreadable: it is listed with its size and without a digest rather than
95
+ // failing the whole inventory.
96
+ function inventoryEntry(file, limit) {
97
+ try {
98
+ const bytes = readBytes(file, limit)
99
+ return { bytes: bytes.length, digest: hash(bytes) }
100
+ } catch (error) {
101
+ if (error.code === 'ENOENT') return null
102
+ let size = null
103
+ try { size = fs.lstatSync(file).size } catch {}
104
+ return { bytes: size, digest: null }
105
+ }
106
+ }
107
+ const eligibility = (personalHome, generationId) => {
108
+ try { composePersonalWorkspace({ personalHome, generationId }); return { eligible: true, reason: null } }
109
+ catch (error) { if (error instanceof PersonalWorkspaceRefusal) return { eligible: false, reason: error.code }; throw error }
110
+ }
111
+ // Validates exact bytes with the module's own loader, through a private temporary copy.
112
+ function validateBytes(personalHome, bytes, kind) {
113
+ const temporary = path.join(personalHome, `.validate-${randomUUID()}.json`)
114
+ writeExclusive(temporary, bytes)
115
+ try { return (kind === 'manifest' ? loadPersonalManifest : loadPersonalOverlay)(temporary, { personalHome }) }
116
+ finally { fs.unlinkSync(temporary) }
117
+ }
118
+
119
+ // The selection history is an append-only hash chain of numbered records. It
120
+ // proves internal consistency only: an edited record whose successors were not
121
+ // re-linked, a gap, or a reordering refuses. Truncating, or consistently rewriting,
122
+ // any trailing run of records (up to the whole history) yields another valid chain
123
+ // and is detectable only by comparing the head with one the host observed earlier.
124
+ function readHistory(personalHome) {
125
+ const dir = privateDir(personalHome, 'selections', false)
126
+ if (!dir) return { records: [], files: [], head: GENESIS }
127
+ const names = fs.readdirSync(dir).filter((name) => !name.startsWith('.')).sort()
128
+ const records = [], files = []
129
+ let head = GENESIS
130
+ for (const [index, name] of names.entries()) {
131
+ if (name !== `${String(index + 1).padStart(6, '0')}.json`) refuse('selection-history-corrupt')
132
+ let bytes
133
+ try { bytes = readBytes(path.join(dir, name), MAX_RECORD, 'selection-history-corrupt') } catch { refuse('selection-history-corrupt') }
134
+ let record
135
+ try { record = JSON.parse(bytes) } catch { refuse('selection-history-corrupt') }
136
+ if (record?.schema !== SELECTION_SCHEMA || record.sequence !== index + 1 || record.previous !== head || !ID.test(record.generationId ?? '') ||
137
+ canonical(record) !== bytes.toString('utf8')) refuse('selection-history-corrupt')
138
+ records.push(record)
139
+ head = hash(bytes)
140
+ files.push({ name, bytes: bytes.length, digest: head })
141
+ }
142
+ return { records, files, head }
143
+ }
144
+ export function selectionConfirmDigest(options) {
145
+ return safe(() => {
146
+ const { generationId, previous } = options ?? {}
147
+ if (typeof generationId !== 'string' || typeof previous !== 'string') refuse('malformed-input')
148
+ return `sha256:${hash(canonical({ action: 'select', generationId, previous }))}`
149
+ }, 'malformed-input')
150
+ }
151
+
152
+ // Records the person's explicit choice of the currently eligible generation.
153
+ // The caller first reads the current head and shows the person what it selects.
154
+ export function selectPersonalGeneration(options) {
155
+ return safe(() => {
156
+ const { personalHome, generationId, confirm } = options ?? {}
157
+ privateHome(personalHome)
158
+ if (typeof generationId !== 'string' || !ID.test(generationId)) refuse('malformed-input')
159
+ const { records, head } = readHistory(personalHome)
160
+ if (confirm !== selectionConfirmDigest({ generationId, previous: head })) refuse('confirmation-mismatch')
161
+ const { eligible, reason } = eligibility(personalHome, generationId)
162
+ if (!eligible) refuse(reason)
163
+ const dir = privateDir(personalHome, 'selections', true)
164
+ const record = { schema: SELECTION_SCHEMA, sequence: records.length + 1, previous: head, generationId }
165
+ const content = canonical(record)
166
+ try { publishExclusive(dir, `${String(record.sequence).padStart(6, '0')}.json`, content, 'selection') }
167
+ catch (error) { if (error.code === 'EEXIST') refuse('selection-concurrent'); throw error }
168
+ return freeze({ generationId, sequence: record.sequence, head: hash(content) })
169
+ }, 'selection-write-failed')
170
+ }
171
+
172
+ // The current selection and whether it is still eligible now. A selection never
173
+ // keeps a generation eligible after its inputs, roots or enrollment change.
174
+ export function readPersonalSelection(options) {
175
+ return safe(() => {
176
+ const { personalHome } = options ?? {}
177
+ privateHome(personalHome)
178
+ const { records, head } = readHistory(personalHome)
179
+ const current = records.at(-1) ?? null
180
+ if (!current) return freeze({ selected: null, head, eligible: false, reason: 'nothing-selected' })
181
+ return freeze({ selected: current.generationId, sequence: current.sequence, head, ...eligibility(personalHome, current.generationId) })
182
+ }, 'personal-home-unavailable')
183
+ }
184
+
185
+ // Lists entries without failing the whole inventory: a concurrently removed entry
186
+ // is skipped; an unreadable, foreign or oversized one is reported without a digest.
187
+ function listFiles(dir, keep) {
188
+ if (!dir) return []
189
+ const listed = []
190
+ for (const name of fs.readdirSync(dir).filter(keep).sort()) {
191
+ try {
192
+ const bytes = readBytes(path.join(dir, name), MAX_RESTORE_RECORD, 'record-too-large')
193
+ listed.push({ name, bytes: bytes.length, digest: hash(bytes) })
194
+ } catch (error) {
195
+ if (error.code === 'ENOENT') continue
196
+ let size = null
197
+ try { size = fs.lstatSync(path.join(dir, name)).size } catch {}
198
+ listed.push({ name, bytes: size, digest: null, unreadable: true })
199
+ }
200
+ }
201
+ return listed
202
+ }
203
+ // A read-only inventory of what this module writes in the private home (authored
204
+ // inputs, generations, staging, selection and restore records, and its temporary
205
+ // files), for a person to review before any deletion they choose to confirm. It
206
+ // writes and deletes nothing.
207
+ export function inventoryPersonalHome(options) {
208
+ return safe(() => {
209
+ const { personalHome } = options ?? {}
210
+ privateHome(personalHome)
211
+ const authored = [MANIFEST_FILE, OVERLAY_FILE].map((name) => {
212
+ const entry = inventoryEntry(path.join(personalHome, name), MAX_RECORD)
213
+ return { name, bytes: entry?.bytes ?? null, digest: entry?.digest ?? null }
214
+ })
215
+ const root = privateDir(personalHome, 'generations', false)
216
+ const entries = root ? fs.readdirSync(root).sort() : []
217
+ const generations = entries.filter((name) => ID.test(name)).map((generationId) => {
218
+ const record = inventoryEntry(path.join(root, generationId, 'generation.json'), MAX_GENERATION_FILE)
219
+ return { generationId, recordDigest: record?.digest ?? null, ...eligibility(personalHome, generationId) }
220
+ })
221
+ const staging = entries.filter((name) => name.startsWith('.staging-'))
222
+ const selectionsDir = privateDir(personalHome, 'selections', false)
223
+ const restoresDir = privateDir(personalHome, 'restores', false)
224
+ let history
225
+ try { history = readHistory(personalHome) } catch (error) {
226
+ if (!(error instanceof PersonalWorkspaceRefusal)) throw error
227
+ history = { files: listFiles(selectionsDir, (name) => !name.startsWith('.')), head: null, corrupt: true }
228
+ }
229
+ const { files: selections, head } = history
230
+ const restores = listFiles(restoresDir, (name) => !name.startsWith('.'))
231
+ const temporary = [
232
+ ...listFiles(personalHome, (name) => TEMPORARY.test(name)).map((file) => ({ ...file, location: '.' })),
233
+ ...listFiles(selectionsDir, (name) => TEMPORARY.test(name)).map((file) => ({ ...file, location: 'selections' })),
234
+ ...listFiles(restoresDir, (name) => TEMPORARY.test(name)).map((file) => ({ ...file, location: 'restores' })),
235
+ ]
236
+ return freeze({ authored, generations, staging, selections: { head, records: selections, corrupt: history.corrupt === true }, restores, temporary })
237
+ }, 'personal-home-unavailable')
238
+ }
239
+
240
+ // A generation's recorded inputs, verified against its own id: the id is the
241
+ // digest of the canonical inputs and this private home, so altered or moved
242
+ // history cannot be restored.
243
+ function recordedInputs(personalHome, generationId) {
244
+ if (typeof generationId !== 'string' || !ID.test(generationId)) refuse('malformed-input')
245
+ const root = privateDir(personalHome, 'generations', false)
246
+ if (!root) refuse('generation-missing')
247
+ let entry
248
+ try { entry = fs.lstatSync(path.join(root, generationId)) } catch { refuse('generation-missing') }
249
+ if (!entry.isDirectory() || entry.isSymbolicLink()) refuse('generation-corrupt')
250
+ let bytes
251
+ try { bytes = readBytes(path.join(root, generationId, 'inputs.json'), MAX_GENERATION_FILE, 'generation-inputs-too-large') }
252
+ catch (error) { if (error instanceof PersonalWorkspaceRefusal) throw error; refuse('generation-corrupt') }
253
+ if (hash(canonical({ inputsDigest: hash(bytes), personalHome })) !== generationId) refuse('generation-corrupt')
254
+ let inputs
255
+ try { inputs = JSON.parse(bytes) } catch { refuse('generation-corrupt') }
256
+ if (!inputs?.manifest || !inputs?.overlay || canonical(inputs) !== bytes.toString('utf8')) refuse('generation-corrupt')
257
+ return inputs
258
+ }
259
+ // The current authored file, read once: its exact bytes, digest and validated value.
260
+ function currentAuthored(personalHome, name, kind) {
261
+ const bytes = bytesOrNull(path.join(personalHome, name), MAX_RECORD)
262
+ if (bytes === null) {
263
+ // A restore interrupted between moving the file aside and linking its
264
+ // replacement leaves the name absent and the aside listed by the inventory.
265
+ if (fs.readdirSync(personalHome).some((entry) => ASIDE(kind).test(entry))) refuse('restore-interrupted')
266
+ refuse('malformed-input')
267
+ }
268
+ return { bytes, digest: hash(bytes), value: validateBytes(personalHome, bytes, kind) }
269
+ }
270
+ // A restore may narrow enrollment and bindings, never widen them: every
271
+ // repository the target enrolls must be enrolled now with the same root and
272
+ // remote, and every binding the target declares must be declared now.
273
+ function wideningCheck(target, current) {
274
+ const now = new Map(current.repos.filter((r) => r.enrolled).map((r) => [r.repoId, r]))
275
+ const readmitted = [], changed = []
276
+ for (const repo of target.repos.filter((r) => r.enrolled)) {
277
+ const present = now.get(repo.repoId)
278
+ if (!present) readmitted.push(repo.repoId)
279
+ else if (present.root !== repo.root || present.remote !== repo.remote) changed.push(repo.repoId)
280
+ }
281
+ if (readmitted.length) refuse('rollback-readmits-repository', { repoIds: Object.freeze(readmitted.sort()) })
282
+ if (changed.length) refuse('rollback-identity-changed', { repoIds: Object.freeze(changed.sort()) })
283
+ const added = target.bindings.filter((binding) => !current.bindings.includes(binding))
284
+ if (added.length) refuse('rollback-readds-binding', { count: added.length })
285
+ }
286
+ const summaryOf = (target, current, targets) => ({
287
+ enrollmentRemoved: current.repos.filter((r) => r.enrolled && !target.repos.some((t) => t.enrolled && t.repoId === r.repoId)).map((r) => r.repoId).sort(),
288
+ bindingsRemoved: current.bindings.filter((b) => !target.bindings.includes(b)).length,
289
+ manifestChanged: targets.manifestChanged,
290
+ overlayChanged: targets.overlayChanged,
291
+ })
292
+ export const restoreConfirmDigest = (plan) => `sha256:${hash(canonical({ action: 'restore', generationId: plan.generationId, from: plan.from, to: plan.to }))}`
293
+
294
+ // Plans restoring the authored manifest and overlay recorded by an earlier
295
+ // generation. The target files are validated now. Re-admission or a re-added
296
+ // binding is the person's fresh edit, never a restore's side effect. Restored
297
+ // files are written in the generation's canonical form (sorted keys, no
298
+ // indentation), not the person's original formatting.
299
+ export function planPersonalRestore(options) {
300
+ return safe(() => {
301
+ const { personalHome, generationId } = options ?? {}
302
+ privateHome(personalHome)
303
+ const inputs = recordedInputs(personalHome, generationId)
304
+ validateBytes(personalHome, canonical(inputs.manifest), 'manifest')
305
+ validateBytes(personalHome, canonical(inputs.overlay), 'overlay')
306
+ const manifest = currentAuthored(personalHome, MANIFEST_FILE, 'manifest')
307
+ const overlay = currentAuthored(personalHome, OVERLAY_FILE, 'overlay')
308
+ wideningCheck(inputs.manifest, manifest.value)
309
+ const to = { manifest: hash(canonical(inputs.manifest)), overlay: hash(canonical(inputs.overlay)) }
310
+ const from = { manifest: manifest.digest, overlay: overlay.digest }
311
+ const fields = { generationId, from, to }
312
+ const plan = freeze({ ...fields, summary: summaryOf(inputs.manifest, manifest.value, { manifestChanged: from.manifest !== to.manifest, overlayChanged: from.overlay !== to.overlay }),
313
+ confirm: restoreConfirmDigest(fields) })
314
+ restorePlans.add(plan)
315
+ return plan
316
+ }, 'personal-home-unavailable')
317
+ }
318
+
319
+ // Replaces `file` with `staged` only if `file` still holds exactly `expected`:
320
+ // the current file is moved aside, its bytes are verified, and the staged file
321
+ // is linked into place, which fails if another writer recreated the name. On any
322
+ // mismatch or failure to read the moved file, it is put back; when its name was
323
+ // taken meanwhile it stays as a listed temporary file. Writers that replace or
324
+ // recreate the file by path therefore keep their bytes. While the swap is in
325
+ // progress the name is briefly absent.
326
+ function compareAndSwap(personalHome, file, staged, expected, key) {
327
+ const aside = path.join(personalHome, `.restore-${randomUUID()}-aside-${key}.json`)
328
+ fs.renameSync(file, aside)
329
+ const putBack = () => {
330
+ try { fs.linkSync(aside, file) } catch { return false }
331
+ try { fs.unlinkSync(aside) } catch {}
332
+ try { syncDir(personalHome) } catch {}
333
+ return true
334
+ }
335
+ let moved
336
+ try { moved = readBytes(aside, MAX_RECORD) } catch { putBack(); refuse('authored-input-changed') }
337
+ if (hash(moved) !== expected) { putBack(); refuse('authored-input-changed') }
338
+ try { fs.linkSync(staged, file) } catch (error) {
339
+ if (error.code === 'EEXIST') refuse('authored-input-changed')
340
+ putBack(); throw error
341
+ }
342
+ fs.unlinkSync(staged)
343
+ fs.unlinkSync(aside)
344
+ syncDir(personalHome)
345
+ }
346
+
347
+ // Restores a plan from planPersonalRestore. The current authored bytes are read
348
+ // once, validated, required to match the plan and re-checked for widening; those
349
+ // exact bytes are kept in an append-only restore record first. Both restored
350
+ // files are staged and validated before either is replaced, then each is
351
+ // replaced by compare-and-swap. A rerun completes an interrupted restore.
352
+ export function restorePersonalInputs(plan, options) {
353
+ return safe(() => {
354
+ const { personalHome, confirm } = options ?? {}
355
+ privateHome(personalHome)
356
+ if (!restorePlans.has(plan) || confirm !== plan.confirm || confirm !== restoreConfirmDigest(plan)) refuse('confirmation-mismatch')
357
+ const inputs = recordedInputs(personalHome, plan.generationId)
358
+ const targets = { manifest: [MANIFEST_FILE, canonical(inputs.manifest)], overlay: [OVERLAY_FILE, canonical(inputs.overlay)] }
359
+ const current = {}
360
+ for (const [key, [name, content]] of Object.entries(targets)) {
361
+ if (hash(content) !== plan.to[key]) refuse('generation-corrupt')
362
+ current[key] = currentAuthored(personalHome, name, key)
363
+ if (current[key].digest !== plan.from[key] && current[key].digest !== plan.to[key]) refuse('authored-input-changed')
364
+ }
365
+ wideningCheck(inputs.manifest, current.manifest.value)
366
+ const dir = privateDir(personalHome, 'restores', true)
367
+ const record = canonical({ schema: RESTORE_SCHEMA, generationId: plan.generationId, from: plan.from, to: plan.to,
368
+ replaced: { manifest: current.manifest.bytes.toString('base64'), overlay: current.overlay.bytes.toString('base64') } })
369
+ publishExclusive(dir, `${Date.now()}-${randomUUID()}.json`, record, 'restore')
370
+ const pending = Object.entries(targets).filter(([key]) => current[key].digest !== plan.to[key])
371
+ const staged = []
372
+ try {
373
+ for (const [key, [, content]] of pending) {
374
+ const file = path.join(personalHome, `.restore-${randomUUID()}-${key}.json`)
375
+ writeExclusive(file, content)
376
+ staged.push([key, file])
377
+ ;(key === 'manifest' ? loadPersonalManifest : loadPersonalOverlay)(file, { personalHome })
378
+ }
379
+ for (const [key, file] of staged) compareAndSwap(personalHome, path.join(personalHome, targets[key][0]), file, current[key].digest, key)
380
+ } finally {
381
+ for (const [, file] of staged) fs.rmSync(file, { force: true })
382
+ }
383
+ return freeze({ generationId: plan.generationId, ...eligibility(personalHome, plan.generationId) })
384
+ }, 'restore-write-failed')
385
+ }
@@ -556,6 +556,14 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
556
556
  if (setup.profile != null && !['single-repo', 'private-domain', 'shared-project', 'multi-repo', 'monorepo', 'control-workspace'].includes(setup.profile)) {
557
557
  errors.push('setup.profile is invalid')
558
558
  }
559
+ // The census reads these as path patterns relative to the project config's
560
+ // folder (docs/knowledge-graph.md), under the contract's pathString rules.
561
+ for (const key of ['include', 'exclude']) {
562
+ errors.push(...stringPathErrors(setup[key], `setup.${key}`))
563
+ if (typeof setup[key] === 'string' && (setup[key].includes('\u0000') || /^[a-z][a-z0-9+.-]*:/.test(setup[key]) || path.isAbsolute(setup[key]))) {
564
+ errors.push(`setup.${key} must be a path pattern relative to the project config folder`)
565
+ }
566
+ }
559
567
  if (alignment.appRepo != null && !firstString(alignment.appRepo)) errors.push('alignment.appRepo must be a non-empty string')
560
568
 
561
569
  const repoNames = new Set()
@@ -149,7 +149,9 @@ export function createVerifiedFileSequence({ directory, initial, apply, ignoreFi
149
149
  if (next.length < names.length) throw new Error('history shortened; preserve and inspect');
150
150
  const ids = next.map(n => identity(path.join(directory, n)));
151
151
  const reuse = names.every((n, i) => n === next[i] && identities[i] === ids[i]);
152
- let result = reuse ? value : initial();
152
+ // Replay into a copy: a refused read must not leave the verified value
153
+ // half-applied, or every later read of this reader would be refused.
154
+ let result = reuse ? structuredClone(value) : initial();
153
155
  for (let i = reuse ? names.length : 0; i < next.length; i++) {
154
156
  const file = path.join(directory, next[i]);
155
157
  result = apply(readRegularTextNoFollow(file), result, i + 1, next[i]);
@@ -188,7 +188,7 @@ export function classifyManagedPath(relativePath) {
188
188
  // moves up a level. Any other failure (a directory that cannot be searched,
189
189
  // a link loop) is thrown: a lexical path standing in for a real one would
190
190
  // weaken the alias check exactly where it matters.
191
- function realLocation(target, realpath) {
191
+ export function realLocation(target, realpath = fs.realpathSync.native) {
192
192
  const missing = []
193
193
  let current = path.resolve(target)
194
194
  for (;;) {
@@ -9,10 +9,11 @@ import { canonicalJson, compareText, isoTime } from '../../../runtime/obsidian/d
9
9
  import { readObsidianEnablement } from '../../../runtime/obsidian/enablement.mjs'
10
10
  import { ObsidianMaintenanceRefusal } from '../../../runtime/obsidian/errors.mjs'
11
11
  import { protectedRoots, readInstalledApplyPolicy, readLocalPointer, readMachineSettings, resolveDataRoot, workspaceStateRoot } from '../../../runtime/obsidian/machine-settings.mjs'
12
- import { DEFAULT_ELIGIBILITY, createProductionSeams } from '../../../runtime/obsidian/pipeline.mjs'
12
+ import { createProductionSeams, eligibilityFor } from '../../../runtime/obsidian/pipeline.mjs'
13
13
  import { OPEN_EDIT_STATES, createMaintenanceStateStore } from '../../../runtime/obsidian/state-store.mjs'
14
14
  import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, manifestLayoutVersion } from '../contracts.mjs'
15
15
  import { readMarkdownLens } from '../materialize/byte-lens.mjs'
16
+ import { personalOverlayRepoOf } from '../personal-workspace.mjs'
16
17
  import { exchangeFiles, probeExchange } from '../publication/exchange.mjs'
17
18
  import { createJournal, newJournalId } from '../recovery/journal.mjs'
18
19
  import { recheckDisplacedFiles } from '../recovery/late-writer.mjs'
@@ -91,6 +92,7 @@ export const SOURCE_APPLY_REFUSALS = Object.freeze([
91
92
  'source-inside-managed-root', 'source-inside-git-directory', 'source-git-ignored', 'source-ignore-state-unknown', 'sibling-edit-unobservable', 'lease-held', 'object-conflicted',
92
93
  'stale-source', 'edit-not-applicable', 'change-outside-authored-body', 'no-source-change', 'exchange-unavailable', 'apply-volume-mismatch', 'batch-bound-reached',
93
94
  'concurrent-source-writer', 'source-changed-during-apply', 'interrupted-before-exchange', 'apply-interrupted-needs-person', 'recovery-state-unreadable', 'apply-outcome-unknown', 'source-changed-after-apply',
95
+ 'personal-overlay-proposals-only',
94
96
  ])
95
97
 
96
98
  // Refusals that say the source or the object is contested, not that this machine or this request cannot apply.
@@ -137,6 +139,10 @@ function readNoFollow(file, { withMode = false } = {}) {
137
139
  }
138
140
 
139
141
  const NOT_A_FILE = new Set(['EISDIR', 'EINVAL', 'ENOTDIR', 'EMLINK'])
142
+ // How often a file another program replaces by rename is read again: the pointer to the current manifest, as the
143
+ // publisher reads a note, and the published note prepared again. One that keeps changing is a typed refusal.
144
+ const READ_ATTEMPTS = 5
145
+ const PREPARE_ATTEMPTS = 3
140
146
  const UNREADABLE = new Set(['EACCES', 'EPERM', 'EIO'])
141
147
 
142
148
  // The digest of the file, null when nothing is there, 'unreadable' when what is there is not a regular file: whatever
@@ -265,7 +271,7 @@ export function createSourceApplyForOracleTests(primitives = SOURCE_APPLY_PRIMIT
265
271
  // ones the workspace has.
266
272
  return function createSourceApply(context = {}) {
267
273
  const {
268
- loadProject, dataRoot, env = process.env, platform = process.platform, clock = () => new Date(), eligibility = DEFAULT_ELIGIBILITY,
274
+ loadProject, dataRoot, env = process.env, platform = process.platform, clock = () => new Date(), eligibility: fixedEligibility = null,
269
275
  quietPeriodMs = DEFAULT_APPLY_QUIET_PERIOD_MS, recheckWindowMs = DEFAULT_APPLY_RECHECK_WINDOW_MS, exchangeOptions = {},
270
276
  crash = () => {}, beforeExchange = async () => {}, leasePid, extraManagedRoots = [], manualBatchBound = MAX_MANUAL_BATCH_SIZE, objectStore = openObjectStore,
271
277
  } = context
@@ -303,26 +309,41 @@ export function createSourceApplyForOracleTests(primitives = SOURCE_APPLY_PRIMIT
303
309
  return workspace
304
310
  }
305
311
 
306
- // The canonical graph as it is now, and the corpus profile of this machine. Built once per call.
312
+ // The canonical graph as it is now, and the corpus profile of this machine. Built once per call, with the notes the
313
+ // machine settings let into a view, exactly as the engine builds it (eligibilityFor), unless a test fixes them.
307
314
  function currentCorpus(workspace) {
308
- // The graph reads every enrolled source. A file this process may not read is a refusal that names no file.
315
+ // The graph reads every enrolled source. A file this process may not read is a refusal that names no file. So is
316
+ // one another program removes or replaces while the graph is built (a save by delete and create, a checkout):
317
+ // the graph cannot be built from what is there at this moment, and the cause says which of the two it was.
309
318
  try {
310
319
  workspace.corpus ??= {
311
- graph: seams.buildGraph({ project: workspace.project, eligibility }),
320
+ graph: seams.buildGraph({ project: workspace.project, eligibility: fixedEligibility ?? eligibilityFor({ machine: workspace.machine, project: workspace.project }) }),
312
321
  profile: seams.profileFor({ project: workspace.project, workspaceId: workspace.workspaceId, audienceAllow: workspace.machine.audienceAllow }),
313
322
  }
314
323
  } catch (error) {
315
- if (isTyped(error) || !UNREADABLE.has(error?.code)) throw error
324
+ if (isTyped(error) || !(UNREADABLE.has(error?.code) || GONE.has(error?.code) || error?.code === 'ELEAFCHANGED')) throw error
316
325
  refuse('corpus-unreadable', { cause: error.code })
317
326
  }
318
327
  return workspace.corpus
319
328
  }
320
329
 
330
+ // The pointer to the current manifest is replaced by rename as the last step of every publication, so a
331
+ // publication by the engine can replace it between the look at its path and its open (ELEAFCHANGED). Every rename
332
+ // leaves a complete pointer, so it is read again, as the publisher reads a note; one that keeps changing refuses.
333
+ function currentManifestOf(store) {
334
+ for (let attempt = 1; ; attempt += 1) {
335
+ try { return store.readCurrentManifest() } catch (error) {
336
+ if (error?.code !== 'ELEAFCHANGED') throw error
337
+ if (attempt >= READ_ATTEMPTS) refuse('manifest-unavailable', { cause: 'changed-while-reading' })
338
+ }
339
+ }
340
+ }
341
+
321
342
  // The immutable manifest of one generation of one view. The current one when it is that generation; otherwise
322
343
  // the stored file, whose name carries the generation and the head of its digest.
323
344
  function manifestOf(workspace, scopeId, generationId) {
324
345
  const store = workspace.storeOf(scopeId)
325
- const current = store.readCurrentManifest()
346
+ const current = currentManifestOf(store)
326
347
  if (current?.generationId === generationId) return current
327
348
  const directory = path.join(workspace.workspaceRoot, 'state', 'manifests', segment(scopeId))
328
349
  let names = []
@@ -341,21 +362,35 @@ export function createSourceApplyForOracleTests(primitives = SOURCE_APPLY_PRIMIT
341
362
  // publisher keeps no copy of a note it created, so the note is prepared again from the sources as they are now,
342
363
  // in the vault layout of that generation, and used only when it has the digest the manifest recorded; a retained
343
364
  // object of that digest serves as well.
365
+ //
366
+ // A source replaced between the look at its path and its open (ELEAFCHANGED) is another program writing it, as it
367
+ // is to the publisher. A rename leaves a complete file, so the note is prepared again from what is there now: a
368
+ // source saved with the same bytes, or another source of the scope replaced, then prepares the note as published,
369
+ // and a source with other bytes prepares another note, which the caller tells from the digest and refuses
370
+ // stale-source. A preparation that cannot be carried out answers null, and the caller reads the source once more
371
+ // to tell which typed refusal that is. Only a preparation that succeeded is kept for its scope and generation: a
372
+ // failure tells of one moment, not of the generation, and is never the answer to a later edit.
344
373
  function publishedNoteOf(workspace, { scope, manifest, noteEntry }) {
345
374
  const store = workspace.storeOf(scope.scopeId)
346
375
  try { return store.readObject(noteEntry.noteDigest) } catch (error) { if (error.code !== 'ENOENT' && error.code !== 'recovery-object-corrupt') throw error }
347
376
  workspace.prepared ??= new Map()
348
377
  const preparedKey = `${scope.scopeId}\u0000${manifest.generationId}`
349
378
  if (!workspace.prepared.has(preparedKey)) {
350
- let files = []
351
- try {
352
- const { graph, profile } = currentCorpus(workspace)
353
- const snapshot = seams.captureSnapshot({ project: workspace.project, graph, workspaceId: workspace.workspaceId, index: new Map(), configDigest: manifest.ext?.[EXT]?.configDigest ?? `sha256:${'0'.repeat(64)}`, capturedAt: isoTime(clock) })
354
- files = seams.prepareView({
355
- snapshot, profile, scope, persistentPathRegistry: workspace.stateStore.readPathRegistry(), priorManifest: manifest, existingSettings: null, clock,
356
- vaultRootBytes: Buffer.byteLength(store.vaultRoot, 'utf8'), layout: manifestLayoutVersion(manifest),
357
- }).files
358
- } catch (error) { if (!isTyped(error) && !GONE.has(error?.code) && !UNREADABLE.has(error?.code)) throw error }
379
+ let files
380
+ for (let attempt = 1; ; attempt += 1) {
381
+ try {
382
+ const { graph, profile } = currentCorpus(workspace)
383
+ const snapshot = seams.captureSnapshot({ project: workspace.project, graph, workspaceId: workspace.workspaceId, index: new Map(), configDigest: manifest.ext?.[EXT]?.configDigest ?? `sha256:${'0'.repeat(64)}`, capturedAt: isoTime(clock) })
384
+ files = seams.prepareView({
385
+ snapshot, profile, scope, persistentPathRegistry: workspace.stateStore.readPathRegistry(), priorManifest: manifest, existingSettings: null, clock,
386
+ vaultRootBytes: Buffer.byteLength(store.vaultRoot, 'utf8'), layout: manifestLayoutVersion(manifest),
387
+ }).files
388
+ break
389
+ } catch (error) {
390
+ if (!isTyped(error) && error?.code !== 'ELEAFCHANGED' && !GONE.has(error?.code) && !UNREADABLE.has(error?.code)) throw error
391
+ if (error?.code !== 'ELEAFCHANGED' || attempt >= PREPARE_ATTEMPTS) return null
392
+ }
393
+ }
359
394
  workspace.prepared.set(preparedKey, files)
360
395
  }
361
396
  const file = workspace.prepared.get(preparedKey).find((item) => item.path === noteEntry.path && item.digest === noteEntry.noteDigest)
@@ -533,6 +568,10 @@ export function createSourceApplyForOracleTests(primitives = SOURCE_APPLY_PRIMIT
533
568
  const decideWith = (editClass, attempts) => rules.decide({ request, workspace: { workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId }, graph, profile, object: identity, editClass, attempts })
534
569
  const visibility = decideWith('body-replacement', [])
535
570
  if (!visibility.allowed && visibility.code === 'object-not-visible') refuse('object-not-visible')
571
+ // A private note of a personal workspace is authored in its overlay, and its file belongs to a generation the
572
+ // personal-workspace module owns: an edit to it is never written there. It is not proposed yet either; it stays
573
+ // held in the vault.
574
+ if (identity.repoId === personalOverlayRepoOf(workspace.project)) refuse('personal-overlay-proposals-only')
536
575
 
537
576
  const resolved = resolveEdit(workspace, edit)
538
577
  const key = resolved.idempotencyKey