@jant/core 0.6.14 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/bin/commands/import-site.js +81 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-BxCOR3Uc.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
  13. package/dist/client/.vite/manifest.json +3 -3
  14. package/dist/client/_assets/client-B_eGKN5p.css +2 -0
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
  16. package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
  17. package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
  18. package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
  19. package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
  20. package/dist/index.js +3 -3
  21. package/dist/node.js +5 -5
  22. package/package.json +11 -10
  23. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  24. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  25. package/src/__tests__/export-hugo-build.test.ts +63 -4
  26. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  27. package/src/__tests__/export-service.test.ts +142 -9
  28. package/src/__tests__/helpers/app.ts +3 -0
  29. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  30. package/src/__tests__/import-site-command.test.ts +65 -0
  31. package/src/__tests__/vite-config.test.ts +45 -0
  32. package/src/app.tsx +16 -3
  33. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  34. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  35. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  36. package/src/client/__tests__/compose-bridge.test.ts +64 -0
  37. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  38. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  39. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  40. package/src/client/__tests__/toast.test.ts +18 -1
  41. package/src/client/collection-created-notice.ts +66 -0
  42. package/src/client/collection-form-bridge.ts +17 -0
  43. package/src/client/collection-navigation.ts +44 -0
  44. package/src/client/collection-page-actions.ts +57 -1
  45. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  46. package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
  47. package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
  48. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
  49. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  50. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  51. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  52. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  53. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  54. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  55. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  56. package/src/client/components/collection-manager-types.ts +9 -3
  57. package/src/client/components/compose-types.ts +8 -0
  58. package/src/client/components/jant-collection-directory.ts +165 -30
  59. package/src/client/components/jant-command-palette.ts +2 -1
  60. package/src/client/components/jant-compose-dialog.ts +85 -21
  61. package/src/client/components/jant-compose-editor.ts +13 -7
  62. package/src/client/components/jant-compose-fullscreen.ts +24 -4
  63. package/src/client/components/jant-config-editor.ts +722 -0
  64. package/src/client/components/jant-media-lightbox.ts +100 -18
  65. package/src/client/components/jant-nav-manager.ts +1283 -54
  66. package/src/client/components/jant-post-form.ts +3 -3
  67. package/src/client/components/jant-post-menu.ts +86 -74
  68. package/src/client/components/nav-manager-types.ts +51 -2
  69. package/src/client/components/post-form-types.ts +1 -1
  70. package/src/client/compose-bridge.ts +4 -1
  71. package/src/client/feed-video-player.ts +62 -2
  72. package/src/client/footnote-rail.ts +478 -0
  73. package/src/client/hydrate-partial.ts +2 -0
  74. package/src/client/media-lightbox-events.ts +63 -0
  75. package/src/client/site-header-fragment.ts +38 -0
  76. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  77. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  78. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  79. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  80. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  81. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  82. package/src/client/tiptap/bubble-menu.ts +88 -10
  83. package/src/client/tiptap/continuous-lists.ts +102 -0
  84. package/src/client/tiptap/create-editor.ts +3 -0
  85. package/src/client/tiptap/extensions.ts +14 -5
  86. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  87. package/src/client/tiptap/slash-commands.ts +8 -1
  88. package/src/client/tiptap/structural-keymap.ts +211 -0
  89. package/src/client/tiptap/tab-indent.ts +8 -22
  90. package/src/client/tiptap/table-control-labels.ts +30 -0
  91. package/src/client/tiptap/table-controls.ts +537 -0
  92. package/src/client/tiptap/table-size-picker.ts +254 -0
  93. package/src/client/toast.ts +8 -6
  94. package/src/client-auth.ts +1 -0
  95. package/src/client.ts +1 -0
  96. package/src/db/__tests__/d1-query.test.ts +43 -1
  97. package/src/db/__tests__/migration-runner.test.ts +63 -0
  98. package/src/db/__tests__/migrations.test.ts +469 -0
  99. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  100. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  101. package/src/db/migrations/0028_superb_post.sql +2 -0
  102. package/src/db/migrations/0029_boring_magma.sql +1 -0
  103. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  104. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  105. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  106. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  107. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  108. package/src/db/migrations/meta/_journal.json +28 -0
  109. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  110. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  111. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  112. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  113. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  114. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  115. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  116. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  117. package/src/db/migrations/pg/meta/_journal.json +28 -0
  118. package/src/db/pg/schema.ts +28 -20
  119. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  120. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  121. package/src/db/schema.ts +28 -20
  122. package/src/i18n/__tests__/detect.test.ts +8 -2
  123. package/src/i18n/detect.ts +14 -5
  124. package/src/i18n/locales/public/en.po +133 -30
  125. package/src/i18n/locales/public/en.ts +1 -1
  126. package/src/i18n/locales/public/zh-Hans.po +128 -25
  127. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  128. package/src/i18n/locales/public/zh-Hant.po +128 -25
  129. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  130. package/src/i18n/locales/settings/en.po +513 -10
  131. package/src/i18n/locales/settings/en.ts +1 -1
  132. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  133. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  134. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  135. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  136. package/src/lib/__tests__/api-settings.test.ts +299 -0
  137. package/src/lib/__tests__/constants.test.ts +5 -0
  138. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  139. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  140. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  141. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  142. package/src/lib/__tests__/markdown.test.ts +8 -6
  143. package/src/lib/__tests__/navigation.test.ts +59 -2
  144. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  145. package/src/lib/__tests__/post-display.test.ts +64 -0
  146. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  147. package/src/lib/__tests__/summary.test.ts +82 -0
  148. package/src/lib/__tests__/timeline.test.ts +109 -70
  149. package/src/lib/__tests__/timezones.test.ts +31 -3
  150. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  151. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  152. package/src/lib/__tests__/view.test.ts +53 -0
  153. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  154. package/src/lib/api-settings.ts +239 -12
  155. package/src/lib/collection-sort.ts +7 -5
  156. package/src/lib/constants.ts +2 -0
  157. package/src/lib/feed-path.ts +25 -0
  158. package/src/lib/feed-policy.ts +64 -0
  159. package/src/lib/footnotes.ts +264 -0
  160. package/src/lib/github-sync-site-config.ts +2 -0
  161. package/src/lib/hugo-markdown.ts +10 -3
  162. package/src/lib/jant-branding.ts +1 -0
  163. package/src/lib/markdown-manager.ts +188 -5
  164. package/src/lib/markdown.ts +10 -3
  165. package/src/lib/navigation.ts +16 -3
  166. package/src/lib/post-body-html.ts +131 -0
  167. package/src/lib/post-display.ts +7 -1
  168. package/src/lib/render.tsx +9 -2
  169. package/src/lib/resolve-config.ts +100 -16
  170. package/src/lib/schemas.ts +133 -1
  171. package/src/lib/settings-paths.ts +5 -0
  172. package/src/lib/site-skill.ts +78 -0
  173. package/src/lib/summary.ts +16 -5
  174. package/src/lib/timeline.ts +98 -122
  175. package/src/lib/timezones.ts +54 -14
  176. package/src/lib/tiptap-render.ts +355 -81
  177. package/src/lib/view.ts +30 -20
  178. package/src/lib/worker-response-cache.ts +11 -18
  179. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  180. package/src/middleware/config.ts +2 -2
  181. package/src/middleware/public-content-access.ts +87 -0
  182. package/src/node/__tests__/cli-export.test.ts +14 -0
  183. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  184. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  185. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  186. package/src/node/index.ts +2 -1
  187. package/src/preset.css +16 -2
  188. package/src/routes/__tests__/compose.test.ts +51 -0
  189. package/src/routes/__tests__/site-skill.test.ts +112 -0
  190. package/src/routes/api/__tests__/collections.test.ts +91 -19
  191. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  192. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  193. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  194. package/src/routes/api/__tests__/settings.test.ts +217 -0
  195. package/src/routes/api/collections.ts +22 -26
  196. package/src/routes/api/export.ts +2 -0
  197. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  198. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  199. package/src/routes/api/internal/post-body-html.ts +43 -0
  200. package/src/routes/api/internal/sites.ts +24 -0
  201. package/src/routes/api/nav-items.ts +24 -1
  202. package/src/routes/api/posts.ts +2 -2
  203. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  204. package/src/routes/api/public/__tests__/posts.test.ts +208 -0
  205. package/src/routes/api/public/archive.ts +4 -0
  206. package/src/routes/api/public/posts.ts +41 -24
  207. package/src/routes/api/search.ts +3 -0
  208. package/src/routes/api/settings.ts +33 -0
  209. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  210. package/src/routes/compose.tsx +2 -1
  211. package/src/routes/dash/settings.tsx +45 -2
  212. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  213. package/src/routes/feed/feed.ts +23 -14
  214. package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
  215. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  216. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  217. package/src/routes/pages/__tests__/preview.test.ts +174 -0
  218. package/src/routes/pages/__tests__/search.test.ts +69 -0
  219. package/src/routes/pages/archive.tsx +27 -7
  220. package/src/routes/pages/collection.tsx +32 -19
  221. package/src/routes/pages/collections.tsx +6 -1
  222. package/src/routes/pages/page.tsx +83 -11
  223. package/src/routes/site-skill.ts +18 -0
  224. package/src/services/__tests__/auth.test.ts +7 -5
  225. package/src/services/__tests__/collection.test.ts +159 -44
  226. package/src/services/__tests__/navigation.test.ts +229 -21
  227. package/src/services/__tests__/post-timeline.test.ts +109 -30
  228. package/src/services/__tests__/post.test.ts +567 -5
  229. package/src/services/__tests__/search.test.ts +48 -1
  230. package/src/services/__tests__/settings.test.ts +8 -0
  231. package/src/services/__tests__/site-admin.test.ts +121 -1
  232. package/src/services/auth.ts +2 -2
  233. package/src/services/bootstrap.ts +3 -2
  234. package/src/services/collection.ts +226 -167
  235. package/src/services/export-theme/assets/client-site.css +1 -1
  236. package/src/services/export-theme/assets/client-site.js +46 -25
  237. package/src/services/export-theme/layouts/_default/list.html +18 -4
  238. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  239. package/src/services/export-theme/layouts/featured/list.html +5 -4
  240. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  241. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  242. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  243. package/src/services/export-theme/styles/main.css +30 -2
  244. package/src/services/export.ts +91 -80
  245. package/src/services/mcp.ts +37 -27
  246. package/src/services/media.ts +1 -1
  247. package/src/services/navigation.ts +185 -46
  248. package/src/services/post.ts +874 -271
  249. package/src/services/search.ts +8 -2
  250. package/src/services/settings.ts +2 -10
  251. package/src/services/site-admin.ts +63 -43
  252. package/src/style-cjk-jp.css +1 -1
  253. package/src/style-cjk-kr.css +1 -1
  254. package/src/style-cjk-tc.css +1 -1
  255. package/src/style-cjk.css +1 -1
  256. package/src/styles/components.css +540 -0
  257. package/src/styles/site-media.css +111 -19
  258. package/src/styles/tokens.css +14 -9
  259. package/src/styles/ui.css +829 -29
  260. package/src/types/bindings.ts +3 -0
  261. package/src/types/config.ts +243 -12
  262. package/src/types/constants.ts +22 -1
  263. package/src/types/entities.ts +9 -8
  264. package/src/types/operations.ts +18 -10
  265. package/src/types/props.ts +8 -2
  266. package/src/types/raw-assets.d.ts +5 -0
  267. package/src/types/views.ts +16 -4
  268. package/src/ui/__tests__/font-themes.test.ts +42 -30
  269. package/src/ui/compose/ComposeDialog.tsx +92 -2
  270. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  271. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  272. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  273. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  274. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  275. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  276. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  277. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  278. package/src/ui/feed/LinkCard.tsx +2 -12
  279. package/src/ui/feed/NoteCard.tsx +10 -20
  280. package/src/ui/feed/QuoteCard.tsx +2 -12
  281. package/src/ui/feed/ThreadPreview.tsx +26 -34
  282. package/src/ui/feed/TimelineFeed.tsx +2 -2
  283. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  284. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  285. package/src/ui/feed/thread-preview-state.ts +5 -7
  286. package/src/ui/font-themes.ts +74 -26
  287. package/src/ui/layouts/BaseLayout.tsx +22 -8
  288. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  289. package/src/ui/pages/CollectionPage.tsx +101 -22
  290. package/src/ui/pages/CollectionsPage.tsx +3 -1
  291. package/src/ui/pages/PostPage.tsx +39 -12
  292. package/src/ui/pages/SearchPage.tsx +6 -14
  293. package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
  294. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  295. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  296. package/src/ui/shared/CollectionsManager.tsx +17 -7
  297. package/src/ui/shared/DraftPreviewBar.tsx +33 -0
  298. package/src/ui/shared/HomePageBranding.tsx +2 -2
  299. package/src/ui/shared/MediaGallery.tsx +1 -0
  300. package/src/ui/shared/PostFooter.tsx +3 -1
  301. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  302. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  303. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  304. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  305. package/src/ui/shared/collection-management-labels.ts +33 -2
  306. package/src/ui/shared/navigation-labels.ts +2 -4
  307. package/src/ui/shared/post-article-attributes.ts +46 -0
  308. package/dist/app-DZDlmh7C.js +0 -6
  309. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  310. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  311. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
@@ -0,0 +1,326 @@
1
+ const LEGACY_TABLE = "post_collection";
2
+ const TARGET_TABLE = "thread_collection";
3
+
4
+ function toCount(value, label) {
5
+ const count = Number(value ?? 0);
6
+ if (!Number.isSafeInteger(count) || count < 0) {
7
+ throw new Error(
8
+ `Thread Collection migration check returned an invalid ${label}: ${String(value)}.`,
9
+ );
10
+ }
11
+ return count;
12
+ }
13
+
14
+ function toBoolean(value) {
15
+ return value === true || value === 1 || value === "1" || value === "true";
16
+ }
17
+
18
+ async function readTableState(query, dialect) {
19
+ const rows = await query(
20
+ dialect === "pg"
21
+ ? `
22
+ SELECT
23
+ to_regclass('public.${LEGACY_TABLE}') IS NOT NULL AS legacy_exists,
24
+ to_regclass('public.${TARGET_TABLE}') IS NOT NULL AS target_exists
25
+ `
26
+ : `
27
+ SELECT
28
+ EXISTS(
29
+ SELECT 1 FROM sqlite_master
30
+ WHERE type = 'table' AND name = '${LEGACY_TABLE}'
31
+ ) AS legacy_exists,
32
+ EXISTS(
33
+ SELECT 1 FROM sqlite_master
34
+ WHERE type = 'table' AND name = '${TARGET_TABLE}'
35
+ ) AS target_exists
36
+ `,
37
+ );
38
+ const row = rows[0] ?? {};
39
+ return {
40
+ legacyExists: toBoolean(row.legacy_exists),
41
+ targetExists: toBoolean(row.target_exists),
42
+ };
43
+ }
44
+
45
+ async function readSqlitePostColumns(query) {
46
+ const rows = await query(`PRAGMA table_info("post")`);
47
+ return new Set(rows.map((row) => String(row.name)));
48
+ }
49
+
50
+ async function readPostgresPostColumns(query) {
51
+ const rows = await query(`
52
+ SELECT column_name
53
+ FROM information_schema.columns
54
+ WHERE table_schema = 'public' AND table_name = 'post'
55
+ `);
56
+ return new Set(rows.map((row) => String(row.column_name)));
57
+ }
58
+
59
+ async function readPostColumns(query, dialect) {
60
+ return dialect === "pg"
61
+ ? readPostgresPostColumns(query)
62
+ : readSqlitePostColumns(query);
63
+ }
64
+
65
+ function hasThreadColumns(columns) {
66
+ return (
67
+ columns.has("id") &&
68
+ columns.has("site_id") &&
69
+ columns.has("reply_to_id") &&
70
+ columns.has("thread_id")
71
+ );
72
+ }
73
+
74
+ async function readLegacyStats(query, postColumns) {
75
+ const retainedPostPredicate = postColumns.has("deleted_at")
76
+ ? "post.deleted_at IS NULL"
77
+ : "1 = 1";
78
+ const softDeletedSourceExpression = postColumns.has("deleted_at")
79
+ ? "CASE WHEN post.deleted_at IS NOT NULL THEN 1 ELSE 0 END"
80
+ : "0";
81
+ const rows = await query(`
82
+ SELECT
83
+ COUNT(*) AS source_count,
84
+ COALESCE(SUM(${softDeletedSourceExpression}), 0)
85
+ AS soft_deleted_source_count,
86
+ COALESCE(SUM(CASE WHEN site.id IS NULL THEN 1 ELSE 0 END), 0)
87
+ AS missing_site_count,
88
+ COALESCE(SUM(CASE WHEN post.id IS NULL THEN 1 ELSE 0 END), 0)
89
+ AS missing_post_count,
90
+ COALESCE(SUM(CASE WHEN collection.id IS NULL THEN 1 ELSE 0 END), 0)
91
+ AS missing_collection_count,
92
+ COALESCE(SUM(
93
+ CASE
94
+ WHEN post.id IS NOT NULL AND (
95
+ root.id IS NULL
96
+ OR root.reply_to_id IS NOT NULL
97
+ OR root.thread_id <> root.id
98
+ ) THEN 1
99
+ ELSE 0
100
+ END
101
+ ), 0) AS invalid_thread_count
102
+ FROM post_collection AS membership
103
+ LEFT JOIN site
104
+ ON site.id = membership.site_id
105
+ LEFT JOIN post
106
+ ON post.site_id = membership.site_id
107
+ AND post.id = membership.post_id
108
+ LEFT JOIN post AS root
109
+ ON root.site_id = post.site_id
110
+ AND root.id = post.thread_id
111
+ LEFT JOIN collection
112
+ ON collection.site_id = membership.site_id
113
+ AND collection.id = membership.collection_id
114
+ `);
115
+ const row = rows[0] ?? {};
116
+ const expectedRows = await query(`
117
+ SELECT COUNT(*) AS expected_count
118
+ FROM (
119
+ SELECT
120
+ membership.site_id,
121
+ post.thread_id,
122
+ membership.collection_id
123
+ FROM post_collection AS membership
124
+ INNER JOIN post
125
+ ON post.site_id = membership.site_id
126
+ AND post.id = membership.post_id
127
+ WHERE ${retainedPostPredicate}
128
+ GROUP BY
129
+ membership.site_id,
130
+ post.thread_id,
131
+ membership.collection_id
132
+ ) AS thread_membership
133
+ `);
134
+
135
+ return {
136
+ sourceCount: toCount(row.source_count, "legacy row count"),
137
+ softDeletedSourceCount: toCount(
138
+ row.soft_deleted_source_count,
139
+ "soft-deleted legacy row count",
140
+ ),
141
+ expectedCount: toCount(
142
+ expectedRows[0]?.expected_count,
143
+ "expected Thread membership count",
144
+ ),
145
+ missingSiteCount: toCount(row.missing_site_count, "missing site count"),
146
+ missingPostCount: toCount(row.missing_post_count, "missing post count"),
147
+ missingCollectionCount: toCount(
148
+ row.missing_collection_count,
149
+ "missing Collection count",
150
+ ),
151
+ invalidThreadCount: toCount(
152
+ row.invalid_thread_count,
153
+ "invalid Thread root count",
154
+ ),
155
+ };
156
+ }
157
+
158
+ async function readTargetStats(query) {
159
+ const rows = await query(`
160
+ SELECT
161
+ COUNT(*) AS target_count,
162
+ COALESCE(SUM(CASE WHEN site.id IS NULL THEN 1 ELSE 0 END), 0)
163
+ AS missing_site_count,
164
+ COALESCE(SUM(CASE WHEN root.id IS NULL THEN 1 ELSE 0 END), 0)
165
+ AS missing_thread_count,
166
+ COALESCE(SUM(CASE WHEN collection.id IS NULL THEN 1 ELSE 0 END), 0)
167
+ AS missing_collection_count,
168
+ COALESCE(SUM(
169
+ CASE
170
+ WHEN root.id IS NOT NULL AND (
171
+ root.reply_to_id IS NOT NULL
172
+ OR root.thread_id <> root.id
173
+ ) THEN 1
174
+ ELSE 0
175
+ END
176
+ ), 0) AS invalid_thread_count
177
+ FROM thread_collection AS membership
178
+ LEFT JOIN site
179
+ ON site.id = membership.site_id
180
+ LEFT JOIN post AS root
181
+ ON root.site_id = membership.site_id
182
+ AND root.id = membership.thread_id
183
+ LEFT JOIN collection
184
+ ON collection.site_id = membership.site_id
185
+ AND collection.id = membership.collection_id
186
+ `);
187
+ const row = rows[0] ?? {};
188
+ return {
189
+ targetCount: toCount(row.target_count, "migrated row count"),
190
+ missingSiteCount: toCount(row.missing_site_count, "missing site count"),
191
+ missingThreadCount: toCount(
192
+ row.missing_thread_count,
193
+ "missing Thread count",
194
+ ),
195
+ missingCollectionCount: toCount(
196
+ row.missing_collection_count,
197
+ "missing Collection count",
198
+ ),
199
+ invalidThreadCount: toCount(
200
+ row.invalid_thread_count,
201
+ "invalid Thread root count",
202
+ ),
203
+ };
204
+ }
205
+
206
+ function formatInvalidLegacyStats(stats) {
207
+ return [
208
+ `missing sites=${stats.missingSiteCount}`,
209
+ `missing posts=${stats.missingPostCount}`,
210
+ `missing Collections=${stats.missingCollectionCount}`,
211
+ `invalid Thread roots=${stats.invalidThreadCount}`,
212
+ ].join(", ");
213
+ }
214
+
215
+ function assertLegacyStats(stats) {
216
+ if (
217
+ stats.missingSiteCount > 0 ||
218
+ stats.missingPostCount > 0 ||
219
+ stats.missingCollectionCount > 0 ||
220
+ stats.invalidThreadCount > 0
221
+ ) {
222
+ throw new Error(
223
+ `Thread Collection migration preflight failed (${formatInvalidLegacyStats(stats)}). No migration was applied. Repair or restore the legacy data, then run jant migrate again.`,
224
+ );
225
+ }
226
+ }
227
+
228
+ function assertTargetStats(stats, expectedCount) {
229
+ const invalidReferences =
230
+ stats.missingSiteCount > 0 ||
231
+ stats.missingThreadCount > 0 ||
232
+ stats.missingCollectionCount > 0 ||
233
+ stats.invalidThreadCount > 0;
234
+ const countMismatch =
235
+ expectedCount !== undefined && stats.targetCount !== expectedCount;
236
+
237
+ if (invalidReferences || countMismatch) {
238
+ const expected =
239
+ expectedCount === undefined ? "not recorded" : String(expectedCount);
240
+ throw new Error(
241
+ `Thread Collection post-migration verification failed (expected rows=${expected}, actual rows=${stats.targetCount}, missing sites=${stats.missingSiteCount}, missing Threads=${stats.missingThreadCount}, missing Collections=${stats.missingCollectionCount}, invalid Thread roots=${stats.invalidThreadCount}). The schema migration may already be committed; keep writes stopped and restore or inspect the database before retrying.`,
242
+ );
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Validate legacy Collection membership before the destructive table cutover.
248
+ *
249
+ * @param {{ dialect: "pg" | "sqlite", log?: (message: string) => void, query: (sql: string) => unknown[] | Promise<unknown[]> }} options
250
+ * @returns {Promise<{ expectedCount?: number, phase: "already-migrated" | "deferred" | "fresh" | "legacy", sourceCount?: number }>}
251
+ */
252
+ export async function preflightThreadCollectionMigration(options) {
253
+ const log = options.log ?? console.log;
254
+ const state = await readTableState(options.query, options.dialect);
255
+
256
+ if (state.legacyExists && state.targetExists) {
257
+ throw new Error(
258
+ "Thread Collection migration preflight found both post_collection and thread_collection. No migration was applied. Inspect the migration journal and database state before retrying.",
259
+ );
260
+ }
261
+
262
+ if (state.targetExists) {
263
+ const stats = await readTargetStats(options.query);
264
+ assertTargetStats(stats, undefined);
265
+ log(
266
+ `Thread Collection migration already applied (${stats.targetCount} memberships verified).`,
267
+ );
268
+ return { phase: "already-migrated" };
269
+ }
270
+
271
+ if (!state.legacyExists) {
272
+ return { phase: "fresh" };
273
+ }
274
+
275
+ const postColumns = await readPostColumns(options.query, options.dialect);
276
+ if (!hasThreadColumns(postColumns)) {
277
+ log(
278
+ "Thread Collection migration preflight deferred until earlier schema migrations add Thread columns.",
279
+ );
280
+ return { phase: "deferred" };
281
+ }
282
+
283
+ const stats = await readLegacyStats(options.query, postColumns);
284
+ assertLegacyStats(stats);
285
+ const softDeletedMembershipLabel =
286
+ stats.softDeletedSourceCount === 1 ? "membership" : "memberships";
287
+ const anticipatedDeletion =
288
+ stats.softDeletedSourceCount > 0
289
+ ? `; ${stats.softDeletedSourceCount} soft-deleted post ${softDeletedMembershipLabel} will be removed by an earlier migration`
290
+ : "";
291
+ log(
292
+ `Thread Collection migration preflight passed (${stats.sourceCount} post memberships -> ${stats.expectedCount} Thread memberships${anticipatedDeletion}).`,
293
+ );
294
+ log(
295
+ "Thread Collection cutover requires writes to remain stopped until migration, application deployment, and verification finish.",
296
+ );
297
+ return {
298
+ phase: "legacy",
299
+ expectedCount: stats.expectedCount,
300
+ sourceCount: stats.sourceCount,
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Verify the replacement table after the formal schema migrator returns.
306
+ *
307
+ * @param {{ dialect: "pg" | "sqlite", log?: (message: string) => void, query: (sql: string) => unknown[] | Promise<unknown[]> }} options
308
+ * @param {{ expectedCount?: number, phase: "already-migrated" | "deferred" | "fresh" | "legacy", sourceCount?: number }} preflight
309
+ * @returns {Promise<void>}
310
+ */
311
+ export async function verifyThreadCollectionMigration(options, preflight) {
312
+ const log = options.log ?? console.log;
313
+ const state = await readTableState(options.query, options.dialect);
314
+
315
+ if (state.legacyExists || !state.targetExists) {
316
+ throw new Error(
317
+ `Thread Collection post-migration verification failed (post_collection exists=${state.legacyExists}, thread_collection exists=${state.targetExists}). The schema migration may already be partially applied; keep writes stopped and inspect the database before retrying.`,
318
+ );
319
+ }
320
+
321
+ const stats = await readTargetStats(options.query);
322
+ assertTargetStats(stats, preflight.expectedCount);
323
+ log(
324
+ `Thread Collection post-migration verification passed (${stats.targetCount} memberships).`,
325
+ );
326
+ }
@@ -0,0 +1,6 @@
1
+ import "./url-BMYO-Zlt.js";
2
+ import { t as createApp } from "./app-C-oi_t8W.js";
3
+ import "./export-CsFx6F_M.js";
4
+ import "./env-OHRKGcMj.js";
5
+ import "./github-sync-Cq1pzzOI.js";
6
+ export { createApp };