@gmickel/gno 1.31.0 → 1.33.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 (77) hide show
  1. package/README.md +15 -5
  2. package/assets/skill/SKILL.md +34 -0
  3. package/assets/skill/cli-reference.md +10 -2
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.31.0.zip → gno-browser-clipper-v1.33.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.33.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +5 -1
  8. package/spec/cli.md +35 -1
  9. package/spec/db/schema.sql +55 -0
  10. package/spec/mcp.md +114 -11
  11. package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
  12. package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
  13. package/spec/output-schemas/publish-artifact.schema.json +76 -1
  14. package/src/cli/commands/publish.ts +43 -7
  15. package/src/core/document-capabilities.ts +13 -0
  16. package/src/core/file-ops.ts +129 -1
  17. package/src/core/file-refactor-adapter.ts +329 -0
  18. package/src/core/file-refactor-apply-edits.ts +61 -0
  19. package/src/core/file-refactor-apply-fs.ts +512 -0
  20. package/src/core/file-refactor-apply-safety.ts +340 -0
  21. package/src/core/file-refactor-apply-validate.ts +401 -0
  22. package/src/core/file-refactor-contract.ts +486 -0
  23. package/src/core/file-refactor-destination.ts +123 -0
  24. package/src/core/file-refactor-from-snapshot.ts +148 -0
  25. package/src/core/file-refactor-journal-port.ts +150 -0
  26. package/src/core/file-refactor-journal.ts +347 -0
  27. package/src/core/file-refactor-paths.ts +60 -0
  28. package/src/core/file-refactor-plan-classify.ts +208 -0
  29. package/src/core/file-refactor-plan-validate.ts +169 -0
  30. package/src/core/file-refactor-planner-types.ts +62 -0
  31. package/src/core/file-refactor-planner.ts +423 -0
  32. package/src/core/file-refactor-resolve.ts +280 -0
  33. package/src/core/file-refactor-service.ts +468 -0
  34. package/src/core/file-refactors.ts +84 -56
  35. package/src/core/link-destination-parse.ts +275 -0
  36. package/src/core/link-inventory-markdown.ts +454 -0
  37. package/src/core/link-inventory-opaque.ts +244 -0
  38. package/src/core/link-inventory-types.ts +47 -0
  39. package/src/core/link-inventory.ts +182 -0
  40. package/src/core/link-relevance.ts +150 -0
  41. package/src/ingestion/strip.ts +152 -26
  42. package/src/mcp/tools/index.ts +18 -17
  43. package/src/mcp/tools/workspace-write.ts +215 -97
  44. package/src/publish/artifact-asset-codec.ts +75 -0
  45. package/src/publish/artifact-asset-contract.ts +152 -0
  46. package/src/publish/artifact-asset-parse.ts +401 -0
  47. package/src/publish/artifact-asset-sniff.ts +108 -0
  48. package/src/publish/artifact-asset-validate.ts +209 -0
  49. package/src/publish/artifact-assets.ts +58 -0
  50. package/src/publish/artifact-validation.ts +32 -6
  51. package/src/publish/artifact.ts +50 -3
  52. package/src/publish/attachment-bundle.ts +145 -0
  53. package/src/publish/attachment-discover.ts +203 -0
  54. package/src/publish/attachment-load.ts +133 -0
  55. package/src/publish/attachment-obsidian.ts +45 -0
  56. package/src/publish/attachment-path.ts +334 -0
  57. package/src/publish/attachment-raster.ts +852 -0
  58. package/src/publish/attachment-resolver.ts +280 -0
  59. package/src/publish/attachment-types.ts +54 -0
  60. package/src/publish/encrypted-export.ts +121 -44
  61. package/src/publish/export-attachments.ts +224 -0
  62. package/src/publish/export-service.ts +142 -80
  63. package/src/publish/obsidian-sanitize.ts +121 -13
  64. package/src/sdk/client.ts +167 -115
  65. package/src/sdk/index.ts +7 -0
  66. package/src/sdk/types.ts +32 -2
  67. package/src/serve/file-refactor-http.ts +239 -0
  68. package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
  69. package/src/serve/public/globals.built.css +1 -1
  70. package/src/serve/public/pages/DocView.tsx +176 -41
  71. package/src/serve/routes/api.ts +193 -105
  72. package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
  73. package/src/store/migrations/index.ts +2 -0
  74. package/src/store/sqlite/adapter.ts +452 -0
  75. package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
  76. package/src/store/types.ts +84 -0
  77. 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;
@@ -1257,7 +1280,7 @@ export async function handlePublishExport(
1257
1280
  }
1258
1281
 
1259
1282
  try {
1260
- const { artifact, warnings } = await exportPublishArtifact({
1283
+ const { artifact, assetSummary, warnings } = await exportPublishArtifact({
1261
1284
  collections: config.collections,
1262
1285
  options: {
1263
1286
  encryptionPassphrase: body.encryptionPassphrase,
@@ -1272,6 +1295,7 @@ export async function handlePublishExport(
1272
1295
 
1273
1296
  return jsonResponse({
1274
1297
  artifact,
1298
+ assetSummary,
1275
1299
  fileName: derivePublishArtifactFilename(artifact),
1276
1300
  uploadUrl: "https://gno.sh/studio",
1277
1301
  warnings,
@@ -2388,7 +2412,6 @@ export async function handleRenameDoc(
2388
2412
  docId: string,
2389
2413
  req: Request,
2390
2414
  deps?: {
2391
- renameFilePath?: typeof renameFilePath;
2392
2415
  syncCollection?: typeof defaultSyncService.syncCollection;
2393
2416
  }
2394
2417
  ): Promise<Response> {
@@ -2409,6 +2432,11 @@ export async function handleRenameDoc(
2409
2432
  return errorResponse("VALIDATION", "uri must be a string");
2410
2433
  }
2411
2434
 
2435
+ const confirmation = parseRefactorApplyConfirmation(body);
2436
+ if ("code" in confirmation) {
2437
+ return fileRefactorHttpErrorResponse(confirmation);
2438
+ }
2439
+
2412
2440
  const docResult = await resolveDocumentReference(store, docId, body.uri);
2413
2441
  if (!docResult.ok) {
2414
2442
  return errorResponse("RUNTIME", docResult.error.message, 500);
@@ -2444,71 +2472,85 @@ export async function handleRenameDoc(
2444
2472
  return errorResponse("FILE_NOT_FOUND", "Source file no longer exists", 404);
2445
2473
  }
2446
2474
 
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()) {
2475
+ let target;
2476
+ try {
2477
+ target = resolveRenameTarget({
2478
+ collection: collection.name,
2479
+ currentRelPath: doc.relPath,
2480
+ nextName: body.name,
2481
+ });
2482
+ } catch (error) {
2461
2483
  return errorResponse(
2462
- "CONFLICT",
2463
- "A file with that name already exists",
2464
- 409
2484
+ "VALIDATION",
2485
+ error instanceof Error ? error.message : "Invalid rename target"
2465
2486
  );
2466
2487
  }
2467
2488
 
2489
+ if (target.nextRelPath === doc.relPath) {
2490
+ return errorResponse("VALIDATION", "New name matches current file");
2491
+ }
2492
+
2493
+ const nodePath = await import("node:path"); // no bun equivalent
2494
+ const nextFullPath = nodePath.join(collection.path, target.nextRelPath);
2495
+
2468
2496
  try {
2497
+ const plan = await buildCanonicalRefactorPlan({
2498
+ operation: "rename",
2499
+ doc,
2500
+ collection,
2501
+ sourceFullPath: fullPath,
2502
+ target,
2503
+ store,
2504
+ sourceEditable: capabilities.editable,
2505
+ });
2506
+
2469
2507
  const syncCollection =
2470
2508
  deps?.syncCollection ??
2471
2509
  ((collectionArg, storeArg, optionsArg) =>
2472
2510
  defaultSyncService.syncCollection(collectionArg, storeArg, optionsArg));
2511
+
2473
2512
  ctxHolder.watchService?.suppress(fullPath);
2474
2513
  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,
2514
+
2515
+ const applyResult = await applyCanonicalFileRefactor({
2516
+ plan,
2517
+ confirmation,
2518
+ deps: buildFileRefactorApplyDeps({
2481
2519
  collection,
2482
2520
  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.";
2521
+ syncAfterCommit: async () => {
2522
+ const syncResult = await syncResidentCollection(
2523
+ ctxHolder,
2524
+ collection,
2525
+ store,
2526
+ withContentTypeRules({ runUpdateCmd: false }, ctxHolder.config),
2527
+ syncCollection
2528
+ );
2529
+ assertFileRefactorSyncConverged(syncResult);
2530
+ },
2531
+ }),
2532
+ signal: req.signal,
2533
+ });
2534
+
2535
+ const mapped = mapApplyResultToHttpSuccess({
2536
+ plan,
2537
+ result: applyResult,
2538
+ targetFullPath: nextFullPath,
2539
+ operationLabel: "renamed",
2540
+ });
2541
+ if ("code" in mapped) {
2542
+ return fileRefactorHttpErrorResponse(mapped);
2489
2543
  }
2544
+
2490
2545
  ctxHolder.eventBus?.emit({
2491
2546
  type: "document-changed",
2492
- uri: nextUri,
2547
+ uri: mapped.uri,
2493
2548
  collection: collection.name,
2494
- relPath: nextRelPath,
2549
+ relPath: mapped.relPath,
2495
2550
  origin: "save",
2496
2551
  changedAt: new Date().toISOString(),
2497
2552
  });
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
- });
2553
+ return jsonResponse(mapped);
2512
2554
  } catch (error) {
2513
2555
  return errorResponse(
2514
2556
  "RUNTIME",
@@ -2550,7 +2592,6 @@ export async function handleRefactorPlan(
2550
2592
  );
2551
2593
  }
2552
2594
 
2553
- const snapshot = await getRefactorSnapshot(store, doc.id);
2554
2595
  const collection = getCollectionByName(
2555
2596
  ctxHolder.config.collections,
2556
2597
  doc.collection
@@ -2568,41 +2609,69 @@ export async function handleRefactorPlan(
2568
2609
  if (!body.name?.trim()) {
2569
2610
  return errorResponse("VALIDATION", "Missing or invalid name");
2570
2611
  }
2571
- const plan = planRenameRefactor({
2612
+ const resolvedDocPath = await resolveAbsoluteDocPath(
2613
+ ctxHolder.config.collections,
2614
+ doc
2615
+ );
2616
+ if (!resolvedDocPath) {
2617
+ return errorResponse(
2618
+ "NOT_FOUND",
2619
+ `Collection not found: ${doc.collection}`,
2620
+ 404
2621
+ );
2622
+ }
2623
+ const target = resolveRenameTarget({
2572
2624
  collection: collection.name,
2573
2625
  currentRelPath: doc.relPath,
2574
2626
  nextName: body.name.trim(),
2575
2627
  });
2576
- return jsonResponse({
2577
- operation: body.operation,
2578
- ...plan,
2579
- refactorWarnings: buildRefactorWarnings(snapshot, {
2580
- filenameChanged: true,
2581
- }),
2628
+ const plan = await buildCanonicalRefactorPlan({
2629
+ operation: "rename",
2630
+ doc,
2631
+ collection,
2632
+ sourceFullPath: resolvedDocPath.fullPath,
2633
+ target,
2634
+ store,
2635
+ sourceEditable: capabilities.editable,
2582
2636
  });
2637
+ return jsonResponse(toRefactorPlanResponse(plan));
2583
2638
  }
2584
2639
 
2585
2640
  if (body.operation === "move") {
2586
2641
  if (!body.folderPath?.trim()) {
2587
2642
  return errorResponse("VALIDATION", "Missing or invalid folderPath");
2588
2643
  }
2589
- const plan = planMoveRefactor({
2644
+ const resolvedDocPath = await resolveAbsoluteDocPath(
2645
+ ctxHolder.config.collections,
2646
+ doc
2647
+ );
2648
+ if (!resolvedDocPath) {
2649
+ return errorResponse(
2650
+ "NOT_FOUND",
2651
+ `Collection not found: ${doc.collection}`,
2652
+ 404
2653
+ );
2654
+ }
2655
+ const target = resolveMoveTarget({
2590
2656
  collection: collection.name,
2591
2657
  currentRelPath: doc.relPath,
2592
2658
  folderPath: body.folderPath.trim(),
2593
2659
  nextName: body.name?.trim(),
2594
2660
  });
2595
- return jsonResponse({
2596
- operation: body.operation,
2597
- ...plan,
2598
- refactorWarnings: buildRefactorWarnings(snapshot, {
2599
- folderChanged: true,
2600
- filenameChanged: Boolean(body.name?.trim()),
2601
- }),
2661
+ const plan = await buildCanonicalRefactorPlan({
2662
+ operation: "move",
2663
+ doc,
2664
+ collection,
2665
+ sourceFullPath: resolvedDocPath.fullPath,
2666
+ target,
2667
+ store,
2668
+ sourceEditable: capabilities.editable,
2602
2669
  });
2670
+ return jsonResponse(toRefactorPlanResponse(plan));
2603
2671
  }
2604
2672
 
2605
2673
  if (body.operation === "duplicate") {
2674
+ const snapshot = await getRefactorSnapshot(store, doc.id);
2606
2675
  const plan = planDuplicateRefactor({
2607
2676
  collection: collection.name,
2608
2677
  currentRelPath: doc.relPath,
@@ -2731,7 +2800,10 @@ export async function handleMoveDoc(
2731
2800
  ctxHolder: ContextHolder,
2732
2801
  store: SqliteAdapter,
2733
2802
  docId: string,
2734
- req: Request
2803
+ req: Request,
2804
+ deps?: {
2805
+ syncCollection?: typeof defaultSyncService.syncCollection;
2806
+ }
2735
2807
  ): Promise<Response> {
2736
2808
  let body: MoveDocRequestBody;
2737
2809
  try {
@@ -2750,6 +2822,11 @@ export async function handleMoveDoc(
2750
2822
  return errorResponse("VALIDATION", "uri must be a string");
2751
2823
  }
2752
2824
 
2825
+ const confirmation = parseRefactorApplyConfirmation(body);
2826
+ if ("code" in confirmation) {
2827
+ return fileRefactorHttpErrorResponse(confirmation);
2828
+ }
2829
+
2753
2830
  const docResult = await resolveDocumentReference(store, docId, body.uri);
2754
2831
  if (!docResult.ok) {
2755
2832
  return errorResponse("RUNTIME", docResult.error.message, 500);
@@ -2780,9 +2857,9 @@ export async function handleMoveDoc(
2780
2857
  }
2781
2858
  const { collection, fullPath } = resolvedDocPath;
2782
2859
 
2783
- let plan;
2860
+ let target;
2784
2861
  try {
2785
- plan = planMoveRefactor({
2862
+ target = resolveMoveTarget({
2786
2863
  collection: collection.name,
2787
2864
  currentRelPath: doc.relPath,
2788
2865
  folderPath: body.folderPath,
@@ -2796,55 +2873,66 @@ export async function handleMoveDoc(
2796
2873
  }
2797
2874
 
2798
2875
  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
- }
2876
+ const nextFullPath = nodePath.join(collection.path, target.nextRelPath);
2807
2877
 
2808
2878
  try {
2879
+ const plan = await buildCanonicalRefactorPlan({
2880
+ operation: "move",
2881
+ doc,
2882
+ collection,
2883
+ sourceFullPath: fullPath,
2884
+ target,
2885
+ store,
2886
+ sourceEditable: capabilities.editable,
2887
+ });
2888
+
2889
+ const syncCollection =
2890
+ deps?.syncCollection ??
2891
+ ((collectionArg, storeArg, optionsArg) =>
2892
+ defaultSyncService.syncCollection(collectionArg, storeArg, optionsArg));
2893
+
2809
2894
  ctxHolder.watchService?.suppress(fullPath);
2810
2895
  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,
2896
+
2897
+ const applyResult = await applyCanonicalFileRefactor({
2898
+ plan,
2899
+ confirmation,
2900
+ deps: buildFileRefactorApplyDeps({
2818
2901
  collection,
2819
2902
  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.";
2903
+ syncAfterCommit: async () => {
2904
+ const syncResult = await syncResidentCollection(
2905
+ ctxHolder,
2906
+ collection,
2907
+ store,
2908
+ withContentTypeRules({ runUpdateCmd: false }, ctxHolder.config),
2909
+ syncCollection
2910
+ );
2911
+ assertFileRefactorSyncConverged(syncResult);
2912
+ },
2913
+ }),
2914
+ signal: req.signal,
2915
+ });
2916
+
2917
+ const mapped = mapApplyResultToHttpSuccess({
2918
+ plan,
2919
+ result: applyResult,
2920
+ targetFullPath: nextFullPath,
2921
+ operationLabel: "moved",
2922
+ });
2923
+ if ("code" in mapped) {
2924
+ return fileRefactorHttpErrorResponse(mapped);
2825
2925
  }
2926
+
2826
2927
  ctxHolder.eventBus?.emit({
2827
2928
  type: "document-changed",
2828
- uri: plan.nextUri,
2929
+ uri: mapped.uri,
2829
2930
  collection: collection.name,
2830
- relPath: plan.nextRelPath,
2931
+ relPath: mapped.relPath,
2831
2932
  origin: "save",
2832
2933
  changedAt: new Date().toISOString(),
2833
2934
  });
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
- });
2935
+ return jsonResponse(mapped);
2848
2936
  } catch (error) {
2849
2937
  return errorResponse(
2850
2938
  "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
  ];