@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,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
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Canonical refactor impact preview for rename/move dialogs.
3
+ * Scholarly Dusk vocabulary: rail-style labels, mono paths, amber caution.
4
+ *
5
+ * @module src/serve/public/components/RefactorImpactPreview
6
+ */
7
+
8
+ import type { FileRefactorPreviewPlan } from "../../../core/file-refactor-contract";
9
+
10
+ const UNRESOLVED_CLASSIFICATIONS = new Set([
11
+ "ambiguous",
12
+ "unsupported",
13
+ "malformed",
14
+ "invalid",
15
+ ]);
16
+
17
+ function shortDigest(digest: string): string {
18
+ return `${digest.slice(0, 8)}…${digest.slice(-6)}`;
19
+ }
20
+
21
+ export interface RefactorImpactPreviewProps {
22
+ plan: FileRefactorPreviewPlan | null;
23
+ loading?: boolean;
24
+ confirmed: boolean;
25
+ onConfirmedChange: (confirmed: boolean) => void;
26
+ /** Accessible outcome message after apply (sync-pending, recovery, etc.). */
27
+ outcomeMessage?: string | null;
28
+ outcomeTone?: "warning" | "error" | "success";
29
+ }
30
+
31
+ export function RefactorImpactPreview({
32
+ plan,
33
+ loading = false,
34
+ confirmed,
35
+ onConfirmedChange,
36
+ outcomeMessage,
37
+ outcomeTone = "warning",
38
+ }: RefactorImpactPreviewProps) {
39
+ if (loading && !plan) {
40
+ return (
41
+ <div
42
+ aria-busy="true"
43
+ aria-live="polite"
44
+ className="rounded-md border border-border/30 bg-muted/20 px-3 py-2 font-mono text-[11px] text-muted-foreground"
45
+ >
46
+ Computing reference impact…
47
+ </div>
48
+ );
49
+ }
50
+
51
+ if (!plan) {
52
+ return null;
53
+ }
54
+
55
+ const rewriteable = plan.examinedReferences.filter(
56
+ (ref) => ref.classification === "rewriteable"
57
+ );
58
+ const unresolved = plan.examinedReferences.filter((ref) =>
59
+ UNRESOLVED_CLASSIFICATIONS.has(ref.classification)
60
+ );
61
+ const affectedPaths = plan.affectedDocuments.map((doc) => doc.relPath);
62
+ const confirmId = `refactor-confirm-${plan.planDigest.slice(0, 12)}`;
63
+
64
+ const outcomeClass =
65
+ outcomeTone === "error"
66
+ ? "border-destructive/40 bg-destructive/10 text-destructive"
67
+ : outcomeTone === "success"
68
+ ? "border-primary/30 bg-primary/10 text-primary"
69
+ : "border-amber-500/30 bg-amber-500/10 text-amber-500";
70
+
71
+ return (
72
+ <div className="max-h-[40vh] space-y-3 overflow-y-auto pr-1">
73
+ <div className="space-y-1">
74
+ <div className="font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
75
+ Safety
76
+ </div>
77
+ <div
78
+ aria-live="polite"
79
+ className={
80
+ plan.canApply
81
+ ? "rounded-md border border-primary/25 bg-primary/10 px-3 py-2 text-[13px] text-primary"
82
+ : "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[13px] text-destructive"
83
+ }
84
+ role="status"
85
+ >
86
+ {plan.canApply
87
+ ? "Plan is safe to apply with the exact digest below."
88
+ : `Cannot apply: ${(plan.safety.blockingReasonCodes[0] ?? "blocked").replaceAll("_", " ")}.`}
89
+ </div>
90
+ <p className="font-mono text-[11px] text-muted-foreground/70 break-all">
91
+ Digest {shortDigest(plan.planDigest)}
92
+ </p>
93
+ </div>
94
+
95
+ <div className="space-y-1">
96
+ <div className="font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
97
+ Target
98
+ </div>
99
+ <p className="font-mono text-[11px] text-muted-foreground/80 break-all">
100
+ {plan.source.relPath} → {plan.target.relPath}
101
+ </p>
102
+ <p className="font-mono text-[10px] text-muted-foreground/50 tabular-nums">
103
+ {plan.safety.rewriteableCount} rewrite
104
+ {plan.safety.rewriteableCount === 1 ? "" : "s"} ·{" "}
105
+ {plan.safety.unchangedCount} unchanged · {unresolved.length}{" "}
106
+ unresolved
107
+ </p>
108
+ </div>
109
+
110
+ {affectedPaths.length > 0 && (
111
+ <div className="space-y-1">
112
+ <div className="font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
113
+ Affected documents
114
+ </div>
115
+ <ul className="space-y-0.5">
116
+ {affectedPaths.map((path) => (
117
+ <li
118
+ className="font-mono text-[11px] text-muted-foreground/80 break-all"
119
+ key={path}
120
+ >
121
+ {path}
122
+ </li>
123
+ ))}
124
+ </ul>
125
+ </div>
126
+ )}
127
+
128
+ {rewriteable.length > 0 && (
129
+ <div className="space-y-1">
130
+ <div className="font-mono text-[10px] text-muted-foreground/60 uppercase tracking-[0.15em]">
131
+ Rewrites
132
+ </div>
133
+ <ul className="space-y-1">
134
+ {rewriteable.slice(0, 12).map((ref) => (
135
+ <li
136
+ className="rounded-sm px-2 py-1 font-mono text-[11px] text-muted-foreground hover:bg-muted/20"
137
+ key={`${ref.documentRelPath}:${ref.startLine ?? 0}:${ref.startCol ?? 0}:${ref.originalDestination ?? ""}`}
138
+ >
139
+ <span className="text-foreground/90">
140
+ {ref.documentRelPath}
141
+ </span>
142
+ <span className="text-muted-foreground/50"> · </span>
143
+ <span>
144
+ {ref.originalDestination ?? "?"} →{" "}
145
+ {ref.proposedDestination ?? "?"}
146
+ </span>
147
+ </li>
148
+ ))}
149
+ {rewriteable.length > 12 && (
150
+ <li className="font-mono text-[10px] text-muted-foreground/50">
151
+ +{rewriteable.length - 12} more
152
+ </li>
153
+ )}
154
+ </ul>
155
+ </div>
156
+ )}
157
+
158
+ {unresolved.length > 0 && (
159
+ <div className="space-y-1">
160
+ <div className="font-mono text-[10px] text-amber-500/70 uppercase tracking-[0.15em]">
161
+ Unresolved / unsupported
162
+ </div>
163
+ <ul className="space-y-1">
164
+ {unresolved.slice(0, 12).map((ref) => (
165
+ <li
166
+ className="rounded-sm border border-amber-500/20 bg-amber-500/5 px-2 py-1 font-mono text-[11px] text-amber-500/90"
167
+ key={`${ref.documentRelPath}:${ref.classification}:${ref.startLine ?? 0}:${ref.originalDestination ?? ""}`}
168
+ >
169
+ <span>
170
+ {ref.documentRelPath}
171
+ {ref.startLine ? `:${ref.startLine}` : ""}
172
+ </span>
173
+ <span className="opacity-60"> · {ref.classification}</span>
174
+ {ref.reasonCode && (
175
+ <span className="opacity-60"> · {ref.reasonCode}</span>
176
+ )}
177
+ </li>
178
+ ))}
179
+ {unresolved.length > 12 && (
180
+ <li className="font-mono text-[10px] text-amber-500/60">
181
+ +{unresolved.length - 12} more
182
+ </li>
183
+ )}
184
+ </ul>
185
+ </div>
186
+ )}
187
+
188
+ {plan.safety.warnings.length > 0 && (
189
+ <div
190
+ className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-amber-500 text-sm"
191
+ role="status"
192
+ >
193
+ {plan.safety.warnings.map((warning) => (
194
+ <div key={warning}>{warning}</div>
195
+ ))}
196
+ </div>
197
+ )}
198
+
199
+ <div className="flex items-start gap-2 rounded-md border border-border/30 bg-muted/15 px-3 py-2">
200
+ <input
201
+ checked={confirmed}
202
+ className="mt-0.5 size-4 accent-primary"
203
+ disabled={!plan.canApply}
204
+ id={confirmId}
205
+ onChange={(event) => onConfirmedChange(event.target.checked)}
206
+ type="checkbox"
207
+ />
208
+ <label className="text-[13px] leading-snug" htmlFor={confirmId}>
209
+ Confirm exact plan digest{" "}
210
+ <span className="font-mono text-[11px] text-muted-foreground">
211
+ {shortDigest(plan.planDigest)}
212
+ </span>
213
+ </label>
214
+ </div>
215
+
216
+ {outcomeMessage && (
217
+ <div
218
+ aria-live="assertive"
219
+ className={`rounded-md border px-3 py-2 text-sm ${outcomeClass}`}
220
+ role="alert"
221
+ >
222
+ {outcomeMessage}
223
+ </div>
224
+ )}
225
+ </div>
226
+ );
227
+ }