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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (560) hide show
  1. package/CHANGELOG.md +1005 -0
  2. package/README.md +149 -9
  3. package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
  4. package/contracts/atelier-architecture.v1.schema.json +258 -0
  5. package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
  6. package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
  7. package/contracts/atelier-build.v1.schema.json +1076 -0
  8. package/contracts/atelier-capability.v1.schema.json +1252 -0
  9. package/contracts/atelier-coordination.v1.schema.json +1050 -0
  10. package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
  11. package/contracts/atelier-harness.v1.schema.json +234 -0
  12. package/contracts/atelier-ingestion.v1.schema.json +806 -0
  13. package/contracts/atelier-inquiry.v1.schema.json +1772 -0
  14. package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
  15. package/contracts/atelier-interaction.v1.schema.json +553 -0
  16. package/contracts/atelier-judgment.v1.schema.json +690 -0
  17. package/contracts/atelier-knowledge.v1.schema.json +1512 -0
  18. package/contracts/atelier-learning.v1.schema.json +1095 -0
  19. package/contracts/atelier-migration.v3.schema.json +64 -0
  20. package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
  21. package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
  22. package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
  23. package/contracts/atelier-template-adoption.v1.schema.json +223 -0
  24. package/contracts/atelier-template-profile.v1.schema.json +1168 -0
  25. package/contracts/atelier-trackable.v1.schema.json +781 -0
  26. package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
  27. package/contracts/compat-baseline.json +1 -1
  28. package/docs/architecture.md +89 -0
  29. package/docs/assurance-controls.md +4 -3
  30. package/docs/blocks/capability-stewardship.md +17 -0
  31. package/docs/blocks/claims.md +16 -3
  32. package/docs/blocks/inquiry-harnesses.md +19 -0
  33. package/docs/blocks/learning-harnesses.md +17 -0
  34. package/docs/capability-stewardship.md +269 -0
  35. package/docs/coordination.md +51 -0
  36. package/docs/decision-practices.md +111 -0
  37. package/docs/decisions.md +29 -0
  38. package/docs/evidence-navigation.md +237 -0
  39. package/docs/exact-upgrades.md +3 -2
  40. package/docs/ingestion.md +181 -0
  41. package/docs/inquiry-harnesses.md +211 -0
  42. package/docs/install.md +80 -3
  43. package/docs/intake-and-guides.md +5 -0
  44. package/docs/integration-contract-decisions.md +24 -2
  45. package/docs/interaction-and-reflection.md +72 -0
  46. package/docs/knowledge-graph.md +20 -0
  47. package/docs/knowledge-health/api.md +30 -0
  48. package/docs/knowledge-health/assessment/api.md +96 -0
  49. package/docs/knowledge-health/assessment/capabilities.md +40 -0
  50. package/docs/knowledge-health/assessment/evaluation.md +27 -0
  51. package/docs/knowledge-health/assessment/facilitator.md +30 -0
  52. package/docs/knowledge-health/assessment/quickstart.md +61 -0
  53. package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
  54. package/docs/knowledge-health/devday/facilitator.md +16 -0
  55. package/docs/knowledge-health/devday/participant-response.md +22 -0
  56. package/docs/knowledge-health/devday/public-default.md +107 -0
  57. package/docs/knowledge-health/devday/quickstart.md +18 -0
  58. package/docs/knowledge-health/devday/support.md +22 -0
  59. package/docs/knowledge-health/exercise.md +17 -0
  60. package/docs/knowledge-health/facilitator.md +20 -0
  61. package/docs/knowledge-health/quickstart.md +94 -0
  62. package/docs/knowledge-health/readme.md +15 -0
  63. package/docs/knowledge-health/support.md +34 -0
  64. package/docs/knowledge-setup.md +248 -0
  65. package/docs/knowledge-workspace.md +264 -0
  66. package/docs/learning-harnesses.md +238 -0
  67. package/docs/learning.md +215 -0
  68. package/docs/local-services.md +242 -26
  69. package/docs/obsidian-contract.md +878 -36
  70. package/docs/obsidian-plugin.md +619 -0
  71. package/docs/obsidian.md +809 -64
  72. package/docs/optional-ingestion.md +83 -0
  73. package/docs/personal-workspace.md +257 -0
  74. package/docs/practical-judgment.md +92 -0
  75. package/docs/progressive-ingestion-acceptance.md +89 -0
  76. package/docs/progressive-ingestion.md +235 -0
  77. package/docs/project-command-inventory.md +1 -0
  78. package/docs/release-engineering.md +56 -1
  79. package/docs/repo-boundary-guard.md +94 -0
  80. package/docs/research-starter-quickstart.md +66 -0
  81. package/docs/research-starter.md +114 -0
  82. package/docs/semantic-ingestion.md +198 -0
  83. package/docs/semantic-operation.md +317 -0
  84. package/docs/skill-steward.md +6 -0
  85. package/docs/template-profile.md +164 -0
  86. package/docs/template-upgrades.md +114 -0
  87. package/docs/trackables.md +111 -0
  88. package/docs/upgrade.md +68 -3
  89. package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
  90. package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
  91. package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
  92. package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
  93. package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
  94. package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
  95. package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
  96. package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
  97. package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
  98. package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
  99. package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
  100. package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
  101. package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
  102. package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
  103. package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
  104. package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
  105. package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
  106. package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
  107. package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
  108. package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
  109. package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
  110. package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
  111. package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
  112. package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
  113. package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
  114. package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
  115. package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
  116. package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
  117. package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
  118. package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
  119. package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
  120. package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
  121. package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
  122. package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
  123. package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
  124. package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
  125. package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
  126. package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
  127. package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
  128. package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
  129. package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
  130. package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
  131. package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
  132. package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
  133. package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
  134. package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
  135. package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
  136. package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
  137. package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
  138. package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
  139. package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
  140. package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
  141. package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
  142. package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
  143. package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
  144. package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
  145. package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
  146. package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
  147. package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
  148. package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
  149. package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
  150. package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
  151. package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
  152. package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
  153. package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
  154. package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
  155. package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
  156. package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
  157. package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
  158. package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
  159. package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
  160. package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
  161. package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
  162. package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
  163. package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
  164. package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
  165. package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
  166. package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
  167. package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
  168. package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
  169. package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
  170. package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
  171. package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
  172. package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
  173. package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
  174. package/fixtures/atelier-interaction/act/valid/document.json +24 -0
  175. package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
  176. package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
  177. package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
  178. package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
  179. package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
  180. package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
  181. package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
  182. package/fixtures/atelier-judgment/case/valid/document.json +75 -0
  183. package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
  184. package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
  185. package/fixtures/atelier-judgment/native-decision.md +3 -0
  186. package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
  187. package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
  188. package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
  189. package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
  190. package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
  191. package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
  192. package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
  193. package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
  194. package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
  195. package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
  196. package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
  197. package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
  198. package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
  199. package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
  200. package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
  201. package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
  202. package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
  203. package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
  204. package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
  205. package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
  206. package/fixtures/atelier-learning/invalid/authority.json +14 -0
  207. package/fixtures/atelier-learning/valid/capture.json +13 -0
  208. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
  209. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
  210. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
  211. package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
  212. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
  213. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
  214. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
  215. package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
  216. package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
  217. package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
  218. package/fixtures/atelier-trackable/request/valid/document.json +22 -0
  219. package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
  220. package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
  221. package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
  222. package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
  223. package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
  224. package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
  225. package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
  226. package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
  227. package/fixtures/capability-packages/adoption.example.json +70 -0
  228. package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
  229. package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
  230. package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
  231. package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
  232. package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
  233. package/fixtures/capability-packages/research/capability-package.json +88 -0
  234. package/fixtures/capability-packages/research/capability-release.json +134 -0
  235. package/fixtures/capability-packages/research/evaluation.md +15 -0
  236. package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
  237. package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
  238. package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
  239. package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
  240. package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
  241. package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
  242. package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
  243. package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
  244. package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
  245. package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
  246. package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
  247. package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
  248. package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
  249. package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
  250. package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
  251. package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
  252. package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
  253. package/fixtures/evidence-navigation/valid/claim.json +36 -0
  254. package/fixtures/evidence-navigation/valid/host.json +16 -0
  255. package/fixtures/evidence-navigation/valid/profile.json +16 -0
  256. package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
  257. package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
  258. package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
  259. package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
  260. package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
  261. package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
  262. package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
  263. package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
  264. package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
  265. package/fixtures/harnesses/learning-cycle.json +780 -0
  266. package/fixtures/inquiry/workshop.json +675 -0
  267. package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
  268. package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
  269. package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
  270. package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
  271. package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
  272. package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
  273. package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
  274. package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
  275. package/fixtures/knowledge-health/calibration-suite.json +26 -0
  276. package/fixtures/knowledge-health/calibration-trial.json +37 -0
  277. package/fixtures/knowledge-health/corrected/checklist.md +18 -0
  278. package/fixtures/knowledge-health/devday/participant-record.json +19 -0
  279. package/fixtures/knowledge-health/expected-summary.json +27 -0
  280. package/fixtures/knowledge-health/workshop/README.md +20 -0
  281. package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
  282. package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
  283. package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
  284. package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
  285. package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
  286. package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
  287. package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
  288. package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
  289. package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
  290. package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
  291. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
  292. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
  293. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
  294. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
  295. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
  296. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
  297. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
  298. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
  299. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
  300. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
  301. package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
  302. package/fixtures/obsidian/edits/workspace.json +1 -1
  303. package/fixtures/obsidian/materialization/expected-full.json +21 -21
  304. package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
  305. package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
  306. package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
  307. package/fixtures/personal-workspace/overlay.json +24 -0
  308. package/fixtures/templates/local-library.v1.json +75 -0
  309. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
  310. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
  311. package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
  312. package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
  313. package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
  314. package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
  315. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
  316. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
  317. package/package.json +64 -6
  318. package/plugins/obsidian/main.js +444 -0
  319. package/plugins/obsidian/manifest.json +9 -0
  320. package/plugins/obsidian/styles.css +22 -0
  321. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  322. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  323. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  324. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  325. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  326. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  327. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  328. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  329. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  330. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  331. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  332. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  333. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  334. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  335. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  336. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  337. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  338. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  339. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  340. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  341. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  342. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  343. package/src/architecture/catalog.mjs +355 -0
  344. package/src/architecture/index.mjs +93 -0
  345. package/src/boundary/content-rules.mjs +4 -2
  346. package/src/boundary/delegation-command.mjs +158 -0
  347. package/src/boundary/delegation.mjs +396 -0
  348. package/src/boundary/policy.mjs +52 -10
  349. package/src/build/git.mjs +18 -0
  350. package/src/build/index.mjs +3 -0
  351. package/src/build/ledger.mjs +88 -0
  352. package/src/capabilities/adoption.mjs +381 -0
  353. package/src/capabilities/evidence.mjs +140 -0
  354. package/src/capabilities/files.mjs +124 -0
  355. package/src/capabilities/index.mjs +4 -0
  356. package/src/capabilities/instructions.mjs +250 -0
  357. package/src/capabilities/package.mjs +136 -0
  358. package/src/cli/command-failure.mjs +36 -0
  359. package/src/cli/execute-command.mjs +16 -2
  360. package/src/cli/run.mjs +65 -9
  361. package/src/coauthor/store.mjs +43 -2
  362. package/src/collaboration/proposals.mjs +189 -0
  363. package/src/commands/architecture.mjs +27 -0
  364. package/src/commands/boundary.mjs +3 -0
  365. package/src/commands/capability.mjs +73 -0
  366. package/src/commands/enroll.mjs +3 -0
  367. package/src/commands/harness.mjs +79 -0
  368. package/src/commands/ingest.mjs +39 -0
  369. package/src/commands/init.mjs +4 -1
  370. package/src/commands/inquiry.mjs +66 -0
  371. package/src/commands/knowledge-semantic.mjs +128 -0
  372. package/src/commands/knowledge.mjs +119 -0
  373. package/src/commands/learn.mjs +61 -0
  374. package/src/commands/obsidian.mjs +832 -62
  375. package/src/commands/practice.mjs +24 -0
  376. package/src/commands/setup.mjs +22 -2
  377. package/src/commands/trackable.mjs +19 -0
  378. package/src/contracts/corpus.mjs +36 -0
  379. package/src/coordination/index.mjs +110 -0
  380. package/src/egress/forbidden-egress.mjs +2 -0
  381. package/src/evidence-navigation/contracts.mjs +60 -0
  382. package/src/evidence-navigation/evaluate.mjs +69 -0
  383. package/src/evidence-navigation/generation.mjs +22 -0
  384. package/src/evidence-navigation/index.mjs +2 -0
  385. package/src/evidence-navigation/local.mjs +167 -0
  386. package/src/evidence-navigation/schema.generated.mjs +604 -0
  387. package/src/evidence-navigation/types.mjs +12 -0
  388. package/src/export/atelier-export-contract.mjs +35 -8
  389. package/src/graph/enroll.mjs +248 -0
  390. package/src/graph/graph.mjs +17 -1
  391. package/src/graph/knowledge-graph.mjs +58 -8
  392. package/src/harnesses/contracts.mjs +41 -0
  393. package/src/harnesses/exchange.mjs +154 -0
  394. package/src/harnesses/feedback.mjs +16 -0
  395. package/src/harnesses/history.mjs +37 -0
  396. package/src/harnesses/index.mjs +4 -0
  397. package/src/harnesses/store.mjs +29 -0
  398. package/src/ingestion/contracts.mjs +41 -0
  399. package/src/ingestion/evaluation.mjs +119 -0
  400. package/src/ingestion/optional/composition/index.mjs +171 -0
  401. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  402. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  403. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  404. package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
  405. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  406. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  407. package/src/ingestion/optional/index.mjs +9 -0
  408. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  409. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  410. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  411. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  412. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  413. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  414. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  415. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  416. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  417. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  418. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  419. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  420. package/src/ingestion/processors.mjs +218 -0
  421. package/src/ingestion/semantic.mjs +203 -0
  422. package/src/ingestion/store.mjs +372 -0
  423. package/src/inquiry/beliefs.mjs +32 -0
  424. package/src/inquiry/contracts.mjs +23 -0
  425. package/src/inquiry/index.mjs +5 -0
  426. package/src/inquiry/ledger.mjs +135 -0
  427. package/src/inquiry/lenses.mjs +15 -0
  428. package/src/inquiry/projection.mjs +77 -0
  429. package/src/inquiry/store.mjs +35 -0
  430. package/src/intake/read-scope.mjs +12 -0
  431. package/src/intake/store.mjs +69 -6
  432. package/src/interaction/index.mjs +91 -0
  433. package/src/judgment/index.mjs +82 -0
  434. package/src/judgment/practice-evaluation.mjs +70 -0
  435. package/src/judgment/practice.mjs +144 -0
  436. package/src/knowledge/context.mjs +217 -0
  437. package/src/knowledge/index.mjs +5 -0
  438. package/src/knowledge/ingestion.mjs +79 -0
  439. package/src/knowledge/intake.mjs +33 -0
  440. package/src/knowledge/ledger.mjs +110 -0
  441. package/src/knowledge/participatory/guidance.mjs +36 -0
  442. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  443. package/src/knowledge/participatory/index.mjs +2 -0
  444. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  445. package/src/knowledge/plan.mjs +67 -0
  446. package/src/knowledge/projection.mjs +43 -0
  447. package/src/knowledge/semantic-operation.mjs +567 -0
  448. package/src/knowledge/sessions.mjs +467 -0
  449. package/src/knowledge/workspace.mjs +295 -0
  450. package/src/knowledge-health/assessment.mjs +165 -0
  451. package/src/knowledge-health/capabilities.mjs +350 -0
  452. package/src/knowledge-health/context.mjs +62 -0
  453. package/src/knowledge-health/example.mjs +47 -0
  454. package/src/knowledge-health/index.mjs +10 -0
  455. package/src/knowledge-health/owner-join.mjs +88 -0
  456. package/src/knowledge-health/proof/common.mjs +24 -0
  457. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  458. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  459. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  460. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  461. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  462. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  463. package/src/knowledge-health/rule-identity.mjs +44 -0
  464. package/src/knowledge-health/workshop-composition.mjs +19 -0
  465. package/src/knowledge-health/workshop-profile.mjs +176 -0
  466. package/src/knowledge-health/workshop.mjs +99 -0
  467. package/src/learning/adapters.mjs +68 -0
  468. package/src/learning/assistance.mjs +127 -0
  469. package/src/learning/contracts.mjs +65 -0
  470. package/src/learning/store.mjs +272 -0
  471. package/src/personal-workspace/index.mjs +391 -0
  472. package/src/personal-workspace/selection.mjs +385 -0
  473. package/src/project/config.mjs +8 -0
  474. package/src/project/durable-state.mjs +3 -1
  475. package/src/project/file-class.mjs +1 -1
  476. package/src/project/private-state.mjs +10 -1
  477. package/src/projection/obsidian/contracts.mjs +224 -18
  478. package/src/projection/obsidian/edits/apply.mjs +63 -19
  479. package/src/projection/obsidian/edits/regions.mjs +137 -42
  480. package/src/projection/obsidian/materialize/index.mjs +6 -3
  481. package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
  482. package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
  483. package/src/projection/obsidian/materialize/redaction.mjs +327 -0
  484. package/src/projection/obsidian/materialize/settings.mjs +113 -34
  485. package/src/projection/obsidian/personal-workspace.mjs +436 -0
  486. package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
  487. package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
  488. package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
  489. package/src/projection/obsidian/proposals/adapter.mjs +47 -8
  490. package/src/projection/obsidian/proposals/observation.mjs +20 -7
  491. package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
  492. package/src/projection/obsidian/publication/index.mjs +2 -1
  493. package/src/projection/obsidian/publication/publisher.mjs +312 -36
  494. package/src/projection/obsidian/publication/transport.mjs +63 -8
  495. package/src/projection/obsidian/publication/vault-list.mjs +200 -0
  496. package/src/projection/obsidian/recovery/index.mjs +4 -1
  497. package/src/projection/obsidian/recovery/restart.mjs +3 -1
  498. package/src/projection/obsidian/recovery/store.mjs +168 -8
  499. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  500. package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
  501. package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
  502. package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
  503. package/src/readiness/readiness.mjs +10 -3
  504. package/src/reflection/index.mjs +56 -0
  505. package/src/runtime/obsidian/app-capability.mjs +104 -12
  506. package/src/runtime/obsidian/app-production-seams.mjs +161 -42
  507. package/src/runtime/obsidian/app-registration.mjs +313 -0
  508. package/src/runtime/obsidian/app-restart.mjs +154 -0
  509. package/src/runtime/obsidian/engine.mjs +288 -29
  510. package/src/runtime/obsidian/extension-points.mjs +1 -1
  511. package/src/runtime/obsidian/index.mjs +15 -8
  512. package/src/runtime/obsidian/launch-plan.mjs +91 -0
  513. package/src/runtime/obsidian/lifecycle.mjs +193 -22
  514. package/src/runtime/obsidian/login-item.mjs +332 -0
  515. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  516. package/src/runtime/obsidian/opening.mjs +369 -63
  517. package/src/runtime/obsidian/pending-edits.mjs +10 -0
  518. package/src/runtime/obsidian/pipeline.mjs +71 -3
  519. package/src/runtime/obsidian/plugin-channel.mjs +289 -0
  520. package/src/runtime/obsidian/plugin-choice.mjs +182 -0
  521. package/src/runtime/obsidian/plugin-drift.mjs +124 -0
  522. package/src/runtime/obsidian/plugin-presence.mjs +61 -0
  523. package/src/runtime/obsidian/process-identity.mjs +20 -10
  524. package/src/runtime/obsidian/project-views.mjs +282 -0
  525. package/src/runtime/obsidian/questions.mjs +48 -0
  526. package/src/runtime/obsidian/release-watch.mjs +56 -0
  527. package/src/runtime/obsidian/service-main.mjs +113 -17
  528. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  529. package/src/runtime/obsidian/service-managers.mjs +275 -0
  530. package/src/runtime/obsidian/service-record.mjs +105 -5
  531. package/src/runtime/obsidian/service-server.mjs +50 -12
  532. package/src/runtime/obsidian/service.mjs +134 -7
  533. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  534. package/src/runtime/obsidian/state-store.mjs +13 -1
  535. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  536. package/src/runtime/obsidian/vault-location.mjs +300 -0
  537. package/src/runtime/obsidian/view-counts.mjs +34 -0
  538. package/src/server/local-sidecar.mjs +193 -7
  539. package/src/server/server.mjs +45 -6
  540. package/src/template-bindings/project.mjs +92 -0
  541. package/src/templates/profile.mjs +225 -0
  542. package/src/trackables/domain.mjs +175 -0
  543. package/src/trackables/store.mjs +114 -0
  544. package/src/ui/knowledge-page.mjs +1388 -0
  545. package/src/upgrade/explanation.mjs +1 -1
  546. package/src/upgrade/template-participant.mjs +171 -0
  547. package/src/upgrade/transaction.mjs +72 -25
  548. package/src/upgrade/upgrade.mjs +2 -1
  549. package/templates/external-project-workspace/pack-lifecycle.json +6 -1
  550. package/templates/knowledge-workspace/.gitattributes +2 -0
  551. package/templates/knowledge-workspace/AGENTS.md +27 -0
  552. package/templates/knowledge-workspace/README.md +44 -0
  553. package/templates/knowledge-workspace/atelier.project.json +22 -0
  554. package/templates/knowledge-workspace/gitignore +6 -0
  555. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  556. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  557. package/templates/knowledge-workspace/records/forecast.md +22 -0
  558. package/templates/knowledge-workspace/records/inspection.md +25 -0
  559. package/templates/knowledge-workspace/records/telescope.md +24 -0
  560. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -1,37 +1,414 @@
1
- import { identitySuffix, notePath } from '../contracts.mjs'
1
+ import { createHash } from 'node:crypto'
2
+ import {
3
+ FOLDER_NAME_BYTES, MIN_NAME_BYTES, QUALIFIER_STEM_BYTES, collisionKey, cutName, extensionOf, fileNameParts, fileStemOf, folderNameOf, identitySuffix,
4
+ isReadableVaultPath, joinFileName, notePath, noteNameOf, qualifierIdOf, vaultName,
5
+ } from '../contracts.mjs'
2
6
  import { refuse } from './byte-lens.mjs'
3
7
 
4
- // The persistent path registry: one readable note path per canonical identity,
5
- // allocated once per workspace and reused by every scope and generation. A
6
- // path, once allocated, never changes because a source title changed.
8
+ export { collisionKey }
9
+
10
+ // Vault paths of layout 2 (see "Vault layout" in docs/obsidian-contract.md):
11
+ //
12
+ // <repository folder>/<source directory>/<title>.md a Markdown note
13
+ // <repository folder>/<source directory>/<file name> a wrapped file
14
+ // <repository folder>/<source directory>/<file name>.md its note
15
+ // <repository folder>/<source directory>/<file name> an embedded asset
16
+ //
17
+ // Each view allocates among its own notes and embedded assets only, seeded
18
+ // from what its own prior generation published. A path stays while its note
19
+ // is in the view; a note that leaves the view releases its path there; a
20
+ // note of another view, or one that was never in this one, never causes a
21
+ // qualifier. One identity may have different paths in different views.
22
+ // Paths the caller names as occupied (files held for an edit, which stay in
23
+ // the vault) are never taken.
24
+ //
25
+ // A source that does not fit is laid out anyway, and reported: a folder chain
26
+ // too long for the path budget keeps a readable prefix and a short stable id,
27
+ // and a source folder that meets a file of the same name is qualified with
28
+ // one. Each is a diagnostic naming the note.
7
29
  //
8
- // The registry is workspace-wide, not view-wide: preparing any view allocates
9
- // for every visible node of the workspace and returns the whole grown registry
10
- // to the caller, which holds it as machine-private trusted state. It therefore
11
- // names visible nodes outside the view's selection. It never includes a
12
- // withheld node: allocation runs over the visible nodes only. A view's
13
- // manifest and notes carry only the paths of the notes the view holds.
30
+ // The persistent path registry holds each view's last allocation, for a
31
+ // selection resolved without the view's generation: { schema, workspaceId,
32
+ // layout: 2, entries: [], assets: [], views: { <scopeId>: { entries, assets }
33
+ // } }, entries { repoId, nodeId, path[, attachment] }, assets { repoId,
34
+ // assetPath, path }. The top-level `entries` and `assets` stay empty; they
35
+ // held the workspace-wide allocation of an earlier revision. A registry
36
+ // without `layout: 2` is a layout 1 registry and holds no view.
14
37
 
15
38
  export const PATH_REGISTRY_SCHEMA = 'atelier-obsidian-path-registry/v1'
16
-
17
- const NOTE_PATH = /^notes\/([^/\\\u0000]+)--([0-9a-f]{12,64})\.md$/
18
- const MAX_COMPONENT_BYTES = 255
19
- const MAX_SUFFIX = 64
20
- // Bytes left for the readable title once the longest suffix, the separator and
21
- // the extension are reserved, so lengthening a suffix never overflows a name.
22
- const TITLE_BYTE_BUDGET = MAX_COMPONENT_BYTES - '--'.length - MAX_SUFFIX - '.md'.length
39
+ export const PATH_REGISTRY_LAYOUT = 2
23
40
  export const DEFAULT_MAX_FULL_PATH_BYTES = 1024
41
+ const MAX_COMPONENT_BYTES = 255
42
+ const MAX_QUALIFIER_ID = 16
43
+ // What a folder chain leaves for the name after it, beyond the separator,
44
+ // the shortest title and the extensions: nothing at first, and room for both
45
+ // qualifiers at their longest when the name had to be qualified.
46
+ const QUALIFIED_NAME_BYTES = (QUALIFIER_STEM_BYTES + 3) + (MAX_QUALIFIER_ID + 3)
47
+ // The folders layout 1 kept every note and file in. An upgrade retires their
48
+ // files but leaves the folders, so a repository folder that differs from one
49
+ // only in case or normalization, which a case-insensitive file system would
50
+ // merge with it, is told apart; a repository named exactly `notes` uses it.
51
+ const LAYOUT_1_FOLDERS = ['notes', 'attachments']
24
52
 
25
53
  const compare = (left, right) => (left < right ? -1 : left > right ? 1 : 0)
26
54
  export const identityKey = (repoId, nodeId) => `${repoId}\u0000${nodeId}`
55
+ const assetKey = (repoId, assetPath) => `${repoId}\u0000${assetPath}`
56
+ const bytesOf = (value) => Buffer.byteLength(value, 'utf8')
57
+ const baseOf = (filePath) => filePath.slice(filePath.lastIndexOf('/') + 1)
58
+ const byIdentity = (left, right) => compare(left.repoId, right.repoId) || compare(left.nodeId, right.nodeId)
59
+ const byAsset = (left, right) => compare(left.repoId, right.repoId) || compare(left.assetPath, right.assetPath)
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // The registry document
63
+ // ---------------------------------------------------------------------------
27
64
 
28
- // Two paths that a case-insensitive or normalization-insensitive filesystem
29
- // would treat as one share a collision key.
30
- export function collisionKey(value) {
31
- return value.normalize('NFKC').toUpperCase().toLowerCase().normalize('NFKC')
65
+ export function emptyPathRegistry(workspaceId) {
66
+ return { schema: PATH_REGISTRY_SCHEMA, workspaceId, layout: PATH_REGISTRY_LAYOUT, entries: [], assets: [], views: {} }
32
67
  }
33
68
 
34
- export function titleWithinBudget(title, budget = TITLE_BYTE_BUDGET) {
69
+ const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
70
+
71
+ // The views of a registry document, or {} for none (no registry, a layout 1
72
+ // registry, or one written before views). A document of another shape or
73
+ // another workspace refuses.
74
+ export function viewsOfRegistry(registry, workspaceId) {
75
+ if (registry === undefined || registry === null) return {}
76
+ if (registry.schema !== PATH_REGISTRY_SCHEMA || !Array.isArray(registry.entries)) refuse('invalid-path-registry', 'path registry has an unknown shape')
77
+ if (registry.workspaceId !== workspaceId) refuse('invalid-path-registry', 'path registry belongs to another workspace')
78
+ if (registry.layout !== PATH_REGISTRY_LAYOUT || registry.views === undefined) return {}
79
+ if (!isPlainObject(registry.views)) refuse('invalid-path-registry', 'path registry has an unknown shape')
80
+ for (const section of Object.values(registry.views)) {
81
+ if (!isPlainObject(section) || !Array.isArray(section.entries) || !Array.isArray(section.assets)) refuse('invalid-path-registry', 'path registry has an unknown shape')
82
+ }
83
+ return registry.views
84
+ }
85
+
86
+ // The registry document with one view's allocation replaced. With `keep`, the
87
+ // sections of views it does not name (views no longer maintained) are dropped;
88
+ // this view's own section is always kept.
89
+ export function withViewSection(registry, { workspaceId, scopeId, section, keep = null }) {
90
+ const kept = Object.entries(viewsOfRegistry(registry, workspaceId)).filter(([id]) => keep === null || keep.includes(id))
91
+ return { ...emptyPathRegistry(workspaceId), views: { ...Object.fromEntries(kept), [scopeId]: section } }
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Allocation of one view
96
+ // ---------------------------------------------------------------------------
97
+
98
+ // Everything allocated so far: files and folders by collision key, so a
99
+ // newcomer is compared the way a case- and normalization-insensitive file
100
+ // system compares, and the one spelling every folder has.
101
+ function newState() {
102
+ return { byIdentity: new Map(), byAsset: new Map(), files: new Map(), folders: new Map(), repoFolders: new Map(), repoFolderOwners: new Map() }
103
+ }
104
+
105
+ function claimFolders(state, filePath) {
106
+ const parts = filePath.split('/').slice(0, -1)
107
+ for (let index = 1; index <= parts.length; index += 1) {
108
+ const folder = parts.slice(0, index).join('/')
109
+ const key = collisionKey(folder)
110
+ const spelled = state.folders.get(key)
111
+ if (spelled === undefined) {
112
+ if (state.files.has(key)) refuse('path-collision', 'a folder would take the name of an allocated file')
113
+ state.folders.set(key, folder)
114
+ } else if (spelled !== folder) {
115
+ refuse('invalid-path-registry', 'the published paths spell one folder two ways')
116
+ }
117
+ }
118
+ }
119
+
120
+ function claimFile(state, filePath, owner) {
121
+ const key = collisionKey(filePath)
122
+ if (state.files.has(key) || state.folders.has(key)) refuse('path-collision', 'the published paths give one path to more than one file')
123
+ claimFolders(state, filePath)
124
+ state.files.set(key, owner)
125
+ }
126
+
127
+ function claimRepositoryFolder(state, repoId, folder) {
128
+ const known = state.repoFolders.get(repoId)
129
+ if (known !== undefined) {
130
+ if (known !== folder) refuse('invalid-path-registry', 'the published paths give one repository two folders')
131
+ return
132
+ }
133
+ if (state.repoFolderOwners.has(collisionKey(folder))) refuse('invalid-path-registry', 'the published paths give two repositories one folder')
134
+ state.repoFolders.set(repoId, folder)
135
+ state.repoFolderOwners.set(collisionKey(folder), repoId)
136
+ }
137
+
138
+ // Whether `filePath` could be claimed as spelled, without claiming it.
139
+ function claimable(state, filePath) {
140
+ const key = collisionKey(filePath)
141
+ if (state.files.has(key) || state.folders.has(key)) return false
142
+ const parts = filePath.split('/').slice(0, -1)
143
+ for (let index = 1; index <= parts.length; index += 1) {
144
+ const folder = parts.slice(0, index).join('/')
145
+ const spelled = state.folders.get(collisionKey(folder))
146
+ if (spelled === undefined ? state.files.has(collisionKey(folder)) : spelled !== folder) return false
147
+ }
148
+ return true
149
+ }
150
+
151
+ // How many bytes the longest of `paths` is over a limit: 255 bytes a name, and
152
+ // `maxFullPathBytes` with the vault root and its separator.
153
+ function overflowOf(paths, { vaultRootBytes, maxFullPathBytes }) {
154
+ return Math.max(0, ...paths.map((filePath) => Math.max(bytesOf(baseOf(filePath)) - MAX_COMPONENT_BYTES, vaultRootBytes + 1 + bytesOf(filePath) - maxFullPathBytes)))
155
+ }
156
+
157
+ // What a view's prior generation published, taken as it is for every note and
158
+ // asset still in the view. A path that no longer fits the limits (a vault root
159
+ // that grew) is allocated again. `published.notes` are { repoId, nodeId, path[,
160
+ // attachment] }, `published.assets` { repoId, assetPath, path }.
161
+ function seed(state, published, { nodeKeys, assetKeys, limits }) {
162
+ for (const note of [...published.notes].sort(byIdentity)) {
163
+ const wellFormed = typeof note?.repoId === 'string' && note.repoId !== '' && typeof note.nodeId === 'string' && note.nodeId !== ''
164
+ && isReadableVaultPath(note.path) && note.path.endsWith('.md') && note.path.includes('/')
165
+ && (note.attachment === undefined || (isReadableVaultPath(note.attachment) && note.path === `${note.attachment}.md`))
166
+ if (!wellFormed) refuse('invalid-path-registry', 'a published path is malformed')
167
+ const key = identityKey(note.repoId, note.nodeId)
168
+ if (state.byIdentity.has(key)) refuse('duplicate-identity', 'the published paths repeat a canonical identity')
169
+ const paths = [note.path, ...(note.attachment === undefined ? [] : [note.attachment])]
170
+ if (!nodeKeys.has(key) || overflowOf(paths, limits) > 0) continue
171
+ claimRepositoryFolder(state, note.repoId, note.path.split('/')[0])
172
+ for (const filePath of paths) claimFile(state, filePath, key)
173
+ state.byIdentity.set(key, { path: note.path, ...(note.attachment === undefined ? {} : { attachment: note.attachment }) })
174
+ }
175
+ for (const asset of [...published.assets].sort(byAsset)) {
176
+ const wellFormed = typeof asset?.repoId === 'string' && asset.repoId !== '' && typeof asset.assetPath === 'string' && asset.assetPath !== ''
177
+ && isReadableVaultPath(asset.path) && asset.path.includes('/')
178
+ if (!wellFormed) refuse('invalid-path-registry', 'a published path is malformed')
179
+ const key = assetKey(asset.repoId, asset.assetPath)
180
+ if (state.byAsset.has(key)) refuse('duplicate-identity', 'the published paths repeat an asset')
181
+ if (!assetKeys.has(key) || overflowOf([asset.path], limits) > 0) continue
182
+ claimRepositoryFolder(state, asset.repoId, asset.path.split('/')[0])
183
+ claimFile(state, asset.path, `asset\u0000${key}`)
184
+ state.byAsset.set(key, asset.path)
185
+ }
186
+ }
187
+
188
+ // The folder of a repository: its identity made safe, or, when that name is
189
+ // taken (by another repository of the view, a file, or a folder spelled
190
+ // another way, such as the `notes` folder of a held layout 1 file), the same
191
+ // with a short stable id.
192
+ function repositoryFolder(state, repoId) {
193
+ const known = state.repoFolders.get(repoId)
194
+ if (known !== undefined) return known
195
+ const plain = folderNameOf(repoId)
196
+ const hex = createHash('sha256').update(repoId).digest('hex')
197
+ for (let length = 0; length <= MAX_QUALIFIER_ID; length += length === 0 ? 6 : 2) {
198
+ const candidate = length === 0 ? plain : `${plain} (${hex.slice(0, length)})`
199
+ const key = collisionKey(candidate)
200
+ if (!state.repoFolderOwners.has(key) && !state.files.has(key) && (state.folders.get(key) ?? candidate) === candidate) return candidate
201
+ }
202
+ return refuse('path-collision', 'unable to allocate a distinct repository folder')
203
+ }
204
+
205
+ // The short stable id of a source folder: hexadecimal characters of the
206
+ // SHA-256 of the repository identity and the folder's source path.
207
+ const folderIdOf = (repoId, sourceFolder, length = 6) => createHash('sha256').update(`${repoId}\u0000${sourceFolder}`).digest('hex').slice(0, length)
208
+
209
+ // The folder a source directory mirrors to. Each segment is the source
210
+ // folder's name made safe, spelled as a folder already allocated under the
211
+ // same parent is spelled when the two differ only in case or normalization; a
212
+ // segment that meets an allocated file carries the id of its source folder. A
213
+ // chain that leaves less than `reserve` bytes for the name keeps as many
214
+ // leading folders as fit, and its last kept folder, cut, carries the id of the
215
+ // whole source directory; when not even that fits, the note sits directly in
216
+ // its repository's folder.
217
+ function mirroredFolder(state, { repoFolder, repoId, sourcePath, limits, reserve }) {
218
+ const sourceSegments = sourcePath.split('/').slice(0, -1)
219
+ const sourceOf = (count) => sourceSegments.slice(0, count).join('/')
220
+ // One segment under `folder`: its spelling, qualified when it meets a file.
221
+ const under = (folder, segment, source) => {
222
+ let candidate = `${folder}/${segment}`
223
+ let qualified = false
224
+ for (let length = 6; state.files.has(collisionKey(candidate)); length += 2) {
225
+ if (length > MAX_QUALIFIER_ID) refuse('path-collision', 'unable to allocate a distinct folder')
226
+ qualified = true
227
+ candidate = `${folder}/${cutName(segment, FOLDER_NAME_BYTES - (length + 3))} (${folderIdOf(repoId, source, length)})`
228
+ }
229
+ return { folder: state.folders.get(collisionKey(candidate)) ?? candidate, qualified }
230
+ }
231
+ const budget = limits.maxFullPathBytes - limits.vaultRootBytes - 1 - reserve
232
+ let folder = repoFolder
233
+ let qualified = false
234
+ const walked = [repoFolder]
235
+ sourceSegments.forEach((segment, index) => {
236
+ const next = under(folder, folderNameOf(segment), sourceOf(index + 1))
237
+ folder = next.folder
238
+ qualified ||= next.qualified
239
+ walked.push(folder)
240
+ })
241
+ if (bytesOf(folder) <= budget) return { folder, shortened: false, qualified }
242
+ // Too long: the longest walked prefix that leaves room for a cut folder carrying the id of the whole directory.
243
+ const suffix = ` (${folderIdOf(repoId, sourceOf(sourceSegments.length))})`
244
+ for (let count = sourceSegments.length - 1; count >= 0; count -= 1) {
245
+ const room = Math.min(budget - bytesOf(walked[count]) - 1, FOLDER_NAME_BYTES) - bytesOf(suffix)
246
+ const head = room > 0 ? cutName(folderNameOf(sourceSegments[count]), room) : ''
247
+ if (head === '' && count > 0) continue
248
+ const tail = under(walked[count], `${head}${suffix}`.trim(), sourceOf(sourceSegments.length))
249
+ if (bytesOf(tail.folder) <= budget) return { folder: tail.folder, shortened: true, qualified: qualified || tail.qualified }
250
+ }
251
+ if (bytesOf(repoFolder) <= budget) return { folder: repoFolder, shortened: true, qualified: false }
252
+ return refuse('path-too-long', 'the vault root leaves no room for a name in this repository folder')
253
+ }
254
+
255
+ // Fits a file name into the limits by cutting the title part `base` further,
256
+ // never below MIN_NAME_BYTES. `build(base)` returns the path(s) the name makes.
257
+ function fitted(base, build, limits) {
258
+ const paths = build(base)
259
+ const overflow = overflowOf(paths, limits)
260
+ if (overflow === 0) return paths
261
+ const budget = bytesOf(base) - overflow
262
+ if (budget < MIN_NAME_BYTES) refuse('path-too-long', 'an allocated path exceeds the supported path length')
263
+ const shorter = build(cutName(base, budget))
264
+ if (overflowOf(shorter, limits) > 0) refuse('path-too-long', 'an allocated path exceeds the supported path length')
265
+ return shorter
266
+ }
267
+
268
+ const isFree = (state, paths) => paths.every((filePath) => {
269
+ const key = collisionKey(filePath)
270
+ return !state.files.has(key) && !state.folders.has(key)
271
+ })
272
+
273
+ // The names a note tries in order: its title, then qualified by its source
274
+ // file stem, then by a short stable id lengthened until it is free.
275
+ function noteCandidates(node) {
276
+ const base = noteNameOf(node)
277
+ const stem = vaultName(fileStemOf(node.path), QUALIFIER_STEM_BYTES)
278
+ const names = [(title) => title]
279
+ if (stem !== '') names.push((title) => `${title} (${stem})`)
280
+ for (let length = 6; length <= MAX_QUALIFIER_ID; length += 2) {
281
+ const id = qualifierIdOf(node.repo, node.id, length)
282
+ names.push(stem === '' ? (title) => `${title} (${id})` : (title) => `${title} (${stem}) (${id})`)
283
+ }
284
+ return { base, names }
285
+ }
286
+
287
+ // A file keeps its own name; a collision qualifies its stem with a short id.
288
+ function fileCandidates(sourcePath, repoId, identity) {
289
+ const { stem, extension } = fileNameParts(sourcePath)
290
+ const names = [(base) => joinFileName({ stem: base, extension })]
291
+ for (let length = 6; length <= MAX_QUALIFIER_ID; length += 2) {
292
+ const id = qualifierIdOf(repoId, identity, length)
293
+ names.push((base) => joinFileName({ stem: `${base} (${id})`, extension }))
294
+ }
295
+ return { base: stem, names }
296
+ }
297
+
298
+ function allocate(state, { folder, candidates, build, limits }) {
299
+ for (const name of candidates.names) {
300
+ const paths = fitted(candidates.base, (base) => build(`${folder}/${name(base)}`), limits)
301
+ if (isFree(state, paths)) return paths
302
+ }
303
+ return refuse('path-collision', 'unable to allocate a distinct vault path')
304
+ }
305
+
306
+ // The paths of one file (a note, a wrapped file and its note, or an asset),
307
+ // allocated fresh and claimed, and what was degraded to lay it out. The folder
308
+ // leaves room for the shortest name first; a name that then has to be
309
+ // qualified and no longer fits is laid out again under a shorter folder.
310
+ function place(state, { repoId, sourcePath, owner, candidates, build, nameBytes, limits }) {
311
+ const repoFolder = repositoryFolder(state, repoId)
312
+ let placed = null
313
+ for (const reserve of [nameBytes, nameBytes + QUALIFIED_NAME_BYTES]) {
314
+ const { folder, shortened, qualified } = mirroredFolder(state, { repoFolder, repoId, sourcePath, limits, reserve })
315
+ try {
316
+ placed = { paths: allocate(state, { folder, candidates, build, limits }), shortened, qualified }
317
+ break
318
+ } catch (error) {
319
+ if (error?.code !== 'path-too-long' || reserve !== nameBytes) throw error
320
+ }
321
+ }
322
+ claimRepositoryFolder(state, repoId, repoFolder)
323
+ for (const filePath of placed.paths) claimFile(state, filePath, owner)
324
+ return { paths: placed.paths, degraded: [...(placed.shortened ? ['folder-shortened'] : []), ...(placed.qualified ? ['folder-qualified'] : [])] }
325
+ }
326
+
327
+ // Allocates the paths of one view: `nodes` are the view's canonical nodes ({
328
+ // repo, id, path, title, extension }), `assets` the assets its notes embed ({
329
+ // repo, id, path }), `published` what the view's prior generation published (or
330
+ // null), `occupied` vault paths that must stay free (files held for an edit).
331
+ // Published paths come first, then new nodes and new assets, each in canonical
332
+ // identity order, so the result depends on the inputs alone and never on the
333
+ // order they are listed in. Returns the view's registry section, the lookups,
334
+ // and a diagnostic for every note or asset whose folder was degraded.
335
+ export function allocateViewPaths({ published = null, nodes, assets = [], occupied = [], vaultRootBytes = 0, maxFullPathBytes = DEFAULT_MAX_FULL_PATH_BYTES }) {
336
+ const state = newState()
337
+ const limits = { vaultRootBytes, maxFullPathBytes }
338
+ const sortedNodes = [...nodes].sort((left, right) => compare(left.repo, right.repo) || compare(left.id, right.id))
339
+ const nodeKeys = new Set()
340
+ for (const node of sortedNodes) {
341
+ const key = identityKey(node.repo, node.id)
342
+ if (nodeKeys.has(key)) refuse('duplicate-identity', 'more than one node carries the same canonical identity')
343
+ nodeKeys.add(key)
344
+ }
345
+ const sortedAssets = [...new Map(assets.map((asset) => [assetKey(asset.repo, asset.path), asset])).values()].sort((left, right) => compare(left.repo, right.repo) || compare(left.path, right.path))
346
+ const assetKeys = new Set(sortedAssets.map((asset) => assetKey(asset.repo, asset.path)))
347
+ seed(state, { notes: published?.notes ?? published?.entries ?? [], assets: published?.assets ?? [] }, { nodeKeys, assetKeys, limits })
348
+ for (const filePath of [...new Set(occupied)].sort()) {
349
+ if (typeof filePath !== 'string' || !isReadableVaultPath(filePath) || state.files.has(collisionKey(filePath)) || !claimable(state, filePath)) continue
350
+ claimFile(state, filePath, 'occupied')
351
+ }
352
+ for (const folder of LAYOUT_1_FOLDERS) {
353
+ const key = collisionKey(folder)
354
+ if (!state.folders.has(key) && !state.files.has(key)) state.folders.set(key, folder)
355
+ }
356
+ const diagnostics = []
357
+ for (const node of sortedNodes) {
358
+ const key = identityKey(node.repo, node.id)
359
+ if (state.byIdentity.has(key)) continue
360
+ const wrapped = extensionOf(node) !== 'md'
361
+ const { paths, degraded } = place(state, {
362
+ repoId: node.repo, sourcePath: node.path, owner: key, limits,
363
+ candidates: wrapped ? fileCandidates(node.path, node.repo, node.id) : noteCandidates(node),
364
+ build: wrapped ? (base) => [base, `${base}.md`] : (base) => [`${base}.md`],
365
+ nameBytes: 1 + MIN_NAME_BYTES + (wrapped ? bytesOf(joinFileName({ stem: '', extension: fileNameParts(node.path).extension })) : 0) + '.md'.length,
366
+ })
367
+ // A wrapped file's paths are [file, note]; a Markdown note's [note].
368
+ const allocated = wrapped ? { path: paths[1], attachment: paths[0] } : { path: paths[0] }
369
+ state.byIdentity.set(key, allocated)
370
+ for (const code of degraded) diagnostics.push({ code, repoId: node.repo, nodeId: node.id, notePath: allocated.path })
371
+ }
372
+ for (const asset of sortedAssets) {
373
+ const key = assetKey(asset.repo, asset.path)
374
+ if (state.byAsset.has(key)) continue
375
+ const { paths, degraded } = place(state, {
376
+ repoId: asset.repo, sourcePath: asset.path, owner: `asset\u0000${key}`, limits,
377
+ candidates: fileCandidates(asset.path, asset.repo, asset.id ?? `${asset.repo}:asset:${asset.path}`), build: (base) => [base],
378
+ nameBytes: 1 + MIN_NAME_BYTES + bytesOf(joinFileName({ stem: '', extension: fileNameParts(asset.path).extension })),
379
+ })
380
+ state.byAsset.set(key, paths[0])
381
+ for (const code of degraded) diagnostics.push({ code, repoId: asset.repo, assetPath: asset.path, filePath: paths[0] })
382
+ }
383
+
384
+ const entries = [...state.byIdentity].map(([key, allocated]) => {
385
+ const [repoId, nodeId] = key.split('\u0000')
386
+ return { repoId, nodeId, path: allocated.path, ...(allocated.attachment === undefined ? {} : { attachment: allocated.attachment }) }
387
+ }).sort(byIdentity)
388
+ const assetEntries = [...state.byAsset].map(([key, allocatedPath]) => {
389
+ const [repoId, assetPath] = key.split('\u0000')
390
+ return { repoId, assetPath, path: allocatedPath }
391
+ }).sort(byAsset)
392
+ return {
393
+ section: { entries, assets: assetEntries },
394
+ pathOf: (repoId, nodeId) => state.byIdentity.get(identityKey(repoId, nodeId))?.path ?? null,
395
+ attachmentOf: (repoId, nodeId) => state.byIdentity.get(identityKey(repoId, nodeId))?.attachment ?? null,
396
+ assetPathOf: (repoId, assetPathValue) => state.byAsset.get(assetKey(repoId, assetPathValue)) ?? null,
397
+ diagnostics,
398
+ }
399
+ }
400
+
401
+ // ---------------------------------------------------------------------------
402
+ // Layout 1
403
+ // ---------------------------------------------------------------------------
404
+
405
+ const LEGACY_NOTE_PATH = /^notes\/([^/\\\u0000]+)--([0-9a-f]{12,64})\.md$/
406
+ const LEGACY_SUFFIX = 64
407
+ // Bytes left for the readable title once the longest suffix, the separator and
408
+ // the extension are reserved, so lengthening a suffix never overflows a name.
409
+ const LEGACY_TITLE_BYTE_BUDGET = MAX_COMPONENT_BYTES - '--'.length - LEGACY_SUFFIX - '.md'.length
410
+
411
+ export function titleWithinBudget(title, budget = LEGACY_TITLE_BYTE_BUDGET) {
35
412
  let kept = ''
36
413
  let bytes = 0
37
414
  for (const character of String(title ?? '').normalize('NFC')) {
@@ -42,39 +419,20 @@ export function titleWithinBudget(title, budget = TITLE_BYTE_BUDGET) {
42
419
  return kept
43
420
  }
44
421
 
45
- export function emptyPathRegistry(workspaceId) {
46
- return { schema: PATH_REGISTRY_SCHEMA, workspaceId, entries: [] }
47
- }
48
-
49
- function indexRegistry(registry, workspaceId) {
50
- if (registry === undefined || registry === null) return { byIdentity: new Map(), taken: new Map() }
51
- if (registry.schema !== PATH_REGISTRY_SCHEMA || !Array.isArray(registry.entries)) refuse('invalid-path-registry', 'path registry has an unknown shape')
52
- if (registry.workspaceId !== workspaceId) refuse('invalid-path-registry', 'path registry belongs to another workspace')
422
+ // Layout 1 paths, as the earlier release allocated them: the paths a layout 1
423
+ // prior generation recorded, then `notes/<title>--<suffix>.md` for every other
424
+ // node in canonical identity order, the suffix lengthened on collision.
425
+ export function allocateLegacyPaths({ nodes, priorManifest = null, vaultRootBytes = 0, maxFullPathBytes = DEFAULT_MAX_FULL_PATH_BYTES }) {
53
426
  const byIdentity = new Map()
54
- const taken = new Map()
55
- for (const entry of registry.entries) {
56
- const match = typeof entry?.path === 'string' ? entry.path.match(NOTE_PATH) : null
57
- if (!match || typeof entry.repoId !== 'string' || typeof entry.nodeId !== 'string') refuse('invalid-path-registry', 'path registry entry is malformed')
58
- if (identitySuffix(entry.repoId, entry.nodeId, 64).slice(0, match[2].length) !== match[2]) {
59
- refuse('invalid-path-registry', 'path registry entry carries a suffix that is not derived from its identity')
60
- }
61
- const key = identityKey(entry.repoId, entry.nodeId)
62
- if (byIdentity.has(key)) refuse('duplicate-identity', 'path registry repeats a canonical identity')
63
- if (taken.has(collisionKey(entry.path))) refuse('path-collision', 'path registry allocates one path to more than one identity')
64
- byIdentity.set(key, entry.path)
65
- taken.set(collisionKey(entry.path), key)
427
+ const taken = new Set()
428
+ for (const note of priorManifest?.notes ?? []) {
429
+ const match = LEGACY_NOTE_PATH.exec(note.path)
430
+ if (!match || identitySuffix(note.repoId, note.nodeId, LEGACY_SUFFIX).slice(0, match[2].length) !== match[2]) refuse('invalid-path-registry', 'a layout 1 manifest entry carries a suffix that is not derived from its identity')
431
+ byIdentity.set(identityKey(note.repoId, note.nodeId), note.path)
432
+ taken.add(collisionKey(note.path))
66
433
  }
67
- return { byIdentity, taken }
68
- }
69
-
70
- // Allocates a path for every node that lacks one and returns the grown
71
- // registry. `nodes` are canonical nodes ({ repo, id, title }); allocation order
72
- // is canonical-identity order, so the result depends on the inputs alone.
73
- export function allocateWorkspacePaths({ registry, workspaceId, nodes, vaultRootBytes = 0, maxFullPathBytes = DEFAULT_MAX_FULL_PATH_BYTES }) {
74
- const { byIdentity, taken } = indexRegistry(registry, workspaceId)
75
434
  const seen = new Set()
76
- const ordered = [...nodes].sort((left, right) => compare(left.repo, right.repo) || compare(left.id, right.id))
77
- for (const node of ordered) {
435
+ for (const node of [...nodes].sort((left, right) => compare(left.repo, right.repo) || compare(left.id, right.id))) {
78
436
  const key = identityKey(node.repo, node.id)
79
437
  if (seen.has(key)) refuse('duplicate-identity', 'more than one node carries the same canonical identity')
80
438
  seen.add(key)
@@ -84,11 +442,11 @@ export function allocateWorkspacePaths({ registry, workspaceId, nodes, vaultRoot
84
442
  let candidate = notePath(titled, length)
85
443
  while (taken.has(collisionKey(candidate))) {
86
444
  length += 4
87
- if (length > MAX_SUFFIX) refuse('path-collision', 'unable to allocate a distinct note path')
445
+ if (length > LEGACY_SUFFIX) refuse('path-collision', 'unable to allocate a distinct note path')
88
446
  candidate = notePath(titled, length)
89
447
  }
90
448
  byIdentity.set(key, candidate)
91
- taken.set(collisionKey(candidate), key)
449
+ taken.add(collisionKey(candidate))
92
450
  }
93
451
  for (const candidate of byIdentity.values()) {
94
452
  const name = candidate.slice('notes/'.length)
@@ -99,7 +457,35 @@ export function allocateWorkspacePaths({ registry, workspaceId, nodes, vaultRoot
99
457
  const entries = [...byIdentity].map(([key, allocated]) => {
100
458
  const [repoId, nodeId] = key.split('\u0000')
101
459
  return { repoId, nodeId, path: allocated }
102
- })
103
- entries.sort((left, right) => compare(left.repoId, right.repoId) || compare(left.nodeId, right.nodeId))
104
- return { registry: { schema: PATH_REGISTRY_SCHEMA, workspaceId, entries }, pathOf: (repoId, nodeId) => byIdentity.get(identityKey(repoId, nodeId)) ?? null }
460
+ }).sort((left, right) => compare(left.repoId, right.repoId) || compare(left.nodeId, right.nodeId))
461
+ return { entries, pathOf: (repoId, nodeId) => byIdentity.get(identityKey(repoId, nodeId)) ?? null }
462
+ }
463
+
464
+ // Deprecated. The earlier release's allocation, kept for callers of the
465
+ // published subpath with the same inputs, checks and result: layout 1 note
466
+ // paths (`notes/<title>--<suffix>.md`) for every node of a workspace, reusing
467
+ // and returning a registry of `{ repoId, nodeId, path }` entries. Atelier no
468
+ // longer calls it: views are laid out in layout 2, each by allocateViewPaths.
469
+ // A layout 2 registry holds no layout 1 entries and is read as empty.
470
+ export function allocateWorkspacePaths({ registry, workspaceId, nodes, vaultRootBytes = 0, maxFullPathBytes = DEFAULT_MAX_FULL_PATH_BYTES }) {
471
+ const notes = []
472
+ if (registry !== undefined && registry !== null) {
473
+ if (registry.schema !== PATH_REGISTRY_SCHEMA || !Array.isArray(registry.entries)) refuse('invalid-path-registry', 'path registry has an unknown shape')
474
+ if (registry.workspaceId !== workspaceId) refuse('invalid-path-registry', 'path registry belongs to another workspace')
475
+ const seen = new Set()
476
+ const taken = new Set()
477
+ for (const entry of registry.entries) {
478
+ const match = typeof entry?.path === 'string' ? LEGACY_NOTE_PATH.exec(entry.path) : null
479
+ if (!match || typeof entry.repoId !== 'string' || typeof entry.nodeId !== 'string') refuse('invalid-path-registry', 'path registry entry is malformed')
480
+ if (identitySuffix(entry.repoId, entry.nodeId, LEGACY_SUFFIX).slice(0, match[2].length) !== match[2]) refuse('invalid-path-registry', 'path registry entry carries a suffix that is not derived from its identity')
481
+ const key = identityKey(entry.repoId, entry.nodeId)
482
+ if (seen.has(key)) refuse('duplicate-identity', 'path registry repeats a canonical identity')
483
+ if (taken.has(collisionKey(entry.path))) refuse('path-collision', 'path registry allocates one path to more than one identity')
484
+ seen.add(key)
485
+ taken.add(collisionKey(entry.path))
486
+ notes.push({ repoId: entry.repoId, nodeId: entry.nodeId, path: entry.path })
487
+ }
488
+ }
489
+ const { entries, pathOf } = allocateLegacyPaths({ nodes, priorManifest: { notes }, vaultRootBytes, maxFullPathBytes })
490
+ return { registry: { schema: PATH_REGISTRY_SCHEMA, workspaceId, entries }, pathOf }
105
491
  }