@gmickel/gno 1.31.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +5 -4
  2. package/assets/skill/SKILL.md +23 -0
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.31.0.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +1 -1
  7. package/spec/cli.md +19 -0
  8. package/spec/db/schema.sql +55 -0
  9. package/spec/mcp.md +114 -11
  10. package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
  11. package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
  12. package/src/core/document-capabilities.ts +13 -0
  13. package/src/core/file-ops.ts +129 -1
  14. package/src/core/file-refactor-adapter.ts +329 -0
  15. package/src/core/file-refactor-apply-edits.ts +61 -0
  16. package/src/core/file-refactor-apply-fs.ts +512 -0
  17. package/src/core/file-refactor-apply-safety.ts +340 -0
  18. package/src/core/file-refactor-apply-validate.ts +401 -0
  19. package/src/core/file-refactor-contract.ts +486 -0
  20. package/src/core/file-refactor-destination.ts +123 -0
  21. package/src/core/file-refactor-from-snapshot.ts +148 -0
  22. package/src/core/file-refactor-journal-port.ts +150 -0
  23. package/src/core/file-refactor-journal.ts +347 -0
  24. package/src/core/file-refactor-paths.ts +60 -0
  25. package/src/core/file-refactor-plan-classify.ts +208 -0
  26. package/src/core/file-refactor-plan-validate.ts +169 -0
  27. package/src/core/file-refactor-planner-types.ts +62 -0
  28. package/src/core/file-refactor-planner.ts +423 -0
  29. package/src/core/file-refactor-resolve.ts +280 -0
  30. package/src/core/file-refactor-service.ts +468 -0
  31. package/src/core/file-refactors.ts +84 -56
  32. package/src/core/link-destination-parse.ts +275 -0
  33. package/src/core/link-inventory-markdown.ts +454 -0
  34. package/src/core/link-inventory-opaque.ts +244 -0
  35. package/src/core/link-inventory-types.ts +47 -0
  36. package/src/core/link-inventory.ts +182 -0
  37. package/src/core/link-relevance.ts +150 -0
  38. package/src/mcp/tools/index.ts +18 -17
  39. package/src/mcp/tools/workspace-write.ts +215 -97
  40. package/src/sdk/client.ts +167 -115
  41. package/src/sdk/index.ts +7 -0
  42. package/src/sdk/types.ts +32 -2
  43. package/src/serve/file-refactor-http.ts +239 -0
  44. package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
  45. package/src/serve/public/globals.built.css +1 -1
  46. package/src/serve/public/pages/DocView.tsx +176 -41
  47. package/src/serve/routes/api.ts +191 -104
  48. package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
  49. package/src/store/migrations/index.ts +2 -0
  50. package/src/store/sqlite/adapter.ts +452 -0
  51. package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
  52. package/src/store/types.ts +84 -0
  53. package/browser-extension/artifacts/gno-browser-clipper-v1.31.0.zip.sha256 +0 -1
@@ -71,16 +71,14 @@ import {
71
71
  atomicWrite,
72
72
  copyFilePath,
73
73
  createFolderPath,
74
- renameFilePath,
75
74
  revealFilePath,
76
75
  trashFilePath,
77
76
  } from "../../core/file-ops";
78
77
  import {
78
+ assertFileRefactorSyncConverged,
79
79
  buildRefactorWarnings,
80
80
  planCreateFolder,
81
81
  planDuplicateRefactor,
82
- planMoveRefactor,
83
- planRenameRefactor,
84
82
  } from "../../core/file-refactors";
85
83
  import {
86
84
  hasContentMutation,
@@ -166,6 +164,17 @@ import {
166
164
  resetDownloadState,
167
165
  type ServerContext,
168
166
  } from "../context";
167
+ import {
168
+ applyCanonicalFileRefactor,
169
+ buildCanonicalRefactorPlan,
170
+ buildFileRefactorApplyDeps,
171
+ mapApplyResultToHttpSuccess,
172
+ parseRefactorApplyConfirmation,
173
+ resolveMoveTarget,
174
+ resolveRenameTarget,
175
+ toRefactorPlanResponse,
176
+ type FileRefactorHttpError,
177
+ } from "../file-refactor-http";
169
178
  import { analyzeImportPath } from "../import-preview";
170
179
  import { getActiveJob, getJobStatus, startJob } from "../jobs";
171
180
  import {
@@ -465,12 +474,22 @@ export interface CreateCaptureRequestBody extends PublicCaptureInput {}
465
474
  export interface RenameDocRequestBody {
466
475
  name: string;
467
476
  uri?: string;
477
+ /** Exact plan digest from preview. */
478
+ planDigest: string;
479
+ /** Exact destructive confirmation token. */
480
+ confirmation: string;
481
+ schemaVersion: string;
468
482
  }
469
483
 
470
484
  export interface MoveDocRequestBody {
471
485
  folderPath: string;
472
486
  name?: string;
473
487
  uri?: string;
488
+ /** Exact plan digest from preview. */
489
+ planDigest: string;
490
+ /** Exact destructive confirmation token. */
491
+ confirmation: string;
492
+ schemaVersion: string;
474
493
  }
475
494
 
476
495
  export interface DuplicateDocRequestBody {
@@ -546,6 +565,10 @@ function errorResponse(
546
565
  );
547
566
  }
548
567
 
568
+ function fileRefactorHttpErrorResponse(error: FileRefactorHttpError): Response {
569
+ return errorResponse(error.code, error.message, error.status, error.details);
570
+ }
571
+
549
572
  interface RestTraceStart {
550
573
  session: RetrievalTraceSession | null;
551
574
  error: Response | null;
@@ -2388,7 +2411,6 @@ export async function handleRenameDoc(
2388
2411
  docId: string,
2389
2412
  req: Request,
2390
2413
  deps?: {
2391
- renameFilePath?: typeof renameFilePath;
2392
2414
  syncCollection?: typeof defaultSyncService.syncCollection;
2393
2415
  }
2394
2416
  ): Promise<Response> {
@@ -2409,6 +2431,11 @@ export async function handleRenameDoc(
2409
2431
  return errorResponse("VALIDATION", "uri must be a string");
2410
2432
  }
2411
2433
 
2434
+ const confirmation = parseRefactorApplyConfirmation(body);
2435
+ if ("code" in confirmation) {
2436
+ return fileRefactorHttpErrorResponse(confirmation);
2437
+ }
2438
+
2412
2439
  const docResult = await resolveDocumentReference(store, docId, body.uri);
2413
2440
  if (!docResult.ok) {
2414
2441
  return errorResponse("RUNTIME", docResult.error.message, 500);
@@ -2444,71 +2471,85 @@ export async function handleRenameDoc(
2444
2471
  return errorResponse("FILE_NOT_FOUND", "Source file no longer exists", 404);
2445
2472
  }
2446
2473
 
2447
- const nodePath = await import("node:path"); // no bun equivalent
2448
- const directory = nodePath.dirname(doc.relPath);
2449
- const currentExt = nodePath.extname(doc.relPath);
2450
- const targetName = nodePath.extname(body.name)
2451
- ? body.name
2452
- : `${body.name}${currentExt}`;
2453
- const nextRelPath =
2454
- directory === "." ? targetName : `${directory}/${targetName}`;
2455
- const nextFullPath = nodePath.join(collection.path, nextRelPath);
2456
-
2457
- if (nextFullPath === fullPath) {
2458
- return errorResponse("VALIDATION", "New name matches current file");
2459
- }
2460
- if (await Bun.file(nextFullPath).exists()) {
2474
+ let target;
2475
+ try {
2476
+ target = resolveRenameTarget({
2477
+ collection: collection.name,
2478
+ currentRelPath: doc.relPath,
2479
+ nextName: body.name,
2480
+ });
2481
+ } catch (error) {
2461
2482
  return errorResponse(
2462
- "CONFLICT",
2463
- "A file with that name already exists",
2464
- 409
2483
+ "VALIDATION",
2484
+ error instanceof Error ? error.message : "Invalid rename target"
2465
2485
  );
2466
2486
  }
2467
2487
 
2488
+ if (target.nextRelPath === doc.relPath) {
2489
+ return errorResponse("VALIDATION", "New name matches current file");
2490
+ }
2491
+
2492
+ const nodePath = await import("node:path"); // no bun equivalent
2493
+ const nextFullPath = nodePath.join(collection.path, target.nextRelPath);
2494
+
2468
2495
  try {
2496
+ const plan = await buildCanonicalRefactorPlan({
2497
+ operation: "rename",
2498
+ doc,
2499
+ collection,
2500
+ sourceFullPath: fullPath,
2501
+ target,
2502
+ store,
2503
+ sourceEditable: capabilities.editable,
2504
+ });
2505
+
2469
2506
  const syncCollection =
2470
2507
  deps?.syncCollection ??
2471
2508
  ((collectionArg, storeArg, optionsArg) =>
2472
2509
  defaultSyncService.syncCollection(collectionArg, storeArg, optionsArg));
2510
+
2473
2511
  ctxHolder.watchService?.suppress(fullPath);
2474
2512
  ctxHolder.watchService?.suppress(nextFullPath);
2475
- await (deps?.renameFilePath ?? renameFilePath)(fullPath, nextFullPath);
2476
- const nextUri = `gno://${collection.name}/${nextRelPath}`;
2477
- let warning: string | undefined;
2478
- try {
2479
- await syncResidentCollection(
2480
- ctxHolder,
2513
+
2514
+ const applyResult = await applyCanonicalFileRefactor({
2515
+ plan,
2516
+ confirmation,
2517
+ deps: buildFileRefactorApplyDeps({
2481
2518
  collection,
2482
2519
  store,
2483
- withContentTypeRules({ runUpdateCmd: false }, ctxHolder.config),
2484
- syncCollection
2485
- );
2486
- } catch {
2487
- warning =
2488
- "File renamed on disk, but index refresh failed. Run Update All to reconcile the workspace.";
2520
+ syncAfterCommit: async () => {
2521
+ const syncResult = await syncResidentCollection(
2522
+ ctxHolder,
2523
+ collection,
2524
+ store,
2525
+ withContentTypeRules({ runUpdateCmd: false }, ctxHolder.config),
2526
+ syncCollection
2527
+ );
2528
+ assertFileRefactorSyncConverged(syncResult);
2529
+ },
2530
+ }),
2531
+ signal: req.signal,
2532
+ });
2533
+
2534
+ const mapped = mapApplyResultToHttpSuccess({
2535
+ plan,
2536
+ result: applyResult,
2537
+ targetFullPath: nextFullPath,
2538
+ operationLabel: "renamed",
2539
+ });
2540
+ if ("code" in mapped) {
2541
+ return fileRefactorHttpErrorResponse(mapped);
2489
2542
  }
2543
+
2490
2544
  ctxHolder.eventBus?.emit({
2491
2545
  type: "document-changed",
2492
- uri: nextUri,
2546
+ uri: mapped.uri,
2493
2547
  collection: collection.name,
2494
- relPath: nextRelPath,
2548
+ relPath: mapped.relPath,
2495
2549
  origin: "save",
2496
2550
  changedAt: new Date().toISOString(),
2497
2551
  });
2498
- const refactorWarnings = buildRefactorWarnings(
2499
- await getRefactorSnapshot(store, doc.id),
2500
- {
2501
- filenameChanged: true,
2502
- }
2503
- );
2504
- return jsonResponse({
2505
- success: true,
2506
- uri: nextUri,
2507
- path: nextFullPath,
2508
- relPath: nextRelPath,
2509
- refactorWarnings,
2510
- warning,
2511
- });
2552
+ return jsonResponse(mapped);
2512
2553
  } catch (error) {
2513
2554
  return errorResponse(
2514
2555
  "RUNTIME",
@@ -2550,7 +2591,6 @@ export async function handleRefactorPlan(
2550
2591
  );
2551
2592
  }
2552
2593
 
2553
- const snapshot = await getRefactorSnapshot(store, doc.id);
2554
2594
  const collection = getCollectionByName(
2555
2595
  ctxHolder.config.collections,
2556
2596
  doc.collection
@@ -2568,41 +2608,69 @@ export async function handleRefactorPlan(
2568
2608
  if (!body.name?.trim()) {
2569
2609
  return errorResponse("VALIDATION", "Missing or invalid name");
2570
2610
  }
2571
- const plan = planRenameRefactor({
2611
+ const resolvedDocPath = await resolveAbsoluteDocPath(
2612
+ ctxHolder.config.collections,
2613
+ doc
2614
+ );
2615
+ if (!resolvedDocPath) {
2616
+ return errorResponse(
2617
+ "NOT_FOUND",
2618
+ `Collection not found: ${doc.collection}`,
2619
+ 404
2620
+ );
2621
+ }
2622
+ const target = resolveRenameTarget({
2572
2623
  collection: collection.name,
2573
2624
  currentRelPath: doc.relPath,
2574
2625
  nextName: body.name.trim(),
2575
2626
  });
2576
- return jsonResponse({
2577
- operation: body.operation,
2578
- ...plan,
2579
- refactorWarnings: buildRefactorWarnings(snapshot, {
2580
- filenameChanged: true,
2581
- }),
2627
+ const plan = await buildCanonicalRefactorPlan({
2628
+ operation: "rename",
2629
+ doc,
2630
+ collection,
2631
+ sourceFullPath: resolvedDocPath.fullPath,
2632
+ target,
2633
+ store,
2634
+ sourceEditable: capabilities.editable,
2582
2635
  });
2636
+ return jsonResponse(toRefactorPlanResponse(plan));
2583
2637
  }
2584
2638
 
2585
2639
  if (body.operation === "move") {
2586
2640
  if (!body.folderPath?.trim()) {
2587
2641
  return errorResponse("VALIDATION", "Missing or invalid folderPath");
2588
2642
  }
2589
- const plan = planMoveRefactor({
2643
+ const resolvedDocPath = await resolveAbsoluteDocPath(
2644
+ ctxHolder.config.collections,
2645
+ doc
2646
+ );
2647
+ if (!resolvedDocPath) {
2648
+ return errorResponse(
2649
+ "NOT_FOUND",
2650
+ `Collection not found: ${doc.collection}`,
2651
+ 404
2652
+ );
2653
+ }
2654
+ const target = resolveMoveTarget({
2590
2655
  collection: collection.name,
2591
2656
  currentRelPath: doc.relPath,
2592
2657
  folderPath: body.folderPath.trim(),
2593
2658
  nextName: body.name?.trim(),
2594
2659
  });
2595
- return jsonResponse({
2596
- operation: body.operation,
2597
- ...plan,
2598
- refactorWarnings: buildRefactorWarnings(snapshot, {
2599
- folderChanged: true,
2600
- filenameChanged: Boolean(body.name?.trim()),
2601
- }),
2660
+ const plan = await buildCanonicalRefactorPlan({
2661
+ operation: "move",
2662
+ doc,
2663
+ collection,
2664
+ sourceFullPath: resolvedDocPath.fullPath,
2665
+ target,
2666
+ store,
2667
+ sourceEditable: capabilities.editable,
2602
2668
  });
2669
+ return jsonResponse(toRefactorPlanResponse(plan));
2603
2670
  }
2604
2671
 
2605
2672
  if (body.operation === "duplicate") {
2673
+ const snapshot = await getRefactorSnapshot(store, doc.id);
2606
2674
  const plan = planDuplicateRefactor({
2607
2675
  collection: collection.name,
2608
2676
  currentRelPath: doc.relPath,
@@ -2731,7 +2799,10 @@ export async function handleMoveDoc(
2731
2799
  ctxHolder: ContextHolder,
2732
2800
  store: SqliteAdapter,
2733
2801
  docId: string,
2734
- req: Request
2802
+ req: Request,
2803
+ deps?: {
2804
+ syncCollection?: typeof defaultSyncService.syncCollection;
2805
+ }
2735
2806
  ): Promise<Response> {
2736
2807
  let body: MoveDocRequestBody;
2737
2808
  try {
@@ -2750,6 +2821,11 @@ export async function handleMoveDoc(
2750
2821
  return errorResponse("VALIDATION", "uri must be a string");
2751
2822
  }
2752
2823
 
2824
+ const confirmation = parseRefactorApplyConfirmation(body);
2825
+ if ("code" in confirmation) {
2826
+ return fileRefactorHttpErrorResponse(confirmation);
2827
+ }
2828
+
2753
2829
  const docResult = await resolveDocumentReference(store, docId, body.uri);
2754
2830
  if (!docResult.ok) {
2755
2831
  return errorResponse("RUNTIME", docResult.error.message, 500);
@@ -2780,9 +2856,9 @@ export async function handleMoveDoc(
2780
2856
  }
2781
2857
  const { collection, fullPath } = resolvedDocPath;
2782
2858
 
2783
- let plan;
2859
+ let target;
2784
2860
  try {
2785
- plan = planMoveRefactor({
2861
+ target = resolveMoveTarget({
2786
2862
  collection: collection.name,
2787
2863
  currentRelPath: doc.relPath,
2788
2864
  folderPath: body.folderPath,
@@ -2796,55 +2872,66 @@ export async function handleMoveDoc(
2796
2872
  }
2797
2873
 
2798
2874
  const nodePath = await import("node:path"); // no bun equivalent
2799
- const nextFullPath = nodePath.join(collection.path, plan.nextRelPath);
2800
- if (await Bun.file(nextFullPath).exists()) {
2801
- return errorResponse(
2802
- "CONFLICT",
2803
- "A file with that name already exists at the destination",
2804
- 409
2805
- );
2806
- }
2875
+ const nextFullPath = nodePath.join(collection.path, target.nextRelPath);
2807
2876
 
2808
2877
  try {
2878
+ const plan = await buildCanonicalRefactorPlan({
2879
+ operation: "move",
2880
+ doc,
2881
+ collection,
2882
+ sourceFullPath: fullPath,
2883
+ target,
2884
+ store,
2885
+ sourceEditable: capabilities.editable,
2886
+ });
2887
+
2888
+ const syncCollection =
2889
+ deps?.syncCollection ??
2890
+ ((collectionArg, storeArg, optionsArg) =>
2891
+ defaultSyncService.syncCollection(collectionArg, storeArg, optionsArg));
2892
+
2809
2893
  ctxHolder.watchService?.suppress(fullPath);
2810
2894
  ctxHolder.watchService?.suppress(nextFullPath);
2811
- const { mkdir } = await import("node:fs/promises"); // structure ops need fs
2812
- await mkdir(nodePath.dirname(nextFullPath), { recursive: true });
2813
- await renameFilePath(fullPath, nextFullPath);
2814
- let warning: string | undefined;
2815
- try {
2816
- await syncResidentCollection(
2817
- ctxHolder,
2895
+
2896
+ const applyResult = await applyCanonicalFileRefactor({
2897
+ plan,
2898
+ confirmation,
2899
+ deps: buildFileRefactorApplyDeps({
2818
2900
  collection,
2819
2901
  store,
2820
- withContentTypeRules({ runUpdateCmd: false }, ctxHolder.config)
2821
- );
2822
- } catch {
2823
- warning =
2824
- "File moved on disk, but index refresh failed. Run Update All to reconcile the workspace.";
2902
+ syncAfterCommit: async () => {
2903
+ const syncResult = await syncResidentCollection(
2904
+ ctxHolder,
2905
+ collection,
2906
+ store,
2907
+ withContentTypeRules({ runUpdateCmd: false }, ctxHolder.config),
2908
+ syncCollection
2909
+ );
2910
+ assertFileRefactorSyncConverged(syncResult);
2911
+ },
2912
+ }),
2913
+ signal: req.signal,
2914
+ });
2915
+
2916
+ const mapped = mapApplyResultToHttpSuccess({
2917
+ plan,
2918
+ result: applyResult,
2919
+ targetFullPath: nextFullPath,
2920
+ operationLabel: "moved",
2921
+ });
2922
+ if ("code" in mapped) {
2923
+ return fileRefactorHttpErrorResponse(mapped);
2825
2924
  }
2925
+
2826
2926
  ctxHolder.eventBus?.emit({
2827
2927
  type: "document-changed",
2828
- uri: plan.nextUri,
2928
+ uri: mapped.uri,
2829
2929
  collection: collection.name,
2830
- relPath: plan.nextRelPath,
2930
+ relPath: mapped.relPath,
2831
2931
  origin: "save",
2832
2932
  changedAt: new Date().toISOString(),
2833
2933
  });
2834
- return jsonResponse({
2835
- success: true,
2836
- uri: plan.nextUri,
2837
- path: nextFullPath,
2838
- relPath: plan.nextRelPath,
2839
- refactorWarnings: buildRefactorWarnings(
2840
- await getRefactorSnapshot(store, doc.id),
2841
- {
2842
- folderChanged: true,
2843
- filenameChanged: Boolean(body.name?.trim()),
2844
- }
2845
- ),
2846
- warning,
2847
- });
2934
+ return jsonResponse(mapped);
2848
2935
  } catch (error) {
2849
2936
  return errorResponse(
2850
2937
  "RUNTIME",
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Migration: bounded, metadata-only file-refactor recovery journal.
3
+ *
4
+ * Stores IDs, plan digests, collection/relpaths, phase/state, timestamps,
5
+ * and fingerprints/status only. Never stores note bodies or replacements.
6
+ * Create prunes oldest terminal receipts under FILE_REFACTOR_JOURNAL_MAX_RECEIPTS.
7
+ *
8
+ * @module src/store/migrations/026-file-refactor-recovery-journal
9
+ */
10
+
11
+ import type { Database } from "bun:sqlite";
12
+
13
+ import type { Migration } from "./runner";
14
+
15
+ export const migration: Migration = {
16
+ version: 26,
17
+ name: "file_refactor_recovery_journal",
18
+
19
+ up(db: Database): void {
20
+ db.exec(`
21
+ CREATE TABLE IF NOT EXISTS file_refactor_recovery_journal (
22
+ journal_id TEXT PRIMARY KEY,
23
+ plan_digest TEXT NOT NULL,
24
+ collection TEXT NOT NULL,
25
+ operation TEXT NOT NULL
26
+ CHECK (operation IN ('rename', 'move')),
27
+ source_rel_path TEXT NOT NULL,
28
+ target_rel_path TEXT NOT NULL,
29
+ phase TEXT NOT NULL
30
+ CHECK (phase IN (
31
+ 'prepared', 'staging', 'committing', 'committed', 'sync_pending',
32
+ 'converged', 'rolling_back', 'rolled_back', 'recovery_required',
33
+ 'aborted'
34
+ )),
35
+ phase_ordinal INTEGER NOT NULL CHECK (phase_ordinal >= 0),
36
+ filesystem_state TEXT NOT NULL
37
+ CHECK (filesystem_state IN (
38
+ 'unchanged', 'committed', 'rolled_back', 'recovery_required'
39
+ )),
40
+ index_state TEXT NOT NULL
41
+ CHECK (index_state IN (
42
+ 'not_attempted', 'pending', 'converged', 'skipped'
43
+ )),
44
+ file_entries_json TEXT NOT NULL
45
+ CHECK (length(CAST(file_entries_json AS BLOB)) <= 65536),
46
+ created_at_ms INTEGER NOT NULL CHECK (created_at_ms >= 0),
47
+ updated_at_ms INTEGER NOT NULL CHECK (updated_at_ms >= 0),
48
+ CHECK (length(CAST(journal_id AS BLOB)) BETWEEN 1 AND 128),
49
+ CHECK (
50
+ length(plan_digest) = 64
51
+ AND plan_digest NOT GLOB '*[^0-9a-f]*'
52
+ ),
53
+ CHECK (length(CAST(collection AS BLOB)) BETWEEN 1 AND 256),
54
+ CHECK (length(CAST(source_rel_path AS BLOB)) BETWEEN 1 AND 4096),
55
+ CHECK (length(CAST(target_rel_path AS BLOB)) BETWEEN 1 AND 4096),
56
+ CHECK (updated_at_ms >= created_at_ms)
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_file_refactor_journal_plan_digest
60
+ ON file_refactor_recovery_journal(plan_digest, updated_at_ms DESC, journal_id DESC);
61
+
62
+ CREATE INDEX IF NOT EXISTS idx_file_refactor_journal_collection
63
+ ON file_refactor_recovery_journal(collection, updated_at_ms DESC);
64
+ `);
65
+ },
66
+
67
+ down(db: Database): void {
68
+ db.exec("DROP INDEX IF EXISTS idx_file_refactor_journal_collection");
69
+ db.exec("DROP INDEX IF EXISTS idx_file_refactor_journal_plan_digest");
70
+ db.exec("DROP TABLE IF EXISTS file_refactor_recovery_journal");
71
+ },
72
+ };
@@ -39,6 +39,7 @@ import { migration as m022 } from "./022-record-export-lineage";
39
39
  import { migration as m023 } from "./023-collection-egress-policy";
40
40
  import { migration as m024 } from "./024-egress-derived-lineage";
41
41
  import { migration as m025 } from "./025-collection-egress-policy-revision";
42
+ import { migration as m026 } from "./026-file-refactor-recovery-journal";
42
43
 
43
44
  /** All migrations in order */
44
45
  export const migrations = [
@@ -67,4 +68,5 @@ export const migrations = [
67
68
  m023,
68
69
  m024,
69
70
  m025,
71
+ m026,
70
72
  ];