fundus 0.0.4 → 0.2.0

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 (224) hide show
  1. package/README.md +49 -8
  2. package/build/client/_app/immutable/assets/0.inkPq3ni.css +1 -0
  3. package/build/client/_app/immutable/assets/0.inkPq3ni.css.br +0 -0
  4. package/build/client/_app/immutable/assets/0.inkPq3ni.css.gz +0 -0
  5. package/build/client/_app/immutable/assets/3.ChSpCG-T.css +1 -0
  6. package/build/client/_app/immutable/assets/3.ChSpCG-T.css.br +0 -0
  7. package/build/client/_app/immutable/assets/3.ChSpCG-T.css.gz +0 -0
  8. package/build/client/_app/immutable/assets/{LibraryContextMenu.CMefGNE2.css → LibraryContextMenu.BYvU4SY7.css} +1 -1
  9. package/build/client/_app/immutable/assets/LibraryContextMenu.BYvU4SY7.css.br +0 -0
  10. package/build/client/_app/immutable/assets/LibraryContextMenu.BYvU4SY7.css.gz +0 -0
  11. package/build/client/_app/immutable/assets/ManifestSelector.Il8p28r8.css +1 -0
  12. package/build/client/_app/immutable/assets/ManifestSelector.Il8p28r8.css.br +0 -0
  13. package/build/client/_app/immutable/assets/ManifestSelector.Il8p28r8.css.gz +0 -0
  14. package/build/client/_app/immutable/chunks/BAEZPv6c.js +1 -0
  15. package/build/client/_app/immutable/chunks/BAEZPv6c.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/BAEZPv6c.js.gz +0 -0
  17. package/build/client/_app/immutable/chunks/{DVEQFclp.js → ByVSyIj7.js} +1 -1
  18. package/build/client/_app/immutable/chunks/ByVSyIj7.js.br +1 -0
  19. package/build/client/_app/immutable/chunks/ByVSyIj7.js.gz +0 -0
  20. package/build/client/_app/immutable/chunks/C8W3KwDq.js +1 -0
  21. package/build/client/_app/immutable/chunks/C8W3KwDq.js.br +0 -0
  22. package/build/client/_app/immutable/chunks/C8W3KwDq.js.gz +0 -0
  23. package/build/client/_app/immutable/chunks/CP-bnOxU.js +1 -0
  24. package/build/client/_app/immutable/chunks/CP-bnOxU.js.br +0 -0
  25. package/build/client/_app/immutable/chunks/CP-bnOxU.js.gz +0 -0
  26. package/build/client/_app/immutable/chunks/{CNRz3sSw.js → D65cSBhe.js} +2 -2
  27. package/build/client/_app/immutable/chunks/D65cSBhe.js.br +0 -0
  28. package/build/client/_app/immutable/chunks/D65cSBhe.js.gz +0 -0
  29. package/build/client/_app/immutable/chunks/Db8LRH6X.js +108 -0
  30. package/build/client/_app/immutable/chunks/Db8LRH6X.js.br +0 -0
  31. package/build/client/_app/immutable/chunks/Db8LRH6X.js.gz +0 -0
  32. package/build/client/_app/immutable/chunks/TZFh8bT7.js +1 -0
  33. package/build/client/_app/immutable/chunks/TZFh8bT7.js.br +0 -0
  34. package/build/client/_app/immutable/chunks/TZFh8bT7.js.gz +0 -0
  35. package/build/client/_app/immutable/entry/app.nM6SgHKJ.js +2 -0
  36. package/build/client/_app/immutable/entry/app.nM6SgHKJ.js.br +0 -0
  37. package/build/client/_app/immutable/entry/app.nM6SgHKJ.js.gz +0 -0
  38. package/build/client/_app/immutable/entry/start.DNXvkhtP.js +1 -0
  39. package/build/client/_app/immutable/entry/start.DNXvkhtP.js.br +0 -0
  40. package/build/client/_app/immutable/entry/start.DNXvkhtP.js.gz +0 -0
  41. package/build/client/_app/immutable/nodes/0.QeXQNCp2.js +2 -0
  42. package/build/client/_app/immutable/nodes/0.QeXQNCp2.js.br +0 -0
  43. package/build/client/_app/immutable/nodes/0.QeXQNCp2.js.gz +0 -0
  44. package/build/client/_app/immutable/nodes/1.dMklHTc2.js +1 -0
  45. package/build/client/_app/immutable/nodes/1.dMklHTc2.js.br +0 -0
  46. package/build/client/_app/immutable/nodes/1.dMklHTc2.js.gz +0 -0
  47. package/build/client/_app/immutable/nodes/2.CW4VMcIP.js +3 -0
  48. package/build/client/_app/immutable/nodes/2.CW4VMcIP.js.br +0 -0
  49. package/build/client/_app/immutable/nodes/2.CW4VMcIP.js.gz +0 -0
  50. package/build/client/_app/immutable/nodes/3.CTiY_pbK.js +1 -0
  51. package/build/client/_app/immutable/nodes/3.CTiY_pbK.js.br +0 -0
  52. package/build/client/_app/immutable/nodes/3.CTiY_pbK.js.gz +0 -0
  53. package/build/client/_app/immutable/nodes/4.Dgxx52FJ.js +2 -0
  54. package/build/client/_app/immutable/nodes/4.Dgxx52FJ.js.br +0 -0
  55. package/build/client/_app/immutable/nodes/4.Dgxx52FJ.js.gz +0 -0
  56. package/build/client/_app/version.json +1 -1
  57. package/build/client/_app/version.json.br +0 -0
  58. package/build/client/_app/version.json.gz +0 -0
  59. package/build/client/favicon.svg +4 -0
  60. package/build/client/favicon.svg.br +0 -0
  61. package/build/client/favicon.svg.gz +0 -0
  62. package/build/handler.js +7 -7
  63. package/build/index.js +7 -7
  64. package/build/server/chunks/chunks/{context.js-BUZqFfWG.js → context.js-B77ZAVLi.js} +405 -31
  65. package/build/server/chunks/chunks/context.js-B77ZAVLi.js.map +1 -0
  66. package/build/server/chunks/chunks/{core.js-xfrkaQpS.js → core.js-EA_ysb1J.js} +130 -3
  67. package/build/server/chunks/chunks/core.js-EA_ysb1J.js.map +1 -0
  68. package/build/server/chunks/chunks/{exports.js-C0rwMMtk.js → exports.js-DvhE7Ffd.js} +2 -2
  69. package/build/server/chunks/chunks/{exports.js-C0rwMMtk.js.map → exports.js-DvhE7Ffd.js.map} +1 -1
  70. package/build/server/chunks/chunks/{http.js-DvopzwKh.js → http.js-DyoR9XRI.js} +5 -3
  71. package/build/server/chunks/chunks/http.js-DyoR9XRI.js.map +1 -0
  72. package/build/server/chunks/chunks/{internal.js-DGo2jPcg.js → internal.js-Dn37LrAW.js} +4 -4
  73. package/build/server/chunks/chunks/{internal.js-DGo2jPcg.js.map → internal.js-Dn37LrAW.js.map} +1 -1
  74. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  75. package/build/server/chunks/chunks/{library-store.js-ZLudCJlT.js → library-store.js-DJ-JRE-g.js} +388 -20
  76. package/build/server/chunks/chunks/library-store.js-DJ-JRE-g.js.map +1 -0
  77. package/build/server/chunks/chunks/{shared.js-B1gC9MiJ.js → shared.js-Dob0TlTu.js} +2 -2
  78. package/build/server/chunks/chunks/{shared.js-B1gC9MiJ.js.map → shared.js-Dob0TlTu.js.map} +1 -1
  79. package/build/server/chunks/chunks/{utils.js-DnEJ3UjO.js → utils.js-B4YimMxv.js} +3 -3
  80. package/build/server/chunks/chunks/{utils.js-DnEJ3UjO.js.map → utils.js-B4YimMxv.js.map} +1 -1
  81. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-Cq5gXVQT.js → _server.ts.js-H4Pq1KAE.js} +4 -5
  82. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-Cq5gXVQT.js.map → _server.ts.js-H4Pq1KAE.js.map} +1 -1
  83. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-RXbedi8q.js → _server.ts.js-BI3KHSfE.js} +5 -6
  84. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-RXbedi8q.js.map → _server.ts.js-BI3KHSfE.js.map} +1 -1
  85. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/_server.ts.js-BlNYUbXX.js +34 -0
  86. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/_server.ts.js-BlNYUbXX.js.map +1 -0
  87. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-D7Qae9vU.js → _server.ts.js-j-_N540j.js} +9 -7
  88. package/build/server/chunks/entries/endpoints/api/assets/_server.ts.js-j-_N540j.js.map +1 -0
  89. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DtO20kgB.js → _server.ts.js-BHKobzAg.js} +5 -6
  90. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DtO20kgB.js.map → _server.ts.js-BHKobzAg.js.map} +1 -1
  91. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-BUebPbd5.js → _server.ts.js-BFKjIOCv.js} +5 -6
  92. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-BUebPbd5.js.map → _server.ts.js-BFKjIOCv.js.map} +1 -1
  93. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-BjT2qPMT.js → _server.ts.js-BxZeVw8y.js} +5 -6
  94. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-BjT2qPMT.js.map → _server.ts.js-BxZeVw8y.js.map} +1 -1
  95. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-DYRLsoT7.js → _server.ts.js-CO36U_CT.js} +5 -6
  96. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-DYRLsoT7.js.map → _server.ts.js-CO36U_CT.js.map} +1 -1
  97. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-BbN2fGNN.js → _server.ts.js-CBlg1Bkl.js} +5 -6
  98. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-BbN2fGNN.js.map → _server.ts.js-CBlg1Bkl.js.map} +1 -1
  99. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-b_MKZ41b.js → _server.ts.js-DcGLCjIs.js} +5 -6
  100. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-b_MKZ41b.js.map → _server.ts.js-DcGLCjIs.js.map} +1 -1
  101. package/build/server/chunks/entries/endpoints/api/raw/adopt/_server.ts.js-AqhtjOiL.js +32 -0
  102. package/build/server/chunks/entries/endpoints/api/raw/adopt/_server.ts.js-AqhtjOiL.js.map +1 -0
  103. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/_server.ts.js-CSa6-DcU.js +30 -0
  104. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/_server.ts.js-CSa6-DcU.js.map +1 -0
  105. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/_server.ts.js-DnzZUoAo.js +30 -0
  106. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/_server.ts.js-DnzZUoAo.js.map +1 -0
  107. package/build/server/chunks/entries/endpoints/api/reconcile/_server.ts.js-B0KMprKq.js +33 -0
  108. package/build/server/chunks/entries/endpoints/api/reconcile/_server.ts.js-B0KMprKq.js.map +1 -0
  109. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-evEfMpvn.js → _server.ts.js-B1N9o1Ne.js} +5 -6
  110. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-evEfMpvn.js.map → _server.ts.js-B1N9o1Ne.js.map} +1 -1
  111. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-C04XE_1w.js → _server.ts.js-B6HbnRqq.js} +4 -5
  112. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-C04XE_1w.js.map → _server.ts.js-B6HbnRqq.js.map} +1 -1
  113. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-DB8TdoEv.js → _server.ts.js-7wzPyG9v.js} +5 -6
  114. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-DB8TdoEv.js.map → _server.ts.js-7wzPyG9v.js.map} +1 -1
  115. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-BrpaijZO.js → _server.ts.js-BWztxYge.js} +5 -6
  116. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-BrpaijZO.js.map → _server.ts.js-BWztxYge.js.map} +1 -1
  117. package/build/server/chunks/entries/{hooks.server.js-BJJwZW_I.js → hooks.server.js-CVKLksJ1.js} +4 -5
  118. package/build/server/chunks/entries/{hooks.server.js-BJJwZW_I.js.map → hooks.server.js-CVKLksJ1.js.map} +1 -1
  119. package/build/server/chunks/{handler-DnbS2t1B.js → handler-DZ2Sps4a.js} +5 -5
  120. package/build/server/chunks/{handler-DnbS2t1B.js.map → handler-DZ2Sps4a.js.map} +1 -1
  121. package/build/server/chunks/{index.js-g77q3Boj.js → index.js-CdmTWQz8.js} +5 -5
  122. package/build/server/chunks/{index.js-g77q3Boj.js.map → index.js-CdmTWQz8.js.map} +1 -1
  123. package/build/server/chunks/{manifest.js-qmEXbstS.js → manifest.js-GhfNr35x.js} +57 -22
  124. package/build/server/chunks/manifest.js-GhfNr35x.js.map +1 -0
  125. package/build/server/chunks/nodes/0.js-DGT86_i-.js +11 -0
  126. package/build/server/chunks/nodes/0.js-DGT86_i-.js.map +1 -0
  127. package/build/server/chunks/nodes/1.js-DQgYreU0.js +7 -0
  128. package/build/server/chunks/nodes/{1.js-H-qwo4GJ.js.map → 1.js-DQgYreU0.js.map} +1 -1
  129. package/build/server/chunks/nodes/2.js-DItreSrX.js +7 -0
  130. package/build/server/chunks/nodes/2.js-DItreSrX.js.map +1 -0
  131. package/build/server/chunks/nodes/3.js-bE1dJ_6v.js +7 -0
  132. package/build/server/chunks/nodes/3.js-bE1dJ_6v.js.map +1 -0
  133. package/build/server/chunks/nodes/4.js-BebbMVXy.js +7 -0
  134. package/build/server/chunks/nodes/{4.js-BKL7LSSm.js.map → 4.js-BebbMVXy.js.map} +1 -1
  135. package/dist/cli.js +32 -9
  136. package/dist/core/api-types.d.ts +39 -1
  137. package/dist/core/index.d.ts +3 -2
  138. package/dist/core/index.js +2 -1
  139. package/dist/core/ingest.d.ts +2 -0
  140. package/dist/core/ingest.js +7 -0
  141. package/dist/core/raw-path.d.ts +8 -0
  142. package/dist/core/raw-path.js +63 -0
  143. package/dist/server/build.js +11 -2
  144. package/dist/server/check.d.ts +5 -1
  145. package/dist/server/check.js +80 -28
  146. package/dist/server/cli-commands.d.ts +3 -2
  147. package/dist/server/cli-commands.js +47 -4
  148. package/dist/server/cli-init.d.ts +1 -1
  149. package/dist/server/cli-init.js +38 -5
  150. package/dist/server/http.js +5 -1
  151. package/dist/server/index.d.ts +1 -1
  152. package/dist/server/library-store.d.ts +23 -0
  153. package/dist/server/library-store.js +82 -14
  154. package/dist/server/operations.d.ts +12 -2
  155. package/dist/server/operations.js +391 -31
  156. package/dist/server/processing/processor.js +3 -0
  157. package/dist/server/raw-reconciliation.d.ts +53 -0
  158. package/dist/server/raw-reconciliation.js +338 -0
  159. package/dist/server/raw-safety.d.ts +10 -0
  160. package/dist/server/raw-safety.js +73 -0
  161. package/dist/server/start.d.ts +4 -2
  162. package/dist/server/start.js +43 -11
  163. package/package.json +1 -1
  164. package/build/client/_app/immutable/assets/0.DLhIsYA_.css +0 -1
  165. package/build/client/_app/immutable/assets/0.DLhIsYA_.css.br +0 -0
  166. package/build/client/_app/immutable/assets/0.DLhIsYA_.css.gz +0 -0
  167. package/build/client/_app/immutable/assets/3.JwgtcTEd.css +0 -1
  168. package/build/client/_app/immutable/assets/3.JwgtcTEd.css.br +0 -0
  169. package/build/client/_app/immutable/assets/3.JwgtcTEd.css.gz +0 -0
  170. package/build/client/_app/immutable/assets/LibraryContextMenu.CMefGNE2.css.br +0 -0
  171. package/build/client/_app/immutable/assets/LibraryContextMenu.CMefGNE2.css.gz +0 -0
  172. package/build/client/_app/immutable/chunks/CNRz3sSw.js.br +0 -0
  173. package/build/client/_app/immutable/chunks/CNRz3sSw.js.gz +0 -0
  174. package/build/client/_app/immutable/chunks/CO4RBJfa.js +0 -1
  175. package/build/client/_app/immutable/chunks/CO4RBJfa.js.br +0 -0
  176. package/build/client/_app/immutable/chunks/CO4RBJfa.js.gz +0 -0
  177. package/build/client/_app/immutable/chunks/DOpBpjxS.js +0 -108
  178. package/build/client/_app/immutable/chunks/DOpBpjxS.js.br +0 -0
  179. package/build/client/_app/immutable/chunks/DOpBpjxS.js.gz +0 -0
  180. package/build/client/_app/immutable/chunks/DVEQFclp.js.br +0 -1
  181. package/build/client/_app/immutable/chunks/DVEQFclp.js.gz +0 -0
  182. package/build/client/_app/immutable/chunks/DiFoWL2c.js +0 -1
  183. package/build/client/_app/immutable/chunks/DiFoWL2c.js.br +0 -0
  184. package/build/client/_app/immutable/chunks/DiFoWL2c.js.gz +0 -0
  185. package/build/client/_app/immutable/chunks/DkADHpZF.js +0 -1
  186. package/build/client/_app/immutable/chunks/DkADHpZF.js.br +0 -0
  187. package/build/client/_app/immutable/chunks/DkADHpZF.js.gz +0 -0
  188. package/build/client/_app/immutable/entry/app.CKgtYXQb.js +0 -2
  189. package/build/client/_app/immutable/entry/app.CKgtYXQb.js.br +0 -0
  190. package/build/client/_app/immutable/entry/app.CKgtYXQb.js.gz +0 -0
  191. package/build/client/_app/immutable/entry/start.CYZu5qE1.js +0 -1
  192. package/build/client/_app/immutable/entry/start.CYZu5qE1.js.br +0 -0
  193. package/build/client/_app/immutable/entry/start.CYZu5qE1.js.gz +0 -0
  194. package/build/client/_app/immutable/nodes/0.I_1SApYQ.js +0 -2
  195. package/build/client/_app/immutable/nodes/0.I_1SApYQ.js.br +0 -0
  196. package/build/client/_app/immutable/nodes/0.I_1SApYQ.js.gz +0 -0
  197. package/build/client/_app/immutable/nodes/1.v6eUTnWq.js +0 -1
  198. package/build/client/_app/immutable/nodes/1.v6eUTnWq.js.br +0 -2
  199. package/build/client/_app/immutable/nodes/1.v6eUTnWq.js.gz +0 -0
  200. package/build/client/_app/immutable/nodes/2.DoA7Dfl-.js +0 -3
  201. package/build/client/_app/immutable/nodes/2.DoA7Dfl-.js.br +0 -0
  202. package/build/client/_app/immutable/nodes/2.DoA7Dfl-.js.gz +0 -0
  203. package/build/client/_app/immutable/nodes/3.C-DO70Bs.js +0 -1
  204. package/build/client/_app/immutable/nodes/3.C-DO70Bs.js.br +0 -0
  205. package/build/client/_app/immutable/nodes/3.C-DO70Bs.js.gz +0 -0
  206. package/build/client/_app/immutable/nodes/4.CALOuSyz.js +0 -2
  207. package/build/client/_app/immutable/nodes/4.CALOuSyz.js.br +0 -0
  208. package/build/client/_app/immutable/nodes/4.CALOuSyz.js.gz +0 -0
  209. package/build/server/chunks/chunks/context.js-BUZqFfWG.js.map +0 -1
  210. package/build/server/chunks/chunks/core.js-xfrkaQpS.js.map +0 -1
  211. package/build/server/chunks/chunks/folder-path.js-ytx9ucgg.js +0 -64
  212. package/build/server/chunks/chunks/folder-path.js-ytx9ucgg.js.map +0 -1
  213. package/build/server/chunks/chunks/http.js-DvopzwKh.js.map +0 -1
  214. package/build/server/chunks/chunks/library-store.js-ZLudCJlT.js.map +0 -1
  215. package/build/server/chunks/entries/endpoints/api/assets/_server.ts.js-D7Qae9vU.js.map +0 -1
  216. package/build/server/chunks/manifest.js-qmEXbstS.js.map +0 -1
  217. package/build/server/chunks/nodes/0.js-Bk6IZ-vD.js +0 -11
  218. package/build/server/chunks/nodes/0.js-Bk6IZ-vD.js.map +0 -1
  219. package/build/server/chunks/nodes/1.js-H-qwo4GJ.js +0 -7
  220. package/build/server/chunks/nodes/2.js-DRpXVLwp.js +0 -7
  221. package/build/server/chunks/nodes/2.js-DRpXVLwp.js.map +0 -1
  222. package/build/server/chunks/nodes/3.js-CDEzNFeU.js +0 -7
  223. package/build/server/chunks/nodes/3.js-CDEzNFeU.js.map +0 -1
  224. package/build/server/chunks/nodes/4.js-BKL7LSSm.js +0 -7
@@ -6,12 +6,14 @@
6
6
  * operation has exactly one implementation and there is no other save path.
7
7
  */
8
8
  import { join } from 'node:path';
9
- import { allDeliveredAssetIds, collectAllReferences, compareCodeUnits, computeLibraryMembership, folderOfRawPath, rawFileNameOfPath } from '../core/index.js';
9
+ import { assetIdError, allDeliveredAssetIds, collectAllReferences, compareCodeUnits, computeLibraryMembership, folderOfRawPath, parentFolderPath, portableFolderPathKey, proposeAssetId, rawFileNameOfPath, typesForFileName } from '../core/index.js';
10
10
  import { LibraryStore } from './library-store.js';
11
11
  import { generateManifestModules } from './codegen.js';
12
12
  import { descriptorsByType, serverPluginForType, serverPluginsByType } from './plugins.js';
13
13
  import { processAssets, readCurrentProxy } from './processing/processor.js';
14
14
  import { encodeRelativePathForUrl } from './folder-filesystem.js';
15
+ import { classifyRawDirectory, verifyStableRawFile } from './raw-reconciliation.js';
16
+ import { rawSafetyIssues } from './raw-safety.js';
15
17
  /**
16
18
  * The CLI's `--parameters` merge semantics (M7): a SHALLOW merge at the top
17
19
  * level onto the current parameters — a nested object value replaces the whole
@@ -42,6 +44,8 @@ export class FundusOperations {
42
44
  #config;
43
45
  #registry;
44
46
  #store;
47
+ #operationQueue = Promise.resolve();
48
+ #pipelineQueue = Promise.resolve();
45
49
  constructor(config, registry = serverPluginsByType) {
46
50
  this.#config = config;
47
51
  this.#registry = registry;
@@ -53,9 +57,171 @@ export class FundusOperations {
53
57
  get store() {
54
58
  return this.#store;
55
59
  }
56
- /** Read-only state snapshot (no processing). */
57
- async state() {
58
- return this.#buildState(await this.#store.read(), []);
60
+ #enqueueOperation(operation) {
61
+ const run = this.#operationQueue.then(operation, operation);
62
+ this.#operationQueue = run.then(() => { }, () => { });
63
+ return run;
64
+ }
65
+ /** Read-only state snapshot, serialized with mutations but never processing or writing. */
66
+ state() {
67
+ return this.#enqueueOperation(() => this.#state());
68
+ }
69
+ async #state() {
70
+ await this.#pipelineQueue;
71
+ const library = await this.#store.read();
72
+ const classification = await this.#classifyRawDirectory(library);
73
+ return this.#snapshotState(library, classification);
74
+ }
75
+ /** Reconcile only changes that are safe without a user decision: unique moves and deletions. */
76
+ reconcileRawState() {
77
+ return this.#enqueueOperation(async () => {
78
+ let library = await this.#store.read();
79
+ let classification = await this.#classifyRawDirectory(library);
80
+ if (!this.#canAutoReconcile(classification)) {
81
+ return this.#snapshotState(library, classification);
82
+ }
83
+ for (let pass = 0; pass < 2; pass++) {
84
+ const result = await this.#store.transactCandidate(async (candidate, current) => {
85
+ const fresh = await this.#classifyRawDirectory(current);
86
+ const affectedIds = [];
87
+ if (!this.#canAutoReconcile(fresh))
88
+ return { classification: fresh, affectedIds };
89
+ for (const move of fresh.moves) {
90
+ candidate.assets[move.assetId].raw = move.path;
91
+ this.#registerFolderAncestors(candidate, folderOfRawPath(move.path));
92
+ affectedIds.push(move.assetId);
93
+ }
94
+ for (const deletion of fresh.deletions) {
95
+ delete candidate.assets[deletion.assetId];
96
+ affectedIds.push(deletion.assetId);
97
+ }
98
+ return { classification: fresh, affectedIds };
99
+ });
100
+ library = result.library;
101
+ classification = result.value.classification;
102
+ if (!result.changed)
103
+ return this.#snapshotState(library, classification);
104
+ await this.#store.syncFolders(library);
105
+ for (const id of result.value.affectedIds)
106
+ await this.#store.deleteProxiesOf(id);
107
+ const snapshot = await this.#afterMutationSnapshot(library);
108
+ library = snapshot.library;
109
+ classification = snapshot.classification;
110
+ if (classification.moves.length === 0 && classification.deletions.length === 0) {
111
+ return snapshot.state;
112
+ }
113
+ if (!this.#canAutoReconcile(classification, true))
114
+ return snapshot.state;
115
+ }
116
+ return this.#snapshotState(library, classification, [
117
+ {
118
+ message: 'Raw directory kept changing during reconciliation; refresh again after external file operations settle.'
119
+ }
120
+ ]);
121
+ });
122
+ }
123
+ /** Adopt an already-existing raw file without copying, renaming, or deleting it. */
124
+ adoptRawFile(token, id, type, manifests = []) {
125
+ return this.#enqueueOperation(async () => {
126
+ const result = await this.#store.transactCandidate(async (candidate, current) => {
127
+ const classification = await this.#classifyRawDirectory(current);
128
+ const file = classification.unmanaged.find((entry) => this.#newToken(entry.path, entry.hash) === token);
129
+ if (!file)
130
+ throw new Error('This ingestion item is stale; refresh the editor and try again.');
131
+ const idProblem = assetIdError(id);
132
+ if (idProblem)
133
+ throw new Error(idProblem);
134
+ if (Object.hasOwn(candidate.assets, id))
135
+ throw new Error(`An asset with id "${id}" already exists.`);
136
+ const compatibleTypes = this.#compatibleTypes(file.path);
137
+ if (!compatibleTypes.includes(type)) {
138
+ throw new Error(`The ${type} type does not accept "${file.path}".`);
139
+ }
140
+ const uniqueManifests = [...new Set(manifests)];
141
+ for (const name of uniqueManifests) {
142
+ if (!Object.hasOwn(candidate.manifests, name)) {
143
+ throw new Error(`Unknown manifest "${name}".`);
144
+ }
145
+ }
146
+ const plugin = serverPluginForType(type, this.#registry);
147
+ let parameters = plugin.descriptor.defaultParameters(plugin.pluginConfig(this.#config));
148
+ parameters = plugin.descriptor.validateParameters(parameters);
149
+ if (plugin.seedIngestParameters) {
150
+ const seeded = await plugin.seedIngestParameters({
151
+ rawPath: join(this.#config.rawDir, file.path),
152
+ rawFileName: rawFileNameOfPath(file.path),
153
+ parameters,
154
+ config: this.#config
155
+ });
156
+ if (seeded !== undefined)
157
+ parameters = plugin.descriptor.validateParameters(seeded);
158
+ }
159
+ await verifyStableRawFile(join(this.#config.rawDir, file.path), file.hash);
160
+ this.#registerFolderAncestors(candidate, folderOfRawPath(file.path));
161
+ candidate.assets[id] = {
162
+ type,
163
+ raw: file.path,
164
+ rawHash: file.hash,
165
+ parameters,
166
+ manifests: uniqueManifests.sort()
167
+ };
168
+ });
169
+ await this.#store.syncFolders(result.library);
170
+ return this.#afterMutation(result.library);
171
+ });
172
+ }
173
+ /** Reset/reseed edited bytes, optionally changing to another compatible type. */
174
+ acceptEditedRawFile(token, type) {
175
+ return this.#enqueueOperation(async () => {
176
+ const result = await this.#store.transactCandidate(async (candidate, current) => {
177
+ const classification = await this.#classifyRawDirectory(current);
178
+ const edited = this.#editedRawFiles(classification, current).find((entry) => this.#editedToken(entry.assetId, entry.path, entry.hash) === token);
179
+ if (!edited)
180
+ throw new Error('This edited item is stale; refresh the editor and try again.');
181
+ if (!this.#compatibleTypes(edited.path).includes(type)) {
182
+ throw new Error(`The ${type} type does not accept "${edited.path}".`);
183
+ }
184
+ const plugin = serverPluginForType(type, this.#registry);
185
+ let parameters = plugin.descriptor.validateParameters(plugin.descriptor.defaultParameters(plugin.pluginConfig(this.#config)));
186
+ if (plugin.seedIngestParameters) {
187
+ const seeded = await plugin.seedIngestParameters({
188
+ rawPath: join(this.#config.rawDir, edited.path),
189
+ rawFileName: rawFileNameOfPath(edited.path),
190
+ parameters,
191
+ config: this.#config
192
+ });
193
+ if (seeded !== undefined)
194
+ parameters = plugin.descriptor.validateParameters(seeded);
195
+ }
196
+ await verifyStableRawFile(join(this.#config.rawDir, edited.path), edited.hash);
197
+ const record = candidate.assets[edited.assetId];
198
+ record.type = type;
199
+ record.rawHash = edited.hash;
200
+ record.parameters = parameters;
201
+ return edited.assetId;
202
+ });
203
+ await this.#store.deleteProxiesOf(result.value);
204
+ return this.#afterMutation(result.library);
205
+ });
206
+ }
207
+ /** Accept edited bytes while preserving type, parameters, path and memberships. */
208
+ dismissEditedRawFile(token) {
209
+ return this.#enqueueOperation(async () => {
210
+ const result = await this.#store.transactCandidate(async (candidate, current) => {
211
+ const classification = await this.#classifyRawDirectory(current);
212
+ const edited = this.#editedRawFiles(classification, current).find((entry) => this.#editedToken(entry.assetId, entry.path, entry.hash) === token);
213
+ if (!edited)
214
+ throw new Error('This edited item is stale; refresh the editor and try again.');
215
+ const record = current.assets[edited.assetId];
216
+ if (!this.#compatibleTypes(edited.path).includes(record.type)) {
217
+ throw new Error(`The moved file "${edited.path}" is not compatible with its current ${record.type} type; choose a compatible type and process it.`);
218
+ }
219
+ candidate.assets[edited.assetId].rawHash = edited.hash;
220
+ return edited.assetId;
221
+ });
222
+ await this.#store.deleteProxiesOf(result.value);
223
+ return this.#afterMutation(result.library);
224
+ });
59
225
  }
60
226
  /**
61
227
  * Ingest an upload: store the raw file + record, then let the owning
@@ -66,7 +232,10 @@ export class FundusOperations {
66
232
  * explicit membership set in the same operation; a failing application
67
233
  * rolls the whole ingest back, so a failed ingest leaves nothing behind.
68
234
  */
69
- async ingest(options) {
235
+ ingest(options) {
236
+ return this.#enqueueOperation(() => this.#ingest(options));
237
+ }
238
+ async #ingest(options) {
70
239
  let library = await this.#store.ingest(options);
71
240
  try {
72
241
  const plugin = serverPluginForType(options.type, this.#registry);
@@ -96,7 +265,10 @@ export class FundusOperations {
96
265
  return this.#afterMutation(library);
97
266
  }
98
267
  /** Apply a PATCH body: parameters and/or manifests and/or a rename — in that order. */
99
- async patchAsset(id, body) {
268
+ patchAsset(id, body) {
269
+ return this.#enqueueOperation(() => this.#patchAsset(id, body));
270
+ }
271
+ async #patchAsset(id, body) {
100
272
  let library = await this.#store.read();
101
273
  this.#requireAsset(library, id);
102
274
  if (body.parameters !== undefined) {
@@ -122,11 +294,24 @@ export class FundusOperations {
122
294
  }
123
295
  return this.#afterMutation(library);
124
296
  }
125
- async moveAsset(id, folder) {
297
+ moveAsset(id, folder) {
298
+ return this.#enqueueOperation(() => this.#moveAsset(id, folder));
299
+ }
300
+ async #moveAsset(id, folder) {
126
301
  return this.#afterMutation(await this.#store.moveAsset(id, folder));
127
302
  }
303
+ /** Replace the raw bytes/name and run the normal processing + codegen tail. */
304
+ replaceRawFile(id, options) {
305
+ return this.#enqueueOperation(() => this.#replaceRawFile(id, options));
306
+ }
307
+ async #replaceRawFile(id, options) {
308
+ return this.#afterMutation(await this.#store.replaceRawFile(id, options));
309
+ }
128
310
  /** Move a selected asset set as one editor action and roll back on any failure. */
129
- async moveAssets(assetIds, folder) {
311
+ moveAssets(assetIds, folder) {
312
+ return this.#enqueueOperation(() => this.#moveAssets(assetIds, folder));
313
+ }
314
+ async #moveAssets(assetIds, folder) {
130
315
  const uniqueIds = [...new Set(assetIds)];
131
316
  if (uniqueIds.length === 0)
132
317
  throw new Error('At least one asset id is required.');
@@ -154,16 +339,28 @@ export class FundusOperations {
154
339
  throw cause;
155
340
  }
156
341
  }
157
- async createFolder(path) {
342
+ createFolder(path) {
343
+ return this.#enqueueOperation(() => this.#createFolder(path));
344
+ }
345
+ async #createFolder(path) {
158
346
  return this.#afterMutation(await this.#store.createFolder(path));
159
347
  }
160
- async renameFolder(path, newName) {
348
+ renameFolder(path, newName) {
349
+ return this.#enqueueOperation(() => this.#renameFolder(path, newName));
350
+ }
351
+ async #renameFolder(path, newName) {
161
352
  return this.#afterMutation(await this.#store.renameFolder(path, newName));
162
353
  }
163
- async moveFolder(path, parent) {
354
+ moveFolder(path, parent) {
355
+ return this.#enqueueOperation(() => this.#moveFolder(path, parent));
356
+ }
357
+ async #moveFolder(path, parent) {
164
358
  return this.#afterMutation(await this.#store.moveFolder(path, parent));
165
359
  }
166
- async deleteFolder(path) {
360
+ deleteFolder(path) {
361
+ return this.#enqueueOperation(() => this.#deleteFolder(path));
362
+ }
363
+ async #deleteFolder(path) {
167
364
  const result = await this.#store.deleteFolder(path);
168
365
  return this.#afterMutation(result.library);
169
366
  }
@@ -175,7 +372,10 @@ export class FundusOperations {
175
372
  * rejected with nothing written. The REST `patchAsset` keeps the editor's
176
373
  * lenient replace semantics.
177
374
  */
178
- async setAsset(id, changes) {
375
+ setAsset(id, changes) {
376
+ return this.#enqueueOperation(() => this.#setAsset(id, changes));
377
+ }
378
+ async #setAsset(id, changes) {
179
379
  const library = await this.#store.read();
180
380
  this.#requireAsset(library, id);
181
381
  return this.#afterMutation(await this.#applyAssetChanges(library, id, changes));
@@ -185,7 +385,10 @@ export class FundusOperations {
185
385
  * a referrer keeps pointing at the old id, so renaming a referenced asset
186
386
  * introduces dangling-reference errors. Update the referrers first.
187
387
  */
188
- async renameAsset(id, newId) {
388
+ renameAsset(id, newId) {
389
+ return this.#enqueueOperation(() => this.#renameAsset(id, newId));
390
+ }
391
+ async #renameAsset(id, newId) {
189
392
  const library = await this.#store.read();
190
393
  this.#requireAsset(library, id);
191
394
  // A collision or invalid new id is the store's own (clearer) error; the
@@ -204,7 +407,10 @@ export class FundusOperations {
204
407
  * (the asset is a reference target) is rejected before anything —
205
408
  * including the raw file — is removed; the REST route stays lenient.
206
409
  */
207
- async deleteAsset(id, options) {
410
+ deleteAsset(id, options) {
411
+ return this.#enqueueOperation(() => this.#deleteAsset(id, options));
412
+ }
413
+ async #deleteAsset(id, options) {
208
414
  if (options?.failOnIntroducedIssues) {
209
415
  const library = await this.#store.read();
210
416
  this.#requireAsset(library, id);
@@ -215,20 +421,32 @@ export class FundusOperations {
215
421
  return this.#afterMutation(await this.#store.deleteAsset(id));
216
422
  }
217
423
  /** Delete a selected asset set as one lenient editor mutation. */
218
- async deleteAssets(assetIds) {
424
+ deleteAssets(assetIds) {
425
+ return this.#enqueueOperation(() => this.#deleteAssets(assetIds));
426
+ }
427
+ async #deleteAssets(assetIds) {
219
428
  const uniqueIds = [...new Set(assetIds)];
220
429
  if (uniqueIds.length === 0)
221
430
  throw new Error('At least one asset id is required.');
222
431
  return this.#afterMutation(await this.#store.deleteAssets(uniqueIds));
223
432
  }
224
- async createManifest(name) {
433
+ createManifest(name) {
434
+ return this.#enqueueOperation(() => this.#createManifest(name));
435
+ }
436
+ async #createManifest(name) {
225
437
  return this.#afterMutation(await this.#store.createManifest(name));
226
438
  }
227
- async setManifestAppearance(name, appearance) {
439
+ setManifestAppearance(name, appearance) {
440
+ return this.#enqueueOperation(() => this.#setManifestAppearance(name, appearance));
441
+ }
442
+ async #setManifestAppearance(name, appearance) {
228
443
  return this.#afterMutation(await this.#store.setManifestAppearance(name, appearance));
229
444
  }
230
445
  /** Apply a PATCH body: appearance fields and/or a rename — in that order. */
231
- async patchManifest(name, body) {
446
+ patchManifest(name, body) {
447
+ return this.#enqueueOperation(() => this.#patchManifest(name, body));
448
+ }
449
+ async #patchManifest(name, body) {
232
450
  let library = await this.#store.setManifestAppearance(name, {
233
451
  color: body.color,
234
452
  emoji: body.emoji
@@ -247,17 +465,33 @@ export class FundusOperations {
247
465
  * the module must be updated by hand — both the file name and the exported
248
466
  * const change with the manifest name.
249
467
  */
250
- async renameManifest(name, newName) {
468
+ renameManifest(name, newName) {
469
+ return this.#enqueueOperation(() => this.#renameManifest(name, newName));
470
+ }
471
+ async #renameManifest(name, newName) {
251
472
  return this.#afterMutation(await this.#store.renameManifest(name, newName));
252
473
  }
253
- async deleteManifest(name) {
474
+ deleteManifest(name) {
475
+ return this.#enqueueOperation(() => this.#deleteManifest(name));
476
+ }
477
+ async #deleteManifest(name) {
254
478
  return this.#afterMutation(await this.#store.deleteManifest(name));
255
479
  }
256
480
  /** The autosave chain tail: process + regenerate, then report fresh state. */
257
481
  async #afterMutation(library) {
482
+ return (await this.#afterMutationSnapshot(library)).state;
483
+ }
484
+ async #afterMutationSnapshot(library) {
485
+ const run = this.#pipelineQueue.then(() => this.#runAfterMutation(library), () => this.#runAfterMutation(library));
486
+ this.#pipelineQueue = run.then(() => { }, () => { });
487
+ return run;
488
+ }
489
+ async #runAfterMutation(library) {
258
490
  const pipelineIssues = [];
259
- const issues = await this.#store.validate(library);
260
- if (issues.length === 0) {
491
+ const classification = await this.#classifyRawDirectory(library);
492
+ const ordinaryIssues = await this.#store.validate(library);
493
+ const safetyIssues = this.#rawSafetyIssues(library, classification);
494
+ if (ordinaryIssues.length === 0 && safetyIssues.length === 0) {
261
495
  try {
262
496
  const report = await processAssets(this.#config, library, this.#registry);
263
497
  await generateManifestModules(this.#config, library, report.proxies, this.#registry);
@@ -268,13 +502,15 @@ export class FundusOperations {
268
502
  });
269
503
  }
270
504
  }
271
- return this.#buildState(library, pipelineIssues, issues);
505
+ const latestLibrary = await this.#store.read();
506
+ const latestClassification = await this.#classifyRawDirectory(latestLibrary);
507
+ return {
508
+ library: latestLibrary,
509
+ classification: latestClassification,
510
+ state: await this.#snapshotState(latestLibrary, latestClassification, pipelineIssues)
511
+ };
272
512
  }
273
- async #buildState(library, extraIssues, precomputedIssues) {
274
- const issues = [
275
- ...(precomputedIssues ?? (await this.#store.validate(library))),
276
- ...extraIssues
277
- ];
513
+ async #buildState(library, issues, classification) {
278
514
  const referencesByAsset = collectAllReferences(library, descriptorsByType(this.#registry));
279
515
  const membershipByManifest = computeLibraryMembership(library, referencesByAsset);
280
516
  const delivered = allDeliveredAssetIds(membershipByManifest);
@@ -334,7 +570,7 @@ export class FundusOperations {
334
570
  type: record.type,
335
571
  raw: record.raw,
336
572
  folder: folderOfRawPath(record.raw),
337
- rawUrl: `/files/raw/${encodeRelativePathForUrl(record.raw)}`,
573
+ rawUrl: `/files/raw/${encodeRelativePathForUrl(record.raw)}?v=${record.rawHash.slice(0, 12)}`,
338
574
  rawHash: record.rawHash,
339
575
  parameters: record.parameters,
340
576
  manifests: record.manifests,
@@ -374,15 +610,139 @@ export class FundusOperations {
374
610
  for (const [type, plugin] of Object.entries(this.#registry)) {
375
611
  presetNames[type] = Object.keys(plugin.presets(this.#config)).sort();
376
612
  }
613
+ const rawIngestionQueue = this.#buildRawIngestionQueue(library, classification);
377
614
  return {
378
615
  library,
379
616
  assets,
380
617
  issues,
381
618
  presetNames,
382
619
  manifestMemberships,
383
- manifestDeliveredBytes
620
+ manifestDeliveredBytes,
621
+ rawIngestionQueue
384
622
  };
385
623
  }
624
+ #compatibleTypes(path) {
625
+ return typesForFileName(path, Object.values(this.#registry).map((plugin) => plugin.descriptor)).sort(compareCodeUnits);
626
+ }
627
+ #classifyRawDirectory(library) {
628
+ return classifyRawDirectory(this.#config.rawDir, library, (path) => this.#compatibleTypes(path).length > 0);
629
+ }
630
+ #rawSafetyIssues(library, classification) {
631
+ return rawSafetyIssues(library, classification, (path) => this.#compatibleTypes(path));
632
+ }
633
+ async #snapshotState(library, classification, extraIssues = []) {
634
+ const issues = [
635
+ ...(await this.#store.validate(library)),
636
+ ...this.#rawSafetyIssues(library, classification),
637
+ ...extraIssues
638
+ ];
639
+ const seen = new Set();
640
+ const uniqueIssues = issues.filter((issue) => {
641
+ const key = `${issue.assetId ?? ''}|${issue.message}`;
642
+ if (seen.has(key))
643
+ return false;
644
+ seen.add(key);
645
+ return true;
646
+ });
647
+ return this.#buildState(library, uniqueIssues, classification);
648
+ }
649
+ #canAutoReconcile(classification, retry = false) {
650
+ return (classification.complete &&
651
+ classification.blockers.length === 0 &&
652
+ classification.moves.length + classification.deletions.length > 0 &&
653
+ (!retry || (classification.edited.length === 0 && classification.unmanaged.length === 0)));
654
+ }
655
+ #newToken(path, hash) {
656
+ return `new:${path}:${hash}`;
657
+ }
658
+ #editedToken(assetId, path, hash) {
659
+ return `edited:${assetId}:${path}:${hash}`;
660
+ }
661
+ #proposedId(path, reservedIds) {
662
+ const base = proposeAssetId(rawFileNameOfPath(path));
663
+ if (!reservedIds.has(base))
664
+ return base;
665
+ for (let index = 2;; index++) {
666
+ const candidate = `${base}${index}`;
667
+ if (!reservedIds.has(candidate))
668
+ return candidate;
669
+ }
670
+ }
671
+ #buildRawIngestionQueue(library, classification) {
672
+ const queue = [];
673
+ const reservedIds = new Set(Object.keys(library.assets));
674
+ for (const file of classification.unmanaged) {
675
+ const compatibleTypes = this.#compatibleTypes(file.path);
676
+ const proposedId = this.#proposedId(file.path, reservedIds);
677
+ reservedIds.add(proposedId);
678
+ queue.push({
679
+ kind: 'new',
680
+ token: this.#newToken(file.path, file.hash),
681
+ path: file.path,
682
+ hash: file.hash,
683
+ compatibleTypes,
684
+ proposedId
685
+ });
686
+ }
687
+ for (const edited of this.#editedRawFiles(classification, library)) {
688
+ const currentType = library.assets[edited.assetId].type;
689
+ queue.push({
690
+ kind: 'edited',
691
+ token: this.#editedToken(edited.assetId, edited.path, edited.hash),
692
+ path: edited.path,
693
+ hash: edited.hash,
694
+ assetId: edited.assetId,
695
+ currentType,
696
+ compatibleTypes: this.#compatibleTypes(edited.path)
697
+ });
698
+ }
699
+ for (const blocker of classification.blockers) {
700
+ queue.push({
701
+ kind: 'unsupported',
702
+ token: `unsupported:${blocker.path}:${blocker.reason}`,
703
+ path: blocker.path,
704
+ reason: blocker.reason
705
+ });
706
+ }
707
+ for (const diagnostic of classification.diagnostics) {
708
+ queue.push({
709
+ kind: 'unsupported',
710
+ token: `unsupported:raw-directory:${diagnostic.reason}`,
711
+ path: diagnostic.path || 'Raw directory',
712
+ reason: diagnostic.reason
713
+ });
714
+ }
715
+ return queue.sort((a, b) => compareCodeUnits(a.path, b.path));
716
+ }
717
+ #editedRawFiles(classification, library) {
718
+ const edited = new Map(classification.edited.map((entry) => [entry.assetId, entry]));
719
+ for (const assetId of classification.unchangedAssetIds) {
720
+ const record = library.assets[assetId];
721
+ if (!this.#compatibleTypes(record.raw).includes(record.type)) {
722
+ edited.set(assetId, { assetId, path: record.raw, hash: record.rawHash });
723
+ }
724
+ }
725
+ return [...edited.values()].sort((a, b) => compareCodeUnits(a.path, b.path));
726
+ }
727
+ #registerFolderAncestors(library, folder) {
728
+ if (folder === '')
729
+ return;
730
+ const missing = [];
731
+ for (let current = folder; current !== ''; current = parentFolderPath(current)) {
732
+ if (!library.folders.includes(current))
733
+ missing.push(current);
734
+ }
735
+ const keys = new Map(library.folders.map((path) => [portableFolderPathKey(path), path]));
736
+ for (const path of missing.reverse()) {
737
+ const collision = keys.get(portableFolderPathKey(path));
738
+ if (collision !== undefined && collision !== path) {
739
+ throw new Error(`Folder "${path}" collides with existing folder "${collision}".`);
740
+ }
741
+ library.folders.push(path);
742
+ keys.set(portableFolderPathKey(path), path);
743
+ }
744
+ library.folders.sort(compareCodeUnits);
745
+ }
386
746
  #requireAsset(library, id) {
387
747
  // Own-property check — an inherited key ("constructor", "toString", …)
388
748
  // must not pass as an existing asset.
@@ -20,6 +20,7 @@ import { allDeliveredAssetIds, collectAllReferences, computeLibraryMembership, f
20
20
  import { jobHash } from '../job-hash.js';
21
21
  import { descriptorsByType, processingParametersOf, serverPluginForType, serverPluginsByType } from '../plugins.js';
22
22
  import { ensureManagedFolder, listRelativeFiles } from '../folder-filesystem.js';
23
+ import { verifyStableRawFile } from '../raw-reconciliation.js';
23
24
  /** Proxy file name shape; its id segment is pinned to core's asset-id grammar. */
24
25
  const PROXY_FILE_PATTERN = /^[a-z][A-Za-z0-9]*\.[0-9a-f]{8}\.[a-z0-9]+$/;
25
26
  /** Whether a file name in the proxy directory is (shaped like) a fundus proxy. */
@@ -93,6 +94,7 @@ async function processAssetFile(record, rawPath, proxyPath, config, registry) {
93
94
  const stem = basename(proxyPath, extension);
94
95
  const temporaryPath = join(dirname(proxyPath), `.${stem}.${crypto.randomUUID()}.tmp${extension}`);
95
96
  try {
97
+ await verifyStableRawFile(rawPath, record.rawHash);
96
98
  const output = await plugin.process({
97
99
  sourcePath: rawPath,
98
100
  targetPath: temporaryPath,
@@ -100,6 +102,7 @@ async function processAssetFile(record, rawPath, proxyPath, config, registry) {
100
102
  preset: plugin.resolvePreset(parameters, config),
101
103
  config
102
104
  });
105
+ await verifyStableRawFile(rawPath, record.rawHash);
103
106
  await rename(temporaryPath, proxyPath);
104
107
  return output;
105
108
  }
@@ -0,0 +1,53 @@
1
+ import { type Library } from '../core/index.js';
2
+ export interface ScannedRawFile {
3
+ path: string;
4
+ hash: string;
5
+ }
6
+ export type RawEntryKind = 'file' | 'directory' | 'symlink' | 'special';
7
+ export interface RawDiagnostic {
8
+ /** Empty only for a root-level diagnostic. */
9
+ path: string;
10
+ reason: string;
11
+ }
12
+ export interface RawBlocker extends RawDiagnostic {
13
+ assetId?: string;
14
+ }
15
+ export interface RawScan {
16
+ complete: boolean;
17
+ diagnostics: RawDiagnostic[];
18
+ files: ScannedRawFile[];
19
+ blockers: RawBlocker[];
20
+ /** Every retained entry, including traversal directories and blockers. */
21
+ occupiedPaths: Set<string>;
22
+ entryKinds: Map<string, RawEntryKind>;
23
+ /** Unsupported regular files, retained unhashed only for deletion-safety checks. */
24
+ ignoredRegularPaths: string[];
25
+ }
26
+ export interface EditedRawFile extends ScannedRawFile {
27
+ assetId: string;
28
+ }
29
+ export interface RawMove extends ScannedRawFile {
30
+ assetId: string;
31
+ from: string;
32
+ }
33
+ export interface RawClassification {
34
+ complete: boolean;
35
+ diagnostics: RawDiagnostic[];
36
+ unchangedAssetIds: string[];
37
+ edited: EditedRawFile[];
38
+ moves: RawMove[];
39
+ deletions: Array<{
40
+ assetId: string;
41
+ path: string;
42
+ }>;
43
+ unmanaged: ScannedRawFile[];
44
+ blockers: RawBlocker[];
45
+ }
46
+ /** Hash one regular file without following its final path component. */
47
+ export declare function stableRawFileHash(path: string): Promise<string>;
48
+ export declare function verifyStableRawFile(path: string, expectedHash: string): Promise<void>;
49
+ /** Enumerate without following symlinks and retain structural scan completeness. */
50
+ export declare function scanRawDirectory(rawDir: string, includeFile?: (path: string) => boolean): Promise<RawScan>;
51
+ /** Pure deterministic matching: exact path first, then only 1:1 hash groups. */
52
+ export declare function classifyRawFiles(library: Library, scan: RawScan): RawClassification;
53
+ export declare function classifyRawDirectory(rawDir: string, library: Library, includeExternalFile?: (path: string) => boolean): Promise<RawClassification>;