@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.
- package/README.md +6 -5
- package/assets/skill/SKILL.md +25 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.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 +176 -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/spec/output-schemas/section-target-create-result.schema.json +20 -0
- package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
- package/spec/output-schemas/section-target.schema.json +118 -0
- package/spec/output-schemas/section.schema.json +113 -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/core/section-parse.ts +187 -0
- package/src/core/section-target-link.ts +154 -0
- package/src/core/section-target-resolve.ts +351 -0
- package/src/core/section-target-transport.ts +519 -0
- package/src/core/section-target.ts +263 -0
- package/src/core/sections.ts +60 -115
- package/src/mcp/AGENTS.md +1 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +37 -17
- package/src/mcp/tools/sections.ts +512 -0
- package/src/mcp/tools/workspace-write.ts +215 -97
- package/src/sdk/client.ts +238 -116
- package/src/sdk/index.ts +12 -0
- package/src/sdk/types.ts +61 -3
- 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/lib/section-links.ts +189 -0
- package/src/serve/public/pages/DocView.tsx +395 -77
- package/src/serve/routes/api.ts +191 -104
- package/src/serve/routes/section-targets.ts +221 -0
- package/src/serve/server.ts +34 -0
- 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.30.7.zip.sha256 +0 -1
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-neutral collection-scoped apply service for reference-safe refactors.
|
|
3
|
+
*
|
|
4
|
+
* Validates an exact preview plan, stages/commits or restores the full file set,
|
|
5
|
+
* records a content-free recovery receipt, then drives index convergence.
|
|
6
|
+
*
|
|
7
|
+
* @module src/core/file-refactor-service
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { WriteLockHandle } from "./file-lock";
|
|
11
|
+
import type {
|
|
12
|
+
FileRefactorApplyRequest,
|
|
13
|
+
FileRefactorApplyResult,
|
|
14
|
+
FileRefactorPreviewPlan,
|
|
15
|
+
FileRefactorReasonCode,
|
|
16
|
+
} from "./file-refactor-contract";
|
|
17
|
+
import type {
|
|
18
|
+
FileRefactorJournalFileEntry,
|
|
19
|
+
FileRefactorJournalPort,
|
|
20
|
+
FileRefactorRecoveryReceipt,
|
|
21
|
+
} from "./file-refactor-journal";
|
|
22
|
+
|
|
23
|
+
import { acquireWriteLock } from "./file-lock";
|
|
24
|
+
import { removePathRequired } from "./file-ops";
|
|
25
|
+
import {
|
|
26
|
+
assignRefactorArtifactPaths,
|
|
27
|
+
cleanupAfterSuccessfulCommit,
|
|
28
|
+
cleanupReceiptArtifacts,
|
|
29
|
+
commitRefactorFiles,
|
|
30
|
+
rollbackRefactorFiles,
|
|
31
|
+
stageRefactorFiles,
|
|
32
|
+
type FileRefactorBoundaryHook,
|
|
33
|
+
type PreparedRefactorFile,
|
|
34
|
+
type StagedRefactorFile,
|
|
35
|
+
} from "./file-refactor-apply-fs";
|
|
36
|
+
import {
|
|
37
|
+
validateRefactorJournalId,
|
|
38
|
+
type RemoveOwnedEmptyDirsDeps,
|
|
39
|
+
} from "./file-refactor-apply-safety";
|
|
40
|
+
import {
|
|
41
|
+
baseResult,
|
|
42
|
+
unchangedResult,
|
|
43
|
+
validateLiveRefactorPlan,
|
|
44
|
+
validateRefactorPlanStructure,
|
|
45
|
+
} from "./file-refactor-apply-validate";
|
|
46
|
+
import {
|
|
47
|
+
computeFileRefactorPlanDigest,
|
|
48
|
+
FILE_REFACTOR_APPLY_CONFIRMATION,
|
|
49
|
+
FILE_REFACTOR_SCHEMA_VERSION,
|
|
50
|
+
} from "./file-refactor-contract";
|
|
51
|
+
import {
|
|
52
|
+
FILE_REFACTOR_SYNC_PENDING_INSTRUCTION,
|
|
53
|
+
isCommittedFilesystemPhase,
|
|
54
|
+
isUncertainJournalPhase,
|
|
55
|
+
} from "./file-refactor-journal";
|
|
56
|
+
|
|
57
|
+
export type { FileRefactorBoundaryHook } from "./file-refactor-apply-fs";
|
|
58
|
+
export {
|
|
59
|
+
createMemoryFileRefactorJournal,
|
|
60
|
+
journalPortFromStore,
|
|
61
|
+
} from "./file-refactor-journal-port";
|
|
62
|
+
|
|
63
|
+
export type FileRefactorSyncCallback = (input: {
|
|
64
|
+
plan: FileRefactorPreviewPlan;
|
|
65
|
+
receipt: FileRefactorRecoveryReceipt;
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
}) => Promise<void>;
|
|
68
|
+
|
|
69
|
+
export interface ApplyFileRefactorDeps {
|
|
70
|
+
collectionRoot: string;
|
|
71
|
+
lockPath: string;
|
|
72
|
+
journal: FileRefactorJournalPort;
|
|
73
|
+
syncAfterCommit: FileRefactorSyncCallback;
|
|
74
|
+
acquireLock?: typeof acquireWriteLock;
|
|
75
|
+
lockTimeoutMs?: number;
|
|
76
|
+
onBoundary?: FileRefactorBoundaryHook;
|
|
77
|
+
nowMs?: () => number;
|
|
78
|
+
createJournalId?: () => string;
|
|
79
|
+
/** Injectable required removal for race/failure injection tests. */
|
|
80
|
+
removePathRequired?: typeof removePathRequired;
|
|
81
|
+
/** Injectable rmdir for owned-directory cleanup failure seams. */
|
|
82
|
+
rmdir?: RemoveOwnedEmptyDirsDeps["rmdir"];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function receiptFileEntries(
|
|
86
|
+
files: PreparedRefactorFile[]
|
|
87
|
+
): FileRefactorJournalFileEntry[] {
|
|
88
|
+
return files.map((file) => ({
|
|
89
|
+
role: file.role === "source" ? "source" : "affected",
|
|
90
|
+
relPath:
|
|
91
|
+
file.role === "source"
|
|
92
|
+
? (file.sourceRelPath ?? file.relPath)
|
|
93
|
+
: file.relPath,
|
|
94
|
+
stageRelPath: file.stageRelPath,
|
|
95
|
+
backupRelPath: file.backupRelPath,
|
|
96
|
+
originalFingerprint: file.originalFingerprint,
|
|
97
|
+
expectedFingerprint: file.expectedFingerprint,
|
|
98
|
+
status: "pending" as const,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function updateEntriesByRelPath(
|
|
103
|
+
entries: FileRefactorJournalFileEntry[],
|
|
104
|
+
relPath: string,
|
|
105
|
+
status: FileRefactorJournalFileEntry["status"],
|
|
106
|
+
prepared: PreparedRefactorFile[]
|
|
107
|
+
): FileRefactorJournalFileEntry[] {
|
|
108
|
+
const match = prepared.find(
|
|
109
|
+
(file) => file.relPath === relPath || file.sourceRelPath === relPath
|
|
110
|
+
);
|
|
111
|
+
const keys = new Set<string>([relPath]);
|
|
112
|
+
if (match?.sourceRelPath) keys.add(match.sourceRelPath);
|
|
113
|
+
if (match) keys.add(match.relPath);
|
|
114
|
+
return entries.map((entry) =>
|
|
115
|
+
keys.has(entry.relPath) ? { ...entry, status } : entry
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runSyncOnly(
|
|
120
|
+
plan: FileRefactorPreviewPlan,
|
|
121
|
+
receipt: FileRefactorRecoveryReceipt,
|
|
122
|
+
deps: ApplyFileRefactorDeps,
|
|
123
|
+
signal?: AbortSignal
|
|
124
|
+
): Promise<FileRefactorApplyResult> {
|
|
125
|
+
const stamp = (): number =>
|
|
126
|
+
Math.max((deps.nowMs ?? Date.now)(), receipt.updatedAtMs + 1);
|
|
127
|
+
|
|
128
|
+
const syncPending = (): FileRefactorApplyResult => ({
|
|
129
|
+
...baseResult(plan),
|
|
130
|
+
status: "applied_with_sync_pending",
|
|
131
|
+
reasonCode: "sync_pending",
|
|
132
|
+
filesystem: {
|
|
133
|
+
state: "committed",
|
|
134
|
+
recoveryJournalId: receipt.journalId,
|
|
135
|
+
},
|
|
136
|
+
indexConvergence: {
|
|
137
|
+
state: "pending",
|
|
138
|
+
recoveryInstruction: FILE_REFACTOR_SYNC_PENDING_INSTRUCTION,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await deps.syncAfterCommit({ plan, receipt, signal });
|
|
144
|
+
try {
|
|
145
|
+
const converged = await deps.journal.advanceReceipt(receipt.journalId, {
|
|
146
|
+
phase: "converged",
|
|
147
|
+
filesystemState: "committed",
|
|
148
|
+
indexState: "converged",
|
|
149
|
+
updatedAtMs: stamp(),
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
...baseResult(plan),
|
|
153
|
+
status: "applied",
|
|
154
|
+
filesystem: {
|
|
155
|
+
state: "committed",
|
|
156
|
+
recoveryJournalId: converged.journalId,
|
|
157
|
+
},
|
|
158
|
+
indexConvergence: { state: "converged" },
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
return syncPending();
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
try {
|
|
165
|
+
await deps.journal.advanceReceipt(receipt.journalId, {
|
|
166
|
+
phase: "sync_pending",
|
|
167
|
+
filesystemState: "committed",
|
|
168
|
+
indexState: "pending",
|
|
169
|
+
updatedAtMs: stamp(),
|
|
170
|
+
});
|
|
171
|
+
} catch {
|
|
172
|
+
/* preserve mutation-free sync-pending result even if journal write fails */
|
|
173
|
+
}
|
|
174
|
+
return syncPending();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function recoveryRequiredResult(
|
|
179
|
+
plan: FileRefactorPreviewPlan,
|
|
180
|
+
journalId: string
|
|
181
|
+
): FileRefactorApplyResult {
|
|
182
|
+
return {
|
|
183
|
+
...baseResult(plan),
|
|
184
|
+
status: "failed_rolled_back",
|
|
185
|
+
reasonCode: "rollback_recovery_required",
|
|
186
|
+
filesystem: { state: "recovery_required", recoveryJournalId: journalId },
|
|
187
|
+
indexConvergence: { state: "not_attempted" },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function validateApplyRequest(
|
|
192
|
+
plan: FileRefactorPreviewPlan,
|
|
193
|
+
request: FileRefactorApplyRequest
|
|
194
|
+
): FileRefactorApplyResult | null {
|
|
195
|
+
if (request.schemaVersion !== FILE_REFACTOR_SCHEMA_VERSION) {
|
|
196
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
197
|
+
}
|
|
198
|
+
if (request.confirmation !== FILE_REFACTOR_APPLY_CONFIRMATION) {
|
|
199
|
+
return unchangedResult(plan, "unsupported", "capability_denied");
|
|
200
|
+
}
|
|
201
|
+
if (request.planDigest !== plan.planDigest) {
|
|
202
|
+
return unchangedResult(plan, "stale_plan", "stale_plan");
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function validatePlanIntegrity(
|
|
208
|
+
plan: FileRefactorPreviewPlan
|
|
209
|
+
): Promise<FileRefactorApplyResult | null> {
|
|
210
|
+
const structural = validateRefactorPlanStructure(plan);
|
|
211
|
+
if (structural) return structural;
|
|
212
|
+
|
|
213
|
+
const { planDigest: _ignored, ...material } = plan;
|
|
214
|
+
const recomputed = await computeFileRefactorPlanDigest(material);
|
|
215
|
+
if (recomputed !== plan.planDigest) {
|
|
216
|
+
return unchangedResult(plan, "stale_plan", "stale_plan");
|
|
217
|
+
}
|
|
218
|
+
if (!plan.canApply) {
|
|
219
|
+
const reason =
|
|
220
|
+
plan.safety.blockingReasonCodes[0] ??
|
|
221
|
+
("capability_denied" as FileRefactorReasonCode);
|
|
222
|
+
return unchangedResult(plan, "unsupported", reason);
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Apply an already-produced preview plan with an exact apply request.
|
|
229
|
+
* Never throws for ordinary expected conflicts after partial work.
|
|
230
|
+
*/
|
|
231
|
+
export async function applyFileRefactor(
|
|
232
|
+
plan: FileRefactorPreviewPlan,
|
|
233
|
+
request: FileRefactorApplyRequest,
|
|
234
|
+
deps: ApplyFileRefactorDeps,
|
|
235
|
+
signal?: AbortSignal
|
|
236
|
+
): Promise<FileRefactorApplyResult> {
|
|
237
|
+
const acquire = deps.acquireLock ?? acquireWriteLock;
|
|
238
|
+
const lock = await acquire(deps.lockPath, deps.lockTimeoutMs ?? 5000);
|
|
239
|
+
if (!lock) {
|
|
240
|
+
return unchangedResult(plan, "conflict", "unsafe_target");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
return await applyUnderLock(plan, request, deps, lock, signal);
|
|
245
|
+
} finally {
|
|
246
|
+
await lock.release();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function applyUnderLock(
|
|
251
|
+
plan: FileRefactorPreviewPlan,
|
|
252
|
+
request: FileRefactorApplyRequest,
|
|
253
|
+
deps: ApplyFileRefactorDeps,
|
|
254
|
+
_lock: WriteLockHandle,
|
|
255
|
+
signal?: AbortSignal
|
|
256
|
+
): Promise<FileRefactorApplyResult> {
|
|
257
|
+
const nowMs = deps.nowMs ?? Date.now;
|
|
258
|
+
|
|
259
|
+
const requestError = validateApplyRequest(plan, request);
|
|
260
|
+
if (requestError) return requestError;
|
|
261
|
+
|
|
262
|
+
const integrityError = await validatePlanIntegrity(plan);
|
|
263
|
+
if (integrityError) return integrityError;
|
|
264
|
+
|
|
265
|
+
if (signal?.aborted) {
|
|
266
|
+
return unchangedResult(plan, "conflict", "unsafe_target");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const existing = await deps.journal.getLatestReceiptByPlanDigest(
|
|
270
|
+
plan.planDigest
|
|
271
|
+
);
|
|
272
|
+
if (existing) {
|
|
273
|
+
if (isUncertainJournalPhase(existing.phase)) {
|
|
274
|
+
return recoveryRequiredResult(plan, existing.journalId);
|
|
275
|
+
}
|
|
276
|
+
if (isCommittedFilesystemPhase(existing.phase)) {
|
|
277
|
+
if (existing.phase === "converged") {
|
|
278
|
+
return {
|
|
279
|
+
...baseResult(plan),
|
|
280
|
+
status: "applied",
|
|
281
|
+
filesystem: {
|
|
282
|
+
state: "committed",
|
|
283
|
+
recoveryJournalId: existing.journalId,
|
|
284
|
+
},
|
|
285
|
+
indexConvergence: { state: "converged" },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return runSyncOnly(plan, existing, deps, signal);
|
|
289
|
+
}
|
|
290
|
+
if (existing.phase === "aborted" || existing.phase === "rolled_back") {
|
|
291
|
+
const cleaned = await cleanupReceiptArtifacts(
|
|
292
|
+
deps.collectionRoot,
|
|
293
|
+
existing.fileEntries.map((entry) => entry.stageRelPath),
|
|
294
|
+
existing.fileEntries.map((entry) => entry.backupRelPath),
|
|
295
|
+
deps.removePathRequired ?? removePathRequired
|
|
296
|
+
);
|
|
297
|
+
if (!cleaned) {
|
|
298
|
+
return recoveryRequiredResult(plan, existing.journalId);
|
|
299
|
+
}
|
|
300
|
+
// Fall through to a new clean attempt after artifact cleanup.
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const validated = await validateLiveRefactorPlan(plan, deps.collectionRoot);
|
|
305
|
+
if (!validated.ok) return validated.result;
|
|
306
|
+
|
|
307
|
+
if (signal?.aborted) {
|
|
308
|
+
return unchangedResult(plan, "conflict", "unsafe_target");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const journalIdRaw = (deps.createJournalId ?? (() => crypto.randomUUID()))();
|
|
312
|
+
let journalId: string;
|
|
313
|
+
try {
|
|
314
|
+
journalId = validateRefactorJournalId(journalIdRaw);
|
|
315
|
+
} catch {
|
|
316
|
+
return unchangedResult(plan, "unsupported", "unsafe_target");
|
|
317
|
+
}
|
|
318
|
+
const prepared = assignRefactorArtifactPaths(
|
|
319
|
+
validated.files,
|
|
320
|
+
deps.collectionRoot,
|
|
321
|
+
journalId
|
|
322
|
+
);
|
|
323
|
+
let fileEntries = receiptFileEntries(prepared);
|
|
324
|
+
|
|
325
|
+
await deps.journal.createPreparedReceipt({
|
|
326
|
+
journalId,
|
|
327
|
+
planDigest: plan.planDigest,
|
|
328
|
+
collection: plan.source.collection,
|
|
329
|
+
operation: plan.operation,
|
|
330
|
+
sourceRelPath: plan.source.relPath,
|
|
331
|
+
targetRelPath: plan.target.relPath,
|
|
332
|
+
fileEntries,
|
|
333
|
+
createdAtMs: nowMs(),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
let staged: StagedRefactorFile[] = [];
|
|
337
|
+
let commitStarted = false;
|
|
338
|
+
let activePhase: "staging" | "committing" | "rolling_back" = "staging";
|
|
339
|
+
let clock = nowMs();
|
|
340
|
+
const nextTs = (): number => {
|
|
341
|
+
clock += 1;
|
|
342
|
+
return clock;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const fsHooks = {
|
|
346
|
+
onBoundary: deps.onBoundary,
|
|
347
|
+
removePathRequired: deps.removePathRequired,
|
|
348
|
+
collectionRoot: deps.collectionRoot,
|
|
349
|
+
rmdir: deps.rmdir,
|
|
350
|
+
onFileProgress: async (update: {
|
|
351
|
+
relPath: string;
|
|
352
|
+
status: FileRefactorJournalFileEntry["status"];
|
|
353
|
+
}) => {
|
|
354
|
+
fileEntries = updateEntriesByRelPath(
|
|
355
|
+
fileEntries,
|
|
356
|
+
update.relPath,
|
|
357
|
+
update.status,
|
|
358
|
+
prepared
|
|
359
|
+
);
|
|
360
|
+
if (activePhase === "rolling_back") return;
|
|
361
|
+
await deps.journal.advanceReceipt(journalId, {
|
|
362
|
+
phase: activePhase,
|
|
363
|
+
fileEntries,
|
|
364
|
+
updatedAtMs: nextTs(),
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
await deps.journal.advanceReceipt(journalId, {
|
|
371
|
+
phase: "staging",
|
|
372
|
+
updatedAtMs: nextTs(),
|
|
373
|
+
});
|
|
374
|
+
staged = await stageRefactorFiles(prepared, fsHooks, staged);
|
|
375
|
+
|
|
376
|
+
if (deps.onBoundary) {
|
|
377
|
+
await deps.onBoundary({ kind: "before_commit" });
|
|
378
|
+
}
|
|
379
|
+
if (signal?.aborted) {
|
|
380
|
+
await rollbackRefactorFiles(staged, false, fsHooks);
|
|
381
|
+
await deps.journal.advanceReceipt(journalId, {
|
|
382
|
+
phase: "aborted",
|
|
383
|
+
filesystemState: "unchanged",
|
|
384
|
+
indexState: "not_attempted",
|
|
385
|
+
updatedAtMs: nextTs(),
|
|
386
|
+
});
|
|
387
|
+
return unchangedResult(plan, "conflict", "unsafe_target", journalId);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
activePhase = "committing";
|
|
391
|
+
await deps.journal.advanceReceipt(journalId, {
|
|
392
|
+
phase: "committing",
|
|
393
|
+
updatedAtMs: nextTs(),
|
|
394
|
+
});
|
|
395
|
+
commitStarted = true;
|
|
396
|
+
await commitRefactorFiles(staged, fsHooks);
|
|
397
|
+
|
|
398
|
+
fileEntries = fileEntries.map((entry) => ({
|
|
399
|
+
...entry,
|
|
400
|
+
status: "committed" as const,
|
|
401
|
+
}));
|
|
402
|
+
await deps.journal.advanceReceipt(journalId, {
|
|
403
|
+
phase: "committed",
|
|
404
|
+
filesystemState: "committed",
|
|
405
|
+
indexState: "not_attempted",
|
|
406
|
+
fileEntries,
|
|
407
|
+
updatedAtMs: nextTs(),
|
|
408
|
+
});
|
|
409
|
+
await cleanupAfterSuccessfulCommit(staged);
|
|
410
|
+
|
|
411
|
+
const committedReceipt = await deps.journal.getReceiptById(journalId);
|
|
412
|
+
if (!committedReceipt) {
|
|
413
|
+
return recoveryRequiredResult(plan, journalId);
|
|
414
|
+
}
|
|
415
|
+
return runSyncOnly(plan, committedReceipt, deps, signal);
|
|
416
|
+
} catch {
|
|
417
|
+
let verified = false;
|
|
418
|
+
try {
|
|
419
|
+
activePhase = "rolling_back";
|
|
420
|
+
try {
|
|
421
|
+
await deps.journal.advanceReceipt(journalId, {
|
|
422
|
+
phase: "rolling_back",
|
|
423
|
+
updatedAtMs: nextTs(),
|
|
424
|
+
});
|
|
425
|
+
} catch {
|
|
426
|
+
/* continue rollback even if journal advance fails */
|
|
427
|
+
}
|
|
428
|
+
verified = (await rollbackRefactorFiles(staged, commitStarted, fsHooks))
|
|
429
|
+
.verified;
|
|
430
|
+
} catch {
|
|
431
|
+
verified = false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (verified) {
|
|
435
|
+
try {
|
|
436
|
+
await deps.journal.advanceReceipt(journalId, {
|
|
437
|
+
phase: "rolled_back",
|
|
438
|
+
filesystemState: "rolled_back",
|
|
439
|
+
indexState: "not_attempted",
|
|
440
|
+
fileEntries,
|
|
441
|
+
updatedAtMs: nextTs(),
|
|
442
|
+
});
|
|
443
|
+
return {
|
|
444
|
+
...baseResult(plan),
|
|
445
|
+
status: "failed_rolled_back",
|
|
446
|
+
reasonCode: "filesystem_commit_failed",
|
|
447
|
+
filesystem: { state: "rolled_back", recoveryJournalId: journalId },
|
|
448
|
+
indexConvergence: { state: "not_attempted" },
|
|
449
|
+
};
|
|
450
|
+
} catch {
|
|
451
|
+
return recoveryRequiredResult(plan, journalId);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
await deps.journal.advanceReceipt(journalId, {
|
|
457
|
+
phase: "recovery_required",
|
|
458
|
+
filesystemState: "recovery_required",
|
|
459
|
+
indexState: "not_attempted",
|
|
460
|
+
fileEntries,
|
|
461
|
+
updatedAtMs: nextTs(),
|
|
462
|
+
});
|
|
463
|
+
} catch {
|
|
464
|
+
/* still return recovery_required with known journal id */
|
|
465
|
+
}
|
|
466
|
+
return recoveryRequiredResult(plan, journalId);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared file refactor planning helpers.
|
|
2
|
+
* Shared file refactor planning helpers and reference-safe contract exports.
|
|
3
3
|
*
|
|
4
|
-
* Browser-safe path planning
|
|
4
|
+
* Browser-safe path planning and warning generation based on known link data.
|
|
5
|
+
* Versioned preview/apply contracts live in `file-refactor-contract.ts` and are
|
|
6
|
+
* re-exported here for a stable import path.
|
|
5
7
|
*
|
|
6
8
|
* @module src/core/file-refactors
|
|
7
9
|
*/
|
|
@@ -11,6 +13,86 @@ import { posix as pathPosix } from "node:path";
|
|
|
11
13
|
|
|
12
14
|
import { validateRelPath } from "./validation";
|
|
13
15
|
|
|
16
|
+
export {
|
|
17
|
+
applyDestinationOnlyEdit,
|
|
18
|
+
compareExaminedReferences,
|
|
19
|
+
compareUtf16CodeUnits,
|
|
20
|
+
computeFileRefactorPlanDigest,
|
|
21
|
+
deriveCanApply,
|
|
22
|
+
FILE_REFACTOR_APPLY_CONFIRMATION,
|
|
23
|
+
FILE_REFACTOR_MUTATION_BOUNDARY,
|
|
24
|
+
FILE_REFACTOR_REASON_CODES,
|
|
25
|
+
FILE_REFACTOR_SCHEMA_VERSION,
|
|
26
|
+
fingerprintUtf8Content,
|
|
27
|
+
isBytePreservedOutsideSpan,
|
|
28
|
+
isContentPreservedOutsideSpan,
|
|
29
|
+
sortExaminedReferences,
|
|
30
|
+
stableStringify,
|
|
31
|
+
summarizeReferenceClassifications,
|
|
32
|
+
type FileRefactorAffectedDocument,
|
|
33
|
+
type FileRefactorApplyRequest,
|
|
34
|
+
type FileRefactorApplyResult,
|
|
35
|
+
type FileRefactorApplyStatus,
|
|
36
|
+
type FileRefactorConflictPolicy,
|
|
37
|
+
type FileRefactorDestinationSpan,
|
|
38
|
+
type FileRefactorDocumentRef,
|
|
39
|
+
type FileRefactorExaminedReference,
|
|
40
|
+
type FileRefactorFilesystemState,
|
|
41
|
+
type FileRefactorIndexConvergenceState,
|
|
42
|
+
type FileRefactorMutationBoundary,
|
|
43
|
+
type FileRefactorOperation,
|
|
44
|
+
type FileRefactorPreconditions,
|
|
45
|
+
type FileRefactorPreviewPlan,
|
|
46
|
+
type FileRefactorReasonCode,
|
|
47
|
+
type FileRefactorReferenceClassification,
|
|
48
|
+
type FileRefactorReferenceKind,
|
|
49
|
+
type FileRefactorSafetySummary,
|
|
50
|
+
} from "./file-refactor-contract";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
FILE_REFACTOR_PLANNER_CAPS,
|
|
54
|
+
planFileRefactorImpact,
|
|
55
|
+
type FileRefactorPlannerDocument,
|
|
56
|
+
type PlanFileRefactorImpactInput,
|
|
57
|
+
} from "./file-refactor-planner";
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
planFileRefactorImpactFromSnapshot,
|
|
61
|
+
planInputFromResolutionSnapshot,
|
|
62
|
+
type FileRefactorSnapshotLike,
|
|
63
|
+
} from "./file-refactor-from-snapshot";
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
applyFileRefactor,
|
|
67
|
+
createMemoryFileRefactorJournal,
|
|
68
|
+
journalPortFromStore,
|
|
69
|
+
type ApplyFileRefactorDeps,
|
|
70
|
+
type FileRefactorBoundaryHook,
|
|
71
|
+
type FileRefactorSyncCallback,
|
|
72
|
+
} from "./file-refactor-service";
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
applyCanonicalFileRefactor,
|
|
76
|
+
assertFileRefactorSyncConverged,
|
|
77
|
+
buildCanonicalRefactorPlan,
|
|
78
|
+
buildDurableFileRefactorApplyDeps,
|
|
79
|
+
collectionRefactorLockPath,
|
|
80
|
+
FILE_REFACTOR_COLLECTION_LOCK_NAME,
|
|
81
|
+
parseRefactorApplyConfirmation,
|
|
82
|
+
resolveMoveTarget,
|
|
83
|
+
resolveRenameTarget,
|
|
84
|
+
type BuildCanonicalRefactorPlanInput,
|
|
85
|
+
type FileRefactorPathTarget,
|
|
86
|
+
type ParsedRefactorApplyConfirmation,
|
|
87
|
+
} from "./file-refactor-adapter";
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
planMoveRefactor,
|
|
91
|
+
planRenameRefactor,
|
|
92
|
+
type MovePlan,
|
|
93
|
+
type RenamePlan,
|
|
94
|
+
} from "./file-refactor-paths";
|
|
95
|
+
|
|
14
96
|
export interface RefactorWarningSummary {
|
|
15
97
|
warnings: string[];
|
|
16
98
|
backlinkCount: number;
|
|
@@ -24,16 +106,6 @@ export interface RefactorLinkSnapshot {
|
|
|
24
106
|
markdownLinks: number;
|
|
25
107
|
}
|
|
26
108
|
|
|
27
|
-
export interface RenamePlan {
|
|
28
|
-
nextRelPath: string;
|
|
29
|
-
nextUri: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface MovePlan {
|
|
33
|
-
nextRelPath: string;
|
|
34
|
-
nextUri: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
109
|
export interface DuplicatePlan {
|
|
38
110
|
nextRelPath: string;
|
|
39
111
|
nextUri: string;
|
|
@@ -91,50 +163,6 @@ function nextAvailableRelPath(relPath: string, existing: Set<string>): string {
|
|
|
91
163
|
}
|
|
92
164
|
}
|
|
93
165
|
|
|
94
|
-
export function planRenameRefactor(input: {
|
|
95
|
-
collection: string;
|
|
96
|
-
currentRelPath: string;
|
|
97
|
-
nextName: string;
|
|
98
|
-
}): RenamePlan {
|
|
99
|
-
const current = validateRelPath(input.currentRelPath);
|
|
100
|
-
const directory = pathPosix.dirname(current);
|
|
101
|
-
const currentExt = pathPosix.extname(current);
|
|
102
|
-
const nextFilename = pathPosix.extname(input.nextName)
|
|
103
|
-
? input.nextName
|
|
104
|
-
: `${input.nextName}${currentExt}`;
|
|
105
|
-
const nextRelPath =
|
|
106
|
-
directory === "."
|
|
107
|
-
? validateRelPath(nextFilename)
|
|
108
|
-
: validateRelPath(`${directory}/${nextFilename}`);
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
nextRelPath,
|
|
112
|
-
nextUri: `gno://${input.collection}/${nextRelPath}`,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function planMoveRefactor(input: {
|
|
117
|
-
collection: string;
|
|
118
|
-
currentRelPath: string;
|
|
119
|
-
folderPath: string;
|
|
120
|
-
nextName?: string;
|
|
121
|
-
}): MovePlan {
|
|
122
|
-
const current = validateRelPath(input.currentRelPath);
|
|
123
|
-
const safeFolder = validateRelPath(input.folderPath).replace(
|
|
124
|
-
/^\.\/|\/+$/g,
|
|
125
|
-
""
|
|
126
|
-
);
|
|
127
|
-
const filename = input.nextName?.trim() || pathPosix.basename(current);
|
|
128
|
-
const nextRelPath = safeFolder
|
|
129
|
-
? validateRelPath(`${safeFolder}/${filename}`)
|
|
130
|
-
: validateRelPath(filename);
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
nextRelPath,
|
|
134
|
-
nextUri: `gno://${input.collection}/${nextRelPath}`,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
166
|
export function planDuplicateRefactor(input: {
|
|
139
167
|
collection: string;
|
|
140
168
|
currentRelPath: string;
|