@gmickel/gno 1.30.7 → 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 (71) hide show
  1. package/README.md +6 -5
  2. package/assets/skill/SKILL.md +25 -0
  3. package/assets/skill/mcp-reference.md +6 -0
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +1 -1
  8. package/spec/cli.md +19 -0
  9. package/spec/db/schema.sql +55 -0
  10. package/spec/mcp.md +176 -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/section-target-create-result.schema.json +20 -0
  14. package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
  15. package/spec/output-schemas/section-target.schema.json +118 -0
  16. package/spec/output-schemas/section.schema.json +113 -0
  17. package/src/core/document-capabilities.ts +13 -0
  18. package/src/core/file-ops.ts +129 -1
  19. package/src/core/file-refactor-adapter.ts +329 -0
  20. package/src/core/file-refactor-apply-edits.ts +61 -0
  21. package/src/core/file-refactor-apply-fs.ts +512 -0
  22. package/src/core/file-refactor-apply-safety.ts +340 -0
  23. package/src/core/file-refactor-apply-validate.ts +401 -0
  24. package/src/core/file-refactor-contract.ts +486 -0
  25. package/src/core/file-refactor-destination.ts +123 -0
  26. package/src/core/file-refactor-from-snapshot.ts +148 -0
  27. package/src/core/file-refactor-journal-port.ts +150 -0
  28. package/src/core/file-refactor-journal.ts +347 -0
  29. package/src/core/file-refactor-paths.ts +60 -0
  30. package/src/core/file-refactor-plan-classify.ts +208 -0
  31. package/src/core/file-refactor-plan-validate.ts +169 -0
  32. package/src/core/file-refactor-planner-types.ts +62 -0
  33. package/src/core/file-refactor-planner.ts +423 -0
  34. package/src/core/file-refactor-resolve.ts +280 -0
  35. package/src/core/file-refactor-service.ts +468 -0
  36. package/src/core/file-refactors.ts +84 -56
  37. package/src/core/link-destination-parse.ts +275 -0
  38. package/src/core/link-inventory-markdown.ts +454 -0
  39. package/src/core/link-inventory-opaque.ts +244 -0
  40. package/src/core/link-inventory-types.ts +47 -0
  41. package/src/core/link-inventory.ts +182 -0
  42. package/src/core/link-relevance.ts +150 -0
  43. package/src/core/section-parse.ts +187 -0
  44. package/src/core/section-target-link.ts +154 -0
  45. package/src/core/section-target-resolve.ts +351 -0
  46. package/src/core/section-target-transport.ts +519 -0
  47. package/src/core/section-target.ts +263 -0
  48. package/src/core/sections.ts +60 -115
  49. package/src/mcp/AGENTS.md +1 -0
  50. package/src/mcp/CLAUDE.md +1 -0
  51. package/src/mcp/http-egress.ts +1 -0
  52. package/src/mcp/tools/index.ts +37 -17
  53. package/src/mcp/tools/sections.ts +512 -0
  54. package/src/mcp/tools/workspace-write.ts +215 -97
  55. package/src/sdk/client.ts +238 -116
  56. package/src/sdk/index.ts +12 -0
  57. package/src/sdk/types.ts +61 -3
  58. package/src/serve/file-refactor-http.ts +239 -0
  59. package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
  60. package/src/serve/public/globals.built.css +1 -1
  61. package/src/serve/public/lib/section-links.ts +189 -0
  62. package/src/serve/public/pages/DocView.tsx +395 -77
  63. package/src/serve/routes/api.ts +191 -104
  64. package/src/serve/routes/section-targets.ts +221 -0
  65. package/src/serve/server.ts +34 -0
  66. package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
  67. package/src/store/migrations/index.ts +2 -0
  68. package/src/store/sqlite/adapter.ts +452 -0
  69. package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
  70. package/src/store/types.ts +84 -0
  71. package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +0 -1
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Transport-neutral SDK/MCP adapter for canonical reference-safe rename/move.
3
+ *
4
+ * Surfaces must not implement link rewrite logic — they resolve targets, call
5
+ * the core planner/apply service, and expose typed content-free results.
6
+ * Do not import from `src/serve` (REST remains a separate adapter).
7
+ *
8
+ * @module src/core/file-refactor-adapter
9
+ */
10
+
11
+ // node:path join — no Bun path utils
12
+ import { join } from "node:path";
13
+
14
+ import type { Collection } from "../config/types";
15
+ import type { DocumentRow } from "../store/types";
16
+ import type {
17
+ FileRefactorApplyResult,
18
+ FileRefactorOperation,
19
+ FileRefactorPreviewPlan,
20
+ FileRefactorReasonCode,
21
+ } from "./file-refactor-contract";
22
+ import type { FileRefactorSnapshotLike } from "./file-refactor-from-snapshot";
23
+ import type { ApplyFileRefactorDeps } from "./file-refactor-service";
24
+
25
+ import { isRefactorFingerprint } from "./file-refactor-apply-safety";
26
+ import {
27
+ FILE_REFACTOR_APPLY_CONFIRMATION,
28
+ FILE_REFACTOR_SCHEMA_VERSION,
29
+ } from "./file-refactor-contract";
30
+ import { planFileRefactorImpactFromSnapshot } from "./file-refactor-from-snapshot";
31
+ import { planMoveRefactor, planRenameRefactor } from "./file-refactor-paths";
32
+ import { planFileRefactorImpact } from "./file-refactor-planner";
33
+ import {
34
+ applyFileRefactor,
35
+ journalPortFromStore,
36
+ } from "./file-refactor-service";
37
+
38
+ export const FILE_REFACTOR_COLLECTION_LOCK_NAME = ".gno-refactor.lock";
39
+
40
+ export interface FileRefactorPathTarget {
41
+ nextRelPath: string;
42
+ nextUri: string;
43
+ }
44
+
45
+ export interface BuildCanonicalRefactorPlanInput {
46
+ operation: FileRefactorOperation;
47
+ doc: Pick<
48
+ DocumentRow,
49
+ "id" | "uri" | "relPath" | "collection" | "title" | "mirrorHash"
50
+ >;
51
+ collection: Collection;
52
+ /** Absolute path of the live source file. */
53
+ sourceFullPath: string;
54
+ target: FileRefactorPathTarget;
55
+ store: Partial<{
56
+ getFileRefactorResolutionSnapshot: (input: {
57
+ sourceUri: string;
58
+ }) => Promise<{
59
+ ok: boolean;
60
+ value?: FileRefactorSnapshotLike;
61
+ error?: { message: string };
62
+ }>;
63
+ }> &
64
+ object;
65
+ sourceEditable?: boolean;
66
+ }
67
+
68
+ export interface ParsedRefactorApplyConfirmation {
69
+ planDigest: string;
70
+ confirmation: typeof FILE_REFACTOR_APPLY_CONFIRMATION;
71
+ schemaVersion: typeof FILE_REFACTOR_SCHEMA_VERSION;
72
+ }
73
+
74
+ export function collectionRefactorLockPath(collectionRoot: string): string {
75
+ return join(collectionRoot, FILE_REFACTOR_COLLECTION_LOCK_NAME);
76
+ }
77
+
78
+ export function resolveRenameTarget(input: {
79
+ collection: string;
80
+ currentRelPath: string;
81
+ nextName: string;
82
+ }): FileRefactorPathTarget {
83
+ return planRenameRefactor(input);
84
+ }
85
+
86
+ export function resolveMoveTarget(input: {
87
+ collection: string;
88
+ currentRelPath: string;
89
+ folderPath: string;
90
+ nextName?: string;
91
+ }): FileRefactorPathTarget {
92
+ return planMoveRefactor(input);
93
+ }
94
+
95
+ async function targetOccupiedOnDisk(
96
+ collectionRoot: string,
97
+ targetRelPath: string,
98
+ sourceRelPath: string
99
+ ): Promise<boolean> {
100
+ if (targetRelPath === sourceRelPath) {
101
+ return false;
102
+ }
103
+ return Bun.file(join(collectionRoot, targetRelPath)).exists();
104
+ }
105
+
106
+ async function buildPlanFromLiveDisk(
107
+ input: BuildCanonicalRefactorPlanInput,
108
+ occupied: boolean
109
+ ): Promise<FileRefactorPreviewPlan> {
110
+ const content = await Bun.file(input.sourceFullPath).text();
111
+ return planFileRefactorImpact({
112
+ operation: input.operation,
113
+ source: {
114
+ uri: input.doc.uri,
115
+ relPath: input.doc.relPath,
116
+ collection: input.doc.collection,
117
+ title: input.doc.title,
118
+ content,
119
+ editable: input.sourceEditable ?? true,
120
+ },
121
+ target: {
122
+ uri: input.target.nextUri,
123
+ relPath: input.target.nextRelPath,
124
+ collection: input.collection.name,
125
+ title: input.doc.title,
126
+ },
127
+ documents: [
128
+ {
129
+ id: input.doc.id,
130
+ uri: input.doc.uri,
131
+ relPath: input.doc.relPath,
132
+ collection: input.doc.collection,
133
+ title: input.doc.title,
134
+ active: true,
135
+ },
136
+ ],
137
+ targetOccupied: occupied,
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Build the canonical preview plan for rename/move.
143
+ * Prefers the store resolution snapshot; falls back to live disk content
144
+ * when the snapshot seam is unavailable (focused unit tests / stubs).
145
+ */
146
+ export async function buildCanonicalRefactorPlan(
147
+ input: BuildCanonicalRefactorPlanInput
148
+ ): Promise<FileRefactorPreviewPlan> {
149
+ const occupied = await targetOccupiedOnDisk(
150
+ input.collection.path,
151
+ input.target.nextRelPath,
152
+ input.doc.relPath
153
+ );
154
+
155
+ const snapshotFn = input.store.getFileRefactorResolutionSnapshot;
156
+ if (typeof snapshotFn === "function") {
157
+ const snapshotResult = await snapshotFn.call(input.store, {
158
+ sourceUri: input.doc.uri,
159
+ });
160
+ if (!snapshotResult.ok) {
161
+ throw new Error(
162
+ snapshotResult.error?.message ?? "Failed to load refactor snapshot"
163
+ );
164
+ }
165
+ if (!snapshotResult.value) {
166
+ throw new Error("Refactor resolution snapshot was empty");
167
+ }
168
+ return planFileRefactorImpactFromSnapshot({
169
+ operation: input.operation,
170
+ snapshot: snapshotResult.value,
171
+ target: {
172
+ uri: input.target.nextUri,
173
+ relPath: input.target.nextRelPath,
174
+ collection: input.collection.name,
175
+ title: input.doc.title,
176
+ },
177
+ targetOccupied: occupied,
178
+ sourceEditable: input.sourceEditable,
179
+ });
180
+ }
181
+
182
+ return buildPlanFromLiveDisk(input, occupied);
183
+ }
184
+
185
+ function storeHasJournalPort(
186
+ store: object
187
+ ): store is Parameters<typeof journalPortFromStore>[0] {
188
+ const candidate = store as Record<string, unknown>;
189
+ return (
190
+ typeof candidate.createFileRefactorPreparedReceipt === "function" &&
191
+ typeof candidate.advanceFileRefactorReceipt === "function" &&
192
+ typeof candidate.getFileRefactorReceiptById === "function" &&
193
+ typeof candidate.getLatestFileRefactorReceiptByPlanDigest === "function"
194
+ );
195
+ }
196
+
197
+ /**
198
+ * Production apply deps — durable journal only. Never falls back to memory.
199
+ */
200
+ export function buildDurableFileRefactorApplyDeps(input: {
201
+ collection: Collection;
202
+ store: object;
203
+ syncAfterCommit: ApplyFileRefactorDeps["syncAfterCommit"];
204
+ }): ApplyFileRefactorDeps {
205
+ if (!storeHasJournalPort(input.store)) {
206
+ throw new Error(
207
+ "Durable file-refactor journal store is unavailable on this adapter"
208
+ );
209
+ }
210
+ return {
211
+ collectionRoot: input.collection.path,
212
+ lockPath: collectionRefactorLockPath(input.collection.path),
213
+ journal: journalPortFromStore(input.store),
214
+ syncAfterCommit: input.syncAfterCommit,
215
+ };
216
+ }
217
+
218
+ export function parseRefactorApplyConfirmation(body: {
219
+ planDigest?: unknown;
220
+ confirmation?: unknown;
221
+ schemaVersion?: unknown;
222
+ }): ParsedRefactorApplyConfirmation | { error: string; message: string } {
223
+ if (
224
+ typeof body.planDigest !== "string" ||
225
+ !isRefactorFingerprint(body.planDigest)
226
+ ) {
227
+ return {
228
+ error: "INVALID_INPUT",
229
+ message: "planDigest must be a 64-character lowercase SHA-256 digest",
230
+ };
231
+ }
232
+ if (body.confirmation !== FILE_REFACTOR_APPLY_CONFIRMATION) {
233
+ return {
234
+ error: "INVALID_INPUT",
235
+ message: `confirmation must be "${FILE_REFACTOR_APPLY_CONFIRMATION}"`,
236
+ };
237
+ }
238
+ if (body.schemaVersion !== FILE_REFACTOR_SCHEMA_VERSION) {
239
+ return {
240
+ error: "INVALID_INPUT",
241
+ message: `schemaVersion must be "${FILE_REFACTOR_SCHEMA_VERSION}"`,
242
+ };
243
+ }
244
+
245
+ return {
246
+ planDigest: body.planDigest,
247
+ confirmation: FILE_REFACTOR_APPLY_CONFIRMATION,
248
+ schemaVersion: FILE_REFACTOR_SCHEMA_VERSION,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * A collection sync that reports file failures is not convergence. Throwing
254
+ * lets the apply service preserve the durable commit as sync-pending.
255
+ */
256
+ export function assertFileRefactorSyncConverged(result: {
257
+ filesErrored?: number;
258
+ errors?: readonly unknown[];
259
+ }): void {
260
+ if ((result.filesErrored ?? 0) > 0 || (result.errors?.length ?? 0) > 0) {
261
+ throw new Error("File-refactor index convergence reported sync errors");
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Apply a rename/move through the canonical service.
267
+ * The supplied digest must match the freshly computed plan. Callers must
268
+ * preview first; apply never silently manufactures confirmation.
269
+ */
270
+ export async function applyCanonicalFileRefactor(input: {
271
+ plan: FileRefactorPreviewPlan;
272
+ confirmation: ParsedRefactorApplyConfirmation;
273
+ deps: ApplyFileRefactorDeps;
274
+ signal?: AbortSignal;
275
+ }): Promise<FileRefactorApplyResult> {
276
+ const { plan, confirmation, deps, signal } = input;
277
+
278
+ if (confirmation.planDigest !== plan.planDigest) {
279
+ return {
280
+ schemaVersion: FILE_REFACTOR_SCHEMA_VERSION,
281
+ planDigest: plan.planDigest,
282
+ operation: plan.operation,
283
+ source: plan.source,
284
+ target: plan.target,
285
+ status: "stale_plan",
286
+ reasonCode: "stale_plan",
287
+ filesystem: { state: "unchanged" },
288
+ indexConvergence: { state: "not_attempted" },
289
+ };
290
+ }
291
+
292
+ if (!plan.canApply) {
293
+ const targetAbs = join(deps.collectionRoot, plan.target.relPath);
294
+ const occupied =
295
+ plan.target.relPath !== plan.source.relPath &&
296
+ (await Bun.file(targetAbs).exists());
297
+ const reason =
298
+ plan.safety.blockingReasonCodes[0] ??
299
+ (occupied
300
+ ? ("occupied_target" as FileRefactorReasonCode)
301
+ : ("capability_denied" as FileRefactorReasonCode));
302
+ const status =
303
+ reason === "occupied_target" || reason === "unsafe_target"
304
+ ? ("conflict" as const)
305
+ : ("unsupported" as const);
306
+ return {
307
+ schemaVersion: FILE_REFACTOR_SCHEMA_VERSION,
308
+ planDigest: plan.planDigest,
309
+ operation: plan.operation,
310
+ source: plan.source,
311
+ target: plan.target,
312
+ status,
313
+ reasonCode: reason,
314
+ filesystem: { state: "unchanged" },
315
+ indexConvergence: { state: "not_attempted" },
316
+ };
317
+ }
318
+
319
+ return applyFileRefactor(
320
+ plan,
321
+ {
322
+ schemaVersion: FILE_REFACTOR_SCHEMA_VERSION,
323
+ planDigest: plan.planDigest,
324
+ confirmation: FILE_REFACTOR_APPLY_CONFIRMATION,
325
+ },
326
+ deps,
327
+ signal
328
+ );
329
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Destination-only multi-span edits for reference-safe apply.
3
+ *
4
+ * @module src/core/file-refactor-apply-edits
5
+ */
6
+
7
+ import type { FileRefactorDestinationSpan } from "./file-refactor-contract";
8
+
9
+ import { applyDestinationOnlyEdit } from "./file-refactor-contract";
10
+
11
+ export class FileRefactorEditError extends Error {
12
+ readonly code: "stale_span" | "overlapping_spans";
13
+
14
+ constructor(code: "stale_span" | "overlapping_spans", message: string) {
15
+ super(message);
16
+ this.name = "FileRefactorEditError";
17
+ this.code = code;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Apply destination-only edits in descending UTF-16 offsets.
23
+ * Verifies non-overlap against original coordinates and exact original slices.
24
+ */
25
+ export function applyDestinationEditsDescending(
26
+ content: string,
27
+ edits: FileRefactorDestinationSpan[]
28
+ ): string {
29
+ if (edits.length === 0) return content;
30
+
31
+ const ascending = [...edits].sort(
32
+ (left, right) => left.startOffset - right.startOffset
33
+ );
34
+ for (let index = 1; index < ascending.length; index += 1) {
35
+ const prev = ascending[index - 1];
36
+ const next = ascending[index];
37
+ if (!prev || !next) continue;
38
+ if (next.startOffset < prev.endOffset) {
39
+ throw new FileRefactorEditError(
40
+ "overlapping_spans",
41
+ "Destination edit spans overlap"
42
+ );
43
+ }
44
+ }
45
+
46
+ const descending = [...edits].sort(
47
+ (left, right) => right.startOffset - left.startOffset
48
+ );
49
+ let result = content;
50
+ for (const edit of descending) {
51
+ try {
52
+ result = applyDestinationOnlyEdit(result, edit);
53
+ } catch (cause) {
54
+ throw new FileRefactorEditError(
55
+ "stale_span",
56
+ cause instanceof Error ? cause.message : "Stale destination span"
57
+ );
58
+ }
59
+ }
60
+ return result;
61
+ }