fundus 0.1.0 → 0.3.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 (226) hide show
  1. package/README.md +31 -0
  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/2.AA5XNK5k.css +1 -0
  6. package/build/client/_app/immutable/assets/2.AA5XNK5k.css.br +0 -0
  7. package/build/client/_app/immutable/assets/2.AA5XNK5k.css.gz +0 -0
  8. package/build/client/_app/immutable/assets/3.ChSpCG-T.css +1 -0
  9. package/build/client/_app/immutable/assets/3.ChSpCG-T.css.br +0 -0
  10. package/build/client/_app/immutable/assets/3.ChSpCG-T.css.gz +0 -0
  11. package/build/client/_app/immutable/assets/{LibraryContextMenu.CMefGNE2.css → LibraryContextMenu.CVb-Lqdf.css} +1 -1
  12. package/build/client/_app/immutable/assets/LibraryContextMenu.CVb-Lqdf.css.br +0 -0
  13. package/build/client/_app/immutable/assets/LibraryContextMenu.CVb-Lqdf.css.gz +0 -0
  14. package/build/client/_app/immutable/assets/ManifestSelector.Il8p28r8.css +1 -0
  15. package/build/client/_app/immutable/assets/ManifestSelector.Il8p28r8.css.br +0 -0
  16. package/build/client/_app/immutable/assets/ManifestSelector.Il8p28r8.css.gz +0 -0
  17. package/build/client/_app/immutable/chunks/BPJdVAz5.js +1 -0
  18. package/build/client/_app/immutable/chunks/BPJdVAz5.js.br +0 -0
  19. package/build/client/_app/immutable/chunks/BPJdVAz5.js.gz +0 -0
  20. package/build/client/_app/immutable/chunks/CLZr1L0m.js +108 -0
  21. package/build/client/_app/immutable/chunks/CLZr1L0m.js.br +0 -0
  22. package/build/client/_app/immutable/chunks/CLZr1L0m.js.gz +0 -0
  23. package/build/client/_app/immutable/chunks/{D9-155C9.js → CLcR3DTB.js} +1 -1
  24. package/build/client/_app/immutable/chunks/CLcR3DTB.js.br +4 -0
  25. package/build/client/_app/immutable/chunks/CLcR3DTB.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/DkK6dXFi.js +1 -0
  30. package/build/client/_app/immutable/chunks/DkK6dXFi.js.br +0 -0
  31. package/build/client/_app/immutable/chunks/DkK6dXFi.js.gz +0 -0
  32. package/build/client/_app/immutable/chunks/{DVEDrSOn.js → RVmTMq_g.js} +1 -1
  33. package/build/client/_app/immutable/chunks/RVmTMq_g.js.br +0 -0
  34. package/build/client/_app/immutable/chunks/RVmTMq_g.js.gz +0 -0
  35. package/build/client/_app/immutable/chunks/TZFh8bT7.js +1 -0
  36. package/build/client/_app/immutable/chunks/TZFh8bT7.js.br +0 -0
  37. package/build/client/_app/immutable/chunks/TZFh8bT7.js.gz +0 -0
  38. package/build/client/_app/immutable/entry/app.xrZU0FIS.js +2 -0
  39. package/build/client/_app/immutable/entry/app.xrZU0FIS.js.br +0 -0
  40. package/build/client/_app/immutable/entry/app.xrZU0FIS.js.gz +0 -0
  41. package/build/client/_app/immutable/entry/start.BT85omvY.js +1 -0
  42. package/build/client/_app/immutable/entry/start.BT85omvY.js.br +0 -0
  43. package/build/client/_app/immutable/entry/start.BT85omvY.js.gz +0 -0
  44. package/build/client/_app/immutable/nodes/0.BjZZ-SXT.js +2 -0
  45. package/build/client/_app/immutable/nodes/0.BjZZ-SXT.js.br +0 -0
  46. package/build/client/_app/immutable/nodes/0.BjZZ-SXT.js.gz +0 -0
  47. package/build/client/_app/immutable/nodes/1.0KLmHZC4.js +1 -0
  48. package/build/client/_app/immutable/nodes/1.0KLmHZC4.js.br +2 -0
  49. package/build/client/_app/immutable/nodes/1.0KLmHZC4.js.gz +0 -0
  50. package/build/client/_app/immutable/nodes/2.BG8fXI4n.js +3 -0
  51. package/build/client/_app/immutable/nodes/2.BG8fXI4n.js.br +0 -0
  52. package/build/client/_app/immutable/nodes/2.BG8fXI4n.js.gz +0 -0
  53. package/build/client/_app/immutable/nodes/3.DMFh-x1Y.js +1 -0
  54. package/build/client/_app/immutable/nodes/3.DMFh-x1Y.js.br +0 -0
  55. package/build/client/_app/immutable/nodes/3.DMFh-x1Y.js.gz +0 -0
  56. package/build/client/_app/immutable/nodes/4.CsuRmAsC.js +2 -0
  57. package/build/client/_app/immutable/nodes/4.CsuRmAsC.js.br +0 -0
  58. package/build/client/_app/immutable/nodes/4.CsuRmAsC.js.gz +0 -0
  59. package/build/client/_app/version.json +1 -1
  60. package/build/client/_app/version.json.br +0 -0
  61. package/build/client/_app/version.json.gz +0 -0
  62. package/build/client/favicon.svg +4 -0
  63. package/build/client/favicon.svg.br +0 -0
  64. package/build/client/favicon.svg.gz +0 -0
  65. package/build/handler.js +7 -7
  66. package/build/index.js +7 -7
  67. package/build/server/chunks/chunks/{context.js-CGD-b_vR.js → context.js-B77ZAVLi.js} +401 -31
  68. package/build/server/chunks/chunks/context.js-B77ZAVLi.js.map +1 -0
  69. package/build/server/chunks/chunks/{core.js-xfrkaQpS.js → core.js-EA_ysb1J.js} +130 -3
  70. package/build/server/chunks/chunks/core.js-EA_ysb1J.js.map +1 -0
  71. package/build/server/chunks/chunks/{exports.js-B4E0NJIt.js → exports.js-ScnpVC8o.js} +2 -2
  72. package/build/server/chunks/chunks/{exports.js-B4E0NJIt.js.map → exports.js-ScnpVC8o.js.map} +1 -1
  73. package/build/server/chunks/chunks/{http.js-Bu-Ao0Vk.js → http.js-DyoR9XRI.js} +5 -3
  74. package/build/server/chunks/chunks/http.js-DyoR9XRI.js.map +1 -0
  75. package/build/server/chunks/chunks/{internal.js-ClSxY3uX.js → internal.js-D_k3d8Qh.js} +4 -4
  76. package/build/server/chunks/chunks/{internal.js-ClSxY3uX.js.map → internal.js-D_k3d8Qh.js.map} +1 -1
  77. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  78. package/build/server/chunks/chunks/{library-store.js-CZCDpai0.js → library-store.js-DJ-JRE-g.js} +337 -20
  79. package/build/server/chunks/chunks/library-store.js-DJ-JRE-g.js.map +1 -0
  80. package/build/server/chunks/chunks/{shared.js-CHQOW_GN.js → shared.js-MLULcRRJ.js} +2 -2
  81. package/build/server/chunks/chunks/{shared.js-CHQOW_GN.js.map → shared.js-MLULcRRJ.js.map} +1 -1
  82. package/build/server/chunks/chunks/{utils.js-B9L6I-_K.js → utils.js-D64X9Sx8.js} +3 -3
  83. package/build/server/chunks/chunks/{utils.js-B9L6I-_K.js.map → utils.js-D64X9Sx8.js.map} +1 -1
  84. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-PO5RpNzP.js → _server.ts.js-H4Pq1KAE.js} +4 -5
  85. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-PO5RpNzP.js.map → _server.ts.js-H4Pq1KAE.js.map} +1 -1
  86. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-BmgqGEXs.js → _server.ts.js-BI3KHSfE.js} +5 -6
  87. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-BmgqGEXs.js.map → _server.ts.js-BI3KHSfE.js.map} +1 -1
  88. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DnTOlfHs.js → _server.ts.js-BlNYUbXX.js} +5 -6
  89. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DnTOlfHs.js.map → _server.ts.js-BlNYUbXX.js.map} +1 -1
  90. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-DzdC6OJE.js → _server.ts.js-j-_N540j.js} +9 -7
  91. package/build/server/chunks/entries/endpoints/api/assets/_server.ts.js-j-_N540j.js.map +1 -0
  92. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DXJNoYAs.js → _server.ts.js-BHKobzAg.js} +5 -6
  93. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DXJNoYAs.js.map → _server.ts.js-BHKobzAg.js.map} +1 -1
  94. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-CqdKXoZL.js → _server.ts.js-BFKjIOCv.js} +5 -6
  95. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-CqdKXoZL.js.map → _server.ts.js-BFKjIOCv.js.map} +1 -1
  96. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-DIuY66jG.js → _server.ts.js-BxZeVw8y.js} +5 -6
  97. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-DIuY66jG.js.map → _server.ts.js-BxZeVw8y.js.map} +1 -1
  98. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-D9p-cdJQ.js → _server.ts.js-CO36U_CT.js} +5 -6
  99. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-D9p-cdJQ.js.map → _server.ts.js-CO36U_CT.js.map} +1 -1
  100. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DP1x-xAW.js → _server.ts.js-CBlg1Bkl.js} +5 -6
  101. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DP1x-xAW.js.map → _server.ts.js-CBlg1Bkl.js.map} +1 -1
  102. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-r1eNMvPd.js → _server.ts.js-DcGLCjIs.js} +5 -6
  103. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-r1eNMvPd.js.map → _server.ts.js-DcGLCjIs.js.map} +1 -1
  104. package/build/server/chunks/entries/endpoints/api/raw/adopt/_server.ts.js-AqhtjOiL.js +32 -0
  105. package/build/server/chunks/entries/endpoints/api/raw/adopt/_server.ts.js-AqhtjOiL.js.map +1 -0
  106. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/_server.ts.js-CSa6-DcU.js +30 -0
  107. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/_server.ts.js-CSa6-DcU.js.map +1 -0
  108. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/_server.ts.js-DnzZUoAo.js +30 -0
  109. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/_server.ts.js-DnzZUoAo.js.map +1 -0
  110. package/build/server/chunks/entries/endpoints/api/reconcile/_server.ts.js-B0KMprKq.js +33 -0
  111. package/build/server/chunks/entries/endpoints/api/reconcile/_server.ts.js-B0KMprKq.js.map +1 -0
  112. package/build/server/chunks/entries/endpoints/api/reveal/_server.ts.js-Dhj5O9Lv.js +75 -0
  113. package/build/server/chunks/entries/endpoints/api/reveal/_server.ts.js-Dhj5O9Lv.js.map +1 -0
  114. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-D-m3BwRj.js → _server.ts.js-B1N9o1Ne.js} +5 -6
  115. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-D-m3BwRj.js.map → _server.ts.js-B1N9o1Ne.js.map} +1 -1
  116. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-3nyAQ3f0.js → _server.ts.js-B6HbnRqq.js} +4 -5
  117. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-3nyAQ3f0.js.map → _server.ts.js-B6HbnRqq.js.map} +1 -1
  118. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-Ba285FiJ.js → _server.ts.js-7wzPyG9v.js} +5 -6
  119. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-Ba285FiJ.js.map → _server.ts.js-7wzPyG9v.js.map} +1 -1
  120. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-X354l2NP.js → _server.ts.js-BWztxYge.js} +5 -6
  121. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-X354l2NP.js.map → _server.ts.js-BWztxYge.js.map} +1 -1
  122. package/build/server/chunks/entries/{hooks.server.js-eZ7H27L4.js → hooks.server.js-CVKLksJ1.js} +4 -5
  123. package/build/server/chunks/entries/{hooks.server.js-eZ7H27L4.js.map → hooks.server.js-CVKLksJ1.js.map} +1 -1
  124. package/build/server/chunks/{handler-Dcc_-rVh.js → handler-C22elTeC.js} +5 -5
  125. package/build/server/chunks/{handler-Dcc_-rVh.js.map → handler-C22elTeC.js.map} +1 -1
  126. package/build/server/chunks/{index.js-DoVHd4mf.js → index.js-CqN1-wfe.js} +5 -5
  127. package/build/server/chunks/{index.js-DoVHd4mf.js.map → index.js-CqN1-wfe.js.map} +1 -1
  128. package/build/server/chunks/{manifest.js-C_MsDhaa.js → manifest.js-5SY_cfDE.js} +58 -23
  129. package/build/server/chunks/manifest.js-5SY_cfDE.js.map +1 -0
  130. package/build/server/chunks/nodes/0.js-CoD1zSiQ.js +11 -0
  131. package/build/server/chunks/nodes/0.js-CoD1zSiQ.js.map +1 -0
  132. package/build/server/chunks/nodes/1.js-B92oHDrL.js +7 -0
  133. package/build/server/chunks/nodes/{1.js-DKlcrAQP.js.map → 1.js-B92oHDrL.js.map} +1 -1
  134. package/build/server/chunks/nodes/2.js-COlJ22cI.js +7 -0
  135. package/build/server/chunks/nodes/2.js-COlJ22cI.js.map +1 -0
  136. package/build/server/chunks/nodes/3.js-C1yCh8Tq.js +7 -0
  137. package/build/server/chunks/nodes/3.js-C1yCh8Tq.js.map +1 -0
  138. package/build/server/chunks/nodes/4.js-B9BEDKwq.js +7 -0
  139. package/build/server/chunks/nodes/{4.js-JQQZnHWZ.js.map → 4.js-B9BEDKwq.js.map} +1 -1
  140. package/dist/core/api-types.d.ts +38 -0
  141. package/dist/core/index.d.ts +2 -1
  142. package/dist/core/index.js +1 -0
  143. package/dist/core/raw-path.d.ts +8 -0
  144. package/dist/core/raw-path.js +63 -0
  145. package/dist/server/build.js +11 -2
  146. package/dist/server/check.d.ts +5 -1
  147. package/dist/server/check.js +80 -28
  148. package/dist/server/cli-commands.d.ts +1 -1
  149. package/dist/server/cli-commands.js +5 -3
  150. package/dist/server/cli-init.js +16 -1
  151. package/dist/server/file-manager.d.ts +2 -0
  152. package/dist/server/file-manager.js +17 -0
  153. package/dist/server/http.js +5 -1
  154. package/dist/server/library-store.d.ts +12 -0
  155. package/dist/server/library-store.js +25 -14
  156. package/dist/server/operations.d.ts +9 -1
  157. package/dist/server/operations.js +387 -31
  158. package/dist/server/processing/processor.js +3 -0
  159. package/dist/server/raw-reconciliation.d.ts +53 -0
  160. package/dist/server/raw-reconciliation.js +338 -0
  161. package/dist/server/raw-safety.d.ts +10 -0
  162. package/dist/server/raw-safety.js +73 -0
  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/2.ohi-TClG.css +0 -1
  168. package/build/client/_app/immutable/assets/2.ohi-TClG.css.br +0 -0
  169. package/build/client/_app/immutable/assets/2.ohi-TClG.css.gz +0 -0
  170. package/build/client/_app/immutable/assets/3.C-toFWI4.css +0 -1
  171. package/build/client/_app/immutable/assets/3.C-toFWI4.css.br +0 -0
  172. package/build/client/_app/immutable/assets/3.C-toFWI4.css.gz +0 -0
  173. package/build/client/_app/immutable/assets/LibraryContextMenu.CMefGNE2.css.br +0 -0
  174. package/build/client/_app/immutable/assets/LibraryContextMenu.CMefGNE2.css.gz +0 -0
  175. package/build/client/_app/immutable/chunks/BA62n9FX.js +0 -108
  176. package/build/client/_app/immutable/chunks/BA62n9FX.js.br +0 -0
  177. package/build/client/_app/immutable/chunks/BA62n9FX.js.gz +0 -0
  178. package/build/client/_app/immutable/chunks/CNRz3sSw.js.br +0 -0
  179. package/build/client/_app/immutable/chunks/CNRz3sSw.js.gz +0 -0
  180. package/build/client/_app/immutable/chunks/CQsKwm8E.js +0 -1
  181. package/build/client/_app/immutable/chunks/CQsKwm8E.js.br +0 -0
  182. package/build/client/_app/immutable/chunks/CQsKwm8E.js.gz +0 -0
  183. package/build/client/_app/immutable/chunks/D9-155C9.js.br +0 -0
  184. package/build/client/_app/immutable/chunks/D9-155C9.js.gz +0 -0
  185. package/build/client/_app/immutable/chunks/DVEDrSOn.js.br +0 -0
  186. package/build/client/_app/immutable/chunks/DVEDrSOn.js.gz +0 -0
  187. package/build/client/_app/immutable/chunks/cznBUb-E.js +0 -1
  188. package/build/client/_app/immutable/chunks/cznBUb-E.js.br +0 -0
  189. package/build/client/_app/immutable/chunks/cznBUb-E.js.gz +0 -0
  190. package/build/client/_app/immutable/entry/app.DwQWZs1F.js +0 -2
  191. package/build/client/_app/immutable/entry/app.DwQWZs1F.js.br +0 -0
  192. package/build/client/_app/immutable/entry/app.DwQWZs1F.js.gz +0 -0
  193. package/build/client/_app/immutable/entry/start.DEpAL_8Q.js +0 -1
  194. package/build/client/_app/immutable/entry/start.DEpAL_8Q.js.br +0 -0
  195. package/build/client/_app/immutable/entry/start.DEpAL_8Q.js.gz +0 -0
  196. package/build/client/_app/immutable/nodes/0.Sr1WDgiY.js +0 -2
  197. package/build/client/_app/immutable/nodes/0.Sr1WDgiY.js.br +0 -0
  198. package/build/client/_app/immutable/nodes/0.Sr1WDgiY.js.gz +0 -0
  199. package/build/client/_app/immutable/nodes/1.BwHqv728.js +0 -1
  200. package/build/client/_app/immutable/nodes/1.BwHqv728.js.br +0 -2
  201. package/build/client/_app/immutable/nodes/1.BwHqv728.js.gz +0 -0
  202. package/build/client/_app/immutable/nodes/2.DidxYIU-.js +0 -3
  203. package/build/client/_app/immutable/nodes/2.DidxYIU-.js.br +0 -0
  204. package/build/client/_app/immutable/nodes/2.DidxYIU-.js.gz +0 -0
  205. package/build/client/_app/immutable/nodes/3.C1AavoTf.js +0 -1
  206. package/build/client/_app/immutable/nodes/3.C1AavoTf.js.br +0 -0
  207. package/build/client/_app/immutable/nodes/3.C1AavoTf.js.gz +0 -0
  208. package/build/client/_app/immutable/nodes/4.D8UoOONM.js +0 -2
  209. package/build/client/_app/immutable/nodes/4.D8UoOONM.js.br +0 -0
  210. package/build/client/_app/immutable/nodes/4.D8UoOONM.js.gz +0 -0
  211. package/build/server/chunks/chunks/context.js-CGD-b_vR.js.map +0 -1
  212. package/build/server/chunks/chunks/core.js-xfrkaQpS.js.map +0 -1
  213. package/build/server/chunks/chunks/folder-path.js-ytx9ucgg.js +0 -64
  214. package/build/server/chunks/chunks/folder-path.js-ytx9ucgg.js.map +0 -1
  215. package/build/server/chunks/chunks/http.js-Bu-Ao0Vk.js.map +0 -1
  216. package/build/server/chunks/chunks/library-store.js-CZCDpai0.js.map +0 -1
  217. package/build/server/chunks/entries/endpoints/api/assets/_server.ts.js-DzdC6OJE.js.map +0 -1
  218. package/build/server/chunks/manifest.js-C_MsDhaa.js.map +0 -1
  219. package/build/server/chunks/nodes/0.js-BP6hHAK9.js +0 -11
  220. package/build/server/chunks/nodes/0.js-BP6hHAK9.js.map +0 -1
  221. package/build/server/chunks/nodes/1.js-DKlcrAQP.js +0 -7
  222. package/build/server/chunks/nodes/2.js-B-8TTZH_.js +0 -7
  223. package/build/server/chunks/nodes/2.js-B-8TTZH_.js.map +0 -1
  224. package/build/server/chunks/nodes/3.js-Cf7H6K5p.js +0 -7
  225. package/build/server/chunks/nodes/3.js-Cf7H6K5p.js.map +0 -1
  226. package/build/server/chunks/nodes/4.js-JQQZnHWZ.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,15 +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
  }
128
303
  /** Replace the raw bytes/name and run the normal processing + codegen tail. */
129
- async replaceRawFile(id, options) {
304
+ replaceRawFile(id, options) {
305
+ return this.#enqueueOperation(() => this.#replaceRawFile(id, options));
306
+ }
307
+ async #replaceRawFile(id, options) {
130
308
  return this.#afterMutation(await this.#store.replaceRawFile(id, options));
131
309
  }
132
310
  /** Move a selected asset set as one editor action and roll back on any failure. */
133
- async moveAssets(assetIds, folder) {
311
+ moveAssets(assetIds, folder) {
312
+ return this.#enqueueOperation(() => this.#moveAssets(assetIds, folder));
313
+ }
314
+ async #moveAssets(assetIds, folder) {
134
315
  const uniqueIds = [...new Set(assetIds)];
135
316
  if (uniqueIds.length === 0)
136
317
  throw new Error('At least one asset id is required.');
@@ -158,16 +339,28 @@ export class FundusOperations {
158
339
  throw cause;
159
340
  }
160
341
  }
161
- async createFolder(path) {
342
+ createFolder(path) {
343
+ return this.#enqueueOperation(() => this.#createFolder(path));
344
+ }
345
+ async #createFolder(path) {
162
346
  return this.#afterMutation(await this.#store.createFolder(path));
163
347
  }
164
- async renameFolder(path, newName) {
348
+ renameFolder(path, newName) {
349
+ return this.#enqueueOperation(() => this.#renameFolder(path, newName));
350
+ }
351
+ async #renameFolder(path, newName) {
165
352
  return this.#afterMutation(await this.#store.renameFolder(path, newName));
166
353
  }
167
- async moveFolder(path, parent) {
354
+ moveFolder(path, parent) {
355
+ return this.#enqueueOperation(() => this.#moveFolder(path, parent));
356
+ }
357
+ async #moveFolder(path, parent) {
168
358
  return this.#afterMutation(await this.#store.moveFolder(path, parent));
169
359
  }
170
- async deleteFolder(path) {
360
+ deleteFolder(path) {
361
+ return this.#enqueueOperation(() => this.#deleteFolder(path));
362
+ }
363
+ async #deleteFolder(path) {
171
364
  const result = await this.#store.deleteFolder(path);
172
365
  return this.#afterMutation(result.library);
173
366
  }
@@ -179,7 +372,10 @@ export class FundusOperations {
179
372
  * rejected with nothing written. The REST `patchAsset` keeps the editor's
180
373
  * lenient replace semantics.
181
374
  */
182
- async setAsset(id, changes) {
375
+ setAsset(id, changes) {
376
+ return this.#enqueueOperation(() => this.#setAsset(id, changes));
377
+ }
378
+ async #setAsset(id, changes) {
183
379
  const library = await this.#store.read();
184
380
  this.#requireAsset(library, id);
185
381
  return this.#afterMutation(await this.#applyAssetChanges(library, id, changes));
@@ -189,7 +385,10 @@ export class FundusOperations {
189
385
  * a referrer keeps pointing at the old id, so renaming a referenced asset
190
386
  * introduces dangling-reference errors. Update the referrers first.
191
387
  */
192
- async renameAsset(id, newId) {
388
+ renameAsset(id, newId) {
389
+ return this.#enqueueOperation(() => this.#renameAsset(id, newId));
390
+ }
391
+ async #renameAsset(id, newId) {
193
392
  const library = await this.#store.read();
194
393
  this.#requireAsset(library, id);
195
394
  // A collision or invalid new id is the store's own (clearer) error; the
@@ -208,7 +407,10 @@ export class FundusOperations {
208
407
  * (the asset is a reference target) is rejected before anything —
209
408
  * including the raw file — is removed; the REST route stays lenient.
210
409
  */
211
- async deleteAsset(id, options) {
410
+ deleteAsset(id, options) {
411
+ return this.#enqueueOperation(() => this.#deleteAsset(id, options));
412
+ }
413
+ async #deleteAsset(id, options) {
212
414
  if (options?.failOnIntroducedIssues) {
213
415
  const library = await this.#store.read();
214
416
  this.#requireAsset(library, id);
@@ -219,20 +421,32 @@ export class FundusOperations {
219
421
  return this.#afterMutation(await this.#store.deleteAsset(id));
220
422
  }
221
423
  /** Delete a selected asset set as one lenient editor mutation. */
222
- async deleteAssets(assetIds) {
424
+ deleteAssets(assetIds) {
425
+ return this.#enqueueOperation(() => this.#deleteAssets(assetIds));
426
+ }
427
+ async #deleteAssets(assetIds) {
223
428
  const uniqueIds = [...new Set(assetIds)];
224
429
  if (uniqueIds.length === 0)
225
430
  throw new Error('At least one asset id is required.');
226
431
  return this.#afterMutation(await this.#store.deleteAssets(uniqueIds));
227
432
  }
228
- async createManifest(name) {
433
+ createManifest(name) {
434
+ return this.#enqueueOperation(() => this.#createManifest(name));
435
+ }
436
+ async #createManifest(name) {
229
437
  return this.#afterMutation(await this.#store.createManifest(name));
230
438
  }
231
- async setManifestAppearance(name, appearance) {
439
+ setManifestAppearance(name, appearance) {
440
+ return this.#enqueueOperation(() => this.#setManifestAppearance(name, appearance));
441
+ }
442
+ async #setManifestAppearance(name, appearance) {
232
443
  return this.#afterMutation(await this.#store.setManifestAppearance(name, appearance));
233
444
  }
234
445
  /** Apply a PATCH body: appearance fields and/or a rename — in that order. */
235
- async patchManifest(name, body) {
446
+ patchManifest(name, body) {
447
+ return this.#enqueueOperation(() => this.#patchManifest(name, body));
448
+ }
449
+ async #patchManifest(name, body) {
236
450
  let library = await this.#store.setManifestAppearance(name, {
237
451
  color: body.color,
238
452
  emoji: body.emoji
@@ -251,17 +465,33 @@ export class FundusOperations {
251
465
  * the module must be updated by hand — both the file name and the exported
252
466
  * const change with the manifest name.
253
467
  */
254
- async renameManifest(name, newName) {
468
+ renameManifest(name, newName) {
469
+ return this.#enqueueOperation(() => this.#renameManifest(name, newName));
470
+ }
471
+ async #renameManifest(name, newName) {
255
472
  return this.#afterMutation(await this.#store.renameManifest(name, newName));
256
473
  }
257
- async deleteManifest(name) {
474
+ deleteManifest(name) {
475
+ return this.#enqueueOperation(() => this.#deleteManifest(name));
476
+ }
477
+ async #deleteManifest(name) {
258
478
  return this.#afterMutation(await this.#store.deleteManifest(name));
259
479
  }
260
480
  /** The autosave chain tail: process + regenerate, then report fresh state. */
261
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) {
262
490
  const pipelineIssues = [];
263
- const issues = await this.#store.validate(library);
264
- 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) {
265
495
  try {
266
496
  const report = await processAssets(this.#config, library, this.#registry);
267
497
  await generateManifestModules(this.#config, library, report.proxies, this.#registry);
@@ -272,13 +502,15 @@ export class FundusOperations {
272
502
  });
273
503
  }
274
504
  }
275
- 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
+ };
276
512
  }
277
- async #buildState(library, extraIssues, precomputedIssues) {
278
- const issues = [
279
- ...(precomputedIssues ?? (await this.#store.validate(library))),
280
- ...extraIssues
281
- ];
513
+ async #buildState(library, issues, classification) {
282
514
  const referencesByAsset = collectAllReferences(library, descriptorsByType(this.#registry));
283
515
  const membershipByManifest = computeLibraryMembership(library, referencesByAsset);
284
516
  const delivered = allDeliveredAssetIds(membershipByManifest);
@@ -378,15 +610,139 @@ export class FundusOperations {
378
610
  for (const [type, plugin] of Object.entries(this.#registry)) {
379
611
  presetNames[type] = Object.keys(plugin.presets(this.#config)).sort();
380
612
  }
613
+ const rawIngestionQueue = this.#buildRawIngestionQueue(library, classification);
381
614
  return {
382
615
  library,
383
616
  assets,
384
617
  issues,
385
618
  presetNames,
386
619
  manifestMemberships,
387
- manifestDeliveredBytes
620
+ manifestDeliveredBytes,
621
+ rawIngestionQueue
388
622
  };
389
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
+ }
390
746
  #requireAsset(library, id) {
391
747
  // Own-property check — an inherited key ("constructor", "toString", …)
392
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>;