@cleocode/cleo 2026.9.7 → 2026.9.10
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.
- package/dist/cli/index.js +3873 -2234
- package/package.json +14 -13
- package/scripts/check-cleo-tarball-size.mjs +105 -0
- package/scripts/copy-studio-dist.mjs +33 -9
- package/studio-dist/client/_app/immutable/chunks/{wI_OOyoh.js → BB7zPpV2.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{D8NsLz6i.js → BOnrCZaR.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{eK8hf4s7.js → BSRBuVG3.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/C6jhAnil.js +1 -0
- package/studio-dist/client/_app/immutable/chunks/C87Q3uxm.js +1 -0
- package/studio-dist/client/_app/immutable/chunks/{DVHIxkgs.js → CLZBEpLY.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{CqF5Lomf.js → CTLGzgdN.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{o1PZ6Kzq.js → CY3heNub.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{RptFUp2P.js → Cge6qiwc.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{BRgWBQlV.js → CnwXLaZJ.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{Bxz2A0ZC.js → DEH43YwF.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/DGftG5oL.js +3 -0
- package/studio-dist/client/_app/immutable/chunks/DX_JOBSO.js +1 -0
- package/studio-dist/client/_app/immutable/chunks/{CEqOYim7.js → DZrdToAH.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/EZwHPWle.js +2 -0
- package/studio-dist/client/_app/immutable/chunks/cWVw9W0E.js +3 -0
- package/studio-dist/client/_app/immutable/chunks/{B9psmq5-.js → rB_71s8h.js} +1 -1
- package/studio-dist/client/_app/immutable/chunks/{JPJd4fEI.js → rUtW7Hx6.js} +1 -1
- package/studio-dist/client/_app/immutable/entry/app.tvu3-wbp.js +2 -0
- package/studio-dist/client/_app/immutable/entry/start.BS-rpdjH.js +1 -0
- package/studio-dist/client/_app/immutable/nodes/{0.CZzEEyVh.js → 0.DMP7swbx.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{1.Bj3Opbua.js → 1.CYZMFpIp.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{10.xe_Mc2Ko.js → 10.3rxHNnSx.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{11.DuKPA7JD.js → 11.B6jWv3Kz.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{12.BoZ_QrUS.js → 12.BrDHeTEq.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{13.DFJiUiv8.js → 13.CE5bd4JV.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{14.eaAlFU4w.js → 14.DgXD2LRx.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{15.BfXgiwMC.js → 15.Cz1KI9Fv.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{16.mEwy9cqh.js → 16.K9wYQPhb.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{17.DP2QtQGn.js → 17.CtUmdfMg.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{18.Bu0054sX.js → 18.mtosd0sL.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{19.UWt6QbCm.js → 19.su55K8y_.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{2.y5jKpHiR.js → 2.mqgFVWFQ.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{20.B1AIHdkP.js → 20.NJuHLxuS.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{21.CpAN71H8.js → 21.BhwhdQCq.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{22.CrYKrN4c.js → 22.BmJEWwER.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{23.pGRaHQjq.js → 23.CAVIJEH-.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{24.BUoUu37z.js → 24.CMuXs5Sy.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/25.DhMBJYAo.js +39 -0
- package/studio-dist/client/_app/immutable/nodes/26.CJ4FA5FS.js +1 -0
- package/studio-dist/client/_app/immutable/nodes/{28.DcmYM7lb.js → 28.D64RBcfs.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{29.lbHbMqNO.js → 29.yfuBeH1x.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{3.Dyir4xXj.js → 3.y115j1d5.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{30.DXj0Wm2A.js → 30.1WUv5bh9.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/4.D6vXYBoQ.js +1 -0
- package/studio-dist/client/_app/immutable/nodes/{5.VWSdP7oc.js → 5.hjw2O-4e.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{6.Dl-XLwOj.js → 6.CAoDfDt6.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{7.CFuC5L-y.js → 7.CLgPSQ8Z.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{8.BCJ0DhOb.js → 8.zuaO1Hik.js} +1 -1
- package/studio-dist/client/_app/immutable/nodes/{9.Bq4SUzL9.js → 9.Cdsd85m2.js} +1 -1
- package/studio-dist/client/_app/version.json +1 -1
- package/studio-dist/server/chunks/0-BQvtFOrC.js +41 -0
- package/studio-dist/server/chunks/1-Bx2_yoiK.js +9 -0
- package/studio-dist/server/chunks/{10-DvOD4ksa.js → 10-CYLc3d5x.js} +3 -3
- package/studio-dist/server/chunks/{11-DMnBZ02H.js → 11-DcIYR5vm.js} +3 -3
- package/studio-dist/server/chunks/{12-CCyRyRWX.js → 12-WE1u104t.js} +3 -3
- package/studio-dist/server/chunks/{13-Dx1c3a_X.js → 13-joBDoxhK.js} +3 -3
- package/studio-dist/server/chunks/{14-Bo-dTCaZ.js → 14-Doj5F4uJ.js} +3 -3
- package/studio-dist/server/chunks/{15-BvUNl3KL.js → 15-nQTeHGD1.js} +3 -3
- package/studio-dist/server/chunks/{16-De6EzPSa.js → 16-CcTN0e8k.js} +5 -4
- package/studio-dist/server/chunks/{17-CZNsiZhi.js → 17-DWiFnzES.js} +10 -93
- package/studio-dist/server/chunks/{18-CbxxjZox.js → 18-Dcc9GnPo.js} +5 -4
- package/studio-dist/server/chunks/{19-DqHyladS.js → 19-BAQHPrMB.js} +3 -3
- package/studio-dist/server/chunks/{2-COvmA-ZA.js → 2-DXSVVaej.js} +10 -93
- package/studio-dist/server/chunks/20-DfQlGHPW.js +57 -0
- package/studio-dist/server/chunks/{21-DXK9ptAC.js → 21-qhGjkW41.js} +4 -31
- package/studio-dist/server/chunks/{22-DD5CLWJC.js → 22-Byy16hFA.js} +3 -3
- package/studio-dist/server/chunks/23-CZpFrGp4.js +97 -0
- package/studio-dist/server/chunks/24-EEysaT9T.js +67 -0
- package/studio-dist/server/chunks/{25-CAg6Xcmn.js → 25-CgiYn70L.js} +15 -96
- package/studio-dist/server/chunks/{26-_nfVTvNi.js → 26-Cfd6BaUb.js} +12 -93
- package/studio-dist/server/chunks/{27-C9edghG4.js → 27-Cqzlh9gZ.js} +3 -2
- package/studio-dist/server/chunks/{28-VemTJGyc.js → 28-DeYe2rpm.js} +8 -62
- package/studio-dist/server/chunks/{29-CXP-m29t.js → 29-6pjLM0Z1.js} +10 -93
- package/studio-dist/server/chunks/{3-B8KhfvHp.js → 3-E_fWDAkq.js} +9 -25
- package/studio-dist/server/chunks/{30-QiM1S4cJ.js → 30-Dw317sSW.js} +10 -93
- package/studio-dist/server/chunks/{31-BwdY9NnB.js → 31-Q4kHbp8j.js} +3 -2
- package/studio-dist/server/chunks/{4-CSaG4HjO.js → 4-C9BMSh4K.js} +4 -3
- package/studio-dist/server/chunks/{5-i5ry4y3C.js → 5-C0CF4WKG.js} +3 -3
- package/studio-dist/server/chunks/{6-DR1GWBMT.js → 6-BWyOJust.js} +3 -3
- package/studio-dist/server/chunks/{7-DSNwn3aQ.js → 7-CK4MK5gy.js} +3 -3
- package/studio-dist/server/chunks/{8-Bj0bx07K.js → 8-CiX7Xk9k.js} +3 -3
- package/studio-dist/server/chunks/{9-B3Tz0mZq.js → 9-Be5118w4.js} +3 -3
- package/studio-dist/server/chunks/{HeroHeader-v4Q9vaGr.js → HeroHeader-BrGN5WPw.js} +2 -2
- package/studio-dist/server/chunks/{Sparkline-BJ-GuhRd.js → Sparkline-DU_NVxz9.js} +2 -2
- package/studio-dist/server/chunks/{StatBlock-DHJMmOCx.js → StatBlock-jNsxycS1.js} +2 -2
- package/studio-dist/server/chunks/{Tabs-DzG6Bn9a.js → Tabs-DQKDflnT.js} +2 -2
- package/studio-dist/server/chunks/_dispatch-DTWViLGL.js +145 -0
- package/studio-dist/server/chunks/{_layout.svelte-BEIGQcIv.js → _layout.svelte-CL6GfET2.js} +7 -9
- package/studio-dist/server/chunks/{_page.svelte-al08ovKm.js → _page.svelte-6iMiGR11.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-Byz2djto.js → _page.svelte-B-YXGvAW.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-tYN7_LXI.js → _page.svelte-B5E6RqVb.js} +9 -11
- package/studio-dist/server/chunks/{_page.svelte-UYiF7YBK.js → _page.svelte-BCRyjlk2.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-CCoTp89s.js → _page.svelte-BoYafqft.js} +3 -3
- package/studio-dist/server/chunks/{_page.svelte-nzffIbBP.js → _page.svelte-C7Wm6_HB.js} +3 -3
- package/studio-dist/server/chunks/{_page.svelte-C_VSL8NW.js → _page.svelte-Cgqt7ucn.js} +13 -71
- package/studio-dist/server/chunks/{_page.svelte-CophUar3.js → _page.svelte-CqI6hqMm.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-BgwL2Gyu.js → _page.svelte-CvQ2rZZ1.js} +11 -13
- package/studio-dist/server/chunks/{_page.svelte-l4Z_xjl9.js → _page.svelte-CvTyJEyS.js} +9 -11
- package/studio-dist/server/chunks/{_page.svelte-DOWw416I.js → _page.svelte-CvzAttO9.js} +11 -13
- package/studio-dist/server/chunks/{_page.svelte-DYNorA0S.js → _page.svelte-Cx9Ptecp.js} +10 -67
- package/studio-dist/server/chunks/{_page.svelte-Wfx7ww_h.js → _page.svelte-Cx9dPjsT.js} +11 -13
- package/studio-dist/server/chunks/{_page.svelte-2cgtvHwW.js → _page.svelte-DGN5j-At.js} +13 -71
- package/studio-dist/server/chunks/{_page.svelte-BVVTrzTL.js → _page.svelte-DaheDIRs.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-M5D2QnMS.js → _page.svelte-Dm8y5a8S.js} +10 -67
- package/studio-dist/server/chunks/{_page.svelte-B_pq-Z98.js → _page.svelte-DqXxyNsF.js} +7 -7
- package/studio-dist/server/chunks/{_page.svelte-C3aIui4o.js → _page.svelte-DqsbnAhk.js} +4 -4
- package/studio-dist/server/chunks/{_page.svelte-ksTJUXce.js → _page.svelte-Dt0_c1T-.js} +3 -3
- package/studio-dist/server/chunks/{_page.svelte-bGhsrIjS.js → _page.svelte-Dv6iwhu7.js} +3 -3
- package/studio-dist/server/chunks/{_page.svelte-BXyoMME1.js → _page.svelte-GPiHej3P.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-CYDjW1R0.js → _page.svelte-LuRm8XxE.js} +5 -5
- package/studio-dist/server/chunks/{_page.svelte-Clbkk1gm.js → _page.svelte-Z7nL9SC-.js} +8 -9
- package/studio-dist/server/chunks/{_page.svelte-DUWbZjC7.js → _page.svelte-_kntHS6v.js} +4 -4
- package/studio-dist/server/chunks/{_page.svelte-BIlhmugm.js → _page.svelte-eiomv6Pa.js} +11 -13
- package/studio-dist/server/chunks/{_page.svelte-9OQphlnm.js → _page.svelte-jXJbd8Mx.js} +14 -69
- package/studio-dist/server/chunks/{_page.svelte-CHzXuw2Q.js → _page.svelte-me5SUosG.js} +3 -3
- package/studio-dist/server/chunks/{_server.ts-DSzRdt1U.js → _server.ts--2m_U4SL.js} +11 -94
- package/studio-dist/server/chunks/{_server.ts-ujoDR2_g.js → _server.ts--sGLmBsz.js} +10 -92
- package/studio-dist/server/chunks/{_server.ts-k2fApwOp.js → _server.ts-0ukyuE0I.js} +5 -60
- package/studio-dist/server/chunks/_server.ts-1eW2mTeh.js +40 -0
- package/studio-dist/server/chunks/{_server.ts-Bfr4u3EP.js → _server.ts-3LzqLzt0.js} +10 -92
- package/studio-dist/server/chunks/_server.ts-40h9Y46Y.js +122 -0
- package/studio-dist/server/chunks/_server.ts-5euivmMj.js +38 -0
- package/studio-dist/server/chunks/{_server.ts-CShsBkeE.js → _server.ts-B0uoay97.js} +7 -23
- package/studio-dist/server/chunks/{_server.ts-hL92cV7Z.js → _server.ts-B5vYKkI6.js} +9 -63
- package/studio-dist/server/chunks/{_server.ts-_RJXtmfi.js → _server.ts-B8dTbPHd.js} +8 -23
- package/studio-dist/server/chunks/{_server.ts-C3Jo-ju8.js → _server.ts-BA8Xdegq.js} +10 -92
- package/studio-dist/server/chunks/_server.ts-BBClE0n9.js +54 -0
- package/studio-dist/server/chunks/{_server.ts-ZS1_yMwR.js → _server.ts-BDwMoyZ_.js} +6 -32
- package/studio-dist/server/chunks/{_server.ts-sv4pA808.js → _server.ts-BE-Uhx4N.js} +5 -46
- package/studio-dist/server/chunks/{_server.ts-DV6pzF-M.js → _server.ts-BSHADOUJ.js} +4 -3
- package/studio-dist/server/chunks/_server.ts-BcsqAqXt.js +67 -0
- package/studio-dist/server/chunks/_server.ts-Bl9tMqwZ.js +159 -0
- package/studio-dist/server/chunks/_server.ts-BlzDnRI0.js +51 -0
- package/studio-dist/server/chunks/{_server.ts-DnE0f-iC.js → _server.ts-BtuemCsf.js} +4 -3
- package/studio-dist/server/chunks/{_server.ts-BPcErnS0.js → _server.ts-C2nYqbd9.js} +10 -92
- package/studio-dist/server/chunks/{_server.ts-4p6cp9ke.js → _server.ts-C6IDM_3I.js} +10 -92
- package/studio-dist/server/chunks/_server.ts-C8Z32vuL.js +45 -0
- package/studio-dist/server/chunks/_server.ts-CBXyadsy.js +26 -0
- package/studio-dist/server/chunks/{_server.ts-Do9RIPRo.js → _server.ts-CE6h2ATE.js} +5 -32
- package/studio-dist/server/chunks/_server.ts-CH06JVqZ.js +65 -0
- package/studio-dist/server/chunks/_server.ts-CNqcws-M.js +29 -0
- package/studio-dist/server/chunks/{_server.ts-CJA1djzJ.js → _server.ts-CPf8necd.js} +8 -92
- package/studio-dist/server/chunks/{_server.ts-uUx7uT2Y.js → _server.ts-CYv5YuHd.js} +6 -43
- package/studio-dist/server/chunks/{_server.ts-B2E7fxo9.js → _server.ts-CZ5GZRWX.js} +6 -91
- package/studio-dist/server/chunks/{_server.ts-DSAv66wD.js → _server.ts-CbThsyhY.js} +4 -3
- package/studio-dist/server/chunks/{_server.ts-DgbY5EB3.js → _server.ts-CeQolQOK.js} +10 -92
- package/studio-dist/server/chunks/_server.ts-ChoGyX5C.js +61 -0
- package/studio-dist/server/chunks/_server.ts-CjJYkLJH.js +61 -0
- package/studio-dist/server/chunks/{_server.ts-lPlBUIJX.js → _server.ts-CtjArzxf.js} +5 -60
- package/studio-dist/server/chunks/{_server.ts-Cq60m8Pw.js → _server.ts-D3QWZwt_.js} +4 -3
- package/studio-dist/server/chunks/{_server.ts-CkoJ2HCW.js → _server.ts-D6wP5bCh.js} +10 -92
- package/studio-dist/server/chunks/{_server.ts-DwTRjS4y.js → _server.ts-D7JwQTun.js} +10 -93
- package/studio-dist/server/chunks/_server.ts-DKFJdXPK.js +97 -0
- package/studio-dist/server/chunks/_server.ts-DNAPGKEF.js +55 -0
- package/studio-dist/server/chunks/_server.ts-DQiTiY3E.js +78 -0
- package/studio-dist/server/chunks/{_server.ts-BT0Uq2yq.js → _server.ts-DUyeSXbV.js} +10 -92
- package/studio-dist/server/chunks/{_server.ts-DRVz9D-J.js → _server.ts-DY7BwZCv.js} +9 -24
- package/studio-dist/server/chunks/{_server.ts-Cz0zgaUV.js → _server.ts-D_E276ms.js} +3 -2
- package/studio-dist/server/chunks/{_server.ts-B5g3eScF.js → _server.ts-DgtmTAp5.js} +5 -60
- package/studio-dist/server/chunks/{_server.ts-CboRJY0Q.js → _server.ts-DhTKaw7h.js} +10 -63
- package/studio-dist/server/chunks/_server.ts-DqKBSCPT.js +31 -0
- package/studio-dist/server/chunks/{_server.ts-CpmMp5D8.js → _server.ts-DyXBNnRD.js} +4 -3
- package/studio-dist/server/chunks/_server.ts-DzjUpduu.js +25 -0
- package/studio-dist/server/chunks/{_server.ts-CGiAXy5i.js → _server.ts-Dznqw6Dg.js} +8 -23
- package/studio-dist/server/chunks/{_server.ts-tKMjUFUP.js → _server.ts-EVwS7UXb.js} +4 -3
- package/studio-dist/server/chunks/_server.ts-J17j25VG.js +62 -0
- package/studio-dist/server/chunks/{_server.ts-BdN4AJ6o.js → _server.ts-VQchZxXq.js} +10 -91
- package/studio-dist/server/chunks/{_server.ts-C4BcDGqS.js → _server.ts-XLV5SvjH.js} +7 -63
- package/studio-dist/server/chunks/_server.ts-jZgqFIJz.js +45 -0
- package/studio-dist/server/chunks/{_server.ts-Bw7A4zgb.js → _server.ts-lyiGGv7w.js} +5 -3
- package/studio-dist/server/chunks/{brain-CXR7ZT7S.js → brain-xt_NeshR.js} +2 -2
- package/studio-dist/server/chunks/cleo-home-_XIqkZaL.js +109 -0
- package/studio-dist/server/chunks/{cli-action-DQcaqcfn.js → cli-action-VJPEn1pY.js} +2 -2
- package/studio-dist/server/chunks/{client-CgX38JBu.js → client-D71MZ7Vi.js} +5 -6
- package/studio-dist/server/chunks/cluster-label-layer-CysRMvhi.js +6721 -0
- package/studio-dist/server/chunks/connections-CWLPjwBP.js +15 -0
- package/studio-dist/server/chunks/{connections2-mi7mZqJM.js → connections2-DHVs5ytW.js} +32 -19
- package/studio-dist/server/chunks/{dev-BNpEUZI3.js → dev-Br5zieNA.js} +2 -2
- package/studio-dist/server/chunks/dist-BzgWhfUR.js +18748 -0
- package/studio-dist/server/chunks/{error.svelte-CplNulnS.js → error.svelte-BIToUowC.js} +6 -8
- package/studio-dist/server/chunks/{explorer-loader-BN_mkt3_.js → explorer-loader-B_uCSs9_.js} +2 -2
- package/studio-dist/server/chunks/exports-Dg-gdPPq.js +124 -0
- package/studio-dist/server/chunks/{hooks.server-DBsyphiX.js → hooks.server-CWBRp8NI.js} +10 -93
- package/studio-dist/server/chunks/{http-CRTZe306.js → http-DPU3HHl2.js} +3 -4
- package/studio-dist/server/chunks/{index-server-DocJvixA.js → index-server-BgfA6C0S.js} +17 -4
- package/studio-dist/server/chunks/{internal-D6eWgsHJ.js → internal-DPnIJYMs.js} +56 -92
- package/studio-dist/server/chunks/{memory2-Cgc_4D5q.js → memory-BZHnDZzu.js} +4 -4
- package/studio-dist/server/chunks/{nexus-adapter-9EL_VlGI.js → nexus-adapter-D1ryYG1Z.js} +5 -5
- package/studio-dist/server/chunks/{no-face-up-DcejJLy-.js → no-face-up-B_3h6Hgs.js} +2 -2
- package/studio-dist/server/chunks/project-context-BAXQmgch.js +188 -0
- package/studio-dist/server/chunks/{src-BUqGet2c.js → src-D2rW9bIF.js} +422 -5
- package/studio-dist/server/chunks/{stores-5DmdjPCH.js → stores-BZdYTyl7.js} +3 -3
- package/studio-dist/server/chunks/{tasks2-Bf9VHrv-.js → tasks-sWuEmiGW.js} +2349 -9
- package/studio-dist/server/chunks/{ui-DR8zOSpC.js → ui-CkQ-pCs3.js} +2 -2
- package/studio-dist/server/chunks/utils-g564cUMV.js +151 -0
- package/studio-dist/server/index.js +5 -16
- package/studio-dist/server/manifest.js +88 -88
- package/dist/cli/index.js.map +0 -6
- package/studio-dist/client/_app/immutable/assets/parse-worker.CSEr67Qi.js +0 -1127
- package/studio-dist/client/_app/immutable/chunks/2mEksghu2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/3KebaI-z.js +0 -53
- package/studio-dist/client/_app/immutable/chunks/3y2yNYEk2.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/7UXYlaI1.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/83VLmCvk.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/84Rhmi6r.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/9CV9TXTg2.js +0 -92
- package/studio-dist/client/_app/immutable/chunks/9LFtcz24.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/9bLMgKwu2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/9kKqohVR2.js +0 -37
- package/studio-dist/client/_app/immutable/chunks/B-Ye28EY.js +0 -7
- package/studio-dist/client/_app/immutable/chunks/B-jnUl7o.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B0ORuy8x2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B0xCM1wN2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B0zCN4sX.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B1wt-iGW.js +0 -7
- package/studio-dist/client/_app/immutable/chunks/B2F3KUIU.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B2_6RrlL2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B2qKEYxA2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B2u2rNyI2.js +0 -8
- package/studio-dist/client/_app/immutable/chunks/B3_X3Lhd2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B3sNtbqf.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/B4NQP1Ax2.js +0 -6
- package/studio-dist/client/_app/immutable/chunks/BAtZg5v5.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BB6WKLm82.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BBX8UDtn.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BBs1U1iU.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BEli_4Jp2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BEt2wIUt.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BEyM8qSW2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BFZA3Dsk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BGdPweG1.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BGiusSqE.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BGmpyfzD.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BH5rhSIR.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BHciFt922.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BHnFwTQY.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BIvye3XP2.js +0 -59
- package/studio-dist/client/_app/immutable/chunks/BJ8mcpQA2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BK17qSHw.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BLkgUtyb.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BMUko3r62.js +0 -5
- package/studio-dist/client/_app/immutable/chunks/BMwzdEaa.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BQZx6brg.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BUPJTTTd.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BVNGAeZe2.js +0 -146
- package/studio-dist/client/_app/immutable/chunks/BWfLryGm.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BXIfeogw.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BXTfFyUU.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BYJC6_MM.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BZV1ZJzH.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BZu9rehd.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Bbzm48C52.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BfGnlwjH.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Bfjb9LPp.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BieFfu-R.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Bmv63uXe2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BtBW1A0u.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/BxISGQCl2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/ByNR_KvT.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BzjhD6Hr2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/BzpWnq0c2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C-yJVcyI2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C0cQ-8kS2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C1vWmhYe.js +0 -48
- package/studio-dist/client/_app/immutable/chunks/C1w6bBje.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C243sn7r.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/C2oZaDvt.js +0 -67
- package/studio-dist/client/_app/immutable/chunks/C3OziSQx.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C3hFPbMK.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/C4P9oFvU2.js +0 -6
- package/studio-dist/client/_app/immutable/chunks/C5Nlanei.js +0 -8
- package/studio-dist/client/_app/immutable/chunks/C6KBAXPl.js +0 -15
- package/studio-dist/client/_app/immutable/chunks/C8MZnWta.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CAoan3K12.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CBpcINYH.js +0 -29
- package/studio-dist/client/_app/immutable/chunks/CE7phgWq.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CFb4Co_K.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CGWoRK3t2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CH3t4xo1.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CHJgk6g-.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CHgPjuDQ.js +0 -17
- package/studio-dist/client/_app/immutable/chunks/CHkCZrnr.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CI_91WUN.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/CJ7-hP0H.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CKIrNnkY2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CKoQ7Ko5.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CM8O4zqs.js +0 -56
- package/studio-dist/client/_app/immutable/chunks/CMz3AVRO.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CNToUR3P.js +0 -13
- package/studio-dist/client/_app/immutable/chunks/CNuQG1yl2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CQLIZtxj.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CQYVNgM0.js +0 -20
- package/studio-dist/client/_app/immutable/chunks/CQeVOmFO2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CSPHHq09.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CTJEdbIn.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CTTT3wYw.js +0 -35
- package/studio-dist/client/_app/immutable/chunks/CV4ck7fz.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CXQrVnHf.js +0 -11
- package/studio-dist/client/_app/immutable/chunks/CXTCtOUQ.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C_CsU7Qq.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/C_IZx3682.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CbTnu2Po2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cba8RZ1a2.js +0 -8
- package/studio-dist/client/_app/immutable/chunks/Cc8xxDr5.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CdPxoldu.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CdqDTozW.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CdsmcCfC.js +0 -25
- package/studio-dist/client/_app/immutable/chunks/CeYPAoY9.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cf4kGS9y.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CfWEqXtJ2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cg7Wj2b9.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CgTpKZfi2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Chpy9ca7.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CiLKuLrQ.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/CjNDJjwu.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cjde0ZKr2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cjgnade02.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/Ckp90om52.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/ClAPVhCD.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/ClT89UdO.js +0 -11
- package/studio-dist/client/_app/immutable/chunks/CnJf5tML.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CpF4Rrcc2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CqCka5yX2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CqtqZrOl.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CrJdywQt.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Cs0zC2l2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CsMn5fBs.js +0 -141
- package/studio-dist/client/_app/immutable/chunks/CsvKLHQQ2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Ctaresvj.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Ctetf8vc2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Cv7yW6MG.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CxRIz-Zd.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/CxbvtjIH.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/CxfeoXkZ.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/CySLscL2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/D0PKQAbM.js +0 -87
- package/studio-dist/client/_app/immutable/chunks/D0WQsLTu.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D1LBVx6r2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D1NlFXu9.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D1QfC4hu2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D1yrHAn72.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D2pa4reh.js +0 -7
- package/studio-dist/client/_app/immutable/chunks/D3QsWcIw2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/D4OkQuPl2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D6UDnATk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D7w7HllH.js +0 -38
- package/studio-dist/client/_app/immutable/chunks/D93KpglN.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D99fQj2G2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DB5ifLM-.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/DBhP8ifO2.js +0 -15
- package/studio-dist/client/_app/immutable/chunks/DF_4OWhj.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DFpNK1vs2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DGRYMk-W.js +0 -142
- package/studio-dist/client/_app/immutable/chunks/DH3CcQwq.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DHQfJeLU.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/DL89yEDy2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DLRku_0p2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DMZkjPYT2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DNP72foU.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DO2GztVm.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DOVNXmCK2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DOqzs8kl2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DQnR63hh2.js +0 -7
- package/studio-dist/client/_app/immutable/chunks/DTxtWPkw.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DVDgCC9A2.js +0 -20
- package/studio-dist/client/_app/immutable/chunks/DVVCJYjz.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DWRhYUMj2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DWavBJBI.js +0 -4
- package/studio-dist/client/_app/immutable/chunks/DXDSWRw-.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DXoHaH4a.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DXpCX2Hi.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYP3EJMY.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYQphR_V.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYUlIivP.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYo2ye8X2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DYxFu8yV.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DZ66sRb6.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/D_XTiylm2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DaRv9jn22.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DaXb9i_E.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dccf4VP2.js +0 -24
- package/studio-dist/client/_app/immutable/chunks/DdODs-Sb.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DdekYWer.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DeHaovsF.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DeRRkRO52.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DeWI5qCn.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DfivR-Md.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DgmxpSo7.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DhPLWrun2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DhhrVdfi.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Djr2dbcc2.js +0 -37
- package/studio-dist/client/_app/immutable/chunks/DjuRo5vv.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Djz4EDzO.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DkRfaw-K2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DksB5g4W2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dmqenb5z2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DnbToVyf.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DpGZwlmj.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dq0lE5C32.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dq54XqHT2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DqK84mbb.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DqjFtSJD.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DrKWWDpL.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DsQIgu-4.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DsqdgJHS2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DtNkZUkq.js +0 -17
- package/studio-dist/client/_app/immutable/chunks/DutIls4V2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DvxYFW4C.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Dwp-76GQ.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/DxSZd548.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DxiUMhk0.js +0 -5
- package/studio-dist/client/_app/immutable/chunks/DxslLswf.js +0 -23
- package/studio-dist/client/_app/immutable/chunks/Dy7EjGgb2.js +0 -9
- package/studio-dist/client/_app/immutable/chunks/DyGQ_VXC.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/DySe3kTV2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DyzE2Uxw.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/DzhuJaxE2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/FmMHyrh6.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Gxs0J274.js +0 -5
- package/studio-dist/client/_app/immutable/chunks/HwtzDYxN.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/ITRz_Q7H2.js +0 -15
- package/studio-dist/client/_app/immutable/chunks/KDNP98XY2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/LmVjFhpk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/MiAejGV_.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Ml4JR6Jz2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/NU5S3nNs2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Oh5zPjv3.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/QZkvJ9vi.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Rbn8UpBm.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/SPX3gYwn2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/TX-23QW3.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/UyeQuL132.js +0 -12
- package/studio-dist/client/_app/immutable/chunks/VQszoXcV.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/VtZsEMxD.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/W2IeK61p2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Wadww7ZG.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/WgfA1zWk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/WxIgAS1f.js +0 -21
- package/studio-dist/client/_app/immutable/chunks/XrsBoOj9.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/Y3PX4poR.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/YxfwqiF32.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/Z7uO1YaO.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/_4ykQk_8.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/_kRcZFVn.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/aQNM7eVA2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/bHKz_Fb2.js +0 -45
- package/studio-dist/client/_app/immutable/chunks/bJRyRGSZ.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/dlzF6lnR.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/fjRbpfEr2.js +0 -26
- package/studio-dist/client/_app/immutable/chunks/imLk4if5.js +0 -3
- package/studio-dist/client/_app/immutable/chunks/lgRG0R-32.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/mFhCjxQJ2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/mgnH6qYg.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/n6M_5LoW.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/nLIOiDZp2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/nab7WB3u2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/orUmAKoW2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/p-fLQDfY2.js +0 -63
- package/studio-dist/client/_app/immutable/chunks/s0-WkOGk2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/sPuXfbUF.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/t4h0vrqR2.js +0 -2
- package/studio-dist/client/_app/immutable/chunks/uE83FKL-.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/uzhXlm0g2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/vTrEsGlD2.js +0 -1
- package/studio-dist/client/_app/immutable/chunks/xJgCk3dx2.js +0 -28
- package/studio-dist/client/_app/immutable/chunks/z0Mt579s2.js +0 -13
- package/studio-dist/client/_app/immutable/entry/app.BoQ7cgT2.js +0 -2
- package/studio-dist/client/_app/immutable/entry/start.Cg1v8DU4.js +0 -1
- package/studio-dist/client/_app/immutable/nodes/25.ddZ1_EFk.js +0 -1
- package/studio-dist/client/_app/immutable/nodes/26.6OcmQbLV.js +0 -1
- package/studio-dist/client/_app/immutable/nodes/4.CCjOEwVU.js +0 -1
- package/studio-dist/server/chunks/0-CvsKDxQ6.js +0 -126
- package/studio-dist/server/chunks/1-ck_x2Hf0.js +0 -9
- package/studio-dist/server/chunks/20-DVU22np7.js +0 -142
- package/studio-dist/server/chunks/23-D_tUnnoc.js +0 -181
- package/studio-dist/server/chunks/24-CVlymvLF.js +0 -151
- package/studio-dist/server/chunks/AwsRestJsonProtocol-BZuoNBmL.js +0 -416
- package/studio-dist/server/chunks/UnionSerde-BflIkhKj.js +0 -2653
- package/studio-dist/server/chunks/_dispatch-ncuRztUQ.js +0 -5597
- package/studio-dist/server/chunks/_server.ts-4NyKAneX.js +0 -116
- package/studio-dist/server/chunks/_server.ts-7hGdxdEI.js +0 -136
- package/studio-dist/server/chunks/_server.ts-B6b3b-yA.js +0 -131
- package/studio-dist/server/chunks/_server.ts-BOgGqaZp.js +0 -144
- package/studio-dist/server/chunks/_server.ts-BV1HiKZ0.js +0 -120
- package/studio-dist/server/chunks/_server.ts-BWp3g6Cr.js +0 -110
- package/studio-dist/server/chunks/_server.ts-C5NF-UgC.js +0 -122
- package/studio-dist/server/chunks/_server.ts-CDMurH5m.js +0 -2301
- package/studio-dist/server/chunks/_server.ts-COJHCgbq.js +0 -127
- package/studio-dist/server/chunks/_server.ts-CUl-Utkt.js +0 -148
- package/studio-dist/server/chunks/_server.ts-D0GAFJRb.js +0 -156
- package/studio-dist/server/chunks/_server.ts-DA6p3pmi.js +0 -117
- package/studio-dist/server/chunks/_server.ts-DThYFaZj.js +0 -160
- package/studio-dist/server/chunks/_server.ts-D_3Pm0gr.js +0 -179
- package/studio-dist/server/chunks/_server.ts-DcHxdMsm.js +0 -144
- package/studio-dist/server/chunks/_server.ts-Dlfadh2I.js +0 -144
- package/studio-dist/server/chunks/_server.ts-JallJnps.js +0 -240
- package/studio-dist/server/chunks/_server.ts-NEFTbJpM.js +0 -142
- package/studio-dist/server/chunks/_server.ts-d-2NvRwX.js +0 -122
- package/studio-dist/server/chunks/_server.ts-rUWMLkIw.js +0 -146
- package/studio-dist/server/chunks/abort-events-CDRAJSqb.js +0 -30
- package/studio-dist/server/chunks/adapters-BkMgPNUM.js +0 -359
- package/studio-dist/server/chunks/adapters2-CQ3PzoaK.js +0 -28
- package/studio-dist/server/chunks/api-mode2-3W-I1-XO.js +0 -36
- package/studio-dist/server/chunks/api2-CmNQzb0b.js +0 -96
- package/studio-dist/server/chunks/attention-consolidate-CidG1jfH.js +0 -485
- package/studio-dist/server/chunks/audit3-DMO_fUGp.js +0 -52
- package/studio-dist/server/chunks/auto-extract-B84La1Vs.js +0 -34
- package/studio-dist/server/chunks/brain-consolidator-JGm6mzFf.js +0 -373
- package/studio-dist/server/chunks/brain-doctor-Dc2WqVbs.js +0 -311
- package/studio-dist/server/chunks/brain-embedding-Djw9ekXu.js +0 -42
- package/studio-dist/server/chunks/brain-lifecycle-BOOa876K.js +0 -969
- package/studio-dist/server/chunks/brain-maintenance-U6VvsvmK.js +0 -133
- package/studio-dist/server/chunks/brain-maintenance2-CwVSmfDT.js +0 -43
- package/studio-dist/server/chunks/brain-retrieval2-exR8mdgh.js +0 -751
- package/studio-dist/server/chunks/brain-search-CpSh-SNi.js +0 -48
- package/studio-dist/server/chunks/brain-search2-CwI2cXgg.js +0 -716
- package/studio-dist/server/chunks/brain-similarity-DI4yzm9e.js +0 -113
- package/studio-dist/server/chunks/brain-stdp2-BxSBIlSr.js +0 -960
- package/studio-dist/server/chunks/brain-writer-handlers-Dwu0QCAA.js +0 -188
- package/studio-dist/server/chunks/brain-writer-thread-6n7wI7Gy.js +0 -360
- package/studio-dist/server/chunks/branch-lock-Cq7zzmFK.js +0 -1118
- package/studio-dist/server/chunks/branch-lock2-CmDc4CZW.js +0 -45
- package/studio-dist/server/chunks/cancelled-child-waiver-audit-DkesrN83.js +0 -54
- package/studio-dist/server/chunks/catalog-model-resolver-Bas9qnoP.js +0 -9
- package/studio-dist/server/chunks/catalog-model-resolver2-Cexk2TN0.js +0 -237
- package/studio-dist/server/chunks/catalog-resolver-CLot3XJO.js +0 -260
- package/studio-dist/server/chunks/catalog-resolver2-JmKb91kY.js +0 -33
- package/studio-dist/server/chunks/chunk-DDP5xfzh.js +0 -39
- package/studio-dist/server/chunks/cleo-home-CCfe6kAQ.js +0 -64
- package/studio-dist/server/chunks/concrete-session2-bb_vxJ2N.js +0 -43
- package/studio-dist/server/chunks/config-DHj9CcFs.js +0 -386
- package/studio-dist/server/chunks/config2-F6PPIn7D.js +0 -35
- package/studio-dist/server/chunks/connections-BMGJVYcS.js +0 -98
- package/studio-dist/server/chunks/credential-pool-vF6NdtT9.js +0 -4600
- package/studio-dist/server/chunks/credentials-C7EJFf5O.js +0 -35
- package/studio-dist/server/chunks/credentials-store-CFygIuB2.js +0 -35
- package/studio-dist/server/chunks/cross-db-cleanup-1LWTUKPi.js +0 -147
- package/studio-dist/server/chunks/data-accessor-CWgVndbR.js +0 -72
- package/studio-dist/server/chunks/data-safety-central-DaFxK27G.js +0 -683
- package/studio-dist/server/chunks/decisions-CmVXV2-H.js +0 -574
- package/studio-dist/server/chunks/decisions2-XQsSYdxa.js +0 -48
- package/studio-dist/server/chunks/depends-waiver-audit-CUmlBYHx.js +0 -56
- package/studio-dist/server/chunks/dialectic-evaluator2-BOyyXhdI.js +0 -96
- package/studio-dist/server/chunks/dist-es2-DQy2Mjn_.js +0 -43
- package/studio-dist/server/chunks/dist-es3-CBygU4n5.js +0 -365
- package/studio-dist/server/chunks/dist-es4-CavbNu4B.js +0 -162
- package/studio-dist/server/chunks/dist-es5-Crpm8Wsl.js +0 -465
- package/studio-dist/server/chunks/dist-es6-CgePHzh4.js +0 -99
- package/studio-dist/server/chunks/dist-es7-k6c5GEd7.js +0 -311
- package/studio-dist/server/chunks/dist-es8-Bd9v0_pE.js +0 -91
- package/studio-dist/server/chunks/dist3-23bsJhRw.js +0 -27
- package/studio-dist/server/chunks/dist4-DmC1zSXi.js +0 -35618
- package/studio-dist/server/chunks/dual-scope-db-DKnWu1KM.js +0 -5672
- package/studio-dist/server/chunks/dual-scope-db2-Dht3luRw.js +0 -31
- package/studio-dist/server/chunks/duplicate-bypass-audit-BN5w1H2y.js +0 -36
- package/studio-dist/server/chunks/duplicate-detector-CaVFMKdE.js +0 -692
- package/studio-dist/server/chunks/edge-types-CgBvb0w1.js +0 -28
- package/studio-dist/server/chunks/embedding-local-xEnym8Ho.js +0 -92
- package/studio-dist/server/chunks/event-streams-Bi2p_fVo.js +0 -232
- package/studio-dist/server/chunks/execution-learning-BVCw7Plq.js +0 -278
- package/studio-dist/server/chunks/exodus-BBuuN6aA.js +0 -2594
- package/studio-dist/server/chunks/externalDataInterceptor-C3C4cObe.js +0 -21
- package/studio-dist/server/chunks/extraction-gate-DDNBq7gu.js +0 -48
- package/studio-dist/server/chunks/extraction-gate2-D77EZBQ6.js +0 -375
- package/studio-dist/server/chunks/getSSOTokenFromFile-yZAJxDci.js +0 -21
- package/studio-dist/server/chunks/governor2-CtfV3wfX.js +0 -923
- package/studio-dist/server/chunks/graph-auto-populate-Dmsr9ZJM.js +0 -42
- package/studio-dist/server/chunks/graph-auto-populate2-Cz0qeebM.js +0 -156
- package/studio-dist/server/chunks/graph-memory-bridge-CY8ft_d7.js +0 -43
- package/studio-dist/server/chunks/graph-memory-bridge2-Cd0XZwqc.js +0 -429
- package/studio-dist/server/chunks/handler-helpers-AANBGA_p.js +0 -49
- package/studio-dist/server/chunks/hash-Cm6yMET4.js +0 -14
- package/studio-dist/server/chunks/health-monitor-FepUZN1m.js +0 -106
- package/studio-dist/server/chunks/identity-DRpurt56.js +0 -30
- package/studio-dist/server/chunks/identity2-DeAB5RXE.js +0 -114
- package/studio-dist/server/chunks/index-61916rmV.js +0 -632
- package/studio-dist/server/chunks/index-DAgVDouG.js +0 -268
- package/studio-dist/server/chunks/index-Dtt_h58T.js +0 -2116
- package/studio-dist/server/chunks/indexes-DZ52grq6.js +0 -615
- package/studio-dist/server/chunks/json-Bfixam3g.js +0 -126
- package/studio-dist/server/chunks/learnings-D6Y3YGMU.js +0 -47
- package/studio-dist/server/chunks/learnings2-MwBdR9vb.js +0 -171
- package/studio-dist/server/chunks/lifecycle-ops2-V2CR5-7X.js +0 -121
- package/studio-dist/server/chunks/list-BhObczvn.js +0 -229
- package/studio-dist/server/chunks/living-brain2-j0B_Ir8j.js +0 -285
- package/studio-dist/server/chunks/llm-backend-resolver-D4ANvUPL.js +0 -295
- package/studio-dist/server/chunks/llm-extraction-DVCu4heR.js +0 -408
- package/studio-dist/server/chunks/loadSharedConfigFiles-umvW7kuz.js +0 -73
- package/studio-dist/server/chunks/loadSso-iidd5iKx.js +0 -599
- package/studio-dist/server/chunks/local-model-fit-B0TxifnX.js +0 -53
- package/studio-dist/server/chunks/local-model-fit2-BMqo6GJm.js +0 -2664
- package/studio-dist/server/chunks/lock-CA7d6lj7.js +0 -57
- package/studio-dist/server/chunks/logger-DQIoyZbq.js +0 -34
- package/studio-dist/server/chunks/memory-BUbicBbE.js +0 -618
- package/studio-dist/server/chunks/memory-accessor-i3P-RHSg.js +0 -42
- package/studio-dist/server/chunks/memory-accessor2-B981tHDR.js +0 -467
- package/studio-dist/server/chunks/memory-bridge-BOlEWg9w.js +0 -424
- package/studio-dist/server/chunks/memory-sqlite2-B7dq8doO.js +0 -42
- package/studio-dist/server/chunks/model-runner2-B71xjp4y.js +0 -43
- package/studio-dist/server/chunks/mvi-helpers-BNsJ8ZpQ.js +0 -59
- package/studio-dist/server/chunks/nexus-plasticity-BTfU8cW_.js +0 -269
- package/studio-dist/server/chunks/nexus-risk-audit-p379-TU5.js +0 -54
- package/studio-dist/server/chunks/nexus-schema-zeygY8OD.js +0 -11
- package/studio-dist/server/chunks/nexus-schema2-DP0Qd7CA.js +0 -444
- package/studio-dist/server/chunks/nexus-sqlite-BVNIpwc7.js +0 -36
- package/studio-dist/server/chunks/nexus-sqlite2-BBfY2xnY.js +0 -1103
- package/studio-dist/server/chunks/node-http-handler-CJI8kn7X.js +0 -386
- package/studio-dist/server/chunks/observe-Bo9veHGe.js +0 -47
- package/studio-dist/server/chunks/observe2-wzO0MaMY.js +0 -245
- package/studio-dist/server/chunks/observer-reflector-BwNOuq1d.js +0 -62
- package/studio-dist/server/chunks/observer-reflector2-ggtC1hmk.js +0 -478
- package/studio-dist/server/chunks/on-open-Wpcw7DV4.js +0 -740
- package/studio-dist/server/chunks/open-cleo-db--8sYNaYY.js +0 -70
- package/studio-dist/server/chunks/parseKnownFiles-Dg0pMFcT.js +0 -18
- package/studio-dist/server/chunks/parseUrl-qgGfl-Vh.js +0 -120
- package/studio-dist/server/chunks/paths-CV7S5-Zm.js +0 -30012
- package/studio-dist/server/chunks/patterns-lh5kn13I.js +0 -48
- package/studio-dist/server/chunks/patterns2-CzIcT-ai.js +0 -177
- package/studio-dist/server/chunks/pipeline-stage-IB_pOft-.js +0 -63
- package/studio-dist/server/chunks/prediction-D1lSGJ4c.js +0 -217
- package/studio-dist/server/chunks/premature-close-audit-VG09uAco.js +0 -53
- package/studio-dist/server/chunks/project-context-BjiqfEBi.js +0 -166
- package/studio-dist/server/chunks/provider-registry2-Cu7rAF0R.js +0 -35
- package/studio-dist/server/chunks/quality-feedback-CuCGEI6S.js +0 -371
- package/studio-dist/server/chunks/quality-scoring-CCPuRQmJ.js +0 -160
- package/studio-dist/server/chunks/query-ops-DCrjbZb9.js +0 -78
- package/studio-dist/server/chunks/query-promise-BkPmGPJy.js +0 -1257
- package/studio-dist/server/chunks/readFile-CxwXhymG.js +0 -31
- package/studio-dist/server/chunks/registry-CL2gN1zd.js +0 -27
- package/studio-dist/server/chunks/registry2-DoPYJnUW.js +0 -233
- package/studio-dist/server/chunks/registry3-BajEb-ll.js +0 -42
- package/studio-dist/server/chunks/registry4-DCqffZkT.js +0 -142
- package/studio-dist/server/chunks/retry-B3oEKYpp.js +0 -105
- package/studio-dist/server/chunks/role-executor-CC8BU54L.js +0 -25379
- package/studio-dist/server/chunks/role-resolver-D9QaDT0e.js +0 -6544
- package/studio-dist/server/chunks/role-resolver2-nkhzS1oy.js +0 -37
- package/studio-dist/server/chunks/rollup-CiefjN_Y.js +0 -194
- package/studio-dist/server/chunks/safety-data-accessor-CniDmGJO.js +0 -276
- package/studio-dist/server/chunks/service-connections-accessor-yBCdcgU-.js +0 -282
- package/studio-dist/server/chunks/session-factory-vbengc42.js +0 -96
- package/studio-dist/server/chunks/session-grade-BF4ZZtN_.js +0 -244
- package/studio-dist/server/chunks/session-journal-Bvey70Ur.js +0 -235
- package/studio-dist/server/chunks/session-narrative-BirZINSU.js +0 -304
- package/studio-dist/server/chunks/session-store--N_xHxUH.js +0 -557
- package/studio-dist/server/chunks/session-store2-DAM-eVTH.js +0 -43
- package/studio-dist/server/chunks/sessions-33DD4hkt.js +0 -2273
- package/studio-dist/server/chunks/sessions2-BHswKRDP.js +0 -50
- package/studio-dist/server/chunks/sigil-B-RznFXz.js +0 -33
- package/studio-dist/server/chunks/sigil2-jRMG5DuR.js +0 -47
- package/studio-dist/server/chunks/signin-BAsW3mM8.js +0 -644
- package/studio-dist/server/chunks/sleep-consolidation-BnSwXrvq.js +0 -823
- package/studio-dist/server/chunks/specialists-B_ZbpQlK.js +0 -441
- package/studio-dist/server/chunks/sqlite-Bm5LzbPs.js +0 -5977
- package/studio-dist/server/chunks/sqlite-backup-CFG1-b2e.js +0 -42
- package/studio-dist/server/chunks/sqlite-data-accessor-DD_RFX6r.js +0 -987
- package/studio-dist/server/chunks/sqlite-data-accessor2-CRmgNBnq.js +0 -44
- package/studio-dist/server/chunks/sqlite-native-BwlIqtbs.js +0 -42
- package/studio-dist/server/chunks/sqlite2-CUPut2fh.js +0 -42
- package/studio-dist/server/chunks/src2-DK8DKufK.js +0 -290
- package/studio-dist/server/chunks/src3-BJjf7XRG.js +0 -798
- package/studio-dist/server/chunks/src4-DNleK_t_.js +0 -28
- package/studio-dist/server/chunks/sso-oidc-eUvwseuL.js +0 -861
- package/studio-dist/server/chunks/storage-B9UA3BHR.js +0 -198
- package/studio-dist/server/chunks/sts-g7LDMLEd.js +0 -4655
- package/studio-dist/server/chunks/surprisal-CwTfWQi8.js +0 -224
- package/studio-dist/server/chunks/surprisal-tree-7dfHsuw1.js +0 -230
- package/studio-dist/server/chunks/system-resolver2-Bzhue_zQ.js +0 -53
- package/studio-dist/server/chunks/tasks-SHX7UPGY.js +0 -19694
- package/studio-dist/server/chunks/tasks-schema2-CTMoQKQy.js +0 -42
- package/studio-dist/server/chunks/temporal-supersession-DP4GPG_B.js +0 -298
- package/studio-dist/server/chunks/token-cAhAD9f9.js +0 -139
- package/studio-dist/server/chunks/token-util-D6drEz8_.js +0 -353
- package/studio-dist/server/chunks/token-util2-8UPSGvbb.js +0 -97
- package/studio-dist/server/chunks/transcript-scanner-kJWhWmlg.js +0 -53
- package/studio-dist/server/chunks/transformers.node-_xbxmK5p.js +0 -38475
- package/studio-dist/server/chunks/typed-query-l1D_QElt.js +0 -18
- package/studio-dist/server/chunks/unique-constraint-BZrrwgwc.js +0 -88
- package/studio-dist/server/chunks/update-B2YVBZG0.js +0 -1594
- package/studio-dist/server/chunks/user-profile-B61ZhVmC.js +0 -89
- package/studio-dist/server/chunks/utils-BPSZyajG.js +0 -1176
- package/studio-dist/server/chunks/validation-rules-CE27rtoF.js +0 -44
- package/studio-dist/server/chunks/zod-DsAeKNLm.js +0 -141
- /package/studio-dist/client/_app/immutable/chunks/{CQCxd1p9.js → B8WnZMYa.js} +0 -0
- /package/studio-dist/client/_app/immutable/chunks/{D13Quv9e2.js → B_0_YCYG.js} +0 -0
- /package/studio-dist/client/_app/immutable/chunks/{djL6V-tr.js → CqgtdAx_.js} +0 -0
- /package/studio-dist/client/_app/immutable/chunks/{_FU7h_8p2.js → KrUPZzXZ.js} +0 -0
|
@@ -1,2594 +0,0 @@
|
|
|
1
|
-
import { y as resolveCleoDir, b as getLogger, g as getCleoHome } from './paths-CV7S5-Zm.js';
|
|
2
|
-
import { r as resolveDualScopeDbPath, o as openDualScopeDbAtPath } from './dual-scope-db-DKnWu1KM.js';
|
|
3
|
-
import { g as getCleoVersion } from './on-open-Wpcw7DV4.js';
|
|
4
|
-
import { o as openCleoDbSnapshot } from './open-cleo-db--8sYNaYY.js';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
|
-
import { existsSync, unlinkSync, mkdirSync, copyFileSync, statSync, statfsSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import 'node:os';
|
|
9
|
-
import 'node:events';
|
|
10
|
-
import 'node:diagnostics_channel';
|
|
11
|
-
import 'fs';
|
|
12
|
-
import 'events';
|
|
13
|
-
import 'util';
|
|
14
|
-
import 'path';
|
|
15
|
-
import 'assert';
|
|
16
|
-
import 'worker_threads';
|
|
17
|
-
import 'module';
|
|
18
|
-
import 'node:url';
|
|
19
|
-
import 'url';
|
|
20
|
-
import 'buffer';
|
|
21
|
-
import 'node:child_process';
|
|
22
|
-
import 'node:crypto';
|
|
23
|
-
import 'node:process';
|
|
24
|
-
import 'node:async_hooks';
|
|
25
|
-
import './indexes-DZ52grq6.js';
|
|
26
|
-
import './utils-BPSZyajG.js';
|
|
27
|
-
import './query-promise-BkPmGPJy.js';
|
|
28
|
-
import './lock-CA7d6lj7.js';
|
|
29
|
-
import './index-Dtt_h58T.js';
|
|
30
|
-
import 'constants';
|
|
31
|
-
import 'stream';
|
|
32
|
-
import 'node:fs/promises';
|
|
33
|
-
import './index-61916rmV.js';
|
|
34
|
-
|
|
35
|
-
//#region ../core/dist/store/exodus/table-name-map.js
|
|
36
|
-
/**
|
|
37
|
-
* Deterministic legacy-to-consolidated table-name resolver for exodus migration.
|
|
38
|
-
*
|
|
39
|
-
* ## Problem (ROOT CAUSE 1 — T11532)
|
|
40
|
-
*
|
|
41
|
-
* Legacy source DBs use UNPREFIXED table names (`tasks`, `messages`, `skills`, …)
|
|
42
|
-
* while the consolidated dual-scope `cleo.db` uses DOMAIN-PREFIXED names
|
|
43
|
-
* (`tasks_tasks`, `conduit_messages`, `skills_skills`, …). Without a mapping,
|
|
44
|
-
* every `INSERT OR IGNORE INTO main."<name>"` silently copies 0 rows because the
|
|
45
|
-
* target table is absent under the legacy name.
|
|
46
|
-
*
|
|
47
|
-
* ## Design
|
|
48
|
-
*
|
|
49
|
-
* The mapping is a static lookup table derived from reading every
|
|
50
|
-
* `cleo-project/` and `cleo-global/` schema file and matching the physical
|
|
51
|
-
* `sqliteTable('<consolidated-name>', …)` names against each legacy DB's schema.
|
|
52
|
-
*
|
|
53
|
-
* Rules:
|
|
54
|
-
* - If a legacy table is already domain-prefixed in the consolidated schema
|
|
55
|
-
* (e.g. `brain_observations`, `nexus_audit_log`, `tasks_goal`), map identity.
|
|
56
|
-
* - If a legacy table has NO consolidated counterpart (virtual tables, orphan
|
|
57
|
-
* telemetry tables, …), return `null` so the caller can log + skip explicitly
|
|
58
|
-
* rather than silently discarding rows.
|
|
59
|
-
*
|
|
60
|
-
* ## Source-DB scope
|
|
61
|
-
*
|
|
62
|
-
* The resolver takes a `sourceName` (the `LegacyDbDescriptor.name` value, e.g.
|
|
63
|
-
* `"tasks"`, `"brain (project)"`, `"conduit"`) to disambiguate tables that
|
|
64
|
-
* share the same legacy name across multiple DBs (e.g. `"attachments"` lives in
|
|
65
|
-
* both conduit.db and attachments.ts/tasks.db; `"sessions"` lives in both
|
|
66
|
-
* tasks.db and signaldock.db).
|
|
67
|
-
*
|
|
68
|
-
* ## Nexus code-graph residency move (ADR-090 · T11539)
|
|
69
|
-
*
|
|
70
|
-
* The four nexus code-graph tables — `nexus_nodes`, `nexus_relations`,
|
|
71
|
-
* `nexus_contracts`, `nexus_code_index` (ADR-090 "Category A") — were removed
|
|
72
|
-
* from the GLOBAL schema (`../schema/cleo-global/nexus.ts`) and now reside in
|
|
73
|
-
* PROJECT scope (`../schema/cleo-project/nexus-graph.ts`). They are still
|
|
74
|
-
* extracted from the legacy GLOBAL `nexus.db` source, but exodus MUST route them
|
|
75
|
-
* into the PROJECT-scope consolidated `cleo.db`, not the global one. The source
|
|
76
|
-
* descriptor `nexus` carries `targetScope: 'global'`, so a per-table scope
|
|
77
|
-
* override is required for these four tables — see
|
|
78
|
-
* {@link NEXUS_GRAPH_PROJECT_TABLES} and {@link resolveTableTargetScope}. The
|
|
79
|
-
* six registry/identity tables (`nexus_project_registry`,
|
|
80
|
-
* `nexus_project_id_aliases`, `nexus_audit_log`, `nexus_schema_meta`,
|
|
81
|
-
* `nexus_user_profile`, `nexus_sigils`) stay GLOBAL.
|
|
82
|
-
*
|
|
83
|
-
* @task T11532 (ROOT CAUSE 1 — name-mapping gap)
|
|
84
|
-
* @task T11533 (ROOT CAUSE 3 — signaldock skills mapping + brain_release_links skip +
|
|
85
|
-
* brain_session_narrative mapping)
|
|
86
|
-
* @task T11546 (no-home-table fixes — schema_meta→tasks_schema_meta, brain_usage_log mapping,
|
|
87
|
-
* brain_schema_meta mapping)
|
|
88
|
-
* @task T11539 (nexus code-graph residency — route the 4 graph tables to PROJECT scope)
|
|
89
|
-
* @epic T11248
|
|
90
|
-
* @saga T11242
|
|
91
|
-
*/
|
|
92
|
-
/**
|
|
93
|
-
* tasks.db — tables from tasks-schema.ts (tasks.ts, audit.ts, background-jobs.ts,
|
|
94
|
-
* chain-schema.ts, agent-schema.ts, lifecycle.ts, evidence-bindings.ts,
|
|
95
|
-
* experiments.ts, attachments.ts, manifest.ts, code-index.ts, playbooks.ts,
|
|
96
|
-
* provenance/releases.ts, provenance/commits.ts, provenance/pull-requests.ts,
|
|
97
|
-
* goal.ts) all prefixed with `tasks_` in the consolidated schema, except the
|
|
98
|
-
* docs tables which become `docs_*`.
|
|
99
|
-
*/
|
|
100
|
-
var TASKS_DB_MAP = new Map([
|
|
101
|
-
["tasks", "tasks_tasks"],
|
|
102
|
-
["task_acceptance_criteria", "tasks_task_acceptance_criteria"],
|
|
103
|
-
["acceptance_projection_state", "tasks_acceptance_projection_state"],
|
|
104
|
-
["acceptance_projection_dirty", "tasks_acceptance_projection_dirty"],
|
|
105
|
-
["task_dependencies", "tasks_task_dependencies"],
|
|
106
|
-
["task_labels", "tasks_task_labels"],
|
|
107
|
-
["task_relations", "tasks_task_relations"],
|
|
108
|
-
["sessions", "tasks_sessions"],
|
|
109
|
-
["session_handoff_entries", "tasks_session_handoff_entries"],
|
|
110
|
-
["task_work_history", "tasks_task_work_history"],
|
|
111
|
-
["task_acceptance_criteria_history", "tasks_task_acceptance_criteria_history"],
|
|
112
|
-
["external_task_links", "tasks_external_task_links"],
|
|
113
|
-
["audit_log", "tasks_audit_log"],
|
|
114
|
-
["token_usage", "tasks_token_usage"],
|
|
115
|
-
["architecture_decisions", "tasks_architecture_decisions"],
|
|
116
|
-
["adr_task_links", "tasks_adr_task_links"],
|
|
117
|
-
["adr_relations", "tasks_adr_relations"],
|
|
118
|
-
["status_registry", "tasks_status_registry"],
|
|
119
|
-
["background_jobs", "tasks_background_jobs"],
|
|
120
|
-
["warp_chains", "tasks_warp_chains"],
|
|
121
|
-
["warp_chain_instances", "tasks_warp_chain_instances"],
|
|
122
|
-
["agent_instances", "tasks_agent_instances"],
|
|
123
|
-
["agent_error_log", "tasks_agent_error_log"],
|
|
124
|
-
["lifecycle_pipelines", "tasks_lifecycle_pipelines"],
|
|
125
|
-
["lifecycle_stages", "tasks_lifecycle_stages"],
|
|
126
|
-
["lifecycle_gate_results", "tasks_lifecycle_gate_results"],
|
|
127
|
-
["lifecycle_evidence", "tasks_lifecycle_evidence"],
|
|
128
|
-
["lifecycle_transitions", "tasks_lifecycle_transitions"],
|
|
129
|
-
["evidence_ac_bindings", "tasks_evidence_ac_bindings"],
|
|
130
|
-
["experiments", "tasks_experiments"],
|
|
131
|
-
["attachments", "docs_attachments"],
|
|
132
|
-
["attachment_refs", "docs_attachment_refs"],
|
|
133
|
-
["manifest_entries", "docs_manifest_entries"],
|
|
134
|
-
["pipeline_manifest", "docs_pipeline_manifest"],
|
|
135
|
-
["commits", "tasks_commits"],
|
|
136
|
-
["task_commits", "tasks_task_commits"],
|
|
137
|
-
["commit_files", "tasks_commit_files"],
|
|
138
|
-
["pull_requests", "tasks_pull_requests"],
|
|
139
|
-
["pr_commits", "tasks_pr_commits"],
|
|
140
|
-
["pr_tasks", "tasks_pr_tasks"],
|
|
141
|
-
["releases", "tasks_releases"],
|
|
142
|
-
["release_commits", "tasks_release_commits"],
|
|
143
|
-
["release_changes", "tasks_release_changes"],
|
|
144
|
-
["release_changesets", "tasks_release_changesets"],
|
|
145
|
-
["release_artifacts", "tasks_release_artifacts"],
|
|
146
|
-
["playbook_runs", "tasks_playbook_runs"],
|
|
147
|
-
["playbook_approvals", "tasks_playbook_approvals"],
|
|
148
|
-
["tasks_goal", "tasks_goal"],
|
|
149
|
-
["schema_meta", "tasks_schema_meta"],
|
|
150
|
-
["agent_credentials", "tasks_agent_credentials"],
|
|
151
|
-
["brain_release_links", "tasks_brain_release_links"]
|
|
152
|
-
]);
|
|
153
|
-
/**
|
|
154
|
-
* brain.db (project) and global brain.db — tables from memory-schema.ts /
|
|
155
|
-
* cleo-shared/brain.ts.
|
|
156
|
-
*
|
|
157
|
-
* Most tables are already `brain_*` prefixed in the legacy schema and keep the
|
|
158
|
-
* same name in the consolidated schema. Three exceptions:
|
|
159
|
-
* - `sticky_tags` → `brain_sticky_tags` (lost prefix in legacy)
|
|
160
|
-
* - `deriver_queue` → `brain_deriver_queue` (lost prefix in legacy)
|
|
161
|
-
* - `session_narrative` → `brain_session_narrative` (lost prefix in legacy)
|
|
162
|
-
*
|
|
163
|
-
* Tables present in the live DB but NOT in the consolidated target schema
|
|
164
|
-
* (virtual tables, orphan telemetry, etc.) map to `null` — they will be
|
|
165
|
-
* logged as explicit skips rather than silently discarded.
|
|
166
|
-
*
|
|
167
|
-
* ## brain_release_links (T11533 → T11549 → T11550 fix)
|
|
168
|
-
*
|
|
169
|
-
* T11533 incorrectly marked `brain_release_links` as `null` (skip). T11549 added
|
|
170
|
-
* the target table `tasks_brain_release_links` to `cleo-project/provenance-orphans.ts`
|
|
171
|
-
* and mapped the table here in BRAIN_DB_MAP — but T11550 discovered that the 8 rows
|
|
172
|
-
* physically live in **tasks.db**, not brain.db (verified via sqlite_master). The
|
|
173
|
-
* correct mapping belongs in TASKS_DB_MAP (added there in T11550). The BRAIN_DB_MAP
|
|
174
|
-
* entry is now `null` (skip) to guard against double-migration if brain.db ever
|
|
175
|
-
* contains a phantom table by this name.
|
|
176
|
-
*
|
|
177
|
-
* ## agent_credentials (T11549 → T11550 fix)
|
|
178
|
-
*
|
|
179
|
-
* Same situation as brain_release_links: the 3 rows physically live in **tasks.db**,
|
|
180
|
-
* not brain.db. T11549 added the target table and mapped it here in BRAIN_DB_MAP,
|
|
181
|
-
* but the source is tasks.db. T11550 moves the mapping to TASKS_DB_MAP and sets
|
|
182
|
-
* this entry to `null` (skip) to prevent double-migration.
|
|
183
|
-
*/
|
|
184
|
-
var BRAIN_DB_MAP = new Map([
|
|
185
|
-
["brain_decisions", "brain_decisions"],
|
|
186
|
-
["brain_patterns", "brain_patterns"],
|
|
187
|
-
["brain_learnings", "brain_learnings"],
|
|
188
|
-
["brain_observations", "brain_observations"],
|
|
189
|
-
["brain_sticky_notes", "brain_sticky_notes"],
|
|
190
|
-
["brain_attention", "brain_attention"],
|
|
191
|
-
["brain_memory_links", "brain_memory_links"],
|
|
192
|
-
["brain_page_nodes", "brain_page_nodes"],
|
|
193
|
-
["brain_page_edges", "brain_page_edges"],
|
|
194
|
-
["brain_retrieval_log", "brain_retrieval_log"],
|
|
195
|
-
["brain_plasticity_events", "brain_plasticity_events"],
|
|
196
|
-
["brain_weight_history", "brain_weight_history"],
|
|
197
|
-
["brain_modulators", "brain_modulators"],
|
|
198
|
-
["brain_consolidation_events", "brain_consolidation_events"],
|
|
199
|
-
["brain_transcript_events", "brain_transcript_events"],
|
|
200
|
-
["brain_promotion_log", "brain_promotion_log"],
|
|
201
|
-
["brain_backfill_runs", "brain_backfill_runs"],
|
|
202
|
-
["brain_memory_trees", "brain_memory_trees"],
|
|
203
|
-
["brain_observations_staging", "brain_observations_staging"],
|
|
204
|
-
["brain_release_links", null],
|
|
205
|
-
["brain_schema_meta", "brain_schema_meta"],
|
|
206
|
-
["sticky_tags", "brain_sticky_tags"],
|
|
207
|
-
["deriver_queue", "brain_deriver_queue"],
|
|
208
|
-
["session_narrative", "brain_session_narrative"],
|
|
209
|
-
["brain_usage_log", "brain_usage_log"],
|
|
210
|
-
["brain_task_observations", null],
|
|
211
|
-
["brain_embeddings", null],
|
|
212
|
-
["brain_embeddings_info", null],
|
|
213
|
-
["agent_credentials", null]
|
|
214
|
-
]);
|
|
215
|
-
/**
|
|
216
|
-
* conduit.db — tables from conduit-schema.ts, all prefixed `conduit_*`.
|
|
217
|
-
*
|
|
218
|
-
* Note: conduit.db also has an `attachments` table (conduit attachment tracking),
|
|
219
|
-
* which maps to `conduit_attachments` — NOT `docs_attachments` (that is from
|
|
220
|
-
* tasks.db/attachments.ts).
|
|
221
|
-
*/
|
|
222
|
-
var CONDUIT_DB_MAP = new Map([
|
|
223
|
-
["messages", "conduit_messages"],
|
|
224
|
-
["delivery_jobs", "conduit_delivery_jobs"],
|
|
225
|
-
["dead_letters", "conduit_dead_letters"],
|
|
226
|
-
["message_pins", "conduit_message_pins"],
|
|
227
|
-
["attachments", "conduit_attachments"],
|
|
228
|
-
["attachment_versions", "conduit_attachment_versions"],
|
|
229
|
-
["attachment_approvals", "conduit_attachment_approvals"],
|
|
230
|
-
["attachment_contributors", "conduit_attachment_contributors"],
|
|
231
|
-
["topics", "conduit_topics"],
|
|
232
|
-
["topic_subscriptions", "conduit_topic_subscriptions"],
|
|
233
|
-
["topic_messages", "conduit_topic_messages"],
|
|
234
|
-
["topic_message_acks", "conduit_topic_message_acks"],
|
|
235
|
-
["conversations", "conduit_conversations"],
|
|
236
|
-
["project_agent_refs", "conduit_project_agent_refs"]
|
|
237
|
-
]);
|
|
238
|
-
/**
|
|
239
|
-
* nexus.db — tables from nexus-schema.ts.
|
|
240
|
-
*
|
|
241
|
-
* Some tables are ALREADY prefixed (`nexus_audit_log`, `nexus_nodes`, etc.) and
|
|
242
|
-
* map identity. Others lack the prefix and gain `nexus_` in consolidated.
|
|
243
|
-
*
|
|
244
|
-
* ## Scope split (ADR-090 · T11539)
|
|
245
|
-
*
|
|
246
|
-
* The legacy `nexus.db` source descriptor carries `targetScope: 'global'`, but
|
|
247
|
-
* the four code-graph tables (`code_index`/`nexus_code_index`, `nexus_nodes`,
|
|
248
|
-
* `nexus_relations`, `nexus_contracts`) now live in PROJECT scope. Their
|
|
249
|
-
* consolidated NAME is unchanged (identity / `nexus_code_index`), but their
|
|
250
|
-
* destination DB is the PROJECT `cleo.db` — see {@link NEXUS_GRAPH_PROJECT_TABLES}
|
|
251
|
-
* and {@link resolveTableTargetScope}. The remaining six registry/identity
|
|
252
|
-
* tables stay GLOBAL.
|
|
253
|
-
*/
|
|
254
|
-
var NEXUS_DB_MAP = new Map([
|
|
255
|
-
["project_registry", "nexus_project_registry"],
|
|
256
|
-
["project_id_aliases", "nexus_project_id_aliases"],
|
|
257
|
-
["user_profile", "nexus_user_profile"],
|
|
258
|
-
["sigils", "nexus_sigils"],
|
|
259
|
-
["code_index", "nexus_code_index"],
|
|
260
|
-
["nexus_audit_log", "nexus_audit_log"],
|
|
261
|
-
["nexus_nodes", "nexus_nodes"],
|
|
262
|
-
["nexus_relations", "nexus_relations"],
|
|
263
|
-
["nexus_relation_weights", "nexus_relation_weights"],
|
|
264
|
-
["nexus_contracts", "nexus_contracts"],
|
|
265
|
-
["nexus_schema_meta", "nexus_schema_meta"]
|
|
266
|
-
]);
|
|
267
|
-
/**
|
|
268
|
-
* Legacy table names of the four nexus code-graph tables that moved from GLOBAL
|
|
269
|
-
* to PROJECT scope (ADR-090 · T11538/T11539).
|
|
270
|
-
*
|
|
271
|
-
* Keyed by the LEGACY physical name as it appears in `nexus.db`:
|
|
272
|
-
* - `nexus_nodes`, `nexus_relations`, `nexus_contracts` — already prefixed.
|
|
273
|
-
* - `code_index` — bare in legacy `nexus.db`; → `nexus_code_index` consolidated.
|
|
274
|
-
*
|
|
275
|
-
* Membership drives {@link resolveTableTargetScope}: exodus copies/verifies
|
|
276
|
-
* these against the PROJECT consolidated `cleo.db`, NOT the GLOBAL one, even
|
|
277
|
-
* though the `nexus` source descriptor's `targetScope` is `'global'`.
|
|
278
|
-
*
|
|
279
|
-
* @task T11539
|
|
280
|
-
* @epic T11248
|
|
281
|
-
* @saga T11242
|
|
282
|
-
* @see cleo docs fetch adr-090-nexus-graph-residency-split
|
|
283
|
-
*/
|
|
284
|
-
var NEXUS_GRAPH_PROJECT_TABLES = new Set([
|
|
285
|
-
"nexus_nodes",
|
|
286
|
-
"nexus_relations",
|
|
287
|
-
"nexus_contracts",
|
|
288
|
-
"code_index"
|
|
289
|
-
]);
|
|
290
|
-
/**
|
|
291
|
-
* Resolve the consolidated TARGET SCOPE for a legacy source table.
|
|
292
|
-
*
|
|
293
|
-
* Most tables share the scope of their source DB descriptor (`sourceScope`).
|
|
294
|
-
* The exception is the four nexus code-graph tables — they are extracted from
|
|
295
|
-
* the GLOBAL legacy `nexus.db` but land in PROJECT scope per ADR-090 (T11539).
|
|
296
|
-
*
|
|
297
|
-
* This is the SSoT consumed by BOTH the exodus migrate runner (insert target DB)
|
|
298
|
-
* and the exodus verifier (verify target DB) so the two never disagree.
|
|
299
|
-
*
|
|
300
|
-
* @param sourceName - `LegacyDbDescriptor.name` (e.g. `"nexus"`, `"tasks"`).
|
|
301
|
-
* @param legacyTable - Physical table name in the legacy source DB.
|
|
302
|
-
* @param sourceScope - The source descriptor's `targetScope`.
|
|
303
|
-
* @returns The scope of the consolidated `cleo.db` this table copies into.
|
|
304
|
-
*
|
|
305
|
-
* @task T11539
|
|
306
|
-
* @epic T11248
|
|
307
|
-
* @saga T11242
|
|
308
|
-
*/
|
|
309
|
-
function resolveTableTargetScope(sourceName, legacyTable, sourceScope) {
|
|
310
|
-
if (inferSourceKind(sourceName) === "nexus" && NEXUS_GRAPH_PROJECT_TABLES.has(legacyTable)) return "project";
|
|
311
|
-
return sourceScope;
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* agent-registry (global) — the legacy on-disk source file is `signaldock.db`
|
|
315
|
-
* (the source-descriptor `name` stays `"signaldock"` — see {@link inferSourceKind} —
|
|
316
|
-
* because that is the genuine artifact name on disk). Its bare tables map to the
|
|
317
|
-
* consolidated PREFIXED `agent_registry_*` tables (renamed from `signaldock_*`
|
|
318
|
-
* under T11622 / SG-AGENT-IDENTITY E4).
|
|
319
|
-
*
|
|
320
|
-
* KEYS are the bare legacy names (the source DB still has the old shape); VALUES
|
|
321
|
-
* are the consolidated `agent_registry_*` target names — these MUST equal the
|
|
322
|
-
* `sqliteTable` names declared in `cleo-global/agent-registry.ts` (asserted by
|
|
323
|
-
* the exodus-map invariant test, the DHQ-046 N→0 deficit catcher).
|
|
324
|
-
*
|
|
325
|
-
* Note: the legacy `signaldock.db` has a `sessions` table (identity session
|
|
326
|
-
* management), which maps to `agent_registry_sessions` — NOT `tasks_sessions`.
|
|
327
|
-
*
|
|
328
|
-
* ## `skills` mapping (T11533 ROOT CAUSE 3 fix)
|
|
329
|
-
*
|
|
330
|
-
* The legacy `signaldock.db` contains a `skills` table (36 rows — pre-seeded skill
|
|
331
|
-
* slug catalog). In the consolidated global schema this becomes
|
|
332
|
-
* `agent_registry_skills`. Without this entry the legacy `skills` table fell
|
|
333
|
-
* through to an identity mapping, looked for a `skills` table in the global
|
|
334
|
-
* cleo.db (absent), and was silently skipped — resulting in
|
|
335
|
-
* agent_registry_skills=0 despite 36 source rows.
|
|
336
|
-
*
|
|
337
|
-
* @task T11622 (signaldock_* → agent_registry_* target rename; folds T11578 AC2)
|
|
338
|
-
*/
|
|
339
|
-
var AGENT_REGISTRY_DB_MAP = new Map([
|
|
340
|
-
["users", "agent_registry_users"],
|
|
341
|
-
["organization", "agent_registry_organization"],
|
|
342
|
-
["agents", "agent_registry_agents"],
|
|
343
|
-
["claim_codes", "agent_registry_claim_codes"],
|
|
344
|
-
["agent_capabilities", "agent_registry_agent_capabilities"],
|
|
345
|
-
["agent_skills", "agent_registry_agent_skills"],
|
|
346
|
-
["agent_connections", "agent_registry_agent_connections"],
|
|
347
|
-
["accounts", "agent_registry_accounts"],
|
|
348
|
-
["sessions", "agent_registry_sessions"],
|
|
349
|
-
["verifications", "agent_registry_verifications"],
|
|
350
|
-
["org_agent_keys", "agent_registry_org_agent_keys"],
|
|
351
|
-
["capabilities", "agent_registry_capabilities"],
|
|
352
|
-
["skills", "agent_registry_skills"]
|
|
353
|
-
]);
|
|
354
|
-
/**
|
|
355
|
-
* skills.db (global) — tables from skills-schema.ts, all prefixed `skills_*`.
|
|
356
|
-
*/
|
|
357
|
-
var SKILLS_DB_MAP = new Map([
|
|
358
|
-
["skills", "skills_skills"],
|
|
359
|
-
["skill_usage", "skills_skill_usage"],
|
|
360
|
-
["skill_reviews", "skills_skill_reviews"],
|
|
361
|
-
["skill_patches", "skills_skill_patches"]
|
|
362
|
-
]);
|
|
363
|
-
/**
|
|
364
|
-
* Infer the source DB kind from `LegacyDbDescriptor.name`.
|
|
365
|
-
*
|
|
366
|
-
* The descriptor names used in `buildSourceDescriptors()` are:
|
|
367
|
-
* `"tasks"`, `"brain (project)"`, `"conduit"`, `"nexus"`, `"signaldock"`, `"skills"`
|
|
368
|
-
*/
|
|
369
|
-
function inferSourceKind(sourceName) {
|
|
370
|
-
const n = sourceName.toLowerCase();
|
|
371
|
-
if (n.startsWith("tasks")) return "tasks";
|
|
372
|
-
if (n.startsWith("brain")) return "brain";
|
|
373
|
-
if (n.startsWith("conduit")) return "conduit";
|
|
374
|
-
if (n.startsWith("nexus")) return "nexus";
|
|
375
|
-
if (n.startsWith("signaldock")) return "signaldock";
|
|
376
|
-
if (n.startsWith("skills")) return "skills";
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Internal bookkeeping tables that exist in a legacy source DB but have NO
|
|
381
|
-
* consolidated counterpart and carry no user data — they are recreated by the
|
|
382
|
-
* runtime, not migrated. Matched by EXACT name.
|
|
383
|
-
*
|
|
384
|
-
* Each legacy per-domain SQLite file (`conduit.db`, `signaldock.db`,
|
|
385
|
-
* `skills.db`, …) carries its OWN private schema-version / migration-ledger
|
|
386
|
-
* table that records HOW that file was built — NOT user-payload data:
|
|
387
|
-
*
|
|
388
|
-
* - `_conduit_meta` / `_conduit_migrations` — conduit-sqlite's own
|
|
389
|
-
* schema-version + migration-ledger tables (see `conduit-sqlite.ts`).
|
|
390
|
-
* - `_signaldock_meta` / `_signaldock_migrations` — agent-registry-store's own
|
|
391
|
-
* ledger tables (same class — T11577 global-scope cutover blocker).
|
|
392
|
-
* - `_skills_meta` — skills.db's own schema-version row (same class — T11577).
|
|
393
|
-
*
|
|
394
|
-
* The consolidated `cleo.db` has its own Drizzle journal (`__drizzle_*`) for the
|
|
395
|
-
* same purpose, so these legacy ledgers have no consolidated home. Row-comparing
|
|
396
|
-
* them would count internal ledger rows as a user-data deficit and abort the
|
|
397
|
-
* cutover. The {@link INTERNAL_LEDGER_PATTERN} below generalises the same skip
|
|
398
|
-
* to ANY future `_<domain>_meta` / `_<domain>_migrations` ledger so a new source
|
|
399
|
-
* DB never re-introduces this false-positive; these exact entries remain for
|
|
400
|
-
* documentation + as a fast path.
|
|
401
|
-
*
|
|
402
|
-
* @task T11572 (parity gate over-abort — internal/meta exclusion)
|
|
403
|
-
* @task T11577 (generalise to signaldock/skills + any per-source ledger)
|
|
404
|
-
*/
|
|
405
|
-
var INTERNAL_BOOKKEEPING_TABLES = new Set([
|
|
406
|
-
"_conduit_meta",
|
|
407
|
-
"_conduit_migrations",
|
|
408
|
-
"_signaldock_meta",
|
|
409
|
-
"_signaldock_migrations",
|
|
410
|
-
"_skills_meta"
|
|
411
|
-
]);
|
|
412
|
-
/**
|
|
413
|
-
* Pattern matching a per-source SQLite **internal ledger** table: an underscore-
|
|
414
|
-
* prefixed `_<domain>_meta` or `_<domain>_migrations` name where `<domain>` is a
|
|
415
|
-
* lowercase identifier (e.g. `_conduit_meta`, `_signaldock_migrations`,
|
|
416
|
-
* `_skills_meta`).
|
|
417
|
-
*
|
|
418
|
-
* Every legacy domain DB (conduit/signaldock/skills/…) materialises one of these
|
|
419
|
-
* to track its own schema version / applied-migration history. They are private
|
|
420
|
-
* bookkeeping — NOT migratable base data — and have no consolidated home (the
|
|
421
|
-
* consolidated `cleo.db` keeps its own `__drizzle_*` journal). Generalising the
|
|
422
|
-
* skip to this shape is future-proof: a NEW source DB that follows the same
|
|
423
|
-
* `_<domain>_(meta|migrations)` convention is excluded automatically, so this
|
|
424
|
-
* exact false-positive class can never abort the cutover again.
|
|
425
|
-
*
|
|
426
|
-
* The pattern is intentionally conservative — it requires the leading underscore
|
|
427
|
-
* and the exact `_meta` / `_migrations` suffix so it can never match a real
|
|
428
|
-
* data table (those are never underscore-prefixed in any legacy source schema).
|
|
429
|
-
*
|
|
430
|
-
* @task T11577 (parity gate over-abort — generalise per-source ledger skip)
|
|
431
|
-
*/
|
|
432
|
-
var INTERNAL_LEDGER_PATTERN = /^_[a-z][a-z0-9]*_(?:meta|migrations)$/;
|
|
433
|
-
/**
|
|
434
|
-
* FTS5 shadow-table suffixes. A full-text index `<base>_fts` (an `fts5` VIRTUAL
|
|
435
|
-
* TABLE — e.g. `brain_decisions_fts`, `messages_fts`) materialises a family of
|
|
436
|
-
* backing tables `<base>_fts_data`, `<base>_fts_idx`, `<base>_fts_docsize`,
|
|
437
|
-
* `<base>_fts_config`, and (for `content=`-less indexes) `<base>_fts_content`.
|
|
438
|
-
*
|
|
439
|
-
* These are DERIVED from their content table and are REBUILT post-migration
|
|
440
|
-
* (`brain-search.ts` issues `INSERT INTO <base>_fts(<base>_fts) VALUES('rebuild')`).
|
|
441
|
-
* Their row counts do NOT correspond 1:1 to user rows, so comparing them against
|
|
442
|
-
* an absent consolidated target produces a spurious N→0 deficit. They must be
|
|
443
|
-
* excluded from the row-count-parity gate, not migrated.
|
|
444
|
-
*
|
|
445
|
-
* @task T11572 (parity gate over-abort — FTS5 shadow-table exclusion)
|
|
446
|
-
*/
|
|
447
|
-
var FTS5_SHADOW_SUFFIXES = [
|
|
448
|
-
"_fts_data",
|
|
449
|
-
"_fts_idx",
|
|
450
|
-
"_fts_docsize",
|
|
451
|
-
"_fts_config",
|
|
452
|
-
"_fts_content"
|
|
453
|
-
];
|
|
454
|
-
/**
|
|
455
|
-
* Return `true` if `tableName` is a DERIVED or INTERNAL table that must be
|
|
456
|
-
* EXCLUDED from the exodus row-count-parity gate (and from the copy path).
|
|
457
|
-
*
|
|
458
|
-
* This is the single, named, documented classification consumed by BOTH the
|
|
459
|
-
* migrate copy loop and the `verifyMigration` parity check, so the two never
|
|
460
|
-
* disagree about which tables carry migratable user data. It recognises:
|
|
461
|
-
*
|
|
462
|
-
* 1. **FTS5 virtual tables** — a bare `*_fts` name (e.g. `brain_decisions_fts`,
|
|
463
|
-
* `messages_fts`). The virtual table itself cannot be `INSERT … SELECT`-ed
|
|
464
|
-
* and is rebuilt from its content table after migration.
|
|
465
|
-
* 2. **FTS5 shadow/backing tables** — `*_fts_data`, `*_fts_idx`,
|
|
466
|
-
* `*_fts_docsize`, `*_fts_config`, `*_fts_content` (see
|
|
467
|
-
* {@link FTS5_SHADOW_SUFFIXES}). Derived; rebuilt post-migration.
|
|
468
|
-
* 3. **Internal bookkeeping** — any per-source schema-version / migration
|
|
469
|
-
* ledger: `_conduit_meta`, `_conduit_migrations`, `_signaldock_meta`,
|
|
470
|
-
* `_signaldock_migrations`, `_skills_meta`, and — generalised via
|
|
471
|
-
* {@link INTERNAL_LEDGER_PATTERN} — ANY `_<domain>_meta` /
|
|
472
|
-
* `_<domain>_migrations` ledger from a future source DB (see
|
|
473
|
-
* {@link INTERNAL_BOOKKEEPING_TABLES}). Schema-version ledgers with no
|
|
474
|
-
* consolidated home.
|
|
475
|
-
*
|
|
476
|
-
* A migration is "safe" iff every BASE-DATA row survives; these derived/internal
|
|
477
|
-
* tables are NOT base data, so a 0-row consolidated counterpart for them is
|
|
478
|
-
* expected and correct — not data loss.
|
|
479
|
-
*
|
|
480
|
-
* @param tableName - Physical table name from a legacy source DB.
|
|
481
|
-
* @returns `true` when the table is derived/internal and must be skipped.
|
|
482
|
-
*
|
|
483
|
-
* @task T11572 (exodus parity gate: exclude FTS5 + internal/meta shadow tables)
|
|
484
|
-
* @task T11577 (generalise internal-ledger skip to signaldock/skills + any per-source ledger)
|
|
485
|
-
* @epic T11249 (E6)
|
|
486
|
-
* @saga T11242
|
|
487
|
-
*/
|
|
488
|
-
function isDerivedOrInternalTable(tableName) {
|
|
489
|
-
if (INTERNAL_BOOKKEEPING_TABLES.has(tableName)) return true;
|
|
490
|
-
if (INTERNAL_LEDGER_PATTERN.test(tableName)) return true;
|
|
491
|
-
if (tableName.endsWith("_fts")) return true;
|
|
492
|
-
return FTS5_SHADOW_SUFFIXES.some((suffix) => tableName.endsWith(suffix));
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
|
-
* Resolve the consolidated target table name for a legacy source table.
|
|
496
|
-
*
|
|
497
|
-
* @param sourceName - `LegacyDbDescriptor.name` (e.g. `"tasks"`, `"brain (project)"`).
|
|
498
|
-
* @param legacyTable - Physical table name from the legacy source DB.
|
|
499
|
-
* @returns A `TableNameResolution` describing how to copy (or skip) the table.
|
|
500
|
-
*/
|
|
501
|
-
function resolveConsolidatedTableName(sourceName, legacyTable) {
|
|
502
|
-
if (isDerivedOrInternalTable(legacyTable)) return {
|
|
503
|
-
kind: "skip",
|
|
504
|
-
reason: legacyTable.includes("_fts") ? `derived FTS5 table — rebuilt post-migration from its content table, not row-compared` : `internal bookkeeping table — no consolidated home (recreated by runtime)`
|
|
505
|
-
};
|
|
506
|
-
const kind = inferSourceKind(sourceName);
|
|
507
|
-
if (kind === null) return {
|
|
508
|
-
kind: "unknown",
|
|
509
|
-
targetName: legacyTable
|
|
510
|
-
};
|
|
511
|
-
let map;
|
|
512
|
-
switch (kind) {
|
|
513
|
-
case "tasks":
|
|
514
|
-
map = TASKS_DB_MAP;
|
|
515
|
-
break;
|
|
516
|
-
case "brain":
|
|
517
|
-
map = BRAIN_DB_MAP;
|
|
518
|
-
break;
|
|
519
|
-
case "conduit":
|
|
520
|
-
map = CONDUIT_DB_MAP;
|
|
521
|
-
break;
|
|
522
|
-
case "nexus":
|
|
523
|
-
map = NEXUS_DB_MAP;
|
|
524
|
-
break;
|
|
525
|
-
case "signaldock":
|
|
526
|
-
map = AGENT_REGISTRY_DB_MAP;
|
|
527
|
-
break;
|
|
528
|
-
case "skills":
|
|
529
|
-
map = SKILLS_DB_MAP;
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
|
-
if (!map.has(legacyTable)) return {
|
|
533
|
-
kind: "mapped",
|
|
534
|
-
targetName: legacyTable
|
|
535
|
-
};
|
|
536
|
-
const consolidated = map.get(legacyTable);
|
|
537
|
-
if (consolidated === null || consolidated === void 0) return {
|
|
538
|
-
kind: "skip",
|
|
539
|
-
reason: getSkipReason(kind, legacyTable)
|
|
540
|
-
};
|
|
541
|
-
return {
|
|
542
|
-
kind: "mapped",
|
|
543
|
-
targetName: consolidated
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
/**
|
|
547
|
-
* Return a human-readable explanation for why a table is intentionally excluded
|
|
548
|
-
* from the consolidated schema.
|
|
549
|
-
*/
|
|
550
|
-
function getSkipReason(sourceKind, legacyTable) {
|
|
551
|
-
return {
|
|
552
|
-
brain_task_observations: "runtime-only observation cache (not Drizzle-managed); recreated lazily after exodus cutover",
|
|
553
|
-
brain_embeddings: "vec0 VIRTUAL TABLE — cannot be migrated via INSERT/SELECT; requires sqlite-vec extension; recreated lazily after exodus cutover",
|
|
554
|
-
brain_embeddings_info: "metadata companion to brain_embeddings vec0 virtual table; excluded from consolidated schema (derived/recreatable)"
|
|
555
|
-
}[legacyTable] ?? `table '${legacyTable}' from ${sourceKind} has no consolidated target`;
|
|
556
|
-
}
|
|
557
|
-
getLogger("exodus-count-parity");
|
|
558
|
-
//#endregion
|
|
559
|
-
//#region ../core/dist/store/exodus/plan.js
|
|
560
|
-
/**
|
|
561
|
-
* Exodus pre-flight plan builder.
|
|
562
|
-
*
|
|
563
|
-
* `buildExodusPlan()` computes the full migration plan — source DB paths,
|
|
564
|
-
* combined size, free-disk availability, staging directory — BEFORE any
|
|
565
|
-
* writes occur. A `--dry-run` caller can print the plan and exit early.
|
|
566
|
-
*
|
|
567
|
-
* ## Disk pre-flight (AC8 · right-sized T11838)
|
|
568
|
-
*
|
|
569
|
-
* The original gate required `availableBytes >= 3 * totalSourceBytes` — an
|
|
570
|
-
* over-estimate that blocked large-fleet migrations even with ample headroom:
|
|
571
|
-
* exodus never holds 3× the SUM of every source on disk at once. It copies one
|
|
572
|
-
* source into staging at a time (lock released before the next) and writes a
|
|
573
|
-
* single consolidated cleo.db whose size approximates the sum of source ROW data.
|
|
574
|
-
*
|
|
575
|
-
* The right-sized requirement is therefore
|
|
576
|
-
* `STAGING_HEADROOM_FACTOR * largestSourceBytes + consolidatedEstimate`, where
|
|
577
|
-
* `consolidatedEstimate ≈ totalSourceBytes` ({@link computeRequiredBytes}). The
|
|
578
|
-
* check uses `statvfs` via `node:fs.statfsSync()` (Node 18+).
|
|
579
|
-
*
|
|
580
|
-
* @task T11248 (E5 · SG-DB-SUBSTRATE-V2)
|
|
581
|
-
* @task T11838 (right-sized preflight + optional staging copy for large sources)
|
|
582
|
-
* @saga T11242
|
|
583
|
-
*/
|
|
584
|
-
/**
|
|
585
|
-
* Headroom multiplier applied to the LARGEST single source when sizing the
|
|
586
|
-
* staging-copy footprint. The staging dir only ever holds ONE source backup at a
|
|
587
|
-
* time (its advisory lock is released before the next source is touched), so the
|
|
588
|
-
* peak staging footprint is the largest source plus a small slack for the
|
|
589
|
-
* write-then-rename journal + SQLite sidecars — hence `1.2×`, not `3×` of the SUM.
|
|
590
|
-
*
|
|
591
|
-
* @task T11838
|
|
592
|
-
*/
|
|
593
|
-
var STAGING_HEADROOM_FACTOR = 1.2;
|
|
594
|
-
/**
|
|
595
|
-
* Per-source byte threshold above which the full staging `copyFileSync` backup
|
|
596
|
-
* is skipped (the source is archived, not deleted, on success). Sized at 256 MiB
|
|
597
|
-
* — comfortably above the small project DBs (tasks/conduit/skills/signaldock are
|
|
598
|
-
* sub-MB to low-MB) but below the large global stores (a multi-GB `brain.db` or
|
|
599
|
-
* `nexus.db`) whose redundant full-file copy is the costly case T11838 removes.
|
|
600
|
-
*
|
|
601
|
-
* @task T11838
|
|
602
|
-
*/
|
|
603
|
-
var STAGING_COPY_SKIP_THRESHOLD_BYTES = 256 * 1024 * 1024;
|
|
604
|
-
/**
|
|
605
|
-
* Compute the right-sized free-disk requirement for an exodus run (T11838).
|
|
606
|
-
*
|
|
607
|
-
* `STAGING_HEADROOM_FACTOR * largestSourceBytes` covers the peak staging-copy
|
|
608
|
-
* footprint (one source at a time, plus slack), and `totalSourceBytes` is the
|
|
609
|
-
* consolidated-cleo.db estimate (every source's row data lands there). The two
|
|
610
|
-
* are additive because staging and the consolidated DB coexist on disk until the
|
|
611
|
-
* sources are archived.
|
|
612
|
-
*
|
|
613
|
-
* @param totalSourceBytes - Combined size of all source DB files in bytes.
|
|
614
|
-
* @param largestSourceBytes - Size of the single largest source DB in bytes.
|
|
615
|
-
* @returns The minimum free bytes the target filesystem must have.
|
|
616
|
-
*/
|
|
617
|
-
function computeRequiredBytes(totalSourceBytes, largestSourceBytes) {
|
|
618
|
-
return Math.ceil(STAGING_HEADROOM_FACTOR * largestSourceBytes) + totalSourceBytes;
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Build the ordered list of legacy source DB descriptors for a given project.
|
|
622
|
-
*
|
|
623
|
-
* Project-scoped sources live under `<project>/.cleo/`.
|
|
624
|
-
* Global-scoped sources live under `getCleoHome()`.
|
|
625
|
-
*/
|
|
626
|
-
function buildSourceDescriptors(cwd) {
|
|
627
|
-
const cleoDir = resolveCleoDir(cwd);
|
|
628
|
-
const cleoHome = getCleoHome();
|
|
629
|
-
return [
|
|
630
|
-
{
|
|
631
|
-
name: "tasks",
|
|
632
|
-
path: join(cleoDir, "tasks.db"),
|
|
633
|
-
targetScope: "project"
|
|
634
|
-
},
|
|
635
|
-
{
|
|
636
|
-
name: "brain (project)",
|
|
637
|
-
path: join(cleoDir, "brain.db"),
|
|
638
|
-
targetScope: "project"
|
|
639
|
-
},
|
|
640
|
-
{
|
|
641
|
-
name: "conduit",
|
|
642
|
-
path: join(cleoDir, "conduit.db"),
|
|
643
|
-
targetScope: "project"
|
|
644
|
-
},
|
|
645
|
-
{
|
|
646
|
-
name: "nexus",
|
|
647
|
-
path: join(cleoHome, "nexus.db"),
|
|
648
|
-
targetScope: "global"
|
|
649
|
-
},
|
|
650
|
-
{
|
|
651
|
-
name: "signaldock",
|
|
652
|
-
path: join(cleoHome, "signaldock.db"),
|
|
653
|
-
targetScope: "global"
|
|
654
|
-
},
|
|
655
|
-
{
|
|
656
|
-
name: "skills",
|
|
657
|
-
path: join(cleoHome, "skills.db"),
|
|
658
|
-
targetScope: "global"
|
|
659
|
-
}
|
|
660
|
-
];
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Safely stat a file and return its size in bytes, or 0 if it does not exist.
|
|
664
|
-
*/
|
|
665
|
-
function safeFileBytes(filePath) {
|
|
666
|
-
try {
|
|
667
|
-
return statSync(filePath).size;
|
|
668
|
-
} catch {
|
|
669
|
-
return 0;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Return available bytes on the filesystem that contains `dir`.
|
|
674
|
-
*
|
|
675
|
-
* Uses `statfsSync` (Node 18+). Falls back to 0 if unavailable.
|
|
676
|
-
*/
|
|
677
|
-
function getAvailableBytes(dir) {
|
|
678
|
-
try {
|
|
679
|
-
const result = statfsSync(dir);
|
|
680
|
-
return (result.bavail ?? result.bfree ?? 0) * (result.bsize ?? 4096);
|
|
681
|
-
} catch {
|
|
682
|
-
return 0;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
/**
|
|
686
|
-
* Derive the staging directory name from the current ISO-8601 timestamp.
|
|
687
|
-
*
|
|
688
|
-
* Pattern: `.cleo/exodus-staging-<YYYYMMDDTHHMMSSZ>`.
|
|
689
|
-
* Colons are replaced with empty string (NTFS + shell-safe).
|
|
690
|
-
*/
|
|
691
|
-
function deriveStagingDirName() {
|
|
692
|
-
return `exodus-staging-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:]/g, "").replace(/\..+Z$/, "Z")}`;
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Scan for an existing staging directory from a previous (possibly crashed)
|
|
696
|
-
* run inside the given `.cleo/` directory.
|
|
697
|
-
*
|
|
698
|
-
* Returns the absolute path to the most-recent staging dir, or `null` if none
|
|
699
|
-
* exists.
|
|
700
|
-
*/
|
|
701
|
-
function findExistingStaging(cleoDir) {
|
|
702
|
-
try {
|
|
703
|
-
const stagingDirs = readdirSync(cleoDir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("exodus-staging-")).map((e) => e.name).sort().reverse();
|
|
704
|
-
if (stagingDirs.length > 0) return join(cleoDir, stagingDirs[0]);
|
|
705
|
-
} catch {}
|
|
706
|
-
return null;
|
|
707
|
-
}
|
|
708
|
-
/**
|
|
709
|
-
* Build the complete exodus plan.
|
|
710
|
-
*
|
|
711
|
-
* This is a pure read operation — no files are created or modified.
|
|
712
|
-
* Pass the result to `runExodusMigrate()` to execute the migration.
|
|
713
|
-
*
|
|
714
|
-
* @param cwd - Working directory used to resolve the project root. Defaults to
|
|
715
|
-
* `process.cwd()`.
|
|
716
|
-
* @returns {@link ExodusPlan} describing sources, disk availability, and paths.
|
|
717
|
-
*
|
|
718
|
-
* @task T11248 (AC8 — disk pre-flight)
|
|
719
|
-
* @task T11838 (right-sized: largest-source factor + consolidated estimate)
|
|
720
|
-
*/
|
|
721
|
-
function buildExodusPlan(cwd) {
|
|
722
|
-
const cleoDir = resolveCleoDir(cwd);
|
|
723
|
-
const sources = buildSourceDescriptors(cwd);
|
|
724
|
-
const sourceBytes = sources.map((s) => safeFileBytes(s.path));
|
|
725
|
-
const totalSourceBytes = sourceBytes.reduce((sum, b) => sum + b, 0);
|
|
726
|
-
const largestSourceBytes = sourceBytes.reduce((max, b) => Math.max(max, b), 0);
|
|
727
|
-
const requiredBytes = computeRequiredBytes(totalSourceBytes, largestSourceBytes);
|
|
728
|
-
const availableBytes = getAvailableBytes(cleoDir);
|
|
729
|
-
const diskPreflight = totalSourceBytes === 0 || availableBytes >= requiredBytes;
|
|
730
|
-
const existingStaging = findExistingStaging(cleoDir);
|
|
731
|
-
return {
|
|
732
|
-
sources,
|
|
733
|
-
totalSourceBytes,
|
|
734
|
-
largestSourceBytes,
|
|
735
|
-
requiredBytes,
|
|
736
|
-
availableBytes,
|
|
737
|
-
diskPreflight,
|
|
738
|
-
stagingCopyThresholdBytes: STAGING_COPY_SKIP_THRESHOLD_BYTES,
|
|
739
|
-
stagingDir: existingStaging ?? join(cleoDir, deriveStagingDirName()),
|
|
740
|
-
resumeFromStaging: existingStaging !== null,
|
|
741
|
-
projectDbPath: resolveDualScopeDbPath("project", cwd),
|
|
742
|
-
globalDbPath: resolveDualScopeDbPath("global")
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
//#endregion
|
|
746
|
-
//#region ../core/dist/store/exodus/column-transforms.js
|
|
747
|
-
/**
|
|
748
|
-
* Shared column-value transform layer for the exodus migration.
|
|
749
|
-
*
|
|
750
|
-
* The exodus copy (`migrate.ts`) does not move source values byte-for-byte into
|
|
751
|
-
* the consolidated `cleo.db`: a handful of columns are TRANSFORMED on the way in
|
|
752
|
-
* so they satisfy the consolidated schema's CHECK constraints (which the legacy
|
|
753
|
-
* runtime DBs did not carry). Three transform classes exist:
|
|
754
|
-
*
|
|
755
|
-
* 1. **Epoch INTEGER → ISO-8601 TEXT** ({@link buildEpochToIsoExpr}) — for any
|
|
756
|
-
* target column carrying an ISO-GLOB CHECK ({@link detectIsoGlobColumns}).
|
|
757
|
-
* A per-row magnitude heuristic ({@link EPOCH_SECONDS_THRESHOLD}) classifies
|
|
758
|
-
* seconds vs milliseconds.
|
|
759
|
-
* 2. **Legacy enum → canonical member** ({@link ENUM_NORMALIZATIONS} +
|
|
760
|
-
* {@link enumNormExpr}) — maps pre-tightening enum aliases (e.g.
|
|
761
|
-
* `tasks_commits.conventional_type` `'style'`/`'merge'` → `'chore'`).
|
|
762
|
-
* 3. **Non-finite REAL → finite** ({@link NUMERIC_CLAMPS} +
|
|
763
|
-
* {@link numericClampExpr}) — `Inf`/`-Inf`/`NaN` → a finite in-range value
|
|
764
|
-
* (`brain_weight_history.delta_weight`).
|
|
765
|
-
*
|
|
766
|
-
* ## Why this module exists (T11809 · AC2)
|
|
767
|
-
*
|
|
768
|
-
* Before T11809 these transforms lived ONLY in `migrate.ts`. The parity verifier
|
|
769
|
-
* (`verify-migration.ts`) digested RAW source values against the TRANSFORMED
|
|
770
|
-
* target values, so every coerced column (epoch INTEGER vs ISO TEXT, legacy enum
|
|
771
|
-
* vs canonical, Inf vs clamped) produced a `hashMatch === false` even on a
|
|
772
|
-
* perfectly lossless migration — a false-negative that aborted the cutover and
|
|
773
|
-
* lost the batch writes accumulated during the migrating open.
|
|
774
|
-
*
|
|
775
|
-
* Extracting the transform logic into ONE shared module lets the verifier digest
|
|
776
|
-
* the SOURCE side **through the same transforms migrate applied**
|
|
777
|
-
* ({@link buildDigestExpr}), so equal logical data digests equal. `migrate.ts`
|
|
778
|
-
* re-imports these primitives so its copy behaviour stays byte-identical; the
|
|
779
|
-
* verifier imports {@link buildDigestExpr} for the digest-oriented variant.
|
|
780
|
-
*
|
|
781
|
-
* @task T11809 (exodus verify applies source-side coercion — hashMatch on equal data)
|
|
782
|
-
* @task T11546 (epoch→ISO coercion — original)
|
|
783
|
-
* @task T11547 (enum normalization — original)
|
|
784
|
-
* @task T11782 (non-finite numeric clamp — original)
|
|
785
|
-
* @epic T11249 (E6)
|
|
786
|
-
* @saga T11242
|
|
787
|
-
*/
|
|
788
|
-
/**
|
|
789
|
-
* Determine a safe SQL literal default for a NOT NULL column with no schema
|
|
790
|
-
* default, given its SQLite type affinity.
|
|
791
|
-
*
|
|
792
|
-
* Used by `migrate.ts` to coalesce NULL source values for target-only NOT NULL
|
|
793
|
-
* columns so that rows are not silently dropped by `INSERT OR IGNORE` when a
|
|
794
|
-
* source value is NULL (T11533 ROOT CAUSE 2 fix).
|
|
795
|
-
*
|
|
796
|
-
* @param colType - Raw `type` string from `PRAGMA table_info` (e.g. `"INTEGER"`,
|
|
797
|
-
* `"TEXT"`, `"REAL"`, `"BLOB"`, or compound forms like `"text NOT NULL"`).
|
|
798
|
-
* @returns A SQL literal string suitable for embedding in a `COALESCE()` call.
|
|
799
|
-
*/
|
|
800
|
-
function typeDefaultLiteral(colType) {
|
|
801
|
-
const upper = colType.toUpperCase();
|
|
802
|
-
if (upper.includes("INT")) return "0";
|
|
803
|
-
if (upper.includes("REAL") || upper.includes("FLOAT") || upper.includes("DOUBLE")) return "0.0";
|
|
804
|
-
if (upper.includes("BLOB")) return "x''";
|
|
805
|
-
return "''";
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Per-(targetTable, column) normalization rules that map legacy enum values to
|
|
809
|
-
* the canonical enum accepted by the consolidated schema CHECK constraints.
|
|
810
|
-
*
|
|
811
|
-
* Each entry is a function that, given the `srcRef` SQL expression for the
|
|
812
|
-
* column, returns a SQL CASE expression that produces the canonical value.
|
|
813
|
-
* Rows with already-canonical values pass through unchanged (the ELSE branch).
|
|
814
|
-
*
|
|
815
|
-
* ## Brain enum normalizations REMOVED (T11647)
|
|
816
|
-
*
|
|
817
|
-
* The brain memory family no longer participates in enum normalization. Its
|
|
818
|
-
* consolidated exodus target now matches the LEGACY RUNTIME shape, which carries
|
|
819
|
-
* NO SQL CHECK constraints (the `text({ enum })` unions are enforced at the
|
|
820
|
-
* application layer only). With no CHECK to satisfy, coercing brain enum values
|
|
821
|
-
* would be data corruption, not a fix — so every brain enum value is copied
|
|
822
|
-
* VERBATIM. The historical brain rules (`brain_observations.{source_type,type}`,
|
|
823
|
-
* `brain_decisions.{confirmation_state,decision_category,confidence,outcome,
|
|
824
|
-
* decided_by}`) were deleted. The TASKS-domain rules below remain because those
|
|
825
|
-
* consolidated tables keep their CHECK constraints.
|
|
826
|
-
*
|
|
827
|
-
* ## nexus/signaldock enum-drift audit (T11809 · AC1)
|
|
828
|
-
*
|
|
829
|
-
* A real-data audit of `nexus.db` (106 MB) and `signaldock.db` (280 KB) against
|
|
830
|
-
* the consolidated CHECK enums found ZERO out-of-enum values for every
|
|
831
|
-
* CHECK-constrained column that has source data: `nexus_nodes.kind` (24,482
|
|
832
|
-
* rows), `nexus_relations.type` (39,163 rows), `nexus_contracts.type` (0 rows),
|
|
833
|
-
* `nexus_sigils.role` (8 rows), `agent_registry_agents.status` (19 rows),
|
|
834
|
-
* `agent_registry_users.role` (0 rows). No new normalization entry was required.
|
|
835
|
-
* The reported "nexus/signaldock drop rows" symptom was in fact the AC2 verify
|
|
836
|
-
* false-negative (epoch→ISO coercion makes the source digest differ from the
|
|
837
|
-
* target digest), which fixing {@link buildDigestExpr} resolves — NOT a CHECK
|
|
838
|
-
* drop. See the T11809 return notes for the full per-column row counts.
|
|
839
|
-
*
|
|
840
|
-
* Lookup key: `${targetTable}.${column}` (lowercase, dotted).
|
|
841
|
-
*
|
|
842
|
-
* @since T11547 (P0 data-loss fix)
|
|
843
|
-
* @since T11548 (P0 final enum coverage)
|
|
844
|
-
* @since T11647 (brain target = runtime shape — brain enum coercions removed)
|
|
845
|
-
*/
|
|
846
|
-
var ENUM_NORMALIZATIONS = new Map([
|
|
847
|
-
["tasks_task_commits.link_source", (src) => `CASE ${src} WHEN 'commit-message' THEN 'commit-subject' ELSE ${src} END`],
|
|
848
|
-
["tasks_architecture_decisions.status", (src) => `CASE WHEN lower(${src}) = 'accepted' OR lower(${src}) LIKE 'accepted %' OR lower(${src}) = 'approved' THEN 'accepted' WHEN lower(${src}) = 'proposed' THEN 'proposed' WHEN lower(${src}) = 'superseded' THEN 'superseded' WHEN lower(${src}) = 'deprecated' THEN 'deprecated' ELSE ${src} END`],
|
|
849
|
-
["tasks_commits.conventional_type", (src) => `CASE WHEN ${src} IS NULL THEN NULL WHEN ${src} IN ('feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'build', 'ci', 'perf', 'revert', 'breaking') THEN ${src} ELSE 'chore' END`],
|
|
850
|
-
["tasks_task_relations.relation_type", (src) => `CASE ${src} WHEN 'grouped-by' THEN 'groups' ELSE ${src} END`],
|
|
851
|
-
["tasks_lifecycle_stages.stage_name", (src) => `CASE ${src} WHEN 'implemented' THEN 'implementation' WHEN 'qaPassed' THEN 'validation' WHEN 'testsPassed' THEN 'testing' ELSE ${src} END`],
|
|
852
|
-
["tasks_architecture_decisions.gate_status", (src) => `CASE WHEN ${src} LIKE 'passed%' THEN 'passed' WHEN ${src} = 'approved' THEN 'passed' ELSE ${src} END`],
|
|
853
|
-
["tasks_evidence_ac_bindings.binding_type", (src) => `CASE WHEN ${src} LIKE 'validator:%' THEN 'direct' ELSE ${src} END`]
|
|
854
|
-
]);
|
|
855
|
-
/**
|
|
856
|
-
* Return a SQL CASE expression that normalises legacy enum values for `col` in
|
|
857
|
-
* `targetTableName` to the canonical values accepted by the consolidated CHECK,
|
|
858
|
-
* or return `null` when no normalization rule exists for this (table, column).
|
|
859
|
-
*
|
|
860
|
-
* @param targetTableName - Physical consolidated target table name.
|
|
861
|
-
* @param col - Column name.
|
|
862
|
-
* @param srcRef - SQL expression referencing the source column.
|
|
863
|
-
* @returns A SQL CASE expression string, or `null` if no rule applies.
|
|
864
|
-
*/
|
|
865
|
-
function enumNormExpr(targetTableName, col, srcRef) {
|
|
866
|
-
const key = `${targetTableName}.${col}`;
|
|
867
|
-
const fn = ENUM_NORMALIZATIONS.get(key);
|
|
868
|
-
return fn ? fn(srcRef) : null;
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Per-(targetTable, column) numeric-clamp rules that coerce non-finite legacy
|
|
872
|
-
* REAL values (`Inf` / `-Inf` / `NaN`) to a finite in-range value so the row is
|
|
873
|
-
* NOT silently dropped by `INSERT OR IGNORE`.
|
|
874
|
-
*
|
|
875
|
-
* ## Why this exists (T11782)
|
|
876
|
-
*
|
|
877
|
-
* 188,926 of 697,780 legacy `brain_weight_history` rows carry
|
|
878
|
-
* `delta_weight = Inf`/`-Inf` (the R-STDP plasticity writer saturated the delta
|
|
879
|
-
* before the value was clamped at write time). SQLite stores ±Inf as the IEEE-754
|
|
880
|
-
* float, but the consolidated `brain_weight_history.delta_weight` column is a
|
|
881
|
-
* plain `real NOT NULL` with NO CHECK — so a verbatim copy would land the Inf
|
|
882
|
-
* value. The historical deficit, however, was that those rows tripped a constraint
|
|
883
|
-
* elsewhere in the copy chain and `INSERT OR IGNORE` dropped them, yielding a
|
|
884
|
-
* deficit that fired the parity-gate abort. Clamping the non-finite value to a
|
|
885
|
-
* finite member of the column's domain guarantees every row lands.
|
|
886
|
-
*
|
|
887
|
-
* The clamp mirrors the {@link ENUM_NORMALIZATIONS} shape: each entry is a
|
|
888
|
-
* function `(srcRef) => CASE …`. Finite values pass through unchanged via the
|
|
889
|
-
* ELSE branch. The `col != col` self-comparison is the canonical SQL NaN guard
|
|
890
|
-
* (NaN is the only value not equal to itself); `9e999` is the SQLite literal that
|
|
891
|
-
* evaluates to `+Infinity` (and `-9e999` to `-Infinity`).
|
|
892
|
-
*
|
|
893
|
-
* Lookup key: `${targetTable}.${column}` (lowercase, dotted).
|
|
894
|
-
*
|
|
895
|
-
* @since T11782 (P0 — brain_weight_history Inf recovery)
|
|
896
|
-
*/
|
|
897
|
-
var NUMERIC_CLAMPS = new Map([["brain_weight_history.delta_weight", (src) => `CASE WHEN ${src} = 9e999 THEN 1.0 WHEN ${src} = -9e999 THEN -1.0 WHEN ${src} != ${src} THEN 0.0 ELSE ${src} END`]]);
|
|
898
|
-
/**
|
|
899
|
-
* Return a SQL CASE expression that clamps non-finite legacy REAL values for
|
|
900
|
-
* `col` in `targetTableName` to a finite in-range value, or `null` when no
|
|
901
|
-
* clamp rule exists for this (table, column).
|
|
902
|
-
*
|
|
903
|
-
* @param targetTableName - Physical consolidated target table name.
|
|
904
|
-
* @param col - Column name.
|
|
905
|
-
* @param srcRef - SQL expression referencing the source column.
|
|
906
|
-
* @returns A SQL CASE expression string, or `null` if no rule applies.
|
|
907
|
-
*/
|
|
908
|
-
function numericClampExpr(targetTableName, col, srcRef) {
|
|
909
|
-
const key = `${targetTableName}.${col}`;
|
|
910
|
-
const fn = NUMERIC_CLAMPS.get(key);
|
|
911
|
-
return fn ? fn(srcRef) : null;
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Regex to detect ISO GLOB CHECK constraints in DDL SQL.
|
|
915
|
-
* Matches: `CHECK ("colname" IS NULL OR "colname" GLOB '[0-9]...')`
|
|
916
|
-
* Uses `\[0-9` to match the literal `[0-9` at the start of the GLOB pattern.
|
|
917
|
-
*/
|
|
918
|
-
var ISO_CHECK_REGEX = /CHECK\s*\(\s*"([^"]+)"\s+IS\s+NULL\s+OR\s+"[^"]+"\s+GLOB\s+'\[0-9/gi;
|
|
919
|
-
/**
|
|
920
|
-
* Magnitude threshold distinguishing epoch SECONDS from epoch MILLISECONDS.
|
|
921
|
-
*
|
|
922
|
-
* A Unix epoch value for years 2020–2100 is roughly 1.6e9 – 4.1e9 seconds,
|
|
923
|
-
* or 1.6e12 – 4.1e12 milliseconds. The safe boundary is 1e11 (100 billion):
|
|
924
|
-
* any value BELOW 1e11 is in seconds (even year 2100 seconds ≈ 4.1e9 < 1e11);
|
|
925
|
-
* any value AT OR ABOVE 1e11 is in milliseconds (year 2020 ms ≈ 1.6e12 > 1e11).
|
|
926
|
-
*
|
|
927
|
-
* This constant is embedded directly in the generated SQL CASE expression so
|
|
928
|
-
* it is evaluated per-row — each row's epoch is classified independently.
|
|
929
|
-
*/
|
|
930
|
-
var EPOCH_SECONDS_THRESHOLD = 1e11;
|
|
931
|
-
/**
|
|
932
|
-
* Build a SQL expression that converts an INTEGER epoch column to ISO-8601 TEXT,
|
|
933
|
-
* automatically detecting whether the stored value is in seconds or milliseconds
|
|
934
|
-
* using a magnitude heuristic (T11549 correctness fix).
|
|
935
|
-
*
|
|
936
|
-
* ## Heuristic
|
|
937
|
-
*
|
|
938
|
-
* A per-row CASE checks whether the column value is below {@link EPOCH_SECONDS_THRESHOLD}
|
|
939
|
-
* (100 billion). If so, the value is treated as seconds and passed directly to
|
|
940
|
-
* `strftime(..., 'unixepoch')`. If at or above the threshold, it is divided by
|
|
941
|
-
* 1000.0 first (milliseconds → seconds).
|
|
942
|
-
*
|
|
943
|
-
* This replaces the previous per-source heuristic which failed when individual
|
|
944
|
-
* columns within a source DB used a different epoch unit than the majority of that
|
|
945
|
-
* source's columns. The specific bug: `nexus.user_profile.{first_observed_at,
|
|
946
|
-
* last_reinforced_at}` stores SECONDS (value ≈ 1.78e9) but the nexus source was
|
|
947
|
-
* labeled `milliseconds`, causing these values to be divided by 1000 and converted
|
|
948
|
-
* to a 1970 date.
|
|
949
|
-
*
|
|
950
|
-
* ## NULL handling
|
|
951
|
-
*
|
|
952
|
-
* A NULL source value is preserved as NULL so it passes the `IS NULL` branch of
|
|
953
|
-
* the ISO GLOB CHECK constraint on the target column.
|
|
954
|
-
*
|
|
955
|
-
* @param srcRef - SQL expression referencing the source column value.
|
|
956
|
-
* @returns A SQL CASE expression producing an ISO-8601 TEXT timestamp.
|
|
957
|
-
*/
|
|
958
|
-
function buildEpochToIsoExpr(srcRef) {
|
|
959
|
-
return `CASE WHEN ${srcRef} IS NULL THEN NULL WHEN ${srcRef} < ${EPOCH_SECONDS_THRESHOLD} THEN strftime('%Y-%m-%dT%H:%M:%fZ', ${srcRef}, 'unixepoch') ELSE strftime('%Y-%m-%dT%H:%M:%fZ', ${srcRef}/1000.0, 'unixepoch') END`;
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Parse the DDL for a given table from `sqlite_master` and return the set of
|
|
963
|
-
* column names that have an ISO GLOB CHECK constraint.
|
|
964
|
-
*
|
|
965
|
-
* Reads the raw DDL text and uses a regex to extract column names appearing in
|
|
966
|
-
* `CHECK ("colname" IS NULL OR "colname" GLOB '[0-9]...')` patterns. This is
|
|
967
|
-
* robust to Drizzle's generated CHECK format (all CHECK constraints generated
|
|
968
|
-
* by T11363 follow this exact pattern).
|
|
969
|
-
*
|
|
970
|
-
* @param db - Target DB with the consolidated schema.
|
|
971
|
-
* @param tableName - Physical table name (consolidated, e.g. `conduit_messages`).
|
|
972
|
-
* @param targetSchema - Schema name the target table lives in (`'main'`, or an
|
|
973
|
-
* ATTACH alias for cross-scope routing — ADR-090 nexus graph residency, T11539).
|
|
974
|
-
* @returns Set of column names that require ISO GLOB validation.
|
|
975
|
-
*/
|
|
976
|
-
function detectIsoGlobColumns(db, tableName, targetSchema = "main") {
|
|
977
|
-
const escapedTable = tableName.replace(/'/g, "''");
|
|
978
|
-
const row = db.prepare(`SELECT sql FROM "${targetSchema}".sqlite_master WHERE type='table' AND name='${escapedTable}'`).get();
|
|
979
|
-
if (!row?.sql) return /* @__PURE__ */ new Set();
|
|
980
|
-
const isoColumns = /* @__PURE__ */ new Set();
|
|
981
|
-
ISO_CHECK_REGEX.lastIndex = 0;
|
|
982
|
-
for (const match of row.sql.matchAll(ISO_CHECK_REGEX)) isoColumns.add(match[1]);
|
|
983
|
-
return isoColumns;
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Return `true` when the source column's type affinity is INTEGER-like, so an
|
|
987
|
-
* epoch→ISO coercion applies when the matching target column carries an ISO GLOB
|
|
988
|
-
* CHECK. Empty affinity and `NUMERIC` are treated as integer-like (matching the
|
|
989
|
-
* historical `buildSelectExpr` behaviour — legacy epoch columns sometimes carry
|
|
990
|
-
* no declared type or a `NUMERIC` affinity).
|
|
991
|
-
*
|
|
992
|
-
* @param srcType - Raw `type` string from the source `PRAGMA table_info`.
|
|
993
|
-
* @returns `true` if the source column should be considered an INTEGER epoch.
|
|
994
|
-
*/
|
|
995
|
-
function isIntegerSourceType(srcType) {
|
|
996
|
-
const upper = srcType.toUpperCase();
|
|
997
|
-
return upper.includes("INT") || upper === "" || upper === "NUMERIC";
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Wrap a digest value expression in the SAME `COALESCE(expr, type_default)`
|
|
1001
|
-
* substitution migrate's `buildSelectExpr` applies, when (and only when) the
|
|
1002
|
-
* matching TARGET column is NOT NULL without a schema default (T11836).
|
|
1003
|
-
*
|
|
1004
|
-
* On a faithful FRESH migration a NULL source value in such a column is stored
|
|
1005
|
-
* as the type default (`''`/`0`/`0.0`/`x''`) by migrate; digesting the source
|
|
1006
|
-
* raw would read NULL and diverge from the stored default — a false
|
|
1007
|
-
* `hashMatch === false` at equal row counts. Mirroring the COALESCE here makes
|
|
1008
|
-
* the source digest reflect what migrate actually stored, so the substitution is
|
|
1009
|
-
* a NON-FATAL diagnostic class rather than a content-corruption failure.
|
|
1010
|
-
*
|
|
1011
|
-
* @param expr - The value expression produced by an upstream transform branch.
|
|
1012
|
-
* @param tgtCol - Target column metadata, or `undefined` when unavailable (no
|
|
1013
|
-
* COALESCE applied — best-effort, biased to surfacing a real difference).
|
|
1014
|
-
* @returns The expression, COALESCE-wrapped when the target is NOT-NULL-without-default.
|
|
1015
|
-
*/
|
|
1016
|
-
function maybeCoalesceNotNull(expr, tgtCol) {
|
|
1017
|
-
if (tgtCol === void 0) return expr;
|
|
1018
|
-
if (!(tgtCol.notnull === 1 && tgtCol.dflt_value === null)) return expr;
|
|
1019
|
-
return `COALESCE(${expr}, ${typeDefaultLiteral(tgtCol.type)})`;
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Build the SQL expression a column's SOURCE value must pass through so it
|
|
1023
|
-
* matches the canonical value the migration STORES in the target — for use by
|
|
1024
|
-
* the parity verifier's content digest (T11809 · AC2).
|
|
1025
|
-
*
|
|
1026
|
-
* This is the digest-oriented sibling of migrate's `buildSelectExpr`. It applies
|
|
1027
|
-
* the SAME value transforms the migration actually performs — and ONLY those:
|
|
1028
|
-
*
|
|
1029
|
-
* 1. **Epoch→ISO-8601** ({@link buildEpochToIsoExpr}) — when the target has an
|
|
1030
|
-
* ISO GLOB CHECK and the source column is INTEGER-typed.
|
|
1031
|
-
* 2. **Non-finite numeric clamp** ({@link numericClampExpr}).
|
|
1032
|
-
* 3. **Enum-value normalization** ({@link enumNormExpr}).
|
|
1033
|
-
* 4. **Plain column reference** otherwise.
|
|
1034
|
-
*
|
|
1035
|
-
* ## NOT-NULL default substitution mirroring (T11836)
|
|
1036
|
-
*
|
|
1037
|
-
* After the value transform, the result is wrapped in the SAME
|
|
1038
|
-
* `COALESCE(expr, type_default)` substitution migrate's `buildSelectExpr` uses
|
|
1039
|
-
* for a TARGET column that is NOT NULL without a schema default (see
|
|
1040
|
-
* {@link maybeCoalesceNotNull}). migrate stores the type default (`''`/`0`/
|
|
1041
|
-
* `0.0`/`x''`) for a NULL source value in such a column; without mirroring it
|
|
1042
|
-
* here the source digest would read NULL and diverge from the stored default — a
|
|
1043
|
-
* false `hashMatch === false` on a faithful FRESH migration (the exact T11836
|
|
1044
|
-
* symptom). Mirroring it makes a NULL→NOT-NULL-default substitution a NON-FATAL
|
|
1045
|
-
* diagnostic class rather than a hash failure. The target metadata is supplied
|
|
1046
|
-
* via the `tgtColByCol` map; when it is absent (best-effort) no COALESCE is
|
|
1047
|
-
* applied and a genuine NULL→'' divergence remains visible as before.
|
|
1048
|
-
*
|
|
1049
|
-
* The returned expression is a bare SQL value expression (no `AS "col"` alias)
|
|
1050
|
-
* suitable for embedding directly in a `SELECT <expr> ... ORDER BY ...` digest
|
|
1051
|
-
* query. When no transform applies, a plain quoted column reference is returned.
|
|
1052
|
-
*
|
|
1053
|
-
* @param targetTableName - Physical consolidated target table name (transform
|
|
1054
|
-
* lookup key).
|
|
1055
|
-
* @param col - Column name (present in BOTH source and target).
|
|
1056
|
-
* @param srcType - Raw `type` string from the source `PRAGMA table_info`.
|
|
1057
|
-
* @param isoGlobCols - Set of target columns carrying an ISO GLOB CHECK.
|
|
1058
|
-
* @param tgtCol - Target column metadata (NOT-NULL flag + default +
|
|
1059
|
-
* affinity) for `col`, or `undefined` when unavailable.
|
|
1060
|
-
* @returns A SQL value expression that maps the raw source value to the canonical
|
|
1061
|
-
* value the target stores.
|
|
1062
|
-
*/
|
|
1063
|
-
function buildDigestExpr(targetTableName, col, srcType, isoGlobCols, tgtCol) {
|
|
1064
|
-
const srcRef = `"${col}"`;
|
|
1065
|
-
if (isoGlobCols.has(col) && isIntegerSourceType(srcType)) return maybeCoalesceNotNull(buildEpochToIsoExpr(srcRef), tgtCol);
|
|
1066
|
-
const clampExpr = numericClampExpr(targetTableName, col, srcRef);
|
|
1067
|
-
if (clampExpr !== null) return maybeCoalesceNotNull(clampExpr, tgtCol);
|
|
1068
|
-
const normExpr = enumNormExpr(targetTableName, col, srcRef);
|
|
1069
|
-
if (normExpr !== null) return maybeCoalesceNotNull(normExpr, tgtCol);
|
|
1070
|
-
return maybeCoalesceNotNull(srcRef, tgtCol);
|
|
1071
|
-
}
|
|
1072
|
-
//#endregion
|
|
1073
|
-
//#region ../core/dist/store/exodus/types.js
|
|
1074
|
-
/**
|
|
1075
|
-
* Shared types for the `cleo exodus` migration subsystem.
|
|
1076
|
-
*
|
|
1077
|
-
* The exodus subsystem migrates data from the legacy multi-DB fleet
|
|
1078
|
-
* (tasks.db · brain.db · conduit.db · nexus.db · signaldock.db · skills.db)
|
|
1079
|
-
* into the consolidated dual-scope `cleo.db` (project + global).
|
|
1080
|
-
*
|
|
1081
|
-
* @task T11248 (E5 · SG-DB-SUBSTRATE-V2)
|
|
1082
|
-
* @saga T11242
|
|
1083
|
-
* @adr ADR-068, ADR-069
|
|
1084
|
-
*/
|
|
1085
|
-
/**
|
|
1086
|
-
* Canonical schema version tag embedded in the exodus journal.
|
|
1087
|
-
*
|
|
1088
|
-
* This constant represents the Drizzle v1.0.0-rc.3 target schema "epoch"
|
|
1089
|
-
* for the dual-scope consolidation (T11245 · E2 · SG-DB-SUBSTRATE-V2).
|
|
1090
|
-
* The exact value is a stable marker — not a live hash — so that import
|
|
1091
|
-
* refuses cross-version migration unless `--force-cross-version` is passed.
|
|
1092
|
-
*/
|
|
1093
|
-
var EXODUS_TARGET_SCHEMA_VERSION = "drizzle-v1.0.0-rc.3/dual-scope/2026-05";
|
|
1094
|
-
//#endregion
|
|
1095
|
-
//#region ../core/dist/store/exodus/migrate.js
|
|
1096
|
-
/**
|
|
1097
|
-
* Exodus migration engine.
|
|
1098
|
-
*
|
|
1099
|
-
* `runExodusMigrate()` performs the actual data migration from legacy DBs
|
|
1100
|
-
* to the consolidated dual-scope `cleo.db`. Key invariants:
|
|
1101
|
-
*
|
|
1102
|
-
* - Source DBs are opened **read-only** via `openCleoDbSnapshot` (AC4).
|
|
1103
|
-
* - Source files are backed up to the staging dir before any writes (AC5).
|
|
1104
|
-
* - Import is wrapped in `BEGIN … COMMIT` per source; partial failure leaves
|
|
1105
|
-
* the target DB untouched (AC6).
|
|
1106
|
-
* - Idempotency keys are propagated where the source row has them; generated
|
|
1107
|
-
* where it does not (AC7).
|
|
1108
|
-
* - The staging journal is written atomically before each table copy so a
|
|
1109
|
-
* crash can be resumed (AC5).
|
|
1110
|
-
*
|
|
1111
|
-
* ## Type coercion — epoch INTEGER → ISO-8601 TEXT (ROOT CAUSE 1 fix — T11546)
|
|
1112
|
-
*
|
|
1113
|
-
* Many legacy tables store timestamps as INTEGER epoch values (seconds or
|
|
1114
|
-
* milliseconds). The consolidated schema declares these columns as `text` with
|
|
1115
|
-
* a CHECK constraint: `CHECK ("col" IS NULL OR "col" GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]*')`.
|
|
1116
|
-
*
|
|
1117
|
-
* When a source INTEGER value is inserted into a target TEXT+GLOB column, SQLite
|
|
1118
|
-
* coerces the integer to its decimal string representation (e.g. `"1717200000"`),
|
|
1119
|
-
* which fails the GLOB check. `INSERT OR IGNORE` then SILENTLY DROPS the entire
|
|
1120
|
-
* row. Result: all conduit_messages, brain_observations, etc. → 0 rows copied
|
|
1121
|
-
* while migrate reports `success: true, rowsCopied: 0`.
|
|
1122
|
-
*
|
|
1123
|
-
* Fix (two parts):
|
|
1124
|
-
* (a) Per-column value transform: `detectEpochToIsoColumns()` reads the target
|
|
1125
|
-
* table DDL from `sqlite_master` to identify columns with an ISO GLOB
|
|
1126
|
-
* CHECK constraint. For those columns, if the source type affinity is
|
|
1127
|
-
* INTEGER, the SELECT expression applies `strftime('%Y-%m-%dT%H:%M:%fZ',
|
|
1128
|
-
* col, 'unixepoch')` (seconds) or the `/1000.0` ms variant depending on
|
|
1129
|
-
* the per-source/table heuristic.
|
|
1130
|
-
* (b) No-swallow assertion: after the bulk INSERT OR IGNORE, the actual
|
|
1131
|
-
* `changes` count is compared against the source row count. Any shortfall
|
|
1132
|
-
* is surfaced as a hard table-level error (not a silent success).
|
|
1133
|
-
* PK/UNIQUE conflicts on resume are tolerated (they are expected and safe
|
|
1134
|
-
* to ignore); unexpected shortfalls are flagged with a detailed error.
|
|
1135
|
-
*
|
|
1136
|
-
* ## ATTACH-once-per-source design (P0 fix — T11531)
|
|
1137
|
-
*
|
|
1138
|
-
* Each legacy source DB is ATTACHed to the target handle ONCE using a unique
|
|
1139
|
-
* per-source alias, all tables from that source are copied under the single
|
|
1140
|
-
* attachment, and then the alias is DETACHed. The ATTACH and DETACH are
|
|
1141
|
-
* performed **outside** the BEGIN/COMMIT transaction block because SQLite
|
|
1142
|
-
* forbids DETACH inside an active multi-statement transaction. The INSERT
|
|
1143
|
-
* statements themselves are issued inside the transaction for atomicity (AC6).
|
|
1144
|
-
*
|
|
1145
|
-
* Prior implementation called ATTACH/DETACH per-table inside an open
|
|
1146
|
-
* transaction — DETACH silently failed (SQLite restriction), the alias stayed
|
|
1147
|
-
* attached, and every subsequent table for the same source threw
|
|
1148
|
-
* "database _exodus_src_ is already in use", causing ~80 % data loss.
|
|
1149
|
-
*
|
|
1150
|
-
* ## FK-defer bulk copy (ROOT CAUSE 1 fix — T11533)
|
|
1151
|
-
*
|
|
1152
|
-
* Legacy tasks.db has self-referential FKs (`tasks.parent_id → tasks.id`),
|
|
1153
|
-
* cross-table FKs, and ~18 tables with FK relationships. When FK enforcement
|
|
1154
|
-
* is ON and tables are copied in arbitrary order (children before parents),
|
|
1155
|
-
* each child INSERT fails with FOREIGN KEY constraint error (SQLite errcode 787)
|
|
1156
|
-
* and the table is silently skipped — causing ~114K rows of data loss.
|
|
1157
|
-
*
|
|
1158
|
-
* Fix: set `PRAGMA foreign_keys = OFF` on the target connection before any
|
|
1159
|
-
* bulk INSERT, then after all tables are committed run `PRAGMA foreign_key_check`
|
|
1160
|
-
* to validate referential integrity. Genuine orphan rows surface as verify
|
|
1161
|
-
* failures; child-before-parent ordering artifacts are NOT dropped.
|
|
1162
|
-
* `PRAGMA foreign_keys = ON` is restored after the check.
|
|
1163
|
-
*
|
|
1164
|
-
* ## Name-mapping (ROOT CAUSE 1 fix — T11532)
|
|
1165
|
-
*
|
|
1166
|
-
* Legacy source DBs use UNPREFIXED table names (`tasks`, `messages`, `skills`,
|
|
1167
|
-
* …) while the consolidated `cleo.db` uses DOMAIN-PREFIXED names
|
|
1168
|
-
* (`tasks_tasks`, `conduit_messages`, `skills_skills`, …). The
|
|
1169
|
-
* `resolveConsolidatedTableName()` function from `table-name-map.ts` performs
|
|
1170
|
-
* the deterministic legacy→consolidated mapping before every copy. Tables with
|
|
1171
|
-
* no consolidated home emit an explicit WARN journal entry rather than being
|
|
1172
|
-
* silently discarded.
|
|
1173
|
-
*
|
|
1174
|
-
* ## Column-drift tolerance (ROOT CAUSE 2 fix — T11532, hardened T11533)
|
|
1175
|
-
*
|
|
1176
|
-
* When the source and target schemas differ (consolidated schema added/changed
|
|
1177
|
-
* columns vs legacy), the copy uses the INTERSECTION of source and target
|
|
1178
|
-
* column names. New target-only columns take their schema defaults; old
|
|
1179
|
-
* source-only columns are dropped. This is implemented by introspecting both
|
|
1180
|
-
* schemas via `PRAGMA table_info` and building an explicit column list.
|
|
1181
|
-
*
|
|
1182
|
-
* **NOT NULL / no-default hazard (T11533)**: target-only NOT NULL columns
|
|
1183
|
-
* WITHOUT a schema default caused `INSERT OR IGNORE` to silently drop rows
|
|
1184
|
-
* whose source value was NULL for that column (constraint violation → IGNORE).
|
|
1185
|
-
* The fix: for each intersection column that is NOT NULL in the target AND
|
|
1186
|
-
* has no `dflt_value`, the SELECT clause wraps the source reference in
|
|
1187
|
-
* `COALESCE(src_col, type_default)` so no row is silently dropped.
|
|
1188
|
-
* A type_default of `''` is used for TEXT affinity and `0` for numeric.
|
|
1189
|
-
*
|
|
1190
|
-
* ## Advisory file lock (AC4)
|
|
1191
|
-
*
|
|
1192
|
-
* The source DB files are opened read-only via `openCleoDbSnapshot` which
|
|
1193
|
-
* calls `new DatabaseSync(path, { readOnly: true })`. Node's SQLite binding
|
|
1194
|
-
* opens with `SQLITE_OPEN_READONLY`, which prevents any writes from this
|
|
1195
|
-
* process. We additionally write a `.lock` sentinel file next to each source
|
|
1196
|
-
* DB for the duration of the migration so that other CLEO processes can detect
|
|
1197
|
-
* an in-progress exodus and refuse to write.
|
|
1198
|
-
*
|
|
1199
|
-
* @task T11248 (E5 · SG-DB-SUBSTRATE-V2)
|
|
1200
|
-
* @task T11531 (P0 attach-leak fix)
|
|
1201
|
-
* @task T11532 (P0 name-mapping + column-drift + verify-rowid fix)
|
|
1202
|
-
* @task T11533 (P0 FK-defer + NOT NULL coalesce + signaldock-global map + nexus hash fix)
|
|
1203
|
-
* @task T11547 (P0 enum normalization — 7,421 rows recovered)
|
|
1204
|
-
* @task T11548 (P0 final enum coverage — 285 remaining rows zero-loss)
|
|
1205
|
-
* @task T11549 (P0 zero-loss final mile — brain_decisions enums + seconds-epoch coercion +
|
|
1206
|
-
* agent_credentials/brain_release_links tables)
|
|
1207
|
-
* @saga T11242
|
|
1208
|
-
*/
|
|
1209
|
-
var log$2 = getLogger("exodus-migrate");
|
|
1210
|
-
var LOCK_SENTINEL_SUFFIX = ".exodus-lock";
|
|
1211
|
-
var JOURNAL_FILENAME = "exodus-journal.json";
|
|
1212
|
-
/**
|
|
1213
|
-
* Get the SQLite version string from an open DatabaseSync handle.
|
|
1214
|
-
*/
|
|
1215
|
-
function getSqliteVersion(db) {
|
|
1216
|
-
try {
|
|
1217
|
-
return db.prepare("SELECT sqlite_version() AS v").get()?.v ?? "unknown";
|
|
1218
|
-
} catch {
|
|
1219
|
-
return "unknown";
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Read the tables list from a legacy SQLite DB (excluding SQLite internals).
|
|
1224
|
-
*/
|
|
1225
|
-
function listTables$1(db) {
|
|
1226
|
-
return db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__drizzle_%' ORDER BY name").all().map((r) => r.name);
|
|
1227
|
-
}
|
|
1228
|
-
/**
|
|
1229
|
-
* Write the journal file atomically (write-then-rename pattern).
|
|
1230
|
-
*/
|
|
1231
|
-
function writeJournal(stagingDir, journal) {
|
|
1232
|
-
const journalPath = join(stagingDir, JOURNAL_FILENAME);
|
|
1233
|
-
const tmpPath = `${journalPath}.tmp`;
|
|
1234
|
-
writeFileSync(tmpPath, JSON.stringify(journal, null, 2) + "\n", "utf8");
|
|
1235
|
-
renameSync(tmpPath, journalPath);
|
|
1236
|
-
}
|
|
1237
|
-
/**
|
|
1238
|
-
* Read an existing journal from the staging dir, or return `null`.
|
|
1239
|
-
*/
|
|
1240
|
-
function readJournal(stagingDir) {
|
|
1241
|
-
const journalPath = join(stagingDir, JOURNAL_FILENAME);
|
|
1242
|
-
if (!existsSync(journalPath)) return null;
|
|
1243
|
-
try {
|
|
1244
|
-
return JSON.parse(readFileSync(journalPath, "utf8"));
|
|
1245
|
-
} catch {
|
|
1246
|
-
return null;
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
/**
|
|
1250
|
-
* Invalidate the migrate journal after an aborted/rolled-back migration so a
|
|
1251
|
-
* post-abort retry RE-COPIES every table from scratch (T11572).
|
|
1252
|
-
*
|
|
1253
|
-
* ## Why this is required for retry correctness
|
|
1254
|
-
*
|
|
1255
|
-
* `runExodusMigrate` is resumable (AC5): after each table copy it writes a
|
|
1256
|
-
* journal entry with `status: 'done'`, and a subsequent run SKIPS any table
|
|
1257
|
-
* already marked `done`. That resume optimisation is correct only while the
|
|
1258
|
-
* consolidated rows it copied still EXIST. When the parity gate aborts, the
|
|
1259
|
-
* exodus-on-open hook truncates the consolidated tables back to EMPTY
|
|
1260
|
-
* (`rollbackConsolidatedToEmpty`) — but the on-disk journal still says every
|
|
1261
|
-
* table is `done`. The next open therefore re-triggers (consolidated empty),
|
|
1262
|
-
* runs `runExodusMigrate` again, finds every table `done` in the journal, copies
|
|
1263
|
-
* NOTHING, re-verifies the still-empty target, and re-aborts — a permanent loop
|
|
1264
|
-
* that pays the full migrate+verify cost on every open.
|
|
1265
|
-
*
|
|
1266
|
-
* Deleting the journal file forces `runExodusMigrate` to `initJournal()` afresh,
|
|
1267
|
-
* so the retry actually re-copies. Idempotent and best-effort: a missing journal
|
|
1268
|
-
* (or a missing staging dir) is a no-op. The staging backups are intentionally
|
|
1269
|
-
* LEFT in place (they are the legacy source-of-truth snapshots — harmless to
|
|
1270
|
-
* keep, valuable for forensics); only the progress journal is cleared.
|
|
1271
|
-
*
|
|
1272
|
-
* @param stagingDir - The staging directory whose `exodus-journal.json` to clear.
|
|
1273
|
-
* @returns `true` if a journal file was removed, `false` if there was nothing to
|
|
1274
|
-
* remove.
|
|
1275
|
-
*
|
|
1276
|
-
* @task T11572 (abort/rollback must invalidate journal so retry re-copies)
|
|
1277
|
-
* @epic T11249 (E6)
|
|
1278
|
-
* @saga T11242
|
|
1279
|
-
*/
|
|
1280
|
-
function clearExodusJournal(stagingDir) {
|
|
1281
|
-
const journalPath = join(stagingDir, JOURNAL_FILENAME);
|
|
1282
|
-
try {
|
|
1283
|
-
if (!existsSync(journalPath)) return false;
|
|
1284
|
-
unlinkSync(journalPath);
|
|
1285
|
-
log$2.info({ stagingDir }, "exodus: cleared migrate journal after abort/rollback — a retry will RE-COPY all tables");
|
|
1286
|
-
return true;
|
|
1287
|
-
} catch (err) {
|
|
1288
|
-
log$2.warn({
|
|
1289
|
-
err,
|
|
1290
|
-
stagingDir
|
|
1291
|
-
}, "exodus: failed to clear migrate journal after rollback (a retry may skip already-\"done\" tables)");
|
|
1292
|
-
return false;
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
/**
|
|
1296
|
-
* Initialise a fresh journal object.
|
|
1297
|
-
*/
|
|
1298
|
-
function initJournal(sqliteVersion) {
|
|
1299
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1300
|
-
return {
|
|
1301
|
-
version: 1,
|
|
1302
|
-
cleoVersion: getCleoVersion(),
|
|
1303
|
-
targetSchemaVersion: EXODUS_TARGET_SCHEMA_VERSION,
|
|
1304
|
-
nodeVersion: process.version,
|
|
1305
|
-
sqliteVersion,
|
|
1306
|
-
startedAt: now,
|
|
1307
|
-
updatedAt: now,
|
|
1308
|
-
tables: []
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
function lockPath(dbPath) {
|
|
1312
|
-
return `${dbPath}${LOCK_SENTINEL_SUFFIX}`;
|
|
1313
|
-
}
|
|
1314
|
-
function acquireAdvisoryLock(dbPath) {
|
|
1315
|
-
writeFileSync(lockPath(dbPath), JSON.stringify({
|
|
1316
|
-
pid: process.pid,
|
|
1317
|
-
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1318
|
-
}), "utf8");
|
|
1319
|
-
}
|
|
1320
|
-
function releaseAdvisoryLock(dbPath) {
|
|
1321
|
-
try {
|
|
1322
|
-
unlinkSync(lockPath(dbPath));
|
|
1323
|
-
} catch {}
|
|
1324
|
-
}
|
|
1325
|
-
/**
|
|
1326
|
-
* Return a file's size in bytes, or `0` when it cannot be stat'd (T11838).
|
|
1327
|
-
*
|
|
1328
|
-
* Used to decide whether a source exceeds the staging-copy skip threshold. A
|
|
1329
|
-
* non-existent / unreadable source is treated as 0 bytes (it is filtered out of
|
|
1330
|
-
* the copy loop separately), so it never trips the large-source skip branch.
|
|
1331
|
-
*
|
|
1332
|
-
* @param filePath - Absolute path to the file to stat.
|
|
1333
|
-
* @returns The file size in bytes, or `0` on any stat failure.
|
|
1334
|
-
*/
|
|
1335
|
-
function safeStatBytes(filePath) {
|
|
1336
|
-
try {
|
|
1337
|
-
return statSync(filePath).size;
|
|
1338
|
-
} catch {
|
|
1339
|
-
return 0;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
/**
|
|
1343
|
-
* Convert a source DB name to a safe, unique SQLite ATTACH alias.
|
|
1344
|
-
*
|
|
1345
|
-
* SQLite identifiers may contain only word characters; we prefix with
|
|
1346
|
-
* `_src_` so they never collide with target table names.
|
|
1347
|
-
*
|
|
1348
|
-
* @param name - Logical name from `LegacyDbDescriptor.name` (e.g. `"brain (project)"`).
|
|
1349
|
-
* @param index - Positional index used when names collide after sanitisation.
|
|
1350
|
-
* @returns A unique, identifier-safe alias string.
|
|
1351
|
-
*/
|
|
1352
|
-
function makeAttachAlias(name, index) {
|
|
1353
|
-
return `_src_${name.replace(/[^a-z0-9]/gi, "_").replace(/_+/g, "_").slice(0, 20)}_${index}`;
|
|
1354
|
-
}
|
|
1355
|
-
/**
|
|
1356
|
-
* Per-source-DB epoch unit lookup — used as a primary hint for the SQL magnitude
|
|
1357
|
-
* heuristic (T11549 coercion fix). When the magnitude heuristic is ambiguous
|
|
1358
|
-
* (value in the overlap zone), the source-level hint wins.
|
|
1359
|
-
*
|
|
1360
|
-
* Verified from schema source comments (§8.1 resolution):
|
|
1361
|
-
* - conduit.db: `Math.floor(Date.now() / 1000)` → SECONDS
|
|
1362
|
-
* - brain.db: `Date.now()` / `unixepoch * 1000` → MILLISECONDS
|
|
1363
|
-
* - signaldock.db: `strftime('%s','now')` → SECONDS
|
|
1364
|
-
* - tasks.db: `Date.now()` → MILLISECONDS (most writers)
|
|
1365
|
-
* - nexus.db: mixed — most columns are MILLISECONDS but `user_profile`
|
|
1366
|
-
* `first_observed_at`/`last_reinforced_at` are SECONDS
|
|
1367
|
-
* (written by PSYCHE dialectic writer via `Math.floor(Date.now() / 1000)`).
|
|
1368
|
-
* The magnitude heuristic handles this automatically.
|
|
1369
|
-
* - skills.db: `Date.now()` → MILLISECONDS
|
|
1370
|
-
*/
|
|
1371
|
-
var SOURCE_EPOCH_UNITS = new Map([
|
|
1372
|
-
["conduit", "seconds"],
|
|
1373
|
-
["brain", "milliseconds"],
|
|
1374
|
-
["brain (project)", "milliseconds"],
|
|
1375
|
-
["brain (global)", "milliseconds"],
|
|
1376
|
-
["signaldock", "seconds"],
|
|
1377
|
-
["tasks", "milliseconds"],
|
|
1378
|
-
["nexus", "milliseconds"],
|
|
1379
|
-
["skills", "milliseconds"]
|
|
1380
|
-
]);
|
|
1381
|
-
/**
|
|
1382
|
-
* Return the epoch unit used by a given source DB's INTEGER timestamp columns.
|
|
1383
|
-
* Defaults to `'seconds'` for unknown sources (safe default — ISO-8601 output
|
|
1384
|
-
* will be off by 1000x only for ms sources, which are already enumerated above).
|
|
1385
|
-
*
|
|
1386
|
-
* NOTE: This is the per-source hint. The actual SQL expression generated by
|
|
1387
|
-
* `buildEpochToIsoExpr` uses a magnitude-based heuristic at the row level so
|
|
1388
|
-
* individual columns that diverge from the source default are handled correctly
|
|
1389
|
-
* (T11549 bug fix — nexus `user_profile` stores seconds despite nexus being
|
|
1390
|
-
* labeled milliseconds at the source level).
|
|
1391
|
-
*/
|
|
1392
|
-
function epochUnitForSource(sourceName) {
|
|
1393
|
-
const key = sourceName.toLowerCase();
|
|
1394
|
-
for (const [pattern, unit] of SOURCE_EPOCH_UNITS) if (key === pattern || key.startsWith(pattern)) return unit;
|
|
1395
|
-
return "seconds";
|
|
1396
|
-
}
|
|
1397
|
-
/**
|
|
1398
|
-
* Build a SQL SELECT expression for a shared column, applying (in priority order):
|
|
1399
|
-
*
|
|
1400
|
-
* 1. **Epoch→ISO-8601 coercion** (T11546): when the target has an ISO GLOB CHECK
|
|
1401
|
-
* and the source column is INTEGER-typed.
|
|
1402
|
-
* 2. **Non-finite numeric clamp** (T11782): when `NUMERIC_CLAMPS` has an entry
|
|
1403
|
-
* for `(targetTableName, col)`, mapping `Inf`/`-Inf`/`NaN` to a finite in-range
|
|
1404
|
-
* value so the row is not silently dropped.
|
|
1405
|
-
* 3. **Enum-value normalization** (T11547): when `ENUM_NORMALIZATIONS` has an
|
|
1406
|
-
* entry for `(targetTableName, col)`, producing a SQL CASE expression that
|
|
1407
|
-
* maps legacy values to canonical enum members without losing semantics.
|
|
1408
|
-
* 4. **NOT NULL coalesce** (T11533): for non-epoch, non-normalized columns whose
|
|
1409
|
-
* target is NOT NULL with no schema default.
|
|
1410
|
-
* 5. **Plain column reference** otherwise.
|
|
1411
|
-
*
|
|
1412
|
-
* The epoch→ISO conversion uses `buildEpochToIsoExpr` (from `column-transforms.ts`)
|
|
1413
|
-
* which detects the epoch scale (seconds vs milliseconds) per-row using a magnitude
|
|
1414
|
-
* heuristic: values below `EPOCH_SECONDS_THRESHOLD` (1e11) are treated as seconds;
|
|
1415
|
-
* values at or above are treated as milliseconds and divided by 1000.0 before conversion
|
|
1416
|
-
* (T11549 correctness fix — the previous per-source unit was incorrect for
|
|
1417
|
-
* individual columns that diverged from the source default, e.g.
|
|
1418
|
-
* `nexus.user_profile.first_observed_at` stores SECONDS even though most nexus
|
|
1419
|
-
* writers use milliseconds).
|
|
1420
|
-
*
|
|
1421
|
-
* A NULL source value is preserved as NULL (passes the `IS NULL` branch of the
|
|
1422
|
-
* GLOB CHECK, and is OK for nullable columns).
|
|
1423
|
-
*
|
|
1424
|
-
* @param attachAlias - ATTACH alias for the source DB.
|
|
1425
|
-
* @param legacyTable - Legacy table name in the source.
|
|
1426
|
-
* @param targetTableName - Physical consolidated target table name (for enum lookup).
|
|
1427
|
-
* @param col - Column name.
|
|
1428
|
-
* @param srcType - Raw type string from source `PRAGMA table_info`.
|
|
1429
|
-
* @param tgtInfo - Target column metadata from `PRAGMA table_info`.
|
|
1430
|
-
* @param isoGlobCols - Set of columns requiring ISO GLOB in the target.
|
|
1431
|
-
* @returns SQL expression string suitable for use in a SELECT clause.
|
|
1432
|
-
*/
|
|
1433
|
-
function buildSelectExpr(attachAlias, legacyTable, targetTableName, col, srcType, tgtInfo, isoGlobCols) {
|
|
1434
|
-
const srcRef = `"${attachAlias}"."${legacyTable}"."${col}"`;
|
|
1435
|
-
const srcUpper = srcType.toUpperCase();
|
|
1436
|
-
const isIntegerSource = srcUpper.includes("INT") || srcUpper === "" || srcUpper === "NUMERIC";
|
|
1437
|
-
const isNotNullWithoutDefault = tgtInfo.notnull === 1 && tgtInfo.dflt_value === null;
|
|
1438
|
-
if (isoGlobCols.has(col) && isIntegerSource) {
|
|
1439
|
-
const isoExpr = buildEpochToIsoExpr(srcRef);
|
|
1440
|
-
if (isNotNullWithoutDefault) return `COALESCE(${isoExpr}, '') AS "${col}"`;
|
|
1441
|
-
return `${isoExpr} AS "${col}"`;
|
|
1442
|
-
}
|
|
1443
|
-
const clampExpr = numericClampExpr(targetTableName, col, srcRef);
|
|
1444
|
-
if (clampExpr !== null) {
|
|
1445
|
-
if (isNotNullWithoutDefault) return `COALESCE(${clampExpr}, ${typeDefaultLiteral(tgtInfo.type)}) AS "${col}"`;
|
|
1446
|
-
return `${clampExpr} AS "${col}"`;
|
|
1447
|
-
}
|
|
1448
|
-
const normExpr = enumNormExpr(targetTableName, col, srcRef);
|
|
1449
|
-
if (normExpr !== null) {
|
|
1450
|
-
if (isNotNullWithoutDefault) return `COALESCE(${normExpr}, ${typeDefaultLiteral(tgtInfo.type)}) AS "${col}"`;
|
|
1451
|
-
return `${normExpr} AS "${col}"`;
|
|
1452
|
-
}
|
|
1453
|
-
if (isNotNullWithoutDefault) return `COALESCE(${srcRef}, ${typeDefaultLiteral(tgtInfo.type)}) AS "${col}"`;
|
|
1454
|
-
return srcRef;
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* Copy all rows from a legacy source table (in the already-attached alias) into
|
|
1458
|
-
* the corresponding consolidated target table.
|
|
1459
|
-
*
|
|
1460
|
-
* ## What changed in T11532 vs the T11531 version:
|
|
1461
|
-
*
|
|
1462
|
-
* 1. **Name mapping (ROOT CAUSE 1 — T11532)**: `legacyTableName` is resolved to
|
|
1463
|
-
* its consolidated name via `resolveConsolidatedTableName()`. Without this,
|
|
1464
|
-
* `tasks` (legacy) was looked up as `main."tasks"` which doesn't exist in
|
|
1465
|
-
* the consolidated schema (the real target is `tasks_tasks`).
|
|
1466
|
-
*
|
|
1467
|
-
* 2. **Column-drift tolerance (ROOT CAUSE 2 — T11532 + T11533)**: the INSERT
|
|
1468
|
-
* uses the INTERSECTION of source and target column lists rather than source
|
|
1469
|
-
* columns verbatim. When the consolidated schema added new columns, old code
|
|
1470
|
-
* failed. Target-only NOT NULL columns without defaults now get COALESCE()
|
|
1471
|
-
* wrapping in the SELECT so NULL source values don't cause silent row drops.
|
|
1472
|
-
*
|
|
1473
|
-
* 3. **Explicit skip (ROOT CAUSE 5)**: tables intentionally excluded from the
|
|
1474
|
-
* consolidated schema (virtual tables, orphan telemetry, etc.) now return
|
|
1475
|
-
* a logged skip result rather than being silently treated as "target not
|
|
1476
|
-
* found".
|
|
1477
|
-
*
|
|
1478
|
-
* 4. **Epoch→ISO coercion (ROOT CAUSE 1 — T11546)**: columns with an ISO GLOB
|
|
1479
|
-
* CHECK in the target that are INTEGER-typed in the source are converted via
|
|
1480
|
-
* `strftime('%Y-%m-%dT%H:%M:%fZ', col[/1000.0], 'unixepoch')`. Without this,
|
|
1481
|
-
* `INSERT OR IGNORE` silently drops ALL rows for those tables (CHECK fails
|
|
1482
|
-
* for every row because an integer like `1717200000` doesn't match the GLOB).
|
|
1483
|
-
*
|
|
1484
|
-
* 5. **No-swallow assertion (ROOT CAUSE 1b — T11546)**: after the bulk INSERT,
|
|
1485
|
-
* `changes` is compared against the source row count. A shortfall is a hard
|
|
1486
|
-
* per-table error. PK/UNIQUE conflicts on idempotent resume are expected and
|
|
1487
|
-
* tolerated (checked via count of existing rows); CHECK constraint drops are
|
|
1488
|
-
* not tolerated.
|
|
1489
|
-
*
|
|
1490
|
-
* **Pre-condition**: the caller has already executed
|
|
1491
|
-
* `ATTACH DATABASE '<path>' AS "<attachAlias>"` on `targetNativeDb`, and
|
|
1492
|
-
* `PRAGMA foreign_keys = OFF` has been set so FK ordering doesn't matter.
|
|
1493
|
-
*
|
|
1494
|
-
* ## Cross-scope routing (ADR-090 · T11539)
|
|
1495
|
-
*
|
|
1496
|
-
* `targetSchema` defaults to `'main'` (the connection's own consolidated DB). For
|
|
1497
|
-
* the four nexus code-graph tables, which are extracted from the GLOBAL `nexus.db`
|
|
1498
|
-
* source but must land in the PROJECT consolidated `cleo.db`, the caller attaches
|
|
1499
|
-
* the project DB under an alias on the SAME connection and passes that alias as
|
|
1500
|
-
* `targetSchema`. All target-side introspection (`sqlite_master`, `PRAGMA
|
|
1501
|
-
* table_info`) and the `INSERT OR IGNORE` are then schema-qualified to that
|
|
1502
|
-
* attached DB instead of `main`.
|
|
1503
|
-
*
|
|
1504
|
-
* @param targetNativeDb - Writable target handle (mid-transaction, FK OFF).
|
|
1505
|
-
* @param srcNativeDb - Read-only source snapshot (for metadata queries).
|
|
1506
|
-
* @param attachAlias - Alias under which the source is attached.
|
|
1507
|
-
* @param legacyTableName - Physical table name in the legacy source DB.
|
|
1508
|
-
* @param sourceName - `LegacyDbDescriptor.name` (for name resolution).
|
|
1509
|
-
* @param targetSchema - Schema the consolidated target table lives in
|
|
1510
|
-
* (`'main'` by default, or an ATTACH alias for cross-scope routing).
|
|
1511
|
-
*/
|
|
1512
|
-
function copyTableFromAttached(targetNativeDb, srcNativeDb, attachAlias, legacyTableName, sourceName, targetSchema = "main") {
|
|
1513
|
-
const resolution = resolveConsolidatedTableName(sourceName, legacyTableName);
|
|
1514
|
-
if (resolution.kind === "skip") {
|
|
1515
|
-
log$2.warn({
|
|
1516
|
-
legacyTableName,
|
|
1517
|
-
sourceName,
|
|
1518
|
-
reason: resolution.reason
|
|
1519
|
-
}, `Exodus: explicitly skipping table — ${resolution.reason}`);
|
|
1520
|
-
return {
|
|
1521
|
-
rowsCopied: 0,
|
|
1522
|
-
skipped: true,
|
|
1523
|
-
reason: resolution.reason
|
|
1524
|
-
};
|
|
1525
|
-
}
|
|
1526
|
-
const targetTableName = resolution.targetName;
|
|
1527
|
-
const srcPragma = srcNativeDb.prepare(`PRAGMA table_info("${legacyTableName}")`).all();
|
|
1528
|
-
const srcColumns = new Set(srcPragma.map((r) => r.name));
|
|
1529
|
-
if (srcColumns.size === 0) return {
|
|
1530
|
-
rowsCopied: 0,
|
|
1531
|
-
skipped: false
|
|
1532
|
-
};
|
|
1533
|
-
const sourceCount = srcNativeDb.prepare(`SELECT COUNT(*) AS c FROM "${legacyTableName}"`).get()?.c ?? 0;
|
|
1534
|
-
if (sourceCount === 0) return {
|
|
1535
|
-
rowsCopied: 0,
|
|
1536
|
-
skipped: false
|
|
1537
|
-
};
|
|
1538
|
-
const escapedTarget = targetTableName.replace(/'/g, "''");
|
|
1539
|
-
if (!targetNativeDb.prepare(`SELECT name FROM "${targetSchema}".sqlite_master WHERE type='table' AND name='${escapedTarget}'`).get()) {
|
|
1540
|
-
const reason = `consolidated target '${targetTableName}' not found (mapped from legacy '${legacyTableName}')`;
|
|
1541
|
-
log$2.warn({
|
|
1542
|
-
legacyTableName,
|
|
1543
|
-
targetTableName,
|
|
1544
|
-
sourceName,
|
|
1545
|
-
attachAlias,
|
|
1546
|
-
targetSchema
|
|
1547
|
-
}, `Exodus: ${reason}`);
|
|
1548
|
-
return {
|
|
1549
|
-
rowsCopied: 0,
|
|
1550
|
-
skipped: true,
|
|
1551
|
-
reason
|
|
1552
|
-
};
|
|
1553
|
-
}
|
|
1554
|
-
const tgtPragma = targetNativeDb.prepare(`PRAGMA "${targetSchema}".table_info("${targetTableName}")`).all();
|
|
1555
|
-
const tgtColMap = new Map(tgtPragma.map((r) => [r.name, r]));
|
|
1556
|
-
const sharedColumns = srcPragma.map((r) => r.name).filter((col) => tgtColMap.has(col));
|
|
1557
|
-
if (sharedColumns.length === 0) {
|
|
1558
|
-
const reason = `no overlapping columns between source '${legacyTableName}' and target '${targetTableName}'`;
|
|
1559
|
-
log$2.warn({
|
|
1560
|
-
legacyTableName,
|
|
1561
|
-
targetTableName,
|
|
1562
|
-
sourceName
|
|
1563
|
-
}, `Exodus: ${reason}`);
|
|
1564
|
-
return {
|
|
1565
|
-
rowsCopied: 0,
|
|
1566
|
-
skipped: true,
|
|
1567
|
-
reason
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
const srcOnlyColumns = srcPragma.map((r) => r.name).filter((c) => !tgtColMap.has(c));
|
|
1571
|
-
const tgtOnlyColumns = tgtPragma.map((r) => r.name).filter((c) => !srcColumns.has(c));
|
|
1572
|
-
if (srcOnlyColumns.length > 0 || tgtOnlyColumns.length > 0) log$2.info({
|
|
1573
|
-
legacyTableName,
|
|
1574
|
-
targetTableName,
|
|
1575
|
-
sourceName,
|
|
1576
|
-
srcOnlyColumns,
|
|
1577
|
-
tgtOnlyColumns
|
|
1578
|
-
}, "Exodus: column drift detected — copying intersection, dropping src-only cols, using defaults for tgt-only cols");
|
|
1579
|
-
const isoGlobCols = detectIsoGlobColumns(targetNativeDb, targetTableName, targetSchema);
|
|
1580
|
-
const epochUnitHint = epochUnitForSource(sourceName);
|
|
1581
|
-
const srcTypeMap = new Map(srcPragma.map((r) => [r.name, r.type]));
|
|
1582
|
-
if (isoGlobCols.size > 0) {
|
|
1583
|
-
const coercedCols = sharedColumns.filter((col) => {
|
|
1584
|
-
const upper = (srcTypeMap.get(col) ?? "").toUpperCase();
|
|
1585
|
-
return isoGlobCols.has(col) && (upper.includes("INT") || upper === "" || upper === "NUMERIC");
|
|
1586
|
-
});
|
|
1587
|
-
if (coercedCols.length > 0) log$2.info({
|
|
1588
|
-
legacyTableName,
|
|
1589
|
-
targetTableName,
|
|
1590
|
-
sourceName,
|
|
1591
|
-
coercedCols,
|
|
1592
|
-
epochUnitHint
|
|
1593
|
-
}, `Exodus: applying epoch→ISO coercion (magnitude heuristic) for ${coercedCols.length} column(s) (T11546+T11549)`);
|
|
1594
|
-
}
|
|
1595
|
-
const normalizedCols = sharedColumns.filter((col) => ENUM_NORMALIZATIONS.has(`${targetTableName}.${col}`));
|
|
1596
|
-
if (normalizedCols.length > 0) log$2.info({
|
|
1597
|
-
legacyTableName,
|
|
1598
|
-
targetTableName,
|
|
1599
|
-
sourceName,
|
|
1600
|
-
normalizedCols
|
|
1601
|
-
}, `Exodus: applying enum-value normalization for ${normalizedCols.length} column(s) (T11547)`);
|
|
1602
|
-
const clampedCols = sharedColumns.filter((col) => NUMERIC_CLAMPS.has(`${targetTableName}.${col}`));
|
|
1603
|
-
if (clampedCols.length > 0) log$2.info({
|
|
1604
|
-
legacyTableName,
|
|
1605
|
-
targetTableName,
|
|
1606
|
-
sourceName,
|
|
1607
|
-
clampedCols
|
|
1608
|
-
}, `Exodus: applying non-finite numeric clamp for ${clampedCols.length} column(s) (T11782)`);
|
|
1609
|
-
const selectExprs = sharedColumns.map((col) => {
|
|
1610
|
-
return buildSelectExpr(attachAlias, legacyTableName, targetTableName, col, srcTypeMap.get(col) ?? "", tgtColMap.get(col), isoGlobCols);
|
|
1611
|
-
});
|
|
1612
|
-
const tgtOnlyNotNullCols = tgtOnlyColumns.filter((col) => {
|
|
1613
|
-
const info = tgtColMap.get(col);
|
|
1614
|
-
return info !== void 0 && info.notnull === 1 && info.dflt_value === null;
|
|
1615
|
-
});
|
|
1616
|
-
const allInsertCols = [...sharedColumns, ...tgtOnlyNotNullCols];
|
|
1617
|
-
const allSelectExprs = [...selectExprs, ...tgtOnlyNotNullCols.map((col) => {
|
|
1618
|
-
return `${typeDefaultLiteral(tgtColMap.get(col).type)} AS "${col}"`;
|
|
1619
|
-
})];
|
|
1620
|
-
const colList = allInsertCols.map((c) => `"${c}"`).join(", ");
|
|
1621
|
-
const selectList = allSelectExprs.join(", ");
|
|
1622
|
-
const existingBeforeRow = targetNativeDb.prepare(`SELECT COUNT(*) AS c FROM "${targetSchema}"."${targetTableName}"`).get();
|
|
1623
|
-
const existingBefore = Number(existingBeforeRow?.c ?? 0);
|
|
1624
|
-
const rowsCopied = targetNativeDb.prepare(`INSERT OR IGNORE INTO "${targetSchema}"."${targetTableName}" (${colList}) SELECT ${selectList} FROM "${attachAlias}"."${legacyTableName}"`).run().changes ?? 0;
|
|
1625
|
-
if (rowsCopied < sourceCount) {
|
|
1626
|
-
const presentAccountedFor = existingBefore + rowsCopied;
|
|
1627
|
-
if (presentAccountedFor >= sourceCount) {
|
|
1628
|
-
log$2.info({
|
|
1629
|
-
legacyTableName,
|
|
1630
|
-
targetTableName,
|
|
1631
|
-
sourceName,
|
|
1632
|
-
sourceCount,
|
|
1633
|
-
rowsCopied,
|
|
1634
|
-
existingBefore
|
|
1635
|
-
}, `Exodus: '${legacyTableName}'→'${targetTableName}' — ${sourceCount - rowsCopied} of ${sourceCount} row(s) already present in target (idempotent PK dedup), ${rowsCopied} newly copied; no loss`);
|
|
1636
|
-
return {
|
|
1637
|
-
rowsCopied,
|
|
1638
|
-
skipped: false
|
|
1639
|
-
};
|
|
1640
|
-
}
|
|
1641
|
-
const missing = sourceCount - presentAccountedFor;
|
|
1642
|
-
const reason = `INSERT OR IGNORE lost ${missing} of ${sourceCount} rows from '${legacyTableName}'→'${targetTableName}' (rowsCopied=${rowsCopied}, existingBefore=${existingBefore}) — a CHECK/NOT NULL/type/enum constraint rejected them (NOT PK dedup); inspect epoch coercion + enum normalization. verify will confirm.`;
|
|
1643
|
-
log$2.error({
|
|
1644
|
-
legacyTableName,
|
|
1645
|
-
targetTableName,
|
|
1646
|
-
sourceName,
|
|
1647
|
-
sourceCount,
|
|
1648
|
-
rowsCopied,
|
|
1649
|
-
existingBefore,
|
|
1650
|
-
missing
|
|
1651
|
-
}, `Exodus: ${reason}`);
|
|
1652
|
-
return {
|
|
1653
|
-
rowsCopied,
|
|
1654
|
-
skipped: false,
|
|
1655
|
-
reason
|
|
1656
|
-
};
|
|
1657
|
-
}
|
|
1658
|
-
return {
|
|
1659
|
-
rowsCopied,
|
|
1660
|
-
skipped: false
|
|
1661
|
-
};
|
|
1662
|
-
}
|
|
1663
|
-
/**
|
|
1664
|
-
* Validate that the journal's schema version matches the current target version.
|
|
1665
|
-
*
|
|
1666
|
-
* When `forceCrossVersion === true`, mismatches are logged but not fatal.
|
|
1667
|
-
*/
|
|
1668
|
-
function checkSchemaVersion(journal, forceCrossVersion) {
|
|
1669
|
-
if (journal.targetSchemaVersion !== "drizzle-v1.0.0-rc.3/dual-scope/2026-05") {
|
|
1670
|
-
const msg = `Schema version mismatch: journal=${journal.targetSchemaVersion}, expected=${EXODUS_TARGET_SCHEMA_VERSION}`;
|
|
1671
|
-
if (forceCrossVersion) {
|
|
1672
|
-
log$2.warn(msg + " (--force-cross-version: continuing anyway)");
|
|
1673
|
-
return true;
|
|
1674
|
-
}
|
|
1675
|
-
log$2.error(msg + " — pass --force-cross-version to override");
|
|
1676
|
-
return false;
|
|
1677
|
-
}
|
|
1678
|
-
return true;
|
|
1679
|
-
}
|
|
1680
|
-
/**
|
|
1681
|
-
* Run the exodus migration.
|
|
1682
|
-
*
|
|
1683
|
-
* @param plan - Pre-flight plan from `buildExodusPlan()`.
|
|
1684
|
-
* @param forceCrossVersion - Skip the schema-version guard (AC9).
|
|
1685
|
-
* @param onProgress - Optional progress callback called after each table.
|
|
1686
|
-
*
|
|
1687
|
-
* @returns {@link ExodusMigrateResult}
|
|
1688
|
-
*
|
|
1689
|
-
* @task T11248 (AC4, AC5, AC6, AC7, AC9)
|
|
1690
|
-
* @task T11531 (P0 attach-leak fix)
|
|
1691
|
-
*/
|
|
1692
|
-
async function runExodusMigrate(plan, forceCrossVersion = false, onProgress) {
|
|
1693
|
-
const { sources, stagingDir, diskPreflight, projectDbPath, globalDbPath } = plan;
|
|
1694
|
-
if (!diskPreflight) {
|
|
1695
|
-
const shortfall = Math.max(0, plan.requiredBytes - plan.availableBytes);
|
|
1696
|
-
return {
|
|
1697
|
-
ok: false,
|
|
1698
|
-
tables: [],
|
|
1699
|
-
stagingDir,
|
|
1700
|
-
backupPaths: [],
|
|
1701
|
-
error: `Insufficient disk space for exodus: need ≥${plan.requiredBytes} bytes (≈${STAGING_HEADROOM_FACTOR}× largest source ${plan.largestSourceBytes} + consolidated estimate ${plan.totalSourceBytes}), but only ${plan.availableBytes} bytes are free on the target filesystem — ${shortfall} bytes short. Free up at least ${shortfall} bytes (e.g. \`cleo backup prune\`, clear caches), or move the .cleo/ directory to a larger volume, then retry.`
|
|
1702
|
-
};
|
|
1703
|
-
}
|
|
1704
|
-
mkdirSync(stagingDir, { recursive: true });
|
|
1705
|
-
let sqliteVersion = "unknown";
|
|
1706
|
-
for (const src of sources) if (existsSync(src.path)) {
|
|
1707
|
-
const snap = openCleoDbSnapshot(src.path, { readOnly: true });
|
|
1708
|
-
sqliteVersion = getSqliteVersion(snap.db);
|
|
1709
|
-
snap.close();
|
|
1710
|
-
break;
|
|
1711
|
-
}
|
|
1712
|
-
let journal = readJournal(stagingDir);
|
|
1713
|
-
if (journal === null) journal = initJournal(sqliteVersion);
|
|
1714
|
-
else {
|
|
1715
|
-
if (!checkSchemaVersion(journal, forceCrossVersion)) return {
|
|
1716
|
-
ok: false,
|
|
1717
|
-
tables: [],
|
|
1718
|
-
stagingDir,
|
|
1719
|
-
backupPaths: [],
|
|
1720
|
-
error: "Schema version mismatch. Pass --force-cross-version to override."
|
|
1721
|
-
};
|
|
1722
|
-
onProgress?.("Resuming from existing staging journal…");
|
|
1723
|
-
}
|
|
1724
|
-
const backupPaths = [];
|
|
1725
|
-
const allTableResults = [];
|
|
1726
|
-
const lockedPaths = [];
|
|
1727
|
-
let projectHandle = null;
|
|
1728
|
-
let globalHandle = null;
|
|
1729
|
-
try {
|
|
1730
|
-
for (const src of sources) {
|
|
1731
|
-
if (!existsSync(src.path)) continue;
|
|
1732
|
-
const backupDest = join(stagingDir, `${src.name.replace(/[^a-z0-9-]/g, "_")}-backup.db`);
|
|
1733
|
-
const srcBytes = safeStatBytes(src.path);
|
|
1734
|
-
if (srcBytes > plan.stagingCopyThresholdBytes) onProgress?.(`Skipping full staging copy of ${src.name} (${srcBytes} bytes > ${plan.stagingCopyThresholdBytes} threshold) — source is archived, not deleted, on success.`);
|
|
1735
|
-
else if (!existsSync(backupDest)) {
|
|
1736
|
-
onProgress?.(`Backing up ${src.name} → staging dir…`);
|
|
1737
|
-
copyFileSync(src.path, backupDest);
|
|
1738
|
-
backupPaths.push(backupDest);
|
|
1739
|
-
}
|
|
1740
|
-
acquireAdvisoryLock(src.path);
|
|
1741
|
-
lockedPaths.push(src.path);
|
|
1742
|
-
}
|
|
1743
|
-
onProgress?.("Opening DEDICATED project-scope cleo.db connection (running migrations)…");
|
|
1744
|
-
projectHandle = await openDualScopeDbAtPath("project", projectDbPath, void 0, { dedicated: true });
|
|
1745
|
-
onProgress?.("Opening DEDICATED global-scope cleo.db connection (running migrations)…");
|
|
1746
|
-
globalHandle = await openDualScopeDbAtPath("global", globalDbPath, void 0, { dedicated: true });
|
|
1747
|
-
function extractNativeDb(handle) {
|
|
1748
|
-
const drizzleHandle = handle.db;
|
|
1749
|
-
const client = drizzleHandle["$client"];
|
|
1750
|
-
if (client && typeof client["prepare"] === "function") return client;
|
|
1751
|
-
if (typeof drizzleHandle["prepare"] === "function") return drizzleHandle;
|
|
1752
|
-
throw new Error("Could not extract native DatabaseSync from dual-scope DB handle");
|
|
1753
|
-
}
|
|
1754
|
-
const projectNative = extractNativeDb(projectHandle);
|
|
1755
|
-
const globalNative = extractNativeDb(globalHandle);
|
|
1756
|
-
const projectSources = sources.filter((s) => s.targetScope === "project" && existsSync(s.path));
|
|
1757
|
-
const globalSources = sources.filter((s) => s.targetScope === "global" && existsSync(s.path));
|
|
1758
|
-
await migrateScope("project", projectSources, projectNative, journal, stagingDir, allTableResults, onProgress);
|
|
1759
|
-
await migrateScope("global", globalSources, globalNative, journal, stagingDir, allTableResults, onProgress, projectDbPath);
|
|
1760
|
-
journal.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1761
|
-
writeJournal(stagingDir, journal);
|
|
1762
|
-
return {
|
|
1763
|
-
ok: true,
|
|
1764
|
-
tables: allTableResults,
|
|
1765
|
-
stagingDir,
|
|
1766
|
-
backupPaths
|
|
1767
|
-
};
|
|
1768
|
-
} catch (err) {
|
|
1769
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
1770
|
-
log$2.error({ err }, "Exodus migration failed");
|
|
1771
|
-
return {
|
|
1772
|
-
ok: false,
|
|
1773
|
-
tables: allTableResults,
|
|
1774
|
-
stagingDir,
|
|
1775
|
-
backupPaths,
|
|
1776
|
-
error
|
|
1777
|
-
};
|
|
1778
|
-
} finally {
|
|
1779
|
-
try {
|
|
1780
|
-
projectHandle?.close();
|
|
1781
|
-
} catch {}
|
|
1782
|
-
try {
|
|
1783
|
-
globalHandle?.close();
|
|
1784
|
-
} catch {}
|
|
1785
|
-
for (const p of lockedPaths) releaseAdvisoryLock(p);
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
/**
|
|
1789
|
-
* Migrate all tables from the given sources into the target native DB.
|
|
1790
|
-
*
|
|
1791
|
-
* ## ATTACH-once-per-source protocol (T11531)
|
|
1792
|
-
*
|
|
1793
|
-
* SQLite forbids `DETACH` inside an active multi-statement transaction.
|
|
1794
|
-
* To avoid the "database alias is already in use" error that caused ~80 %
|
|
1795
|
-
* data loss, we use the following sequence **per source DB**:
|
|
1796
|
-
*
|
|
1797
|
-
* 1. ATTACH the source path under a unique alias (outside any transaction).
|
|
1798
|
-
* 2. Open a read-only snapshot of the source for metadata queries.
|
|
1799
|
-
* 3. BEGIN the write transaction on the target.
|
|
1800
|
-
* 4. INSERT OR IGNORE … SELECT for each table using the attached alias.
|
|
1801
|
-
* 5. COMMIT (or ROLLBACK on error).
|
|
1802
|
-
* 6. DETACH the source alias in `finally` (outside the committed transaction).
|
|
1803
|
-
* 7. Close the read-only snapshot.
|
|
1804
|
-
*
|
|
1805
|
-
* Each source gets its own unique alias (`_src_<name>_<index>`) so multiple
|
|
1806
|
-
* sources can be processed sequentially without alias conflicts.
|
|
1807
|
-
*
|
|
1808
|
-
* ## FK-defer protocol (T11533 ROOT CAUSE 1)
|
|
1809
|
-
*
|
|
1810
|
-
* Before the first INSERT in any scope, foreign-key enforcement is switched OFF
|
|
1811
|
-
* on the target connection (`PRAGMA foreign_keys = OFF`) so copy order does not
|
|
1812
|
-
* matter (avoids "FOREIGN KEY constraint failed" for child-before-parent copies).
|
|
1813
|
-
* After all sources in the scope are committed, `PRAGMA foreign_key_check` is
|
|
1814
|
-
* executed to surface genuinely orphaned rows, and then FK enforcement is
|
|
1815
|
-
* restored (`PRAGMA foreign_keys = ON`).
|
|
1816
|
-
*
|
|
1817
|
-
* Sequence across all sources in a scope:
|
|
1818
|
-
* PRAGMA foreign_keys = OFF
|
|
1819
|
-
* for each source:
|
|
1820
|
-
* ATTACH … AS alias (outside tx)
|
|
1821
|
-
* BEGIN
|
|
1822
|
-
* INSERT … SELECT for each table
|
|
1823
|
-
* COMMIT
|
|
1824
|
-
* DETACH alias (outside tx)
|
|
1825
|
-
* PRAGMA foreign_key_check → log orphans as warnings
|
|
1826
|
-
* PRAGMA foreign_keys = ON
|
|
1827
|
-
*/
|
|
1828
|
-
async function migrateScope(scope, sources, targetNativeDb, journal, stagingDir, allTableResults, onProgress, crossScopeTargetPath) {
|
|
1829
|
-
if (sources.length === 0) return;
|
|
1830
|
-
onProgress?.(`Migrating ${scope}-scope sources…`);
|
|
1831
|
-
targetNativeDb.exec("PRAGMA foreign_keys = OFF");
|
|
1832
|
-
log$2.info({ scope }, "Exodus: foreign_keys=OFF for bulk copy (T11533 FK-defer)");
|
|
1833
|
-
try {
|
|
1834
|
-
for (let i = 0; i < sources.length; i++) {
|
|
1835
|
-
const src = sources[i];
|
|
1836
|
-
const attachAlias = makeAttachAlias(src.name, i);
|
|
1837
|
-
const escapedPath = src.path.replace(/'/g, "''");
|
|
1838
|
-
targetNativeDb.exec(`ATTACH DATABASE '${escapedPath}' AS "${attachAlias}"`);
|
|
1839
|
-
onProgress?.(` [${src.name}] Attached as "${attachAlias}"`);
|
|
1840
|
-
let crossAlias = null;
|
|
1841
|
-
if (crossScopeTargetPath) {
|
|
1842
|
-
crossAlias = `_xscope_${attachAlias}`;
|
|
1843
|
-
const escapedCrossPath = crossScopeTargetPath.replace(/'/g, "''");
|
|
1844
|
-
targetNativeDb.exec(`ATTACH DATABASE '${escapedCrossPath}' AS "${crossAlias}"`);
|
|
1845
|
-
onProgress?.(` [${src.name}] Cross-scope target attached as "${crossAlias}"`);
|
|
1846
|
-
}
|
|
1847
|
-
const snap = openCleoDbSnapshot(src.path, { readOnly: true });
|
|
1848
|
-
try {
|
|
1849
|
-
const tables = listTables$1(snap.db);
|
|
1850
|
-
targetNativeDb.exec("BEGIN");
|
|
1851
|
-
let txOpen = true;
|
|
1852
|
-
try {
|
|
1853
|
-
for (const tableName of tables) {
|
|
1854
|
-
const existing = journal.tables.find((e) => e.sourceDb === src.name && e.tableName === tableName);
|
|
1855
|
-
if (existing?.status === "done") {
|
|
1856
|
-
onProgress?.(` ↳ ${src.name}.${tableName} — already done (resuming)`);
|
|
1857
|
-
allTableResults.push({
|
|
1858
|
-
sourceDb: src.name,
|
|
1859
|
-
tableName,
|
|
1860
|
-
rowsCopied: existing.rowsCopied,
|
|
1861
|
-
skipped: false
|
|
1862
|
-
});
|
|
1863
|
-
continue;
|
|
1864
|
-
}
|
|
1865
|
-
onProgress?.(` ↳ Copying ${src.name}.${tableName}…`);
|
|
1866
|
-
let rowsCopied = 0;
|
|
1867
|
-
let status = "done";
|
|
1868
|
-
let errorMsg;
|
|
1869
|
-
let skipped = false;
|
|
1870
|
-
try {
|
|
1871
|
-
const effectiveScope = resolveTableTargetScope(src.name, tableName, scope);
|
|
1872
|
-
const targetSchema = effectiveScope !== scope && crossAlias ? crossAlias : "main";
|
|
1873
|
-
if (effectiveScope !== scope && !crossAlias) throw new Error(`Table '${tableName}' from '${src.name}' routes to ${effectiveScope} scope but no cross-scope target was attached for the ${scope} pass (ADR-090 T11539)`);
|
|
1874
|
-
const copyResult = copyTableFromAttached(targetNativeDb, snap.db, attachAlias, tableName, src.name, targetSchema);
|
|
1875
|
-
rowsCopied = copyResult.rowsCopied;
|
|
1876
|
-
if (copyResult.skipped) {
|
|
1877
|
-
status = "skipped";
|
|
1878
|
-
errorMsg = copyResult.reason;
|
|
1879
|
-
skipped = true;
|
|
1880
|
-
} else if (copyResult.reason) {
|
|
1881
|
-
status = "partial";
|
|
1882
|
-
errorMsg = copyResult.reason;
|
|
1883
|
-
}
|
|
1884
|
-
} catch (err) {
|
|
1885
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1886
|
-
log$2.warn({
|
|
1887
|
-
tableName,
|
|
1888
|
-
sourceDb: src.name,
|
|
1889
|
-
err
|
|
1890
|
-
}, "Table copy failed — skipping");
|
|
1891
|
-
status = "skipped";
|
|
1892
|
-
errorMsg = msg;
|
|
1893
|
-
skipped = true;
|
|
1894
|
-
}
|
|
1895
|
-
const entry = {
|
|
1896
|
-
sourceDb: src.name,
|
|
1897
|
-
tableName,
|
|
1898
|
-
status,
|
|
1899
|
-
rowsCopied,
|
|
1900
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1901
|
-
...errorMsg ? { error: errorMsg } : {}
|
|
1902
|
-
};
|
|
1903
|
-
const idx = journal.tables.findIndex((e) => e.sourceDb === src.name && e.tableName === tableName);
|
|
1904
|
-
if (idx >= 0) journal.tables[idx] = entry;
|
|
1905
|
-
else journal.tables.push(entry);
|
|
1906
|
-
journal.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1907
|
-
writeJournal(stagingDir, journal);
|
|
1908
|
-
allTableResults.push({
|
|
1909
|
-
sourceDb: src.name,
|
|
1910
|
-
tableName,
|
|
1911
|
-
rowsCopied,
|
|
1912
|
-
skipped,
|
|
1913
|
-
reason: errorMsg
|
|
1914
|
-
});
|
|
1915
|
-
}
|
|
1916
|
-
targetNativeDb.exec("COMMIT");
|
|
1917
|
-
txOpen = false;
|
|
1918
|
-
} catch (err) {
|
|
1919
|
-
if (txOpen) try {
|
|
1920
|
-
targetNativeDb.exec("ROLLBACK");
|
|
1921
|
-
} catch {}
|
|
1922
|
-
throw err;
|
|
1923
|
-
}
|
|
1924
|
-
} finally {
|
|
1925
|
-
snap.close();
|
|
1926
|
-
try {
|
|
1927
|
-
targetNativeDb.exec(`DETACH DATABASE "${attachAlias}"`);
|
|
1928
|
-
onProgress?.(` [${src.name}] Detached "${attachAlias}"`);
|
|
1929
|
-
} catch (detachErr) {
|
|
1930
|
-
log$2.warn({
|
|
1931
|
-
attachAlias,
|
|
1932
|
-
sourceDb: src.name,
|
|
1933
|
-
err: detachErr
|
|
1934
|
-
}, "DETACH failed — alias will be released on DB close");
|
|
1935
|
-
}
|
|
1936
|
-
if (crossAlias) try {
|
|
1937
|
-
targetNativeDb.exec(`DETACH DATABASE "${crossAlias}"`);
|
|
1938
|
-
onProgress?.(` [${src.name}] Cross-scope target detached "${crossAlias}"`);
|
|
1939
|
-
} catch (detachErr) {
|
|
1940
|
-
log$2.warn({
|
|
1941
|
-
crossAlias,
|
|
1942
|
-
sourceDb: src.name,
|
|
1943
|
-
err: detachErr
|
|
1944
|
-
}, "Cross-scope DETACH failed — alias will be released on DB close");
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
} finally {
|
|
1949
|
-
try {
|
|
1950
|
-
const orphans = targetNativeDb.prepare("PRAGMA foreign_key_check").all();
|
|
1951
|
-
if (orphans.length > 0) log$2.warn({
|
|
1952
|
-
scope,
|
|
1953
|
-
orphanCount: orphans.length,
|
|
1954
|
-
sample: orphans.slice(0, 5)
|
|
1955
|
-
}, `Exodus: PRAGMA foreign_key_check found ${orphans.length} orphan row(s) after bulk copy — these are genuine data orphans (not ordering artifacts)`);
|
|
1956
|
-
else log$2.info({ scope }, "Exodus: PRAGMA foreign_key_check PASSED — no orphan rows");
|
|
1957
|
-
} catch (checkErr) {
|
|
1958
|
-
log$2.warn({
|
|
1959
|
-
scope,
|
|
1960
|
-
err: checkErr
|
|
1961
|
-
}, "Exodus: PRAGMA foreign_key_check failed (non-fatal) — target schema may not have FK constraints enabled");
|
|
1962
|
-
}
|
|
1963
|
-
try {
|
|
1964
|
-
targetNativeDb.exec("PRAGMA foreign_keys = ON");
|
|
1965
|
-
log$2.info({ scope }, "Exodus: foreign_keys=ON restored after bulk copy");
|
|
1966
|
-
} catch (fkErr) {
|
|
1967
|
-
log$2.warn({
|
|
1968
|
-
scope,
|
|
1969
|
-
err: fkErr
|
|
1970
|
-
}, "Exodus: could not restore foreign_keys=ON (non-fatal)");
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
getLogger("exodus-seal");
|
|
1975
|
-
//#endregion
|
|
1976
|
-
//#region ../core/dist/store/exodus/verify-migration.js
|
|
1977
|
-
/**
|
|
1978
|
-
* Reusable CORE migration-parity primitive — `verifyMigration()`.
|
|
1979
|
-
*
|
|
1980
|
-
* This is the durable guard (T11551 · DHQ-045) that prevents the exodus
|
|
1981
|
-
* dual-DB consolidation from silently losing rows. It is intentionally generic:
|
|
1982
|
-
* given a set of legacy source DBs and the consolidated target DB paths, it
|
|
1983
|
-
* returns a typed {@link VerifyMigrationResult} covering FOUR failure classes:
|
|
1984
|
-
*
|
|
1985
|
-
* 1. **Per-table row-count parity** — every data-bearing source table's
|
|
1986
|
-
* consolidated counterpart MUST NOT have FEWER rows than the source (a
|
|
1987
|
-
* DEFICIT = data loss → failure). A SURPLUS (target > source — e.g.
|
|
1988
|
-
* `nexus_audit_log` gaining the migration's own audit writes during the
|
|
1989
|
-
* migrating open) is NOT loss and is tolerated with a WARN (T11577). The
|
|
1990
|
-
* per-table `countMatch` field stays a strict `source === target`
|
|
1991
|
-
* diagnostic; only a deficit contributes a failure.
|
|
1992
|
-
* 2. **`PRAGMA foreign_key_check`** — genuine referential orphans on the
|
|
1993
|
-
* consolidated target surface as failures (not copy-order artifacts).
|
|
1994
|
-
* 3. **Content checksum** — an ordered canonical-JSON SHA-256 digest over the
|
|
1995
|
-
* sorted intersection of source/target columns catches content drift even
|
|
1996
|
-
* when counts match.
|
|
1997
|
-
* 4. **Enum/type-drift report** — source values outside the target column's
|
|
1998
|
-
* CHECK enum. This is the EXACT class that `INSERT OR IGNORE` used to drop
|
|
1999
|
-
* silently (the root cause of the ~805K-row exodus loss).
|
|
2000
|
-
*
|
|
2001
|
-
* ## Relationship to `runExodusVerify`
|
|
2002
|
-
*
|
|
2003
|
-
* `runExodusVerify` ({@link ./verify.ts}) is the exodus-specific entry point.
|
|
2004
|
-
* It now DELEGATES the row-count + checksum parity to this primitive (DRY —
|
|
2005
|
-
* T11551 AC2) and additionally surfaces the FK + enum-drift checks this module
|
|
2006
|
-
* adds. The digest, name-mapping, and rowid-safe ordering logic that the exodus
|
|
2007
|
-
* campaign hardened (T11531/32/33) live here as the single implementation.
|
|
2008
|
-
*
|
|
2009
|
-
* ## False-pass guard (T11531, preserved)
|
|
2010
|
-
*
|
|
2011
|
-
* When `ok === false`, `error` is ALWAYS populated with a human-readable
|
|
2012
|
-
* failure summary so a caller that only checks `result.error` cannot mistake a
|
|
2013
|
-
* silent loss for success.
|
|
2014
|
-
*
|
|
2015
|
-
* @task T11551 (DHQ-045 — exodus zero-loss durable guard)
|
|
2016
|
-
* @epic T10878
|
|
2017
|
-
* @saga T11242
|
|
2018
|
-
*/
|
|
2019
|
-
var log = getLogger("verify-migration");
|
|
2020
|
-
var _require = createRequire(import.meta.url);
|
|
2021
|
-
/**
|
|
2022
|
-
* Determine a deterministic ORDER BY clause for a table.
|
|
2023
|
-
*
|
|
2024
|
-
* Uses the table's declared primary-key columns (from `PRAGMA table_info`
|
|
2025
|
-
* where `pk > 0`) so ordering is stable for both WITH ROWID and WITHOUT ROWID
|
|
2026
|
-
* tables. Falls back to `rowid` only for ordinary tables that declare no
|
|
2027
|
-
* explicit primary key. Avoids the `no such column: rowid` crash on virtual /
|
|
2028
|
-
* WITHOUT ROWID tables (T11532 ROOT CAUSE 3).
|
|
2029
|
-
*
|
|
2030
|
-
* @param db - Database handle to introspect.
|
|
2031
|
-
* @param tableName - Physical table name.
|
|
2032
|
-
* @returns A SQL ORDER BY column list.
|
|
2033
|
-
*/
|
|
2034
|
-
function orderByClause(db, tableName) {
|
|
2035
|
-
try {
|
|
2036
|
-
const pkCols = db.prepare(`PRAGMA table_info("${tableName}")`).all().filter((r) => r.pk > 0).sort((a, b) => a.pk - b.pk).map((r) => `"${r.name}"`);
|
|
2037
|
-
if (pkCols.length > 0) return pkCols.join(", ");
|
|
2038
|
-
} catch {}
|
|
2039
|
-
return "rowid";
|
|
2040
|
-
}
|
|
2041
|
-
/**
|
|
2042
|
-
* Compute an ordered canonical-JSON SHA-256 digest (32 hex chars) for all rows
|
|
2043
|
-
* in a table, restricted to the given column list.
|
|
2044
|
-
*
|
|
2045
|
-
* When the caller passes the SORTED INTERSECTION of source and target columns,
|
|
2046
|
-
* both sides produce identically-structured JSON rows, eliminating spurious
|
|
2047
|
-
* hash mismatches from schema-definition column reordering (T11533 ROOT CAUSE
|
|
2048
|
-
* 4). Returns `{ count: 0, hash: '' }` for virtual tables that cannot be
|
|
2049
|
-
* selected from, rather than throwing.
|
|
2050
|
-
*
|
|
2051
|
-
* ## Source-side coercion (T11809 · AC2)
|
|
2052
|
-
*
|
|
2053
|
-
* When `transform` is provided (SOURCE side only), each column is SELECTed
|
|
2054
|
-
* through {@link buildDigestExpr} — the SAME epoch→ISO / enum-normalize /
|
|
2055
|
-
* non-finite-clamp transforms the migration applied — and aliased back to its
|
|
2056
|
-
* original name so the canonical-key serialisation matches the (already
|
|
2057
|
-
* canonical) target row. The TARGET side passes `transform === undefined` and
|
|
2058
|
-
* digests its stored values raw.
|
|
2059
|
-
*
|
|
2060
|
-
* @param db - Database handle to query.
|
|
2061
|
-
* @param tableName - Physical table name.
|
|
2062
|
-
* @param columns - Explicit column list in canonical order, or `null` for
|
|
2063
|
-
* `SELECT *` (used when there is no counterpart table to intersect with).
|
|
2064
|
-
* @param transform - Source-side transform spec, or `undefined` for the raw
|
|
2065
|
-
* (target-side) digest.
|
|
2066
|
-
* @returns `{ count, hash }` for the table.
|
|
2067
|
-
*/
|
|
2068
|
-
function computeTableDigest(db, tableName, columns, transform) {
|
|
2069
|
-
let count = 0;
|
|
2070
|
-
try {
|
|
2071
|
-
const row = db.prepare(`SELECT COUNT(*) AS c FROM "${tableName}"`).get();
|
|
2072
|
-
count = Number(row?.c ?? 0);
|
|
2073
|
-
} catch (err) {
|
|
2074
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2075
|
-
log.warn({
|
|
2076
|
-
tableName,
|
|
2077
|
-
err: msg
|
|
2078
|
-
}, "computeTableDigest: COUNT(*) failed (possibly a virtual/FTS table) — treating as 0 rows");
|
|
2079
|
-
return {
|
|
2080
|
-
count: 0,
|
|
2081
|
-
hash: ""
|
|
2082
|
-
};
|
|
2083
|
-
}
|
|
2084
|
-
const { createHash } = _require("node:crypto");
|
|
2085
|
-
const hasher = createHash("sha256");
|
|
2086
|
-
const orderBy = orderByClause(db, tableName);
|
|
2087
|
-
let selectClause;
|
|
2088
|
-
if (columns !== null && columns.length > 0) selectClause = columns.map((c) => {
|
|
2089
|
-
if (transform === void 0) return `"${c}"`;
|
|
2090
|
-
const srcType = transform.srcTypeByCol.get(c) ?? "";
|
|
2091
|
-
const tgtCol = transform.tgtColByCol.get(c);
|
|
2092
|
-
return `${buildDigestExpr(transform.targetTableName, c, srcType, transform.isoGlobCols, tgtCol)} AS "${c}"`;
|
|
2093
|
-
}).join(", ");
|
|
2094
|
-
else selectClause = "*";
|
|
2095
|
-
try {
|
|
2096
|
-
const stmt = db.prepare(`SELECT ${selectClause} FROM "${tableName}" ORDER BY ${orderBy}`);
|
|
2097
|
-
for (const row of stmt.iterate()) {
|
|
2098
|
-
const rowObj = row;
|
|
2099
|
-
hasher.update(JSON.stringify(rowObj, Object.keys(rowObj).sort()));
|
|
2100
|
-
}
|
|
2101
|
-
} catch (err) {
|
|
2102
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2103
|
-
log.warn({
|
|
2104
|
-
tableName,
|
|
2105
|
-
err: msg
|
|
2106
|
-
}, "computeTableDigest: streamed SELECT failed (possibly a virtual/FTS table) — digest skipped (COUNT(*) parity still enforced)");
|
|
2107
|
-
return {
|
|
2108
|
-
count,
|
|
2109
|
-
hash: ""
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
return {
|
|
2113
|
-
count,
|
|
2114
|
-
hash: hasher.digest("hex").slice(0, 32)
|
|
2115
|
-
};
|
|
2116
|
-
}
|
|
2117
|
-
/**
|
|
2118
|
-
* Return the sorted intersection of column names present in both the source and
|
|
2119
|
-
* target tables, for use as the canonical column ordering in
|
|
2120
|
-
* {@link computeTableDigest}. Returns `null` when either side has no columns
|
|
2121
|
-
* (virtual/FTS-table fallback).
|
|
2122
|
-
*
|
|
2123
|
-
* @param srcDb - Source database handle.
|
|
2124
|
-
* @param srcTable - Physical table name in the source DB.
|
|
2125
|
-
* @param tgtDb - Target database handle.
|
|
2126
|
-
* @param tgtTable - Physical table name in the target DB.
|
|
2127
|
-
*/
|
|
2128
|
-
function sharedColumnsSorted(srcDb, srcTable, tgtDb, tgtTable) {
|
|
2129
|
-
try {
|
|
2130
|
-
const srcCols = srcDb.prepare(`PRAGMA table_info("${srcTable}")`).all().map((r) => r.name);
|
|
2131
|
-
const tgtColSet = new Set(tgtDb.prepare(`PRAGMA table_info("${tgtTable}")`).all().map((r) => r.name));
|
|
2132
|
-
if (srcCols.length === 0 || tgtColSet.size === 0) return null;
|
|
2133
|
-
return srcCols.filter((c) => tgtColSet.has(c)).sort();
|
|
2134
|
-
} catch {
|
|
2135
|
-
return null;
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
/**
|
|
2139
|
-
* Build the source-side {@link DigestTransformSpec} for a source→target table
|
|
2140
|
-
* pair: the source column type affinities, the target's ISO-GLOB columns, and
|
|
2141
|
-
* the target column metadata (NOT-NULL flag + default + affinity), keyed by the
|
|
2142
|
-
* consolidated target name (the transform lookup key). The target metadata
|
|
2143
|
-
* drives the NULL→NOT-NULL-default `COALESCE` mirroring (T11836).
|
|
2144
|
-
*
|
|
2145
|
-
* Returns `undefined` (raw digest, no transform) when the source `PRAGMA
|
|
2146
|
-
* table_info` cannot be read — best-effort, biased to surfacing a real
|
|
2147
|
-
* difference rather than masking one.
|
|
2148
|
-
*
|
|
2149
|
-
* @param srcDb - Source database handle.
|
|
2150
|
-
* @param srcTable - Physical legacy source table name.
|
|
2151
|
-
* @param tgtDb - Target database handle (consolidated cleo.db).
|
|
2152
|
-
* @param targetTableName - Physical consolidated target table name.
|
|
2153
|
-
* @returns A transform spec, or `undefined` when source metadata is unavailable.
|
|
2154
|
-
*/
|
|
2155
|
-
function buildSourceDigestTransform(srcDb, srcTable, tgtDb, targetTableName) {
|
|
2156
|
-
try {
|
|
2157
|
-
return {
|
|
2158
|
-
targetTableName,
|
|
2159
|
-
srcTypeByCol: new Map(srcDb.prepare(`PRAGMA table_info("${srcTable}")`).all().map((r) => [r.name, r.type])),
|
|
2160
|
-
isoGlobCols: detectIsoGlobColumns(tgtDb, targetTableName),
|
|
2161
|
-
tgtColByCol: new Map(tgtDb.prepare(`PRAGMA table_info("${targetTableName}")`).all().map((r) => [r.name, {
|
|
2162
|
-
notnull: r.notnull,
|
|
2163
|
-
dflt_value: r.dflt_value,
|
|
2164
|
-
type: r.type
|
|
2165
|
-
}]))
|
|
2166
|
-
};
|
|
2167
|
-
} catch {
|
|
2168
|
-
return;
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
/**
|
|
2172
|
-
* List user tables in a DB (excluding SQLite internals + Drizzle journal).
|
|
2173
|
-
*/
|
|
2174
|
-
function listTables(db) {
|
|
2175
|
-
return db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__drizzle_%' ORDER BY name").all().map((r) => r.name);
|
|
2176
|
-
}
|
|
2177
|
-
/**
|
|
2178
|
-
* Return `true` if `tableName` exists in `db`. Used to route an FK-orphan row to
|
|
2179
|
-
* the scope DB that actually declares its child table (T11572).
|
|
2180
|
-
*/
|
|
2181
|
-
function tableExists(db, tableName) {
|
|
2182
|
-
try {
|
|
2183
|
-
const escaped = tableName.replace(/'/g, "''");
|
|
2184
|
-
return db.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='${escaped}'`).get() !== void 0;
|
|
2185
|
-
} catch {
|
|
2186
|
-
return false;
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
/**
|
|
2190
|
-
* Regex that extracts a single-column `IN (...)` CHECK enum from a table's DDL.
|
|
2191
|
-
*
|
|
2192
|
-
* Matches the canonical Drizzle-generated form:
|
|
2193
|
-
* `CHECK ("col" IN ('a', 'b', 'c'))`
|
|
2194
|
-
* and the NULL-tolerant variant:
|
|
2195
|
-
* `CHECK ("col" IS NULL OR "col" IN ('a', 'b'))`
|
|
2196
|
-
*
|
|
2197
|
-
* Capture group 1 is the column name; group 2 is the raw `'a', 'b', …` list.
|
|
2198
|
-
* The regex is intentionally conservative — it only recognises the
|
|
2199
|
-
* string-literal `IN` enum shape (the one that drops rows on drift), not GLOB
|
|
2200
|
-
* or arithmetic CHECKs.
|
|
2201
|
-
*/
|
|
2202
|
-
var CHECK_ENUM_REGEX = /CHECK\s*\(\s*"([^"]+)"\s+(?:IS\s+NULL\s+OR\s+"[^"]+"\s+)?IN\s*\(([^)]*)\)\s*\)/gi;
|
|
2203
|
-
/**
|
|
2204
|
-
* Parse the `'a', 'b', 'c'` body of an `IN (...)` clause into the set of
|
|
2205
|
-
* canonical enum members (single-quoted SQL string literals, `''` un-escaped).
|
|
2206
|
-
*/
|
|
2207
|
-
function parseEnumMembers(body) {
|
|
2208
|
-
const members = [];
|
|
2209
|
-
for (const m of body.matchAll(/'((?:[^']|'')*)'/g)) members.push(m[1].replace(/''/g, "'"));
|
|
2210
|
-
return members;
|
|
2211
|
-
}
|
|
2212
|
-
/**
|
|
2213
|
-
* Read a target table's DDL and return a map of `column → allowed enum members`
|
|
2214
|
-
* for every single-column string-literal CHECK enum on that table.
|
|
2215
|
-
*
|
|
2216
|
-
* @param db - Target DB with the consolidated schema.
|
|
2217
|
-
* @param tableName - Physical consolidated table name.
|
|
2218
|
-
* @returns Map from column name to its allowed enum members. Empty when the
|
|
2219
|
-
* table declares no recognised CHECK enums.
|
|
2220
|
-
*/
|
|
2221
|
-
function detectCheckEnums(db, tableName) {
|
|
2222
|
-
const escaped = tableName.replace(/'/g, "''");
|
|
2223
|
-
const row = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='${escaped}'`).get();
|
|
2224
|
-
const out = /* @__PURE__ */ new Map();
|
|
2225
|
-
if (!row?.sql) return out;
|
|
2226
|
-
CHECK_ENUM_REGEX.lastIndex = 0;
|
|
2227
|
-
for (const match of row.sql.matchAll(CHECK_ENUM_REGEX)) {
|
|
2228
|
-
const col = match[1];
|
|
2229
|
-
const members = parseEnumMembers(match[2]);
|
|
2230
|
-
if (members.length > 0) out.set(col, members);
|
|
2231
|
-
}
|
|
2232
|
-
return out;
|
|
2233
|
-
}
|
|
2234
|
-
/**
|
|
2235
|
-
* Detect enum/type drift for one source→target table pair: source values in an
|
|
2236
|
-
* enum-constrained column that are NOT members of the target CHECK enum.
|
|
2237
|
-
*
|
|
2238
|
-
* Reads the DISTINCT non-null values of each enum column from the source table
|
|
2239
|
-
* and compares them against the target's allowed members. Only columns present
|
|
2240
|
-
* in BOTH source and target are inspected. The check is purely diagnostic — it
|
|
2241
|
-
* reports raw source drift; the migration layer is responsible for normalising
|
|
2242
|
-
* known aliases before insert.
|
|
2243
|
-
*
|
|
2244
|
-
* @param srcDb - Source DB handle.
|
|
2245
|
-
* @param srcTable - Physical source table name.
|
|
2246
|
-
* @param tgtDb - Target DB handle.
|
|
2247
|
-
* @param tgtTable - Physical consolidated target table name.
|
|
2248
|
-
* @returns Drift findings for this table (empty when fully canonical).
|
|
2249
|
-
*/
|
|
2250
|
-
function detectTableEnumDrift(srcDb, srcTable, tgtDb, tgtTable) {
|
|
2251
|
-
const enums = detectCheckEnums(tgtDb, tgtTable);
|
|
2252
|
-
if (enums.size === 0) return [];
|
|
2253
|
-
let srcCols;
|
|
2254
|
-
try {
|
|
2255
|
-
srcCols = new Set(srcDb.prepare(`PRAGMA table_info("${srcTable}")`).all().map((r) => r.name));
|
|
2256
|
-
} catch {
|
|
2257
|
-
return [];
|
|
2258
|
-
}
|
|
2259
|
-
const findings = [];
|
|
2260
|
-
for (const [col, allowed] of enums) {
|
|
2261
|
-
if (!srcCols.has(col)) continue;
|
|
2262
|
-
const allowedSet = new Set(allowed);
|
|
2263
|
-
let rows;
|
|
2264
|
-
try {
|
|
2265
|
-
rows = srcDb.prepare(`SELECT "${col}" AS v, COUNT(*) AS c FROM "${srcTable}" WHERE "${col}" IS NOT NULL GROUP BY "${col}"`).all();
|
|
2266
|
-
} catch {
|
|
2267
|
-
continue;
|
|
2268
|
-
}
|
|
2269
|
-
const offending = [];
|
|
2270
|
-
let driftCount = 0;
|
|
2271
|
-
for (const r of rows) {
|
|
2272
|
-
const value = String(r.v);
|
|
2273
|
-
if (!allowedSet.has(value)) {
|
|
2274
|
-
driftCount += r.c;
|
|
2275
|
-
if (offending.length < 20) offending.push(value);
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
if (driftCount > 0) findings.push({
|
|
2279
|
-
targetTable: tgtTable,
|
|
2280
|
-
column: col,
|
|
2281
|
-
offendingValues: offending,
|
|
2282
|
-
allowedValues: allowed,
|
|
2283
|
-
driftCount
|
|
2284
|
-
});
|
|
2285
|
-
}
|
|
2286
|
-
return findings;
|
|
2287
|
-
}
|
|
2288
|
-
/**
|
|
2289
|
-
* Run `PRAGMA foreign_key_check` on a target DB and return any orphan rows.
|
|
2290
|
-
*
|
|
2291
|
-
* @param db - Target DB handle (consolidated cleo.db).
|
|
2292
|
-
* @param scope - Scope label, attached to log context only.
|
|
2293
|
-
* @returns The list of FK violations (empty when referential integrity holds).
|
|
2294
|
-
*/
|
|
2295
|
-
function foreignKeyCheck(db, scope) {
|
|
2296
|
-
try {
|
|
2297
|
-
const rows = db.prepare("PRAGMA foreign_key_check").all();
|
|
2298
|
-
if (rows.length > 0) log.warn({
|
|
2299
|
-
scope,
|
|
2300
|
-
count: rows.length,
|
|
2301
|
-
sample: rows.slice(0, 5)
|
|
2302
|
-
}, `verifyMigration: PRAGMA foreign_key_check found ${rows.length} orphan row(s)`);
|
|
2303
|
-
return rows.map((r) => ({
|
|
2304
|
-
table: r.table,
|
|
2305
|
-
rowid: r.rowid ?? null,
|
|
2306
|
-
parent: r.parent,
|
|
2307
|
-
fkid: r.fkid
|
|
2308
|
-
}));
|
|
2309
|
-
} catch (err) {
|
|
2310
|
-
log.warn({
|
|
2311
|
-
scope,
|
|
2312
|
-
err
|
|
2313
|
-
}, "verifyMigration: PRAGMA foreign_key_check failed (non-fatal)");
|
|
2314
|
-
return [];
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
/**
|
|
2318
|
-
* Compute a **migration-stable, name-mapping-stable signature** for one FK
|
|
2319
|
-
* orphan so the same dangling row can be matched between the legacy SOURCE and
|
|
2320
|
-
* the consolidated TARGET — despite (a) a different `rowid` after the copy AND
|
|
2321
|
-
* (b) the legacy→consolidated table RENAME (`task_relations`→`tasks_task_relations`,
|
|
2322
|
-
* parent `tasks`→`tasks_tasks`).
|
|
2323
|
-
*
|
|
2324
|
-
* `PRAGMA foreign_key_check` returns `{ table, rowid, parent, fkid }`. We key the
|
|
2325
|
-
* orphan on:
|
|
2326
|
-
* - the CONSOLIDATED child-table name (source names are mapped via
|
|
2327
|
-
* {@link resolveConsolidatedTableName} so both sides agree),
|
|
2328
|
-
* - the CONSOLIDATED parent-table name (same mapping),
|
|
2329
|
-
* - the actual VALUES of the foreign-key child columns for that row (read via
|
|
2330
|
-
* `PRAGMA foreign_key_list` + the row at `rowid`) — these travel with the row
|
|
2331
|
-
* unchanged, so the dangling reference is identical on both sides.
|
|
2332
|
-
*
|
|
2333
|
-
* `fkid` is intentionally EXCLUDED — the consolidated table may declare its FKs
|
|
2334
|
-
* in a different order than the legacy one, so the per-table fkid is not stable.
|
|
2335
|
-
* The (consolidated child, consolidated parent, dangling FK column values) triple
|
|
2336
|
-
* uniquely identifies the orphan reference.
|
|
2337
|
-
*
|
|
2338
|
-
* Falls back to a column-name-only shape if the child columns can't be read
|
|
2339
|
-
* (best-effort — a fallback only ever makes two orphans compare as *different*,
|
|
2340
|
-
* which biases toward treating an orphan as "introduced", the SAFE/strict
|
|
2341
|
-
* direction).
|
|
2342
|
-
*
|
|
2343
|
-
* @param db - DB handle the orphan was found in.
|
|
2344
|
-
* @param v - One `PRAGMA foreign_key_check` row.
|
|
2345
|
-
* @param sourceName - When set, the orphan is on the SOURCE side and its
|
|
2346
|
-
* `table`/`parent` names are mapped to their consolidated equivalents so the
|
|
2347
|
-
* signature matches the target side. Omit for the already-consolidated target.
|
|
2348
|
-
* @returns A stable string signature.
|
|
2349
|
-
*/
|
|
2350
|
-
function orphanSignature(db, v, sourceName) {
|
|
2351
|
-
const mapName = (name) => {
|
|
2352
|
-
if (sourceName === void 0) return name;
|
|
2353
|
-
const res = resolveConsolidatedTableName(sourceName, name);
|
|
2354
|
-
return res.kind === "skip" ? name : res.targetName;
|
|
2355
|
-
};
|
|
2356
|
-
const childTable = mapName(v.table);
|
|
2357
|
-
const parentTable = mapName(v.parent);
|
|
2358
|
-
let childCols = [];
|
|
2359
|
-
try {
|
|
2360
|
-
childCols = db.prepare(`PRAGMA foreign_key_list("${v.table}")`).all().filter((r) => r.id === v.fkid).map((r) => r.from);
|
|
2361
|
-
} catch {}
|
|
2362
|
-
if (v.rowid !== null && childCols.length > 0) try {
|
|
2363
|
-
const sel = childCols.map((c) => `"${c}"`).join(", ");
|
|
2364
|
-
const row = db.prepare(`SELECT ${sel} FROM "${v.table}" WHERE rowid = ?`).get(v.rowid);
|
|
2365
|
-
if (row) return `${childTable}|${parentTable}|${childCols.slice().sort().map((c) => `${c}=${JSON.stringify(row[c])}`).join("&")}`;
|
|
2366
|
-
} catch {}
|
|
2367
|
-
return `${childTable}|${parentTable}|rowid:${v.rowid ?? "?"}`;
|
|
2368
|
-
}
|
|
2369
|
-
/**
|
|
2370
|
-
* Collect the set of {@link orphanSignature}s for every FK orphan in a SOURCE DB.
|
|
2371
|
-
*
|
|
2372
|
-
* Used to compute SOURCE-side orphan signatures so the parity gate can subtract
|
|
2373
|
-
* pre-existing orphans from the target's orphan set and fail only on the orphans
|
|
2374
|
-
* the migration INTRODUCED (T11572). The signatures are mapped to consolidated
|
|
2375
|
-
* table names so they compare equal to the target-side signatures.
|
|
2376
|
-
*
|
|
2377
|
-
* @param db - SOURCE DB handle to scan.
|
|
2378
|
-
* @param sourceName - `LegacyDbDescriptor.name` (for table-name mapping).
|
|
2379
|
-
* @param scope - Scope label for diagnostics.
|
|
2380
|
-
* @returns A set of name-mapping-stable orphan signatures.
|
|
2381
|
-
*/
|
|
2382
|
-
function sourceOrphanSignatures(db, sourceName, scope) {
|
|
2383
|
-
const sigs = /* @__PURE__ */ new Set();
|
|
2384
|
-
try {
|
|
2385
|
-
const rows = db.prepare("PRAGMA foreign_key_check").all();
|
|
2386
|
-
for (const r of rows) sigs.add(orphanSignature(db, r, sourceName));
|
|
2387
|
-
if (rows.length > 0) log.warn({
|
|
2388
|
-
scope,
|
|
2389
|
-
count: rows.length,
|
|
2390
|
-
sample: rows.slice(0, 5)
|
|
2391
|
-
}, `verifyMigration: source already has ${rows.length} pre-existing FK orphan(s) — these are tolerated (carried forward losslessly, flagged for data-hygiene)`);
|
|
2392
|
-
} catch (err) {
|
|
2393
|
-
log.warn({
|
|
2394
|
-
scope,
|
|
2395
|
-
err
|
|
2396
|
-
}, "verifyMigration: source PRAGMA foreign_key_check failed (non-fatal)");
|
|
2397
|
-
}
|
|
2398
|
-
return sigs;
|
|
2399
|
-
}
|
|
2400
|
-
/**
|
|
2401
|
-
* Verify that a source→target SQLite migration preserved every row, referential
|
|
2402
|
-
* integrity, content, and enum validity.
|
|
2403
|
-
*
|
|
2404
|
-
* Opens all source DBs and the consolidated target DBs **read-only**, then for
|
|
2405
|
-
* each legacy source table:
|
|
2406
|
-
*
|
|
2407
|
-
* 1. Resolves the consolidated target name via {@link resolveConsolidatedTableName}.
|
|
2408
|
-
* 2. Compares row counts (parity gate).
|
|
2409
|
-
* 3. Computes a column-intersection content digest on both sides.
|
|
2410
|
-
* 4. Records any enum/type drift (source values outside the target CHECK enum).
|
|
2411
|
-
*
|
|
2412
|
-
* After all tables, it runs `PRAGMA foreign_key_check` on each distinct target
|
|
2413
|
-
* DB and folds the orphan rows into the result.
|
|
2414
|
-
*
|
|
2415
|
-
* @param sources - Legacy source descriptors (from `buildExodusPlan()`).
|
|
2416
|
-
* @param projectDbPath - Absolute path to the consolidated project `cleo.db`.
|
|
2417
|
-
* @param globalDbPath - Absolute path to the consolidated global `cleo.db`.
|
|
2418
|
-
* @param onProgress - Optional progress callback.
|
|
2419
|
-
*
|
|
2420
|
-
* @returns A {@link VerifyMigrationResult}. `ok === false` (with `error`
|
|
2421
|
-
* populated) on any count mismatch, content mismatch, FK orphan, or enum
|
|
2422
|
-
* drift.
|
|
2423
|
-
*
|
|
2424
|
-
* @task T11551 (DHQ-045 — exodus zero-loss durable guard · AC1)
|
|
2425
|
-
*/
|
|
2426
|
-
function verifyMigration(sources, projectDbPath, globalDbPath, onProgress) {
|
|
2427
|
-
const tables = [];
|
|
2428
|
-
const enumDrift = [];
|
|
2429
|
-
const foreignKeyViolations = [];
|
|
2430
|
-
const introducedForeignKeyViolations = [];
|
|
2431
|
-
const preExistingForeignKeyViolations = [];
|
|
2432
|
-
const failureLines = [];
|
|
2433
|
-
/**
|
|
2434
|
-
* Signatures of FK orphans ALREADY present in the legacy SOURCE DBs (T11572).
|
|
2435
|
-
* Target orphans whose signature is in this set are pre-existing — carried
|
|
2436
|
-
* forward losslessly — and do NOT fail the parity gate.
|
|
2437
|
-
*/
|
|
2438
|
-
const sourceOrphanSigs = /* @__PURE__ */ new Set();
|
|
2439
|
-
if (!existsSync(projectDbPath)) return {
|
|
2440
|
-
ok: false,
|
|
2441
|
-
tables: [],
|
|
2442
|
-
foreignKeyViolations: [],
|
|
2443
|
-
introducedForeignKeyViolations: [],
|
|
2444
|
-
preExistingForeignKeyViolations: [],
|
|
2445
|
-
enumDrift: [],
|
|
2446
|
-
error: `Consolidated project cleo.db not found at ${projectDbPath}. Run 'cleo exodus migrate' first.`
|
|
2447
|
-
};
|
|
2448
|
-
if (!existsSync(globalDbPath)) return {
|
|
2449
|
-
ok: false,
|
|
2450
|
-
tables: [],
|
|
2451
|
-
foreignKeyViolations: [],
|
|
2452
|
-
introducedForeignKeyViolations: [],
|
|
2453
|
-
preExistingForeignKeyViolations: [],
|
|
2454
|
-
enumDrift: [],
|
|
2455
|
-
error: `Consolidated global cleo.db not found at ${globalDbPath}. Run 'cleo exodus migrate' first.`
|
|
2456
|
-
};
|
|
2457
|
-
const projectSnap = openCleoDbSnapshot(projectDbPath, { readOnly: true });
|
|
2458
|
-
const globalSnap = openCleoDbSnapshot(globalDbPath, { readOnly: true });
|
|
2459
|
-
try {
|
|
2460
|
-
for (const src of sources) {
|
|
2461
|
-
if (!existsSync(src.path)) {
|
|
2462
|
-
onProgress?.(`Skipping ${src.name} (not present)`);
|
|
2463
|
-
continue;
|
|
2464
|
-
}
|
|
2465
|
-
const srcSnap = openCleoDbSnapshot(src.path, { readOnly: true });
|
|
2466
|
-
try {
|
|
2467
|
-
for (const sig of sourceOrphanSignatures(srcSnap.db, src.name, `source:${src.name}`)) sourceOrphanSigs.add(sig);
|
|
2468
|
-
const sourceTables = listTables(srcSnap.db);
|
|
2469
|
-
const projectTables = new Set(listTables(projectSnap.db));
|
|
2470
|
-
const globalTables = new Set(listTables(globalSnap.db));
|
|
2471
|
-
for (const legacyTableName of sourceTables) {
|
|
2472
|
-
onProgress?.(`Verifying ${src.name}.${legacyTableName}…`);
|
|
2473
|
-
const resolution = resolveConsolidatedTableName(src.name, legacyTableName);
|
|
2474
|
-
if (resolution.kind === "skip") {
|
|
2475
|
-
onProgress?.(` [skip] ${src.name}.${legacyTableName} — ${resolution.reason}`);
|
|
2476
|
-
continue;
|
|
2477
|
-
}
|
|
2478
|
-
const targetTableName = resolution.targetName;
|
|
2479
|
-
const scope = resolveTableTargetScope(src.name, legacyTableName, src.targetScope);
|
|
2480
|
-
const targetSnap = scope === "project" ? projectSnap : globalSnap;
|
|
2481
|
-
const targetTables = scope === "project" ? projectTables : globalTables;
|
|
2482
|
-
if (targetTables.has(targetTableName)) {
|
|
2483
|
-
const drift = detectTableEnumDrift(srcSnap.db, legacyTableName, targetSnap.db, targetTableName);
|
|
2484
|
-
if (drift.length > 0) {
|
|
2485
|
-
enumDrift.push(...drift);
|
|
2486
|
-
for (const d of drift) failureLines.push(`[${scope}] ${targetTableName}.${d.column}: ${d.driftCount} row(s) with value(s) outside enum {${d.allowedValues.join(", ")}} — e.g. ${d.offendingValues.map((v) => `'${v}'`).join(", ")}`);
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
if (!targetTables.has(targetTableName)) {
|
|
2490
|
-
const srcResult = computeTableDigest(srcSnap.db, legacyTableName, null);
|
|
2491
|
-
const countMatch = srcResult.count === 0;
|
|
2492
|
-
tables.push({
|
|
2493
|
-
sourceTable: legacyTableName,
|
|
2494
|
-
targetTable: targetTableName,
|
|
2495
|
-
scope,
|
|
2496
|
-
sourceCount: srcResult.count,
|
|
2497
|
-
targetCount: 0,
|
|
2498
|
-
sourceHash: srcResult.hash,
|
|
2499
|
-
targetHash: "",
|
|
2500
|
-
hashMatch: countMatch,
|
|
2501
|
-
countMatch
|
|
2502
|
-
});
|
|
2503
|
-
if (!countMatch) failureLines.push(`[${scope}] ${src.name}.${legacyTableName} → ${targetTableName}: missing from target (source has ${srcResult.count} rows)`);
|
|
2504
|
-
continue;
|
|
2505
|
-
}
|
|
2506
|
-
const cols = sharedColumnsSorted(srcSnap.db, legacyTableName, targetSnap.db, targetTableName);
|
|
2507
|
-
const srcTransform = buildSourceDigestTransform(srcSnap.db, legacyTableName, targetSnap.db, targetTableName);
|
|
2508
|
-
const srcDigest = computeTableDigest(srcSnap.db, legacyTableName, cols, srcTransform);
|
|
2509
|
-
const tgtDigest = computeTableDigest(targetSnap.db, targetTableName, cols);
|
|
2510
|
-
const countMatch = srcDigest.count === tgtDigest.count;
|
|
2511
|
-
const hashMatch = srcDigest.hash === tgtDigest.hash;
|
|
2512
|
-
if (tgtDigest.count < srcDigest.count) failureLines.push(`[${scope}] ${src.name}.${legacyTableName} → ${targetTableName}: DEFICIT — source=${srcDigest.count} rows, target=${tgtDigest.count} rows (${srcDigest.count - tgtDigest.count} missing), hashMatch=${hashMatch}`);
|
|
2513
|
-
else if (tgtDigest.count > srcDigest.count) log.warn({
|
|
2514
|
-
scope,
|
|
2515
|
-
source: src.name,
|
|
2516
|
-
table: targetTableName,
|
|
2517
|
-
sourceCount: srcDigest.count,
|
|
2518
|
-
targetCount: tgtDigest.count,
|
|
2519
|
-
delta: tgtDigest.count - srcDigest.count
|
|
2520
|
-
}, `verifyMigration: ${targetTableName} has ${tgtDigest.count - srcDigest.count} MORE row(s) in target than source (surplus — NOT data loss, tolerated; e.g. migration-time audit writes). Verify it is not an unexpected double-copy.`);
|
|
2521
|
-
else if (!hashMatch) failureLines.push(`[${scope}] ${src.name}.${legacyTableName} → ${targetTableName}: source=${srcDigest.count} rows, target=${tgtDigest.count} rows, hashMatch=${hashMatch}`);
|
|
2522
|
-
tables.push({
|
|
2523
|
-
sourceTable: legacyTableName,
|
|
2524
|
-
targetTable: targetTableName,
|
|
2525
|
-
scope,
|
|
2526
|
-
sourceCount: srcDigest.count,
|
|
2527
|
-
targetCount: tgtDigest.count,
|
|
2528
|
-
sourceHash: srcDigest.hash,
|
|
2529
|
-
targetHash: tgtDigest.hash,
|
|
2530
|
-
hashMatch,
|
|
2531
|
-
countMatch
|
|
2532
|
-
});
|
|
2533
|
-
}
|
|
2534
|
-
} finally {
|
|
2535
|
-
srcSnap.close();
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
2538
|
-
const targetOrphans = [];
|
|
2539
|
-
targetOrphans.push(...foreignKeyCheck(projectSnap.db, "project"));
|
|
2540
|
-
targetOrphans.push(...foreignKeyCheck(globalSnap.db, "global"));
|
|
2541
|
-
foreignKeyViolations.push(...targetOrphans);
|
|
2542
|
-
for (const fk of targetOrphans) {
|
|
2543
|
-
const sig = orphanSignature(tableExists(projectSnap.db, fk.table) ? projectSnap.db : globalSnap.db, fk);
|
|
2544
|
-
if (sourceOrphanSigs.has(sig)) preExistingForeignKeyViolations.push(fk);
|
|
2545
|
-
else {
|
|
2546
|
-
introducedForeignKeyViolations.push(fk);
|
|
2547
|
-
failureLines.push(`[fk] ${fk.table}.rowid=${fk.rowid ?? "?"} references missing ${fk.parent} (fkid=${fk.fkid}) — INTRODUCED by migration`);
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
if (preExistingForeignKeyViolations.length > 0) log.warn({
|
|
2551
|
-
count: preExistingForeignKeyViolations.length,
|
|
2552
|
-
sample: preExistingForeignKeyViolations.slice(0, 5)
|
|
2553
|
-
}, `verifyMigration: ${preExistingForeignKeyViolations.length} pre-existing source FK orphan(s) carried forward losslessly — tolerated (flag for data-hygiene follow-up, NOT a migration failure)`);
|
|
2554
|
-
} catch (err) {
|
|
2555
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
2556
|
-
log.error({ err }, "verifyMigration failed");
|
|
2557
|
-
return {
|
|
2558
|
-
ok: false,
|
|
2559
|
-
tables,
|
|
2560
|
-
foreignKeyViolations,
|
|
2561
|
-
introducedForeignKeyViolations,
|
|
2562
|
-
preExistingForeignKeyViolations,
|
|
2563
|
-
enumDrift,
|
|
2564
|
-
error
|
|
2565
|
-
};
|
|
2566
|
-
} finally {
|
|
2567
|
-
projectSnap.close();
|
|
2568
|
-
globalSnap.close();
|
|
2569
|
-
}
|
|
2570
|
-
if (failureLines.length > 0) {
|
|
2571
|
-
const error = `verifyMigration FAILED: ${failureLines.length} issue(s):\n${failureLines.map((l) => ` • ${l}`).join("\n")}`;
|
|
2572
|
-
log.error({ failureCount: failureLines.length }, error);
|
|
2573
|
-
return {
|
|
2574
|
-
ok: false,
|
|
2575
|
-
tables,
|
|
2576
|
-
foreignKeyViolations,
|
|
2577
|
-
introducedForeignKeyViolations,
|
|
2578
|
-
preExistingForeignKeyViolations,
|
|
2579
|
-
enumDrift,
|
|
2580
|
-
error
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
|
-
return {
|
|
2584
|
-
ok: true,
|
|
2585
|
-
tables,
|
|
2586
|
-
foreignKeyViolations,
|
|
2587
|
-
introducedForeignKeyViolations,
|
|
2588
|
-
preExistingForeignKeyViolations,
|
|
2589
|
-
enumDrift
|
|
2590
|
-
};
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
export { buildExodusPlan, clearExodusJournal, runExodusMigrate, verifyMigration };
|
|
2594
|
-
//# sourceMappingURL=exodus-BBuuN6aA.js.map
|