@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,8 +1,12 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { unclosedFenceAtEnd } from '../../../graph/knowledge-graph.mjs'
3
- import { DERIVED_RELATION_TYPE, RELATION_TYPES, SCOPE_PRIMITIVES, assertObsidianContract, identitySuffix, readableTitle, selectScope } from '../contracts.mjs'
3
+ import {
4
+ DERIVED_RELATION_TYPE, IDENTITY_KEYS, RELATION_TYPES, SCOPE_PRIMITIVES, assertObsidianContract, identityLineTexts, identitySuffix, manifestLayoutVersion, readableTitle,
5
+ selectScope,
6
+ } from '../contracts.mjs'
4
7
  import { assertStrictUtf8, readMarkdownLens, refuse, sha256Digest } from './byte-lens.mjs'
5
- import { allocateWorkspacePaths, collisionKey, emptyPathRegistry, titleWithinBudget } from './path-registry.mjs'
8
+ import { allocateLegacyPaths, allocateViewPaths, collisionKey, titleWithinBudget, viewsOfRegistry, withViewSection } from './path-registry.mjs'
9
+ import { REDACTION_RULES, assertOnlyVaultIdentities, assertViewRedaction, createDenyMatcher } from './redaction.mjs'
6
10
  import { isUserOwnedSettingsPath, prepareSettings } from './settings.mjs'
7
11
 
8
12
  // prepareView: a pure preparation of one Obsidian view. It reads sources
@@ -13,25 +17,38 @@ import { isUserOwnedSettingsPath, prepareSettings } from './settings.mjs'
13
17
  // An embedded asset is copied only when the canonical graph resolved it; the
14
18
  // emitter never infers one from authored bytes.
15
19
  //
20
+ // A view is laid out in vault layout 2: folders mirror the repository, the
21
+ // file name is the note's title and each note names its identity in generated
22
+ // front-matter properties (see "Vault layout" in docs/obsidian-contract.md).
23
+ // Layout 1, the earlier release's, is kept for a view whose prior generation
24
+ // is in layout 1 and holds a note with an open edit, and for recovering a
25
+ // layout 1 generation as it was published.
26
+ //
16
27
  // Redaction happens where bytes are made. Every title, path and identity that
17
28
  // reaches an output is looked up through `vault`, the set selectScope returned
18
- // for this view, so a node outside it cannot be serialized by any branch.
29
+ // for this view, so a node outside it cannot be serialized by any branch; the
30
+ // redaction guard checks the whole result before anything is returned.
19
31
  //
20
32
  // Preparation is incremental when the caller passes a preparation cache (see
21
33
  // createPreparationCache). Every note's bytes and manifest entry are a pure
22
- // function of a small set of inputs: the pinned source digest, the node
23
- // record, the allocated path, the generated rows, the outside-selection count
24
- // and the rewritten occurrences with their emitted targets. Those inputs are
25
- // serialized into a dependency key per note; a note whose key matches the
34
+ // function of a small set of inputs: the layout, the pinned source digest, the
35
+ // node record, the allocated paths, the generated rows, the outside-selection
36
+ // count and the rewritten occurrences with their emitted targets. Those inputs
37
+ // are serialized into a dependency key per note; a note whose key matches the
26
38
  // cached one reuses the cached bytes and manifest entry instead of being
27
39
  // emitted again, so the result is byte-identical to a full preparation by
28
40
  // construction. The cache holds derived state only and can be dropped at any
29
41
  // time; the redaction guard still runs over the whole result.
30
42
 
31
- export const EMITTER_VERSION = '1.0.0'
43
+ export const EMITTER_VERSION = '2.0.0'
44
+ export const VAULT_LAYOUTS = Object.freeze({
45
+ 1: Object.freeze({ version: 1, emitterVersion: '1.0.0', manifestSchema: 'atelier-obsidian-generation-manifest/v1', contractVersion: '1.0.0' }),
46
+ 2: Object.freeze({ version: 2, emitterVersion: EMITTER_VERSION, manifestSchema: 'atelier-obsidian-generation-manifest/v2', contractVersion: '2.0.0' }),
47
+ })
48
+ export const CURRENT_VAULT_LAYOUT = 2
32
49
  const EXT_KEY = 'mnstry.atelier.obsidian'
33
50
  const EMBEDDABLE = new Set(['pdf', 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'])
34
- // Bytes left for an asset's readable stem beside the longest suffix and extension.
51
+ // Bytes left for an asset's readable stem beside the longest suffix and extension (layout 1).
35
52
  const ASSET_STEM_BYTE_BUDGET = 255 - '--'.length - 64 - '.'.length - 16
36
53
  const RELATIONS_HEADING = '## Relations (generated)\n\n%% Generated from the canonical graph. Edits to this section are not applied to any source. %%\n\n'
37
54
 
@@ -55,7 +72,22 @@ function encodeHref(fileName) {
55
72
  return encodeURIComponent(fileName).replace(/[()']/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`)
56
73
  }
57
74
 
58
- const noteBasename = (notePathValue) => notePathValue.slice('notes/'.length, -'.md'.length)
75
+ // A vault path as a Markdown link target: every segment percent-encoded.
76
+ const encodeVaultPath = (vaultPath) => vaultPath.split('/').map(encodeHref).join('/')
77
+
78
+ const legacyBasename = (notePathValue) => notePathValue.slice('notes/'.length, -'.md'.length)
79
+
80
+ // How a link names a note: layout 1 by its unique basename, layout 2 by its
81
+ // full vault path, which the app looks up as an exact path before any other
82
+ // match. A wikilink keeps `.md` so a note named after a file never resolves
83
+ // to the file.
84
+ function noteLinkTarget(layout, notePathValue) {
85
+ if (layout.version === 1) {
86
+ const stem = legacyBasename(notePathValue)
87
+ return { markdown: encodeHref(`${stem}.md`), wikilink: stem }
88
+ }
89
+ return { markdown: encodeVaultPath(notePathValue), wikilink: notePathValue }
90
+ }
59
91
 
60
92
  function edgeIdentifier(edge) {
61
93
  return `e-${createHash('sha256').update(`${edge.source}\u0000${edge.type}\u0000${edge.target}`).digest('hex').slice(0, 32)}`
@@ -114,15 +146,19 @@ function isPreparationCache(cache) {
114
146
 
115
147
  // Everything the emitted bytes and the manifest entry of one note depend on,
116
148
  // in a canonical serialization. A cached note is reused only under an equal
117
- // key. The emitter version is part of the key so an entry states which emitter
118
- // produced it, should a cache ever outlive this module.
119
- function dependencyKey({ node, notePathValue, pinned, rows, outsideCount, occurrences, emittedTarget }) {
149
+ // key. The layout and emitter version are part of the key so an entry states
150
+ // which emitter produced it, should a cache ever outlive this module. The
151
+ // identity block and where it goes are functions of the node record and the
152
+ // source bytes, which the key holds through the pinned digest.
153
+ function dependencyKey({ layout, node, notePathValue, attachmentPathValue, pinned, rows, outsideCount, occurrences, emittedTarget }) {
120
154
  // A wrapper note serializes the record's summary and tags; a Markdown note never does.
121
155
  const wrapper = node.extension === 'md' ? null : JSON.stringify([String(node.summary ?? ''), Array.isArray(node.tags) ? node.tags : null])
122
156
  const parts = [
123
- EMITTER_VERSION, notePathValue, node.repo, node.id, node.path, String(node.extension), String(node.title ?? ''), String(wrapper),
157
+ layout.emitterVersion, notePathValue, node.repo, node.id, node.path, String(node.extension), String(node.title ?? ''), String(wrapper),
124
158
  pinned.rawDigest, String(pinned.byteLength), String(outsideCount), rows.join(''),
125
159
  ]
160
+ // Layout 1 keys are exactly the earlier release's; layout 2 adds its own inputs after them.
161
+ if (layout.version !== 1) parts.push(`layout-${layout.version}`, String(attachmentPathValue ?? ''))
126
162
  for (const occurrence of occurrences) {
127
163
  const target = emittedTarget(occurrence)
128
164
  parts.push(JSON.stringify([
@@ -211,6 +247,81 @@ function applyEdits({ source, from, to, edits, noteOffset }) {
211
247
  return { bytes: Buffer.concat(parts), inversions }
212
248
  }
213
249
 
250
+ // ---------------------------------------------------------------------------
251
+ // The identity block (layout 2)
252
+ // ---------------------------------------------------------------------------
253
+
254
+ const identityText = (node, eol) => identityLineTexts(node).map((line) => `${line}${eol}`).join('')
255
+
256
+ // The key of one line of a block mapping at column 0, or null when the line
257
+ // is not one: a quoted or plain key followed by `:` and a space, a tab or the
258
+ // end of the line.
259
+ function blockMappingKey(line) {
260
+ let key
261
+ let rest
262
+ if (line.startsWith('"')) {
263
+ const match = /^"((?:[^"\\]|\\.)*)"/.exec(line)
264
+ if (!match) return null
265
+ try { key = JSON.parse(`"${match[1]}"`) } catch { return null }
266
+ rest = line.slice(match[0].length)
267
+ } else if (line.startsWith("'")) {
268
+ const match = /^'((?:[^']|'')*)'/.exec(line)
269
+ if (!match) return null
270
+ key = match[1].replaceAll("''", "'")
271
+ rest = line.slice(match[0].length)
272
+ } else {
273
+ if (/^[-?:,[\]{}#&*!|>%@`]/.test(line)) return null
274
+ const colon = /:(?:[ \t]|$)/.exec(line)
275
+ if (!colon) return null
276
+ key = line.slice(0, colon.index).trimEnd()
277
+ rest = line.slice(colon.index)
278
+ if (key === '' || /[ \t]#/.test(key)) return null
279
+ }
280
+ return /^[ \t]*:(?:[ \t]|$)/.test(rest) ? key : null
281
+ }
282
+
283
+ // Whether identity lines can be appended to this front matter without changing
284
+ // what it means: a block mapping at column 0 (blank and comment lines aside,
285
+ // every line at column 0 is `key:`, everything else is indented under one) that
286
+ // does not already use one of the identity keys. Answers 'fits', 'author-keys'
287
+ // (the author wrote one of the three keys, which would then show in Properties
288
+ // instead of the generated one) or 'shape' (a flow or sequence root, an
289
+ // indented root, a directive or document marker, or a line that is no key).
290
+ function identityLinesFit(yaml) {
291
+ let first = true
292
+ let authorKeys = false
293
+ for (const raw of yaml.split('\n')) {
294
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw
295
+ if (line.trim() === '' || /^[ \t]*#/.test(line)) continue
296
+ if (/^[ \t]/.test(line)) {
297
+ if (first || line.startsWith('\t')) return 'shape'
298
+ continue
299
+ }
300
+ first = false
301
+ const key = blockMappingKey(line)
302
+ if (key === null) return 'shape'
303
+ if (IDENTITY_KEYS.includes(key)) authorKeys = true
304
+ }
305
+ return authorKeys ? 'author-keys' : 'fits'
306
+ }
307
+
308
+ // Where the identity lines of a Markdown note go, decided from the source
309
+ // bytes alone: appended to its front matter, in a generated front matter of
310
+ // their own, or, for a front matter that cannot take them, at the end.
311
+ function identityPlacement(source, lens) {
312
+ if (lens.frontmatter === null) {
313
+ const firstBreak = source.indexOf(0x0a, lens.body.start)
314
+ const eol = firstBreak > lens.body.start && source[firstBreak - 1] === 0x0d ? '\r\n' : '\n'
315
+ return { kind: 'generated-frontmatter', eol }
316
+ }
317
+ const { start, end } = lens.frontmatter
318
+ const openingEnd = source.indexOf(0x0a, start) + 1
319
+ const closingStart = source.lastIndexOf(0x0a, end - 2) + 1
320
+ const fit = identityLinesFit(source.subarray(openingEnd, closingStart).toString('utf8'))
321
+ if (fit !== 'fits') return { kind: 'tail', authorKeys: fit === 'author-keys' }
322
+ return { kind: 'frontmatter-lines', at: closingStart, eol: source[closingStart - 2] === 0x0d ? '\r\n' : '\n' }
323
+ }
324
+
214
325
  // ---------------------------------------------------------------------------
215
326
  // Generated sections
216
327
  // ---------------------------------------------------------------------------
@@ -231,14 +342,14 @@ function titleRenderings() {
231
342
  }
232
343
  }
233
344
 
234
- function relationRows({ node, outgoing, incoming, vaultNode, pathOf, titles }) {
345
+ function relationRows({ layout, node, outgoing, incoming, vaultNode, pathOf, titles }) {
235
346
  const rows = []
236
347
  for (const type of RELATION_TYPES) {
237
348
  for (const edge of outgoing) {
238
349
  if (edge.type !== type) continue
239
350
  const target = vaultNode(edge.target)
240
351
  if (!target) continue
241
- rows.push(`- ${type} → [[${noteBasename(pathOf(target))}|${titles.readableOf(target)}]]\n`)
352
+ rows.push(`- ${type} → [[${noteLinkTarget(layout, pathOf(target)).wikilink}|${titles.readableOf(target)}]]\n`)
242
353
  }
243
354
  // An incoming row is plain text: a link here would give this note an
244
355
  // outgoing native link that the canonical graph does not hold.
@@ -269,7 +380,9 @@ function fenceClosureFor(source, finalNewline) {
269
380
  return { fence: line, text: `${finalNewline === 'none' ? eol : ''}${line}${eol}` }
270
381
  }
271
382
 
272
- function generatedSections({ precedingBytes, offset, rows, outsideCount, closure = null }) {
383
+ // `identity`, when given, is the text of identity lines that go at the very
384
+ // end of the note, in a comment block, as the last generated region.
385
+ function generatedSections({ precedingBytes, offset, rows, outsideCount, closure = null, identity = null }) {
273
386
  const regions = []
274
387
  const parts = []
275
388
  let cursor = offset
@@ -290,14 +403,25 @@ function generatedSections({ precedingBytes, offset, rows, outsideCount, closure
290
403
  const lead = rows.length > 0 ? '\n' : `${separatorAfter(precedingBytes)}${RELATIONS_HEADING}`
291
404
  push('outside-selection', `${lead}Relationships leading outside this view: ${outsideCount}\n`)
292
405
  }
406
+ if (identity !== null) push('identity', `${regions.length > 0 ? '\n' : separatorAfter(precedingBytes)}%%\n${identity}%%\n`)
293
407
  return { bytes: Buffer.concat(parts), regions }
294
408
  }
295
409
 
410
+ // The wrapper's own lines. `format` and `source` are what the redaction guard
411
+ // recognises as the node's own extension and path.
412
+ function wrapperLines(node) {
413
+ return {
414
+ format: `- Format: ${plainText(node.extension)}`,
415
+ source: `- Source: ${plainText(node.repo)} · ${node.path.split('/').map(plainText).join(' / ')}`,
416
+ }
417
+ }
418
+
296
419
  function wrapperRepresentation({ node, attachmentPath }) {
420
+ const own = wrapperLines(node)
297
421
  const lines = [`# ${plainText(node.title)}\n`]
298
422
  if (node.summary) lines.push(`\n${plainText(node.summary)}\n`)
299
- lines.push(`\n- Format: ${plainText(node.extension)}\n`)
300
- lines.push(`- Source: ${plainText(node.repo)} · ${node.path.split('/').map(plainText).join(' / ')}\n`)
423
+ lines.push(`\n${own.format}\n`)
424
+ lines.push(`${own.source}\n`)
301
425
  if (Array.isArray(node.tags) && node.tags.length > 0) lines.push(`- Tags: ${node.tags.map(plainText).join(', ')}\n`)
302
426
  lines.push(`- Original: [[${attachmentPath}|Open the original file]]\n`)
303
427
  if (EMBEDDABLE.has(String(node.extension).toLowerCase())) lines.push(`\n![[${attachmentPath}]]\n`)
@@ -320,7 +444,7 @@ function assertContained(relativePath) {
320
444
  // inversions it contributes per rewritten edge, and whether it closed a fence.
321
445
  // Nothing outside the arguments is read, which is what lets the result be
322
446
  // cached under the dependency key.
323
- function emitNote({ node, notePathValue, source: { bytes: source, rawDigest }, rows, outsideCount, occurrences, emittedTarget, key }) {
447
+ function emitNote({ layout, node, notePathValue, attachmentPathValue, source: { bytes: source, rawDigest }, rows, outsideCount, occurrences, emittedTarget, key }) {
324
448
  const sourceRecord = { path: node.path, rawDigest, byteLength: source.length }
325
449
  const assetInversions = new Map()
326
450
  const edgeInversions = new Map()
@@ -329,11 +453,31 @@ function emitNote({ node, notePathValue, source: { bytes: source, rawDigest }, r
329
453
  let authored
330
454
  let regions
331
455
  let closure = null
456
+ let identityAtEnd = null
457
+ let placementOf = null
332
458
 
333
459
  if (node.extension === 'md') {
334
460
  const lens = readMarkdownLens(source, { repoId: node.repo, nodeId: node.id })
335
461
  const edits = linkEdits({ source, lens, occurrences, emittedTarget })
336
- const prefix = source.subarray(0, lens.body.start)
462
+ const placement = layout.version === 1 ? null : identityPlacement(source, lens)
463
+ placementOf = placement
464
+ let prefix = source.subarray(0, lens.body.start)
465
+ let frontmatter = lens.frontmatter
466
+ let identity = null
467
+ if (placement?.kind === 'frontmatter-lines') {
468
+ const block = utf8(identityText(node, placement.eol))
469
+ prefix = Buffer.concat([source.subarray(0, placement.at), block, source.subarray(placement.at, lens.body.start)])
470
+ identity = { start: placement.at, end: placement.at + block.length }
471
+ frontmatter = { start: lens.frontmatter.start, end: lens.frontmatter.end + block.length }
472
+ } else if (placement?.kind === 'generated-frontmatter') {
473
+ // After a byte order prefix, which the app strips before it reads front matter.
474
+ const block = utf8(`---${placement.eol}${identityText(node, placement.eol)}---${placement.eol}`)
475
+ prefix = Buffer.concat([source.subarray(0, lens.body.start), block])
476
+ identity = { start: lens.body.start, end: lens.body.start + block.length }
477
+ frontmatter = identity
478
+ } else if (placement?.kind === 'tail') {
479
+ identityAtEnd = identityText(node, '\n')
480
+ }
337
481
  const body = applyEdits({ source, from: lens.body.start, to: lens.body.end, edits, noteOffset: prefix.length })
338
482
  authored = Buffer.concat([prefix, body.bytes])
339
483
  for (const { edgeKey, assetKey, ...inversion } of body.inversions) {
@@ -346,27 +490,34 @@ function emitNote({ node, notePathValue, source: { bytes: source, rawDigest }, r
346
490
  edgeInversions.get(edgeKey).push(inversion)
347
491
  }
348
492
  regions = {
349
- ...(lens.frontmatter ? { frontmatter: lens.frontmatter } : {}),
350
- body: { start: lens.body.start, end: authored.length },
493
+ ...(frontmatter ? { frontmatter } : {}),
494
+ ...(identity ? { identity } : {}),
495
+ body: { start: prefix.length, end: authored.length },
351
496
  }
352
497
  closure = fenceClosureFor(source, lens.finalNewline)
353
498
  Object.assign(sourceRecord, { kind: 'markdown', finalNewline: lens.finalNewline, ...(lens.bom ? { bom: lens.bom } : {}) })
354
499
  } else {
355
500
  const extension = /^[a-z0-9]{1,16}$/.test(String(node.extension).toLowerCase()) ? String(node.extension).toLowerCase() : 'bin'
356
- const attachmentPath = `attachments/${noteBasename(notePathValue)}.${extension}`
501
+ const attachmentPath = layout.version === 1 ? `attachments/${legacyBasename(notePathValue)}.${extension}` : attachmentPathValue
357
502
  assertContained(attachmentPath)
358
503
  attachment = { path: attachmentPath, digest: rawDigest, byteLength: source.length }
359
504
  files.push({ path: attachmentPath, kind: 'attachment', bytes: source, digest: rawDigest })
360
- authored = wrapperRepresentation({ node, attachmentPath })
361
- regions = { body: { start: authored.length, end: authored.length }, representation: { start: 0, end: authored.length } }
505
+ const identityBlock = layout.version === 1 ? Buffer.alloc(0) : utf8(`---\n${identityText(node, '\n')}---\n`)
506
+ authored = Buffer.concat([identityBlock, wrapperRepresentation({ node, attachmentPath })])
507
+ regions = {
508
+ ...(identityBlock.length > 0 ? { frontmatter: { start: 0, end: identityBlock.length }, identity: { start: 0, end: identityBlock.length } } : {}),
509
+ body: { start: authored.length, end: authored.length },
510
+ representation: { start: identityBlock.length, end: authored.length },
511
+ }
362
512
  Object.assign(sourceRecord, { kind: 'wrapper', attachment: attachmentPath })
363
513
  }
364
514
 
365
- const generated = generatedSections({ precedingBytes: authored, offset: authored.length, rows, outsideCount, closure })
515
+ const generated = generatedSections({ precedingBytes: authored, offset: authored.length, rows, outsideCount, closure, identity: identityAtEnd })
366
516
  const bytes = Buffer.concat([authored, generated.bytes])
367
517
  assertStrictUtf8(bytes)
368
518
  assertContained(notePathValue)
369
519
  const { representation, ...authoredRegions } = regions
520
+ if (identityAtEnd !== null) authoredRegions.identity = { ...generated.regions.at(-1).range }
370
521
  files.push({ path: notePathValue, kind: 'note', bytes, digest: sha256Digest(bytes) })
371
522
  const note = {
372
523
  repoId: node.repo,
@@ -382,12 +533,17 @@ function emitNote({ node, notePathValue, source: { bytes: source, rawDigest }, r
382
533
  [EXT_KEY]: {
383
534
  source: sourceRecord,
384
535
  ...(assetInversions.size > 0
385
- ? { assetEmbeds: [...assetInversions].map(([attachment, inversions]) => ({ attachment, inversions })).sort((left, right) => compare(left.attachment, right.attachment)) }
536
+ ? { assetEmbeds: [...assetInversions].map(([attachmentKey, inversions]) => ({ attachment: attachmentKey, inversions })).sort((left, right) => compare(left.attachment, right.attachment)) }
386
537
  : {}),
387
538
  },
388
539
  },
389
540
  }
390
- return { key, files, attachment, note, edgeInversions: [...edgeInversions], fenceClosed: Boolean(closure && generated.regions.length > 0) }
541
+ // Only the closure that precedes a generated section is emitted; one before the end-of-note identity block counts.
542
+ return {
543
+ key, files, attachment, note, edgeInversions: [...edgeInversions], fenceClosed: Boolean(closure && generated.regions.length > 0),
544
+ // The author wrote an identity key of their own: it, not the generated block, is what Properties shows.
545
+ authorIdentityKeys: placementOf?.authorKeys === true,
546
+ }
391
547
  }
392
548
 
393
549
  // ---------------------------------------------------------------------------
@@ -413,10 +569,11 @@ function visibleAssets(graph, profile) {
413
569
  return visible
414
570
  }
415
571
 
416
- // attachments/<readable stem>--<identity suffix>.<ext>, from the repository and
417
- // asset identity alone. The suffix lengthens only when two assets of this view
418
- // would share a name on a case- or normalization-insensitive filesystem.
419
- function allocateAssetPaths(assets) {
572
+ // Layout 1: attachments/<readable stem>--<identity suffix>.<ext>, from the
573
+ // repository and asset identity alone. The suffix lengthens only when two
574
+ // assets of this view would share a name on a case- or normalization-
575
+ // insensitive filesystem.
576
+ function allocateLegacyAssetPaths(assets) {
420
577
  const taken = new Set()
421
578
  const allocated = new Map()
422
579
  for (const asset of [...assets].sort((left, right) => compare(left.repo, right.repo) || compare(left.id, right.id))) {
@@ -450,18 +607,53 @@ function canonicalSnapshotOf(graph) {
450
607
  return { nodes: graph.nodes, edges, externalEdgeCount: graph.edges.length - edges.length }
451
608
  }
452
609
 
453
- // A path the prior generation published stays allocated even when the caller
454
- // lost the registry: the prior manifest seeds any identity the registry lacks.
455
- function seededRegistry({ registry, priorManifest, workspaceId }) {
456
- if (!priorManifest) return registry
457
- const base = registry ?? emptyPathRegistry(workspaceId)
458
- if (!Array.isArray(base.entries)) return base
459
- const known = new Set(base.entries.map((entry) => `${entry?.repoId}\u0000${entry?.nodeId}`))
460
- const recovered = priorManifest.notes.filter((note) => !known.has(`${note.repoId}\u0000${note.nodeId}`)).map(({ repoId, nodeId, path }) => ({ repoId, nodeId, path }))
461
- return { ...base, entries: [...base.entries, ...recovered] }
610
+ // What a layout 2 prior generation published, which the view's allocation
611
+ // keeps (see allocateViewPaths). A layout 1 prior generation offers nothing:
612
+ // its paths are the earlier layout's.
613
+ function publishedOf(priorManifest) {
614
+ if (!priorManifest || manifestLayoutVersion(priorManifest) !== CURRENT_VAULT_LAYOUT) return null
615
+ return {
616
+ notes: priorManifest.notes.map(({ repoId, nodeId, path: notePathValue, ext }) => {
617
+ const attachment = ext?.[EXT_KEY]?.source?.attachment
618
+ return { repoId, nodeId, path: notePathValue, ...(typeof attachment === 'string' ? { attachment } : {}) }
619
+ }),
620
+ assets: priorManifest.attachments
621
+ .filter((item) => item.ext?.[EXT_KEY]?.kind === 'embedded-asset')
622
+ .map((item) => ({ repoId: item.ext[EXT_KEY].repoId, assetPath: item.ext[EXT_KEY].assetPath, path: item.path })),
623
+ }
624
+ }
625
+
626
+ // `layoutHeld` are the notes that hold a layout 1 view in layout 1: those of
627
+ // open edits and of edits closed on this tick (see layoutHeldPaths in
628
+ // src/runtime/obsidian/pending-edits.mjs).
629
+ function layoutOf({ layout, priorManifest, layoutHeld }) {
630
+ if (layout !== undefined && layout !== null) {
631
+ if (!Object.hasOwn(VAULT_LAYOUTS, layout)) refuse('invalid-layout', 'the vault layout must be 1 or 2')
632
+ return VAULT_LAYOUTS[layout]
633
+ }
634
+ // A layout 1 view that holds a note stays in layout 1 until no note of it is held.
635
+ const held = new Set(layoutHeld)
636
+ if (priorManifest && manifestLayoutVersion(priorManifest) === 1 && priorManifest.notes.some((note) => held.has(note.path))) return VAULT_LAYOUTS[1]
637
+ return VAULT_LAYOUTS[CURRENT_VAULT_LAYOUT]
638
+ }
639
+
640
+ export function prepareView(options = {}) {
641
+ return prepareWithRules(REDACTION_RULES, options)
462
642
  }
463
643
 
464
- export function prepareView({ snapshot, profile, scope, persistentPathRegistry = null, priorManifest = null, existingSettings = null, clock, generationId, vaultRootBytes, maxFullPathBytes, cache = null } = {}) {
644
+ // Test seam only: the mutation controls of the redaction guard substitute a
645
+ // rule through this. Runtime code uses prepareView.
646
+ export function createViewPreparationForOracleTests(rules = REDACTION_RULES) {
647
+ const guard = { ...REDACTION_RULES, ...rules }
648
+ return (options = {}) => prepareWithRules(guard, options)
649
+ }
650
+
651
+ // `heldNotePaths` are the vault files held for an open edit: they stay in the
652
+ // vault, so their names stay taken. `layoutHeldNotePaths`, when given, are the
653
+ // notes that hold a layout 1 view in layout 1 (open edits and edits closed on
654
+ // this tick); without it, `heldNotePaths` do. `viewScopeIds`, when given, are
655
+ // the views still maintained: the registry drops the sections of any other.
656
+ function prepareWithRules(guard, { snapshot, profile, scope, persistentPathRegistry = null, priorManifest = null, existingSettings = null, plugin = null, clock, generationId, vaultRootBytes, maxFullPathBytes, cache = null, heldNotePaths = null, layoutHeldNotePaths = null, viewScopeIds = null, layout: requestedLayout } = {}) {
465
657
  if (cache !== null && !isPreparationCache(cache)) refuse('invalid-preparation-cache', 'the preparation cache must come from createPreparationCache')
466
658
  assertObsidianContract('corpus-profile', profile)
467
659
  assertObsidianContract('scope', scope)
@@ -472,6 +664,11 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
472
664
  assertObsidianContract('generation-manifest', priorManifest)
473
665
  if (priorManifest.scopeId !== scope.scopeId) refuse('prior-manifest-mismatch', 'the prior manifest belongs to another scope')
474
666
  }
667
+ for (const [name, value] of [['heldNotePaths', heldNotePaths], ['layoutHeldNotePaths', layoutHeldNotePaths]]) {
668
+ if (value !== null && !Array.isArray(value)) refuse('invalid-held-notes', `${name} must be an array of note paths`)
669
+ }
670
+ if (viewScopeIds !== null && !(Array.isArray(viewScopeIds) && viewScopeIds.every((id) => typeof id === 'string'))) refuse('invalid-view-scopes', 'viewScopeIds must be an array of scope identities')
671
+ const layout = layoutOf({ layout: requestedLayout, priorManifest, layoutHeld: layoutHeldNotePaths ?? heldNotePaths ?? [] })
475
672
  const checkedAt = timestampFrom(clock)
476
673
  const canonical = canonicalSnapshotOf(snapshot.graph)
477
674
 
@@ -481,19 +678,13 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
481
678
  const nodeById = new Map(canonical.nodes.map((node) => [node.id, node]))
482
679
  const vault = new Set(selection.vaultNodes)
483
680
  const vaultNode = (id) => (vault.has(id) ? nodeById.get(id) : null)
484
-
485
- const { registry, pathOf: allocatedPath } = allocateWorkspacePaths({
486
- registry: seededRegistry({ registry: persistentPathRegistry, priorManifest, workspaceId: profile.workspaceId }),
487
- workspaceId: profile.workspaceId,
488
- nodes: universe.nodes.map((id) => nodeById.get(id)),
489
- ...(vaultRootBytes === undefined ? {} : { vaultRootBytes }),
490
- ...(maxFullPathBytes === undefined ? {} : { maxFullPathBytes }),
491
- })
492
- const pathOf = (node) => allocatedPath(node.repo, node.id)
493
- for (const note of priorManifest?.notes ?? []) {
494
- const current = allocatedPath(note.repoId, note.nodeId)
495
- if (current !== null && current !== note.path) refuse('path-registry-divergence', 'the path registry and the prior manifest disagree about an allocated path')
496
- }
681
+ const universeNodes = universe.nodes.map((id) => nodeById.get(id))
682
+ const assets = visibleAssets(snapshot.graph, profile)
683
+ const censusAssets = Array.isArray(snapshot.graph.assets) ? snapshot.graph.assets : []
684
+ const usable = (item) => item && typeof item.repo === 'string' && item.repo !== '' && typeof item.id === 'string' && item.id !== ''
685
+ const limits = { ...(vaultRootBytes === undefined ? {} : { vaultRootBytes }), ...(maxFullPathBytes === undefined ? {} : { maxFullPathBytes }) }
686
+ // Every view's allocation of the registry, this one's included; a registry of another shape or workspace refuses.
687
+ const views = viewsOfRegistry(persistentPathRegistry, profile.workspaceId)
497
688
 
498
689
  const edgeById = new Map(canonical.edges.map((edge) => [edge.id, edge]))
499
690
  const vaultEdges = selection.vaultEdges.map((id) => edgeById.get(id)).filter((edge) => vault.has(edge.source) && vault.has(edge.target))
@@ -509,7 +700,6 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
509
700
  // Asset embeds: canonical occurrences whose source is a note of this view and
510
701
  // whose asset this view may copy. Anything else is left exactly as authored
511
702
  // and appears in no output.
512
- const assets = visibleAssets(snapshot.graph, profile)
513
703
  const embeddedAssets = new Map()
514
704
  for (const embed of Array.isArray(snapshot.graph.embeds) ? snapshot.graph.embeds : []) {
515
705
  const asset = assets.get(embed?.asset?.id)
@@ -518,14 +708,43 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
518
708
  if (!occurrencesBySource.has(embed.source)) occurrencesBySource.set(embed.source, [])
519
709
  occurrencesBySource.get(embed.source).push(embed)
520
710
  }
521
- const assetPaths = allocateAssetPaths(embeddedAssets.values())
711
+
712
+ // Layout 2 paths are allocated for this view alone, seeded from what its
713
+ // prior generation published; a file held for an open edit keeps its name
714
+ // taken. A view prepared in layout 1 holds layout 1 files, none of which is a
715
+ // layout 2 path. The registry keeps the result as this view's section,
716
+ // whichever layout the view is prepared in.
717
+ const readable = allocateViewPaths({
718
+ published: publishedOf(priorManifest),
719
+ nodes: [...vault].map((id) => nodeById.get(id)),
720
+ assets: [...embeddedAssets.values()],
721
+ occupied: layout.version === 1 ? [] : heldNotePaths ?? [],
722
+ ...limits,
723
+ })
724
+ const legacy = layout.version === 1
725
+ ? allocateLegacyPaths({ nodes: universeNodes, priorManifest: priorManifest && manifestLayoutVersion(priorManifest) === 1 ? priorManifest : null, ...limits })
726
+ : null
727
+ const allocatedPath = legacy ? legacy.pathOf : readable.pathOf
728
+ const pathOf = (node) => allocatedPath(node.repo, node.id)
729
+ const attachmentOf = (node) => (legacy ? null : readable.attachmentOf(node.repo, node.id))
730
+ // Layout 1 paths are derived from identities; a disagreement there is state that cannot be trusted.
731
+ if (legacy && priorManifest && manifestLayoutVersion(priorManifest) === 1) {
732
+ for (const note of priorManifest.notes) {
733
+ const current = allocatedPath(note.repoId, note.nodeId)
734
+ if (current !== null && current !== note.path) refuse('path-registry-divergence', 'the path registry and the prior manifest disagree about an allocated path')
735
+ }
736
+ }
737
+ const assetPaths = legacy
738
+ ? allocateLegacyAssetPaths(embeddedAssets.values())
739
+ : new Map([...embeddedAssets.values()].map((asset) => [asset.id, readable.assetPathOf(asset.repo, asset.path)]))
740
+ for (const assetPath of assetPaths.values()) assertContained(assetPath)
522
741
  const emittedTarget = (occurrence) => {
523
742
  if (occurrence.type === 'embeds_asset') {
524
743
  const attachment = assetPaths.get(occurrence.asset.id)
525
- return { key: { assetKey: attachment }, markdown: attachment.split('/').map(encodeHref).join('/'), wikilink: attachment, alias: false }
744
+ return { key: { assetKey: attachment }, markdown: encodeVaultPath(attachment), wikilink: attachment, alias: false }
526
745
  }
527
- const stem = noteBasename(pathOf(vaultNode(occurrence.target)))
528
- return { key: { edgeKey: occurrence.target }, markdown: encodeHref(`${stem}.md`), wikilink: stem, alias: true }
746
+ const target = noteLinkTarget(layout, pathOf(vaultNode(occurrence.target)))
747
+ return { key: { edgeKey: occurrence.target }, markdown: target.markdown, wikilink: target.wikilink, alias: true }
529
748
  }
530
749
 
531
750
  const read = sourceReader(snapshot)
@@ -554,26 +773,31 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
554
773
  const reusable = isPreparationCache(cache) ? cache.notes : null
555
774
  const nextCache = reusable ? new Map() : null
556
775
  const preparation = { emitted: 0, reused: 0 }
776
+ const reusedPaths = new Set()
777
+ const authorKeyNotes = []
557
778
  const titles = titleRenderings()
558
779
 
559
780
  for (const node of orderedNodes) {
560
781
  const notePathValue = pathOf(node)
782
+ const attachmentPathValue = node.extension === 'md' ? null : attachmentOf(node)
561
783
  const pinned = read.pinned(node.repo, node.path)
562
784
  const outgoing = (outgoingBy.get(node.id) ?? []).slice().sort((left, right) => compare(left.target, right.target))
563
785
  const incoming = (incomingBy.get(node.id) ?? []).slice().sort((left, right) => compare(left.source, right.source))
564
- const rows = relationRows({ node, outgoing, incoming, vaultNode, pathOf, titles })
786
+ const rows = relationRows({ layout, node, outgoing, incoming, vaultNode, pathOf, titles })
565
787
  const outsideCount = outsideCountBy.get(node.id) ?? 0
566
788
  const occurrences = occurrencesBySource.get(node.id) ?? []
567
- const key = reusable ? dependencyKey({ node, notePathValue, pinned, rows, outsideCount, occurrences, emittedTarget }) : null
789
+ const key = reusable ? dependencyKey({ layout, node, notePathValue, attachmentPathValue, pinned, rows, outsideCount, occurrences, emittedTarget }) : null
568
790
  const cached = reusable?.get(notePathValue)
569
791
  let entry
570
792
  if (cached && cached.key === key) {
571
793
  entry = cached
572
794
  preparation.reused += 1
795
+ reusedPaths.add(notePathValue)
573
796
  } else {
574
- entry = emitNote({ node, notePathValue, source: read(node.repo, node.path), rows, outsideCount, occurrences, emittedTarget, key })
797
+ entry = emitNote({ layout, node, notePathValue, attachmentPathValue, source: read(node.repo, node.path), rows, outsideCount, occurrences, emittedTarget, key })
575
798
  preparation.emitted += 1
576
799
  }
800
+ if (entry.authorIdentityKeys) authorKeyNotes.push({ code: 'author-identity-properties', repoId: node.repo, nodeId: node.id, notePath: notePathValue })
577
801
  if (nextCache) nextCache.set(notePathValue, entry)
578
802
  // The result never aliases the cache: a caller may change what it was handed, bytes included.
579
803
  for (const file of entry.files) files.push({ ...file, bytes: Buffer.from(file.bytes) })
@@ -611,7 +835,7 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
611
835
  })
612
836
  .sort((left, right) => compare(left.edgeId, right.edgeId))
613
837
 
614
- const settings = prepareSettings({ existing: existingSettings })
838
+ const settings = prepareSettings({ existing: existingSettings, plugin })
615
839
  files.push(...settings.files)
616
840
  const occupied = new Set()
617
841
  for (const file of files) {
@@ -626,8 +850,9 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
626
850
  const contentDigest = createHash('sha256')
627
851
  for (const file of files) contentDigest.update(`${file.path}\u0000${file.digest}\n`)
628
852
  const manifest = {
629
- schema: 'atelier-obsidian-generation-manifest/v1',
630
- contractVersion: '1.0.0',
853
+ schema: layout.manifestSchema,
854
+ contractVersion: layout.contractVersion,
855
+ ...(layout.version === 1 ? {} : { layoutVersion: layout.version }),
631
856
  generationId: generationId ?? `gen-${contentDigest.digest('hex').slice(0, 32)}`,
632
857
  scopeId: scope.scopeId,
633
858
  snapshotId: snapshot.document.snapshotId,
@@ -636,24 +861,98 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
636
861
  attachments,
637
862
  completeness: { status: 'complete', expectedNotes: orderedNodes.length, writtenNotes: notes.length },
638
863
  freshness: { status: 'current', checkedAt },
639
- ext: { [EXT_KEY]: { emitterVersion: EMITTER_VERSION, mode: selection.mode, settings: settings.ownership } },
864
+ ext: { [EXT_KEY]: { emitterVersion: layout.emitterVersion, mode: selection.mode, settings: settings.ownership } },
640
865
  }
641
866
  assertObsidianContract('generation-manifest', manifest)
642
- const vaultSuffixes = new Set([...orderedNodes, ...embeddedAssets.values()].map((item) => identitySuffix(item.repo, item.id, 64)))
643
- assertOnlyVaultIdentities({
867
+ // What was laid out anyway and should be known, naming the note: a layout 2 generation records it.
868
+ const noteDiagnostics = legacy ? [] : [...readable.diagnostics, ...authorKeyNotes]
869
+
870
+ // The redaction guard, over the whole result. Its deny-list holds the
871
+ // identifiers of every census node and asset outside this view, and follows
872
+ // the audience. Of one the audience may not see (withheld), an unambiguous
873
+ // identifier refuses the view (an identity qualified by any repository of
874
+ // the census, a repository-qualified path, a repository-relative path with a
875
+ // folder, a vault path) and an ambiguous one is reported (a bare-word
876
+ // identity, a file name at a repository's root, such as README.md). Of one
877
+ // the audience may see but this view does not select, every identifier is
878
+ // reported. An identifier this view's own notes share names nothing outside
879
+ // it.
880
+ const viewAttachments = new Set(attachments.map((item) => item.path))
881
+ const visible = new Set(universe.nodes)
882
+ const censusRepositories = new Set([...canonical.nodes, ...censusAssets].filter(usable).map((item) => item.repo))
883
+ const qualified = (id) => id.indexOf(':') > 0 && censusRepositories.has(id.slice(0, id.indexOf(':')))
884
+ const refused = []
885
+ const reported = []
886
+ const unselected = []
887
+ const identifierOf = (item, withheld) => {
888
+ const id = String(item.id)
889
+ for (const [value, unambiguous] of [[id, qualified(id)], [`${item.repo}/${item.path}`, true], [item.path, item.path.includes('/')]]) {
890
+ ;(!withheld ? unselected : unambiguous ? refused : reported).push(value)
891
+ }
892
+ }
893
+ for (const item of canonical.nodes) {
894
+ if (!usable(item) || vault.has(item.id) || typeof item.path !== 'string') continue
895
+ const withheld = !visible.has(item.id)
896
+ identifierOf(item, withheld)
897
+ const allocated = legacy ? legacy.pathOf(item.repo, item.id) : null
898
+ if (allocated !== null) (withheld ? refused : unselected).push(allocated)
899
+ }
900
+ for (const item of censusAssets) if (usable(item) && typeof item.path === 'string' && !embeddedAssets.has(item.id)) identifierOf(item, !assets.has(item.id))
901
+ // Vault paths of notes and files outside this view, what other views hold for them and what this view held
902
+ // before: refused for what the audience may not see, reported otherwise (a note gone from the census included).
903
+ const inView = (repoId, nodeId) => vault.has(nodeId) && nodeById.get(nodeId)?.repo === repoId
904
+ const withheldNode = (repoId, nodeId) => nodeById.get(nodeId)?.repo === repoId && !visible.has(nodeId)
905
+ const copied = new Set([...embeddedAssets.values()].map((asset) => `${asset.repo}\u0000${asset.path}`))
906
+ const withheldAssets = new Set(censusAssets.filter((item) => usable(item) && typeof item.path === 'string' && !assets.has(item.id)).map((item) => `${item.repo}\u0000${item.path}`))
907
+ for (const section of Object.values(views)) {
908
+ for (const entry of section.entries) {
909
+ if (!inView(entry?.repoId, entry?.nodeId)) (withheldNode(entry?.repoId, entry?.nodeId) ? refused : unselected).push(entry?.path, ...(entry?.attachment ? [entry.attachment] : []))
910
+ }
911
+ for (const entry of section.assets) {
912
+ const key = `${entry?.repoId}\u0000${entry?.assetPath}`
913
+ if (!copied.has(key)) (withheldAssets.has(key) ? refused : unselected).push(entry?.path)
914
+ }
915
+ }
916
+ for (const note of priorManifest?.notes ?? []) if (!inView(note.repoId, note.nodeId)) (withheldNode(note.repoId, note.nodeId) ? refused : unselected).push(note.path)
917
+ const own = new Set()
918
+ for (const node of orderedNodes) for (const value of [node.id, `${node.repo}/${node.path}`, node.path, pathOf(node), attachmentOf(node)]) if (typeof value === 'string') own.add(value.normalize('NFC'))
919
+ for (const asset of embeddedAssets.values()) for (const value of [asset.id, `${asset.repo}/${asset.path}`, asset.path, assetPaths.get(asset.id)]) if (typeof value === 'string') own.add(value.normalize('NFC'))
920
+ const outside = (values) => values.filter((value) => typeof value === 'string' && value !== '' && !own.has(value.normalize('NFC')))
921
+ const linkTargets = new Set()
922
+ for (const node of orderedNodes) {
923
+ const target = noteLinkTarget(layout, pathOf(node))
924
+ linkTargets.add(target.markdown).add(target.wikilink)
925
+ }
926
+ for (const attachmentPath of viewAttachments) linkTargets.add(attachmentPath).add(encodeVaultPath(attachmentPath))
927
+ const allocatedInView = new Map(orderedNodes.map((node) => [node.id, pathOf(node)]))
928
+ const wrapperAttachments = new Set(orderedNodes.filter((node) => node.extension !== 'md').map((node) => (legacy ? `attachments/${legacyBasename(pathOf(node))}.${/^[a-z0-9]{1,16}$/.test(String(node.extension).toLowerCase()) ? String(node.extension).toLowerCase() : 'bin'}` : attachmentOf(node))))
929
+ const allowedAttachments = new Set([...wrapperAttachments, ...assetPaths.values()])
930
+ const guardReported = assertViewRedaction({
644
931
  manifest,
645
932
  files,
646
- vaultSuffixes,
647
- // Every census identity that is not part of this view: withheld, out of
648
- // the selection, or an asset this view does not copy.
649
- forbiddenSuffixes: new Set([
650
- // A record without a usable identity is not in any view and has no
651
- // suffix to forbid; it must not abort the view either.
652
- ...[...canonical.nodes, ...(Array.isArray(snapshot.graph.assets) ? snapshot.graph.assets : [])]
653
- .filter((item) => item && typeof item.repo === 'string' && item.repo !== '' && typeof item.id === 'string' && item.id !== '')
654
- .map((item) => identitySuffix(item.repo, item.id, 64)),
655
- ].filter((suffix) => !vaultSuffixes.has(suffix))),
656
- })
933
+ reused: reusedPaths,
934
+ nodeOf: (nodeId) => nodeById.get(nodeId),
935
+ ownOf: (nodeId) => wrapperLines(nodeById.get(nodeId)),
936
+ view: { allocatedPathOf: (nodeId) => allocatedInView.get(nodeId) ?? null, attachments: allowedAttachments, linkTargets },
937
+ deny: createDenyMatcher({ refuse: outside(refused), diagnose: outside(reported), notice: outside(unselected) }),
938
+ layoutVersion: layout.version,
939
+ }, guard)
940
+ if (!legacy) noteDiagnostics.push(...guardReported)
941
+ if (legacy) {
942
+ const vaultSuffixes = new Set([...orderedNodes, ...embeddedAssets.values()].map((item) => identitySuffix(item.repo, item.id, 64)))
943
+ assertOnlyVaultIdentities({
944
+ manifest,
945
+ files,
946
+ vaultSuffixes,
947
+ // Every census identity that is not part of this view: withheld, out of
948
+ // the selection, or an asset this view does not copy.
949
+ forbiddenSuffixes: new Set([
950
+ // A record without a usable identity is not in any view and has no
951
+ // suffix to forbid; it must not abort the view either.
952
+ ...[...canonical.nodes, ...censusAssets].filter(usable).map((item) => identitySuffix(item.repo, item.id, 64)),
953
+ ].filter((suffix) => !vaultSuffixes.has(suffix))),
954
+ })
955
+ }
657
956
 
658
957
  const prior = new Map((priorManifest?.notes ?? []).map((note) => [note.path, note.noteDigest]))
659
958
  const changes = { added: [], changed: [], unchanged: [], removed: [] }
@@ -666,12 +965,13 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
666
965
  // The cache is replaced only by a preparation that passed every check, and
667
966
  // holds exactly the notes of this view.
668
967
  if (nextCache) cache.notes = nextCache
968
+ if (noteDiagnostics.length > 0) manifest.ext[EXT_KEY].diagnostics = noteDiagnostics
669
969
 
670
970
  return {
671
971
  manifest,
672
972
  manifestBytes: utf8(`${JSON.stringify(manifest, null, 2)}\n`),
673
973
  files,
674
- persistentPathRegistry: registry,
974
+ persistentPathRegistry: withViewSection(persistentPathRegistry, { workspaceId: profile.workspaceId, scopeId: scope.scopeId, section: readable.section, keep: viewScopeIds }),
675
975
  changes,
676
976
  // How many notes were emitted on this call and how many were reused from the cache; without a cache every note is emitted.
677
977
  preparation,
@@ -683,55 +983,6 @@ export function prepareView({ snapshot, profile, scope, persistentPathRegistry =
683
983
  }
684
984
  }
685
985
 
686
- // Last check before anything is returned. Two rules, because the two kinds of
687
- // bytes differ: a substring the emitter itself produced from an allocated path
688
- // (note and attachment paths, rewritten link targets) may name only a note or
689
- // asset of this view; free text in a generated region (titles, tags, source
690
- // paths, which are the author's words) is refused only when it names an
691
- // identity of the census that is outside this view. Author text that merely
692
- // looks like an identity suffix, such as a content-hashed asset name, is not
693
- // a redaction failure.
694
- function assertOnlyVaultIdentities({ manifest, files, vaultSuffixes, forbiddenSuffixes }) {
695
- const index = (suffixes) => {
696
- const byPrefix = new Map()
697
- for (const full of suffixes) byPrefix.set(full.slice(0, 12), [...(byPrefix.get(full.slice(0, 12)) ?? []), full])
698
- return (suffix) => (byPrefix.get(suffix.slice(0, 12)) ?? []).some((full) => full.startsWith(suffix))
699
- }
700
- const allowed = index(vaultSuffixes)
701
- const forbidden = index(forbiddenSuffixes)
702
- const suffixesIn = (text) => [...text.matchAll(/--([0-9a-f]{12,64})(?![0-9a-f])/g)].map((match) => match[1])
703
- // An emitted path ends in the allocated suffix (before the extension); the
704
- // readable part before it is the author's title and may look like anything.
705
- // A wikilink rewrite may append `|<the author's own words>`; only the part
706
- // before the first `|` is an emitted path.
707
- const checkEmitted = (text) => {
708
- for (const segment of text.split('|')[0].split('#')[0].split('/')) {
709
- const trailing = /--([0-9a-f]{12,64})(?:\.[^./]+)?$/.exec(segment)
710
- if (trailing && !allowed(trailing[1])) refuse('redaction-failure', 'an emitted path names a note that is not part of this view')
711
- }
712
- for (const suffix of suffixesIn(text)) if (forbidden(suffix)) refuse('redaction-failure', 'an emitted path names a note that is not part of this view')
713
- }
714
- const checkText = (text) => {
715
- for (const suffix of suffixesIn(text)) if (forbidden(suffix)) refuse('redaction-failure', 'generated output names a note that is not part of this view')
716
- }
717
- const byPath = new Map(files.map((file) => [file.path, file.bytes]))
718
- for (const note of manifest.notes) {
719
- const bytes = byPath.get(note.path)
720
- for (const region of note.regions.generated) checkText(bytes.subarray(region.range.start, region.range.end).toString('utf8'))
721
- }
722
- for (const link of manifest.links) {
723
- for (const inversion of link.inversions ?? []) checkEmitted(Buffer.from(inversion.ext[EXT_KEY].emitted, 'base64url').toString('utf8'))
724
- }
725
- for (const note of manifest.notes) {
726
- for (const embed of note.ext[EXT_KEY].assetEmbeds ?? []) {
727
- checkEmitted(embed.attachment)
728
- for (const inversion of embed.inversions) checkEmitted(Buffer.from(inversion.ext[EXT_KEY].emitted, 'base64url').toString('utf8'))
729
- }
730
- }
731
- for (const note of manifest.notes) checkEmitted(note.path)
732
- for (const attachment of manifest.attachments) checkEmitted(attachment.path)
733
- }
734
-
735
986
  // Adds the fail-closed eligibility flag selectScope reads. A node is eligible
736
987
  // only when `isEligible` returns exactly true for it. Embedded assets follow
737
988
  // the same rule through `isAssetEligible`; without it every asset is withheld.