@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.
- package/README.md +5 -4
- package/assets/skill/SKILL.md +23 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.31.0.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +19 -0
- package/spec/db/schema.sql +55 -0
- package/spec/mcp.md +114 -11
- package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
- package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
- package/src/core/document-capabilities.ts +13 -0
- package/src/core/file-ops.ts +129 -1
- package/src/core/file-refactor-adapter.ts +329 -0
- package/src/core/file-refactor-apply-edits.ts +61 -0
- package/src/core/file-refactor-apply-fs.ts +512 -0
- package/src/core/file-refactor-apply-safety.ts +340 -0
- package/src/core/file-refactor-apply-validate.ts +401 -0
- package/src/core/file-refactor-contract.ts +486 -0
- package/src/core/file-refactor-destination.ts +123 -0
- package/src/core/file-refactor-from-snapshot.ts +148 -0
- package/src/core/file-refactor-journal-port.ts +150 -0
- package/src/core/file-refactor-journal.ts +347 -0
- package/src/core/file-refactor-paths.ts +60 -0
- package/src/core/file-refactor-plan-classify.ts +208 -0
- package/src/core/file-refactor-plan-validate.ts +169 -0
- package/src/core/file-refactor-planner-types.ts +62 -0
- package/src/core/file-refactor-planner.ts +423 -0
- package/src/core/file-refactor-resolve.ts +280 -0
- package/src/core/file-refactor-service.ts +468 -0
- package/src/core/file-refactors.ts +84 -56
- package/src/core/link-destination-parse.ts +275 -0
- package/src/core/link-inventory-markdown.ts +454 -0
- package/src/core/link-inventory-opaque.ts +244 -0
- package/src/core/link-inventory-types.ts +47 -0
- package/src/core/link-inventory.ts +182 -0
- package/src/core/link-relevance.ts +150 -0
- package/src/mcp/tools/index.ts +18 -17
- package/src/mcp/tools/workspace-write.ts +215 -97
- package/src/sdk/client.ts +167 -115
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +32 -2
- package/src/serve/file-refactor-http.ts +239 -0
- package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/DocView.tsx +176 -41
- package/src/serve/routes/api.ts +191 -104
- package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +452 -0
- package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
- package/src/store/types.ts +84 -0
- 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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
1711
|
+
async previewRenameNote(
|
|
1661
1712
|
options: GnoRenameNoteOptions
|
|
1662
|
-
): Promise<
|
|
1713
|
+
): Promise<FileRefactorPreviewPlan> {
|
|
1663
1714
|
this.assertOpen();
|
|
1664
|
-
const
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1788
|
+
}),
|
|
1789
|
+
});
|
|
1790
|
+
return apply;
|
|
1717
1791
|
}
|
|
1718
1792
|
|
|
1719
|
-
async moveNote(
|
|
1793
|
+
async moveNote(
|
|
1794
|
+
options: GnoMoveNoteApplyOptions
|
|
1795
|
+
): Promise<FileRefactorApplyResult> {
|
|
1720
1796
|
this.assertOpen();
|
|
1721
|
-
const
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
this.store,
|
|
1752
|
-
|
|
1753
|
-
);
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
-
|
|
1775
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
+
}
|