@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
package/src/sdk/client.ts CHANGED
@@ -34,10 +34,12 @@ import type {
34
34
  GnoIndexResult,
35
35
  GnoIndexStatus,
36
36
  GnoListOptions,
37
+ GnoMoveNoteApplyOptions,
37
38
  GnoMoveNoteOptions,
38
39
  GnoMultiGetOptions,
39
40
  GnoQueryOptions,
40
41
  GnoRefactorNoteResult,
42
+ GnoRenameNoteApplyOptions,
41
43
  GnoRenameNoteOptions,
42
44
  GnoSearchOptions,
43
45
  GnoUpdateOptions,
@@ -86,20 +88,23 @@ import { writeCapturePlanFile } from "../core/capture-write";
86
88
  import { projectCollectionEgressPolicy } from "../core/collection-egress-policy-projection";
87
89
  import { CollectionEgressPolicyService } from "../core/collection-egress-policy-service";
88
90
  import { applyConfigChange } from "../core/config-mutation";
91
+ import { getDocumentCapabilities } from "../core/document-capabilities";
89
92
  import { EgressAuditService } from "../core/egress-audit";
90
93
  import { authorizeCurrentEgress } from "../core/egress-authorization";
94
+ import { atomicWrite, copyFilePath, createFolderPath } from "../core/file-ops";
91
95
  import {
92
- atomicWrite,
93
- copyFilePath,
94
- createFolderPath,
95
- renameFilePath,
96
- } from "../core/file-ops";
97
- import {
96
+ applyCanonicalFileRefactor,
97
+ assertFileRefactorSyncConverged,
98
+ buildCanonicalRefactorPlan,
99
+ buildDurableFileRefactorApplyDeps,
98
100
  buildRefactorWarnings,
101
+ parseRefactorApplyConfirmation,
99
102
  planCreateFolder,
100
103
  planDuplicateRefactor,
101
- planMoveRefactor,
102
- planRenameRefactor,
104
+ resolveMoveTarget,
105
+ resolveRenameTarget,
106
+ type FileRefactorApplyResult,
107
+ type FileRefactorPreviewPlan,
103
108
  } from "../core/file-refactors";
104
109
  import { resolveEffectiveIndex } from "../core/indexed-reference";
105
110
  import {
@@ -334,6 +339,52 @@ class GnoClientImpl implements GnoClient {
334
339
  return filtered;
335
340
  }
336
341
 
342
+ private requireRefactorApplyConfirmation(options: {
343
+ schemaVersion?: unknown;
344
+ planDigest?: unknown;
345
+ confirmation?: unknown;
346
+ }) {
347
+ const parsed = parseRefactorApplyConfirmation(options);
348
+ if ("error" in parsed) {
349
+ throw sdkError("VALIDATION", parsed.message);
350
+ }
351
+ return parsed;
352
+ }
353
+
354
+ private async prepareRefactorSource(ref: string) {
355
+ const doc = await getDocumentByRef(this.store, this.config, ref, {});
356
+ const stored = await this.store.getDocumentByUri(doc.uri);
357
+ if (!stored.ok || !stored.value) {
358
+ throw sdkError("NOT_FOUND", "Document not found");
359
+ }
360
+ const storedDoc = stored.value;
361
+ const collection = this.getCollections(storedDoc.collection)[0];
362
+ if (!collection) {
363
+ throw sdkError(
364
+ "VALIDATION",
365
+ `Collection not found: ${storedDoc.collection}`
366
+ );
367
+ }
368
+ const capabilities = getDocumentCapabilities({
369
+ sourceExt: storedDoc.sourceExt,
370
+ sourceMime: storedDoc.sourceMime,
371
+ contentAvailable: storedDoc.mirrorHash !== null,
372
+ recordKey: storedDoc.recordKey,
373
+ });
374
+ if (!capabilities.editable) {
375
+ throw sdkError(
376
+ "VALIDATION",
377
+ capabilities.reason ?? "Document is read-only in place."
378
+ );
379
+ }
380
+ return {
381
+ storedDoc,
382
+ collection,
383
+ sourceFullPath: `${collection.path}/${storedDoc.relPath}`,
384
+ editable: capabilities.editable,
385
+ };
386
+ }
387
+
337
388
  private async createRuntimePorts(options: {
338
389
  embed?: boolean;
339
390
  expand?: boolean;
@@ -1657,125 +1708,126 @@ class GnoClientImpl implements GnoClient {
1657
1708
  };
1658
1709
  }
1659
1710
 
1660
- async renameNote(
1711
+ async previewRenameNote(
1661
1712
  options: GnoRenameNoteOptions
1662
- ): Promise<GnoRefactorNoteResult> {
1713
+ ): Promise<FileRefactorPreviewPlan> {
1663
1714
  this.assertOpen();
1664
- const doc = await getDocumentByRef(
1665
- this.store,
1666
- this.config,
1667
- options.ref,
1668
- {}
1669
- );
1670
- const stored = await this.store.getDocumentByUri(doc.uri);
1671
- if (!stored.ok || !stored.value) {
1672
- throw sdkError("NOT_FOUND", "Document not found");
1673
- }
1674
- const storedDoc = stored.value;
1675
- const collection = this.getCollections(storedDoc.collection)[0];
1676
- if (!collection) {
1677
- throw sdkError(
1678
- "VALIDATION",
1679
- `Collection not found: ${storedDoc.collection}`
1680
- );
1681
- }
1682
- const plan = planRenameRefactor({
1683
- collection: collection.name,
1684
- currentRelPath: storedDoc.relPath,
1715
+ const prepared = await this.prepareRefactorSource(options.ref);
1716
+ const target = resolveRenameTarget({
1717
+ collection: prepared.collection.name,
1718
+ currentRelPath: prepared.storedDoc.relPath,
1685
1719
  nextName: options.name,
1686
1720
  });
1687
- const currentPath = `${collection.path}/${storedDoc.relPath}`;
1688
- const nextPath = `${collection.path}/${plan.nextRelPath}`;
1689
- await renameFilePath(currentPath, nextPath);
1690
- await defaultSyncService.syncCollection(
1691
- collection,
1692
- this.store,
1693
- withContentTypeRules({ runUpdateCmd: false }, this.config)
1694
- );
1695
- const linksResult = await this.store.getLinksForDoc(storedDoc.id);
1696
- const backlinksResult = await this.store.getBacklinksForDoc(storedDoc.id);
1697
- if (!linksResult.ok || !backlinksResult.ok) {
1698
- throw sdkError("STORE", "Failed to compute refactor warnings");
1699
- }
1700
- return {
1701
- uri: plan.nextUri,
1702
- path: nextPath,
1703
- relPath: plan.nextRelPath,
1704
- warnings: buildRefactorWarnings(
1705
- {
1706
- backlinks: backlinksResult.value.length,
1707
- wikiLinks: linksResult.value.filter(
1708
- (entry) => entry.linkType === "wiki"
1709
- ).length,
1710
- markdownLinks: linksResult.value.filter(
1711
- (entry) => entry.linkType === "markdown"
1712
- ).length,
1721
+ return buildCanonicalRefactorPlan({
1722
+ operation: "rename",
1723
+ doc: prepared.storedDoc,
1724
+ collection: prepared.collection,
1725
+ sourceFullPath: prepared.sourceFullPath,
1726
+ target,
1727
+ store: this.store,
1728
+ sourceEditable: prepared.editable,
1729
+ });
1730
+ }
1731
+
1732
+ async previewMoveNote(
1733
+ options: GnoMoveNoteOptions
1734
+ ): Promise<FileRefactorPreviewPlan> {
1735
+ this.assertOpen();
1736
+ const prepared = await this.prepareRefactorSource(options.ref);
1737
+ const target = resolveMoveTarget({
1738
+ collection: prepared.collection.name,
1739
+ currentRelPath: prepared.storedDoc.relPath,
1740
+ folderPath: options.folderPath,
1741
+ nextName: options.name,
1742
+ });
1743
+ return buildCanonicalRefactorPlan({
1744
+ operation: "move",
1745
+ doc: prepared.storedDoc,
1746
+ collection: prepared.collection,
1747
+ sourceFullPath: prepared.sourceFullPath,
1748
+ target,
1749
+ store: this.store,
1750
+ sourceEditable: prepared.editable,
1751
+ });
1752
+ }
1753
+
1754
+ async renameNote(
1755
+ options: GnoRenameNoteApplyOptions
1756
+ ): Promise<FileRefactorApplyResult> {
1757
+ this.assertOpen();
1758
+ const confirmation = this.requireRefactorApplyConfirmation(options);
1759
+ const prepared = await this.prepareRefactorSource(options.ref);
1760
+ const target = resolveRenameTarget({
1761
+ collection: prepared.collection.name,
1762
+ currentRelPath: prepared.storedDoc.relPath,
1763
+ nextName: options.name,
1764
+ });
1765
+ const plan = await buildCanonicalRefactorPlan({
1766
+ operation: "rename",
1767
+ doc: prepared.storedDoc,
1768
+ collection: prepared.collection,
1769
+ sourceFullPath: prepared.sourceFullPath,
1770
+ target,
1771
+ store: this.store,
1772
+ sourceEditable: prepared.editable,
1773
+ });
1774
+ const apply = await applyCanonicalFileRefactor({
1775
+ plan,
1776
+ confirmation,
1777
+ deps: buildDurableFileRefactorApplyDeps({
1778
+ collection: prepared.collection,
1779
+ store: this.store,
1780
+ syncAfterCommit: async () => {
1781
+ const syncResult = await defaultSyncService.syncCollection(
1782
+ prepared.collection,
1783
+ this.store,
1784
+ withContentTypeRules({ runUpdateCmd: false }, this.config)
1785
+ );
1786
+ assertFileRefactorSyncConverged(syncResult);
1713
1787
  },
1714
- { filenameChanged: true }
1715
- ).warnings,
1716
- };
1788
+ }),
1789
+ });
1790
+ return apply;
1717
1791
  }
1718
1792
 
1719
- async moveNote(options: GnoMoveNoteOptions): Promise<GnoRefactorNoteResult> {
1793
+ async moveNote(
1794
+ options: GnoMoveNoteApplyOptions
1795
+ ): Promise<FileRefactorApplyResult> {
1720
1796
  this.assertOpen();
1721
- const doc = await getDocumentByRef(
1722
- this.store,
1723
- this.config,
1724
- options.ref,
1725
- {}
1726
- );
1727
- const stored = await this.store.getDocumentByUri(doc.uri);
1728
- if (!stored.ok || !stored.value) {
1729
- throw sdkError("NOT_FOUND", "Document not found");
1730
- }
1731
- const storedDoc = stored.value;
1732
- const collection = this.getCollections(storedDoc.collection)[0];
1733
- if (!collection) {
1734
- throw sdkError(
1735
- "VALIDATION",
1736
- `Collection not found: ${storedDoc.collection}`
1737
- );
1738
- }
1739
- const plan = planMoveRefactor({
1740
- collection: collection.name,
1741
- currentRelPath: storedDoc.relPath,
1797
+ const confirmation = this.requireRefactorApplyConfirmation(options);
1798
+ const prepared = await this.prepareRefactorSource(options.ref);
1799
+ const target = resolveMoveTarget({
1800
+ collection: prepared.collection.name,
1801
+ currentRelPath: prepared.storedDoc.relPath,
1742
1802
  folderPath: options.folderPath,
1743
1803
  nextName: options.name,
1744
1804
  });
1745
- const currentPath = `${collection.path}/${storedDoc.relPath}`;
1746
- const nextPath = `${collection.path}/${plan.nextRelPath}`;
1747
- await mkdir(dirname(nextPath), { recursive: true });
1748
- await renameFilePath(currentPath, nextPath);
1749
- await defaultSyncService.syncCollection(
1750
- collection,
1751
- this.store,
1752
- withContentTypeRules({ runUpdateCmd: false }, this.config)
1753
- );
1754
- const linksResult = await this.store.getLinksForDoc(storedDoc.id);
1755
- const backlinksResult = await this.store.getBacklinksForDoc(storedDoc.id);
1756
- if (!linksResult.ok || !backlinksResult.ok) {
1757
- throw sdkError("STORE", "Failed to compute refactor warnings");
1758
- }
1759
- return {
1760
- uri: plan.nextUri,
1761
- path: nextPath,
1762
- relPath: plan.nextRelPath,
1763
- warnings: buildRefactorWarnings(
1764
- {
1765
- backlinks: backlinksResult.value.length,
1766
- wikiLinks: linksResult.value.filter(
1767
- (entry) => entry.linkType === "wiki"
1768
- ).length,
1769
- markdownLinks: linksResult.value.filter(
1770
- (entry) => entry.linkType === "markdown"
1771
- ).length,
1805
+ const plan = await buildCanonicalRefactorPlan({
1806
+ operation: "move",
1807
+ doc: prepared.storedDoc,
1808
+ collection: prepared.collection,
1809
+ sourceFullPath: prepared.sourceFullPath,
1810
+ target,
1811
+ store: this.store,
1812
+ sourceEditable: prepared.editable,
1813
+ });
1814
+ const apply = await applyCanonicalFileRefactor({
1815
+ plan,
1816
+ confirmation,
1817
+ deps: buildDurableFileRefactorApplyDeps({
1818
+ collection: prepared.collection,
1819
+ store: this.store,
1820
+ syncAfterCommit: async () => {
1821
+ const syncResult = await defaultSyncService.syncCollection(
1822
+ prepared.collection,
1823
+ this.store,
1824
+ withContentTypeRules({ runUpdateCmd: false }, this.config)
1825
+ );
1826
+ assertFileRefactorSyncConverged(syncResult);
1772
1827
  },
1773
- {
1774
- folderChanged: true,
1775
- filenameChanged: Boolean(options.name),
1776
- }
1777
- ).warnings,
1778
- };
1828
+ }),
1829
+ });
1830
+ return apply;
1779
1831
  }
1780
1832
 
1781
1833
  async duplicateNote(
package/src/sdk/index.ts CHANGED
@@ -40,6 +40,9 @@ export type {
40
40
  GnoClientInitOptions,
41
41
  GnoEmbedOptions,
42
42
  GnoEmbedResult,
43
+ GnoFileRefactorApplyConfirmation,
44
+ GnoFileRefactorApplyResult,
45
+ GnoFileRefactorPreviewPlan,
43
46
  GnoGetOptions,
44
47
  GnoGetResult,
45
48
  GnoIndexOptions,
@@ -49,10 +52,14 @@ export type {
49
52
  GnoListOptions,
50
53
  GnoListResult,
51
54
  GnoModelOverrides,
55
+ GnoMoveNoteApplyOptions,
56
+ GnoMoveNoteOptions,
52
57
  GnoMultiGetDocument,
53
58
  GnoMultiGetOptions,
54
59
  GnoMultiGetResult,
55
60
  GnoQueryOptions,
61
+ GnoRenameNoteApplyOptions,
62
+ GnoRenameNoteOptions,
56
63
  GnoProjectHintOptions,
57
64
  GnoSearchOptions,
58
65
  GnoSkippedDocument,
package/src/sdk/types.ts CHANGED
@@ -32,6 +32,10 @@ import type {
32
32
  EgressAuditShowResult,
33
33
  EgressAuditStatusResult,
34
34
  } from "../core/egress-audit";
35
+ import type {
36
+ FileRefactorApplyResult,
37
+ FileRefactorPreviewPlan,
38
+ } from "../core/file-refactor-contract";
35
39
  import type {
36
40
  KnowledgeChangesResult,
37
41
  KnowledgeDiffResult,
@@ -262,6 +266,22 @@ export interface GnoMoveNoteOptions {
262
266
  name?: string;
263
267
  }
264
268
 
269
+ /** Canonical apply confirmation — preview first, then pass exact digest. */
270
+ export interface GnoFileRefactorApplyConfirmation {
271
+ schemaVersion: "1.0";
272
+ planDigest: string;
273
+ confirmation: "apply";
274
+ }
275
+
276
+ export interface GnoRenameNoteApplyOptions
277
+ extends GnoRenameNoteOptions, GnoFileRefactorApplyConfirmation {}
278
+
279
+ export interface GnoMoveNoteApplyOptions
280
+ extends GnoMoveNoteOptions, GnoFileRefactorApplyConfirmation {}
281
+
282
+ export type GnoFileRefactorPreviewPlan = FileRefactorPreviewPlan;
283
+ export type GnoFileRefactorApplyResult = FileRefactorApplyResult;
284
+
265
285
  export interface GnoDuplicateNoteOptions {
266
286
  ref: string;
267
287
  folderPath?: string;
@@ -350,8 +370,18 @@ export interface GnoClient {
350
370
  capture(options: GnoCaptureOptions): Promise<GnoCaptureResult>;
351
371
  createNote(options: GnoCreateNoteOptions): Promise<GnoCreateNoteResult>;
352
372
  createFolder(options: GnoCreateFolderOptions): Promise<GnoCreateFolderResult>;
353
- renameNote(options: GnoRenameNoteOptions): Promise<GnoRefactorNoteResult>;
354
- moveNote(options: GnoMoveNoteOptions): Promise<GnoRefactorNoteResult>;
373
+ previewRenameNote(
374
+ options: GnoRenameNoteOptions
375
+ ): Promise<GnoFileRefactorPreviewPlan>;
376
+ previewMoveNote(
377
+ options: GnoMoveNoteOptions
378
+ ): Promise<GnoFileRefactorPreviewPlan>;
379
+ renameNote(
380
+ options: GnoRenameNoteApplyOptions
381
+ ): Promise<GnoFileRefactorApplyResult>;
382
+ moveNote(
383
+ options: GnoMoveNoteApplyOptions
384
+ ): Promise<GnoFileRefactorApplyResult>;
355
385
  duplicateNote(
356
386
  options: GnoDuplicateNoteOptions
357
387
  ): Promise<GnoRefactorNoteResult>;
@@ -0,0 +1,239 @@
1
+ /**
2
+ * REST transport adapter for canonical reference-safe rename/move.
3
+ *
4
+ * Handlers must not implement link rewrite logic — they only build
5
+ * targets, call the core planner/apply service, and map typed results.
6
+ *
7
+ * @module src/serve/file-refactor-http
8
+ */
9
+
10
+ import type {
11
+ FileRefactorApplyResult,
12
+ FileRefactorPreviewPlan,
13
+ FileRefactorReasonCode,
14
+ } from "../core/file-refactors";
15
+
16
+ import {
17
+ applyCanonicalFileRefactor,
18
+ buildCanonicalRefactorPlan,
19
+ buildDurableFileRefactorApplyDeps,
20
+ FILE_REFACTOR_APPLY_CONFIRMATION,
21
+ FILE_REFACTOR_SCHEMA_VERSION,
22
+ parseRefactorApplyConfirmation as parseCoreRefactorApplyConfirmation,
23
+ resolveMoveTarget,
24
+ resolveRenameTarget,
25
+ } from "../core/file-refactors";
26
+
27
+ export {
28
+ applyCanonicalFileRefactor,
29
+ buildCanonicalRefactorPlan,
30
+ buildDurableFileRefactorApplyDeps as buildFileRefactorApplyDeps,
31
+ resolveMoveTarget,
32
+ resolveRenameTarget,
33
+ };
34
+
35
+ export interface FileRefactorHttpError {
36
+ code: string;
37
+ message: string;
38
+ status: number;
39
+ details?: Record<string, unknown>;
40
+ }
41
+
42
+ export interface FileRefactorApplyHttpSuccess {
43
+ success: true;
44
+ uri: string;
45
+ path: string;
46
+ relPath: string;
47
+ planDigest: string;
48
+ status: FileRefactorApplyResult["status"];
49
+ apply: FileRefactorApplyResult;
50
+ refactorWarnings: {
51
+ warnings: string[];
52
+ backlinkCount: number;
53
+ wikiLinkCount: number;
54
+ markdownLinkCount: number;
55
+ };
56
+ warning?: string;
57
+ }
58
+
59
+ function warningsFromPlan(plan: FileRefactorPreviewPlan) {
60
+ return {
61
+ warnings: [...plan.safety.warnings],
62
+ backlinkCount: plan.safety.backlinkCount,
63
+ wikiLinkCount: plan.safety.wikiLinkCount,
64
+ markdownLinkCount: plan.safety.markdownLinkCount,
65
+ };
66
+ }
67
+
68
+ export function toRefactorPlanResponse(plan: FileRefactorPreviewPlan) {
69
+ return {
70
+ ...plan,
71
+ operation: plan.operation,
72
+ nextRelPath: plan.target.relPath,
73
+ nextUri: plan.target.uri,
74
+ refactorWarnings: warningsFromPlan(plan),
75
+ };
76
+ }
77
+
78
+ function reasonMessage(
79
+ status: FileRefactorApplyResult["status"],
80
+ reasonCode?: FileRefactorReasonCode
81
+ ): string {
82
+ switch (status) {
83
+ case "stale_plan":
84
+ return "This refactor plan is stale. Preview again, then confirm the exact plan digest.";
85
+ case "conflict":
86
+ return "Another workspace mutation is in progress or the target is unsafe. Retry after it finishes.";
87
+ case "unsupported":
88
+ if (
89
+ reasonCode === "capability_denied" ||
90
+ reasonCode === "read_only_document"
91
+ ) {
92
+ return "This document cannot be refactored in place from GNO.";
93
+ }
94
+ if (reasonCode === "occupied_target") {
95
+ return "A file with that name already exists at the destination";
96
+ }
97
+ return reasonCode
98
+ ? `Refactor cannot be applied (${reasonCode}).`
99
+ : "Refactor cannot be applied.";
100
+ case "failed_rolled_back":
101
+ if (reasonCode === "rollback_recovery_required") {
102
+ return "Refactor failed and needs recovery. Filesystem state may require manual inspection using the recovery journal id.";
103
+ }
104
+ return "Refactor failed and was rolled back. No partial file set was left behind.";
105
+ default:
106
+ return "Refactor failed.";
107
+ }
108
+ }
109
+
110
+ export function mapApplyResultToHttpError(
111
+ result: FileRefactorApplyResult
112
+ ): FileRefactorHttpError {
113
+ const reasonCode = "reasonCode" in result ? result.reasonCode : undefined;
114
+ const details: Record<string, unknown> = {
115
+ planDigest: result.planDigest,
116
+ status: result.status,
117
+ filesystem: result.filesystem,
118
+ indexConvergence: result.indexConvergence,
119
+ };
120
+ if (reasonCode) {
121
+ details.reasonCode = reasonCode;
122
+ }
123
+ if (result.filesystem.recoveryJournalId) {
124
+ details.recoveryJournalId = result.filesystem.recoveryJournalId;
125
+ }
126
+
127
+ if (result.status === "stale_plan") {
128
+ return {
129
+ code: "STALE_PLAN",
130
+ message: reasonMessage(result.status, reasonCode),
131
+ status: 409,
132
+ details,
133
+ };
134
+ }
135
+ if (result.status === "conflict") {
136
+ return {
137
+ code: "CONFLICT",
138
+ message: reasonMessage(result.status, reasonCode),
139
+ status: 409,
140
+ details,
141
+ };
142
+ }
143
+ if (result.status === "unsupported") {
144
+ if (
145
+ reasonCode === "capability_denied" ||
146
+ reasonCode === "read_only_document"
147
+ ) {
148
+ return {
149
+ code: "READ_ONLY",
150
+ message: reasonMessage(result.status, reasonCode),
151
+ status: 409,
152
+ details,
153
+ };
154
+ }
155
+ if (reasonCode === "occupied_target") {
156
+ return {
157
+ code: "CONFLICT",
158
+ message: reasonMessage(result.status, reasonCode),
159
+ status: 409,
160
+ details,
161
+ };
162
+ }
163
+ return {
164
+ code: "UNSUPPORTED",
165
+ message: reasonMessage(result.status, reasonCode),
166
+ status: 409,
167
+ details,
168
+ };
169
+ }
170
+ if (result.status === "failed_rolled_back") {
171
+ const recovery =
172
+ result.filesystem.state === "recovery_required" ||
173
+ reasonCode === "rollback_recovery_required";
174
+ return {
175
+ code: recovery ? "RECOVERY_REQUIRED" : "ROLLED_BACK",
176
+ message: reasonMessage(result.status, reasonCode),
177
+ status: 500,
178
+ details,
179
+ };
180
+ }
181
+
182
+ return {
183
+ code: "RUNTIME",
184
+ message: reasonMessage(result.status, reasonCode),
185
+ status: 500,
186
+ details,
187
+ };
188
+ }
189
+
190
+ export function mapApplyResultToHttpSuccess(input: {
191
+ plan: FileRefactorPreviewPlan;
192
+ result: FileRefactorApplyResult;
193
+ targetFullPath: string;
194
+ operationLabel: "renamed" | "moved";
195
+ }): FileRefactorApplyHttpSuccess | FileRefactorHttpError {
196
+ const { plan, result, targetFullPath, operationLabel } = input;
197
+
198
+ if (
199
+ result.status !== "applied" &&
200
+ result.status !== "applied_with_sync_pending"
201
+ ) {
202
+ return mapApplyResultToHttpError(result);
203
+ }
204
+
205
+ const warning =
206
+ result.status === "applied_with_sync_pending"
207
+ ? `File ${operationLabel} on disk, but index refresh failed. Run Update All to reconcile the workspace.`
208
+ : undefined;
209
+
210
+ return {
211
+ success: true,
212
+ uri: plan.target.uri,
213
+ path: targetFullPath,
214
+ relPath: plan.target.relPath,
215
+ planDigest: result.planDigest,
216
+ status: result.status,
217
+ apply: result,
218
+ refactorWarnings: warningsFromPlan(plan),
219
+ warning,
220
+ };
221
+ }
222
+
223
+ export interface ParsedRefactorApplyConfirmation {
224
+ planDigest: string;
225
+ confirmation: typeof FILE_REFACTOR_APPLY_CONFIRMATION;
226
+ schemaVersion: typeof FILE_REFACTOR_SCHEMA_VERSION;
227
+ }
228
+
229
+ export function parseRefactorApplyConfirmation(body: {
230
+ planDigest?: unknown;
231
+ confirmation?: unknown;
232
+ schemaVersion?: unknown;
233
+ }): ParsedRefactorApplyConfirmation | FileRefactorHttpError {
234
+ const parsed = parseCoreRefactorApplyConfirmation(body);
235
+ if ("error" in parsed) {
236
+ return { code: "VALIDATION", message: parsed.message, status: 400 };
237
+ }
238
+ return parsed;
239
+ }