@iwonz/ywtc 3.4.0 → 3.5.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/CHANGELOG.md +36 -0
- package/README.md +31 -2
- package/dist/adapters/confluence.d.ts +8 -2
- package/dist/adapters/confluence.js +44 -6
- package/dist/adapters/confluence.js.map +1 -1
- package/dist/adapters/yandex.d.ts +18 -4
- package/dist/adapters/yandex.js +34 -6
- package/dist/adapters/yandex.js.map +1 -1
- package/dist/cli.js +6 -4
- package/dist/cli.js.map +1 -1
- package/dist/content-model.d.ts +1 -0
- package/dist/content-model.js +13 -0
- package/dist/content-model.js.map +1 -1
- package/dist/domain.d.ts +12 -0
- package/dist/domain.js +10 -0
- package/dist/domain.js.map +1 -1
- package/dist/graph.js +11 -2
- package/dist/graph.js.map +1 -1
- package/dist/issues.d.ts +10 -1
- package/dist/issues.js +9 -1
- package/dist/issues.js.map +1 -1
- package/dist/migrator.d.ts +1 -1
- package/dist/migrator.js +344 -155
- package/dist/migrator.js.map +1 -1
- package/dist/report-catalog.d.ts +14 -0
- package/dist/report-catalog.js +14 -0
- package/dist/report-catalog.js.map +1 -1
- package/dist/report-contract.d.ts +259 -0
- package/dist/report-contract.js +15 -0
- package/dist/report-contract.js.map +1 -0
- package/dist/report-ui/index.html +92 -0
- package/dist/report.d.ts +13 -192
- package/dist/report.js +273 -176
- package/dist/report.js.map +1 -1
- package/dist/state.d.ts +24 -6
- package/dist/state.js +54 -3
- package/dist/state.js.map +1 -1
- package/dist/title-allocation.d.ts +2 -0
- package/dist/title-allocation.js +43 -0
- package/dist/title-allocation.js.map +1 -0
- package/docs/assets/report-header-transparent.png +0 -0
- package/package.json +24 -3
package/dist/migrator.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
4
5
|
import PQueue from 'p-queue';
|
|
5
6
|
import { createProviderAdapter } from './adapters/factory.js';
|
|
6
|
-
import {
|
|
7
|
+
import { secretValues } from './config.js';
|
|
8
|
+
import { appendGridSnapshots, assertDestinationContent, emptyRendererFallbackReason, isLegacyTransferPlaceholder, normalizePageContent, normalizedPreviewHtml, rawContentFallback, renderedPreviewHtml, renderConfluenceStorage, renderYfm, setSourceResourceBinding, sourceResourceBindings, verifyDestinationContent, } from './content-model.js';
|
|
7
9
|
import { commentAttachmentUses, createCommentArchiveZip, renderCommentArchive, renderCommentArchiveIndex, resolveCommentAttachmentReferences, verifyCommentArchive, } from './comments.js';
|
|
8
10
|
import { ConfirmedDestinationMissingError, verifyConfirmedDestinationPage } from './consistency.js';
|
|
11
|
+
import { DestinationTitleConflictError } from './domain.js';
|
|
9
12
|
import { assertTransferCapabilities } from './domain.js';
|
|
10
13
|
import { FilesystemExporter } from './filesystem-export.js';
|
|
11
14
|
import { discoverPageGraph } from './graph.js';
|
|
15
|
+
import { HttpRequestError } from './http.js';
|
|
12
16
|
import { safeFilename } from './input.js';
|
|
13
17
|
import { profileBaseUrl, sameRemoteInstance } from './locators.js';
|
|
14
18
|
import { TransferReport, } from './report.js';
|
|
15
19
|
import { DestinationNameAllocator, preferredDestinationName, verifiedCacheFile } from './resource-reconciliation.js';
|
|
20
|
+
import { destinationTitleCandidates } from './title-allocation.js';
|
|
16
21
|
import { resolveStateLocation, StateStore, } from './state.js';
|
|
17
22
|
function sourceIdentity(locator, root) {
|
|
18
23
|
return {
|
|
@@ -65,10 +70,6 @@ async function cacheDocument(cacheDirectory, canonicalId, document) {
|
|
|
65
70
|
async function loadCachedDocument(cacheDirectory, canonicalId) {
|
|
66
71
|
return JSON.parse(await readFile(cachedDocumentPath(cacheDirectory, canonicalId), 'utf8'));
|
|
67
72
|
}
|
|
68
|
-
function collisionTitle(page) {
|
|
69
|
-
const suffix = page.locator.split('/').filter(Boolean).at(-1) ?? page.id;
|
|
70
|
-
return `${page.title} [${suffix}]`;
|
|
71
|
-
}
|
|
72
73
|
function cachePath(cacheDirectory, page, resource) {
|
|
73
74
|
const pageKey = sha256(page.canonicalId).slice(0, 12);
|
|
74
75
|
const resourceKey = sha256(`${resource.id}:${resource.name}`).slice(0, 12);
|
|
@@ -104,6 +105,73 @@ function sourceDerivedTextMatches(expectedBody, actualBody, provider, page) {
|
|
|
104
105
|
const text = (value) => renderedPreviewHtml(value, provider).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
105
106
|
return text(expectedBody) === text(actualBody);
|
|
106
107
|
}
|
|
108
|
+
function ambiguousCreateOutcome(error) {
|
|
109
|
+
return error instanceof HttpRequestError
|
|
110
|
+
&& (error.status === undefined || error.status >= 500);
|
|
111
|
+
}
|
|
112
|
+
async function createPageWithAllocatedTitle(input) {
|
|
113
|
+
const candidates = destinationTitleCandidates(input.sourcePage, input.preferredTitle);
|
|
114
|
+
for (const [index, title] of candidates.entries()) {
|
|
115
|
+
let child;
|
|
116
|
+
try {
|
|
117
|
+
child = input.destination.findRecoverablePage
|
|
118
|
+
? await input.destination.findRecoverablePage(input.parent, title)
|
|
119
|
+
: await input.destination.findChild(input.parent, title);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
input.report.issue({
|
|
123
|
+
code: 'destination-recovery-lookup-failed',
|
|
124
|
+
severity: 'warning',
|
|
125
|
+
phase: 'page',
|
|
126
|
+
pageId: input.sourcePage.canonicalId,
|
|
127
|
+
retryable: true,
|
|
128
|
+
message: `${input.sourcePage.title}: parent-scoped recovery lookup failed for "${title}"; authoritative creation will be attempted: ${error instanceof Error ? error.message : String(error)}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (child && sourceDerivedTextMatches(input.bootstrap.body, child.content, input.destination.kind, input.sourcePage)) {
|
|
132
|
+
return { page: child, reused: true };
|
|
133
|
+
}
|
|
134
|
+
const occupied = child ?? (input.destination.findTitleConflict
|
|
135
|
+
? await input.destination.findTitleConflict(input.parent, title)
|
|
136
|
+
: undefined);
|
|
137
|
+
if (occupied)
|
|
138
|
+
continue;
|
|
139
|
+
let created;
|
|
140
|
+
try {
|
|
141
|
+
created = await input.destination.createPage({ container: input.parent, title, body: input.bootstrap.body });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof DestinationTitleConflictError)
|
|
145
|
+
continue;
|
|
146
|
+
if (ambiguousCreateOutcome(error)) {
|
|
147
|
+
const recovered = input.destination.findRecoverablePage
|
|
148
|
+
? await input.destination.findRecoverablePage(input.parent, title)
|
|
149
|
+
: await input.destination.findChild(input.parent, title);
|
|
150
|
+
if (recovered && sourceDerivedTextMatches(input.bootstrap.body, recovered.content, input.destination.kind, input.sourcePage)) {
|
|
151
|
+
return { page: recovered, reused: true };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
created = await verifyConfirmedDestinationPage(input.destination, created, input.consistencyOptions);
|
|
157
|
+
assertDestinationContent(input.bootstrap.body, created.content, input.destination.kind, input.sourcePage);
|
|
158
|
+
if (created.parentId !== input.parent.id) {
|
|
159
|
+
throw new Error(`Destination hierarchy verification failed for ${input.sourcePage.title}: expected parent ${input.parent.id}, received ${created.parentId ?? 'none'}`);
|
|
160
|
+
}
|
|
161
|
+
if (index > 0) {
|
|
162
|
+
input.report.issue({
|
|
163
|
+
code: 'destination-title-renamed',
|
|
164
|
+
severity: 'normalization',
|
|
165
|
+
phase: 'page',
|
|
166
|
+
pageId: input.sourcePage.canonicalId,
|
|
167
|
+
retryable: false,
|
|
168
|
+
message: `${input.sourcePage.title}: destination title was allocated as "${created.title}" because an earlier candidate was occupied`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return { page: created, reused: false };
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`Confluence rejected all ${candidates.length} deterministic destination titles for ${input.sourcePage.title} because they are occupied`);
|
|
174
|
+
}
|
|
107
175
|
function destinationFromState(state, adapter) {
|
|
108
176
|
if (!state.destinationId || !state.destinationCanonicalId || !state.destinationLocator) {
|
|
109
177
|
throw new Error(`Incomplete mapped destination for ${state.sourceTitle}`);
|
|
@@ -120,6 +188,19 @@ function destinationFromState(state, adapter) {
|
|
|
120
188
|
};
|
|
121
189
|
}
|
|
122
190
|
function reportPage(input) {
|
|
191
|
+
const inventory = input.commentInventory;
|
|
192
|
+
const commentResourceIds = new Set();
|
|
193
|
+
for (const thread of inventory?.threads ?? []) {
|
|
194
|
+
const stack = [thread.root];
|
|
195
|
+
while (stack.length > 0) {
|
|
196
|
+
const entry = stack.pop();
|
|
197
|
+
for (const attachment of entry.attachments) {
|
|
198
|
+
if (attachment.resourceId)
|
|
199
|
+
commentResourceIds.add(attachment.resourceId);
|
|
200
|
+
}
|
|
201
|
+
stack.push(...entry.replies);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
123
204
|
const transferable = input.resources.filter((resource) => resource.kind !== 'grid');
|
|
124
205
|
const resourceItems = transferable.map((resource) => {
|
|
125
206
|
const state = input.resourceStates?.[resource.id];
|
|
@@ -128,36 +209,74 @@ function reportPage(input) {
|
|
|
128
209
|
return {
|
|
129
210
|
sourceId: resource.id,
|
|
130
211
|
sourceName: resource.name,
|
|
131
|
-
|
|
212
|
+
...(resource.downloadUrl ? { sourceUrl: resource.downloadUrl } : {}),
|
|
213
|
+
...(resource.referenceAliases?.[0] ? { sourceReference: resource.referenceAliases[0] } : {}),
|
|
214
|
+
kind: commentResourceIds.has(resource.id) ? 'comment-attachment' : resource.kind,
|
|
132
215
|
status,
|
|
133
216
|
...(sourceBytes !== undefined ? { sourceBytes } : {}),
|
|
134
217
|
...(status === 'complete' && sourceBytes !== undefined ? { transferredBytes: sourceBytes } : {}),
|
|
135
218
|
...(state?.destinationId ? { destinationId: state.destinationId } : {}),
|
|
136
219
|
...(state?.destinationName ? { destinationName: state.destinationName } : {}),
|
|
220
|
+
...(state?.destinationUrl ? { destinationUrl: state.destinationUrl } : {}),
|
|
221
|
+
...(state?.sha256 ? { sha256: state.sha256 } : {}),
|
|
137
222
|
...(state?.lastError ? { error: state.lastError } : {}),
|
|
223
|
+
...(status === 'blocked' && input.blockedBy ? input.blockedBy : {}),
|
|
138
224
|
};
|
|
139
225
|
});
|
|
140
226
|
const knownBytes = resourceItems.reduce((total, resource) => total + (resource.sourceBytes ?? 0), 0);
|
|
141
227
|
const transferredBytes = resourceItems.reduce((total, resource) => total + (resource.transferredBytes ?? 0), 0);
|
|
142
228
|
const commentsEnabled = input.commentsEnabled ?? true;
|
|
143
|
-
const inventory = input.commentInventory;
|
|
144
229
|
const commentState = input.commentState;
|
|
145
230
|
const commentStatus = !commentsEnabled
|
|
146
231
|
? 'skipped'
|
|
147
232
|
: commentState?.status
|
|
148
233
|
?? (inventory?.status === 'failed' ? 'failed' : inventory?.status === 'empty' ? 'complete' : 'planned');
|
|
149
234
|
const commentError = commentState?.lastError ?? inventory?.error;
|
|
235
|
+
const commentItems = inventory?.threads.flatMap((thread) => {
|
|
236
|
+
const result = [];
|
|
237
|
+
const visit = (entry) => {
|
|
238
|
+
const author = entry.author.displayName
|
|
239
|
+
?? entry.author.username
|
|
240
|
+
?? entry.author.publicName
|
|
241
|
+
?? entry.author.providerId;
|
|
242
|
+
result.push({
|
|
243
|
+
threadId: thread.id,
|
|
244
|
+
commentId: entry.id,
|
|
245
|
+
...(entry.parentId ? { parentId: entry.parentId } : {}),
|
|
246
|
+
author,
|
|
247
|
+
...(entry.createdAt ? { createdAt: entry.createdAt } : {}),
|
|
248
|
+
...(entry.status ? { sourceStatus: entry.status } : {}),
|
|
249
|
+
outcome: commentStatus,
|
|
250
|
+
sourceUrl: `${input.page.url}#comment-${encodeURIComponent(entry.id)}`,
|
|
251
|
+
...(commentState?.destinationUrl ? { destinationUrl: commentState.destinationUrl } : {}),
|
|
252
|
+
...(commentError ? { error: commentError } : {}),
|
|
253
|
+
});
|
|
254
|
+
for (const reply of entry.replies)
|
|
255
|
+
visit(reply);
|
|
256
|
+
};
|
|
257
|
+
visit(thread.root);
|
|
258
|
+
return result;
|
|
259
|
+
});
|
|
150
260
|
return {
|
|
151
261
|
sourceCanonicalId: input.page.canonicalId,
|
|
152
262
|
sourceId: input.page.id,
|
|
153
263
|
sourceTitle: input.page.title,
|
|
154
264
|
sourceUrl: input.page.url,
|
|
265
|
+
...(input.page.parentId ? { sourceParentId: input.page.parentId } : {}),
|
|
266
|
+
...(input.page.ancestorIds?.length ? { sourceAncestorIds: input.page.ancestorIds } : {}),
|
|
155
267
|
...(input.destination ? {
|
|
156
268
|
destinationId: input.destination.id,
|
|
157
269
|
destinationTitle: input.destination.title,
|
|
158
270
|
destinationUrl: input.destination.url,
|
|
271
|
+
...(input.destination.parentId ? { destinationParentId: input.destination.parentId } : {}),
|
|
159
272
|
} : {}),
|
|
160
|
-
...(input.
|
|
273
|
+
...(input.destinationFile ? {
|
|
274
|
+
destinationId: input.destinationFile.id,
|
|
275
|
+
destinationTitle: input.page.title,
|
|
276
|
+
destinationUrl: input.destinationFile.url,
|
|
277
|
+
...(input.destinationFile.parentId ? { destinationParentId: input.destinationFile.parentId } : {}),
|
|
278
|
+
} : {}),
|
|
279
|
+
...(input.page.projectedParentId ? { parentId: input.page.projectedParentId, projectedParentId: input.page.projectedParentId } : {}),
|
|
161
280
|
depth: input.page.depth,
|
|
162
281
|
status: input.status,
|
|
163
282
|
inventoryStatus: input.inventoryStatus ?? 'complete',
|
|
@@ -176,6 +295,7 @@ function reportPage(input) {
|
|
|
176
295
|
warnings: [...new Set([...input.document.warnings, ...(input.warnings ?? [])])],
|
|
177
296
|
normalizations: [...new Set([...(input.document.normalizations ?? []), ...(input.normalizations ?? [])])],
|
|
178
297
|
...(input.error ? { error: input.error } : {}),
|
|
298
|
+
...(input.status === 'blocked' && input.blockedBy ? input.blockedBy : {}),
|
|
179
299
|
comments: {
|
|
180
300
|
status: commentStatus,
|
|
181
301
|
threads: commentState?.threads ?? inventory?.threads.length ?? 0,
|
|
@@ -184,14 +304,51 @@ function reportPage(input) {
|
|
|
184
304
|
attachments: commentState?.attachments ?? inventory?.attachments ?? 0,
|
|
185
305
|
bytes: commentState?.bytes ?? inventory?.knownBytes ?? 0,
|
|
186
306
|
...(commentState?.archiveName ? { archiveName: commentState.archiveName } : {}),
|
|
307
|
+
...(commentState?.archiveUrl ? { archiveUrl: commentState.archiveUrl } : {}),
|
|
187
308
|
...(commentState?.destinationId ? { destinationId: commentState.destinationId } : {}),
|
|
309
|
+
...(commentState?.destinationUrl ? { destinationUrl: commentState.destinationUrl } : {}),
|
|
188
310
|
...(commentState?.previousDestinationIds?.length ? { revision: true } : {}),
|
|
189
311
|
...(commentState?.status === 'stale' ? { stale: true } : {}),
|
|
190
312
|
...(commentError ? { error: commentError } : {}),
|
|
191
313
|
...(commentState?.preview ? { preview: commentState.preview } : {}),
|
|
314
|
+
...(commentItems?.length ? { items: commentItems } : {}),
|
|
315
|
+
...(commentStatus === 'blocked' && input.blockedBy ? input.blockedBy : {}),
|
|
192
316
|
},
|
|
193
317
|
};
|
|
194
318
|
}
|
|
319
|
+
function blockedResourceStates(resources, previous, cause) {
|
|
320
|
+
return {
|
|
321
|
+
...previous,
|
|
322
|
+
...Object.fromEntries(resources.filter((resource) => resource.kind !== 'grid').map((resource) => {
|
|
323
|
+
const existing = previous[resource.id];
|
|
324
|
+
return [resource.id, {
|
|
325
|
+
...(existing ?? {}),
|
|
326
|
+
sourceId: resource.id,
|
|
327
|
+
sourceName: resource.name,
|
|
328
|
+
destinationName: existing?.destinationName ?? preferredDestinationName(resource),
|
|
329
|
+
...(existing?.size !== undefined ? { size: existing.size } : resource.size !== undefined ? { size: resource.size } : {}),
|
|
330
|
+
status: 'blocked',
|
|
331
|
+
...cause,
|
|
332
|
+
lastError: cause.blockedReason,
|
|
333
|
+
updatedAt: new Date().toISOString(),
|
|
334
|
+
}];
|
|
335
|
+
})),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function blockedCommentState(inventory, previous, cause) {
|
|
339
|
+
return {
|
|
340
|
+
...(previous ?? {}),
|
|
341
|
+
status: 'blocked',
|
|
342
|
+
threads: previous?.threads ?? inventory?.threads.length ?? 0,
|
|
343
|
+
comments: previous?.comments ?? inventory?.comments ?? 0,
|
|
344
|
+
participants: previous?.participants ?? inventory?.participants ?? 0,
|
|
345
|
+
attachments: previous?.attachments ?? inventory?.attachments ?? 0,
|
|
346
|
+
bytes: previous?.bytes ?? inventory?.knownBytes ?? 0,
|
|
347
|
+
...cause,
|
|
348
|
+
lastError: cause.blockedReason,
|
|
349
|
+
updatedAt: new Date().toISOString(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
195
352
|
function runStatus(errors, warnings, degradations) {
|
|
196
353
|
if (errors > 0)
|
|
197
354
|
return 'partial';
|
|
@@ -200,6 +357,17 @@ function runStatus(errors, warnings, degradations) {
|
|
|
200
357
|
function attentionCount(summary) {
|
|
201
358
|
return summary.warnings + summary.degradations;
|
|
202
359
|
}
|
|
360
|
+
function runOutcomeCounts(summary, pages) {
|
|
361
|
+
return {
|
|
362
|
+
pages,
|
|
363
|
+
warnings: attentionCount(summary),
|
|
364
|
+
errors: summary.errors,
|
|
365
|
+
rootErrors: summary.rootErrors,
|
|
366
|
+
dependentPages: summary.dependentPages,
|
|
367
|
+
dependentFiles: summary.dependentFiles,
|
|
368
|
+
dependentComments: summary.dependentComments,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
203
371
|
export class MigrationRunError extends Error {
|
|
204
372
|
reportPaths;
|
|
205
373
|
stateFilename;
|
|
@@ -216,7 +384,7 @@ async function renderRemoteDocument(input) {
|
|
|
216
384
|
? await renderConfluenceStorage(input.document, input.page, input.context, input.container.space ?? '')
|
|
217
385
|
: await renderYfm(input.document, input.context);
|
|
218
386
|
if (!rendered.body.trim())
|
|
219
|
-
return rawContentFallback(input.page, input.destination.kind,
|
|
387
|
+
return rawContentFallback(input.page, input.destination.kind, emptyRendererFallbackReason(input.document));
|
|
220
388
|
return rendered;
|
|
221
389
|
}
|
|
222
390
|
catch (error) {
|
|
@@ -267,6 +435,7 @@ export class Migrator {
|
|
|
267
435
|
await store.save();
|
|
268
436
|
const run = await store.beginRun(options.scope, options.dryRun, sourceStatus, commentsEnabled);
|
|
269
437
|
const report = new TransferReport({
|
|
438
|
+
secrets: secretValues(this.config),
|
|
270
439
|
dryRun: options.dryRun,
|
|
271
440
|
commentsEnabled,
|
|
272
441
|
scope: options.scope,
|
|
@@ -329,11 +498,7 @@ export class Migrator {
|
|
|
329
498
|
report.finish(finalStatus);
|
|
330
499
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
331
500
|
if (!options.dryRun)
|
|
332
|
-
await store.finishRun(run.id, finalStatus,
|
|
333
|
-
pages: 0,
|
|
334
|
-
warnings: attentionCount(report.data.summary),
|
|
335
|
-
errors: report.data.summary.errors,
|
|
336
|
-
});
|
|
501
|
+
await store.finishRun(run.id, finalStatus, runOutcomeCounts(report.data.summary, 0));
|
|
337
502
|
throw new MigrationRunError(message, reportPaths, stateLocation.filename, { cause: error });
|
|
338
503
|
}
|
|
339
504
|
const resourcesByPage = new Map();
|
|
@@ -419,13 +584,58 @@ export class Migrator {
|
|
|
419
584
|
report.finish(finalStatus);
|
|
420
585
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
421
586
|
if (!options.dryRun)
|
|
422
|
-
await store.finishRun(run.id, finalStatus,
|
|
423
|
-
pages: graph.pages.length,
|
|
424
|
-
warnings: attentionCount(report.data.summary),
|
|
425
|
-
errors: report.data.summary.errors,
|
|
426
|
-
});
|
|
587
|
+
await store.finishRun(run.id, finalStatus, runOutcomeCounts(report.data.summary, graph.pages.length));
|
|
427
588
|
throw new MigrationRunError('Resource preflight failed before destination mutation', reportPaths, stateLocation.filename);
|
|
428
589
|
}
|
|
590
|
+
if (!options.dryRun) {
|
|
591
|
+
for (const page of graph.pages) {
|
|
592
|
+
if (store.value.pages[page.canonicalId])
|
|
593
|
+
continue;
|
|
594
|
+
const commentInventory = commentsByPage.get(page.canonicalId);
|
|
595
|
+
const commentState = !commentsEnabled
|
|
596
|
+
? { status: 'skipped', threads: 0, comments: 0, participants: 0, attachments: 0, bytes: 0, updatedAt: new Date().toISOString() }
|
|
597
|
+
: commentInventory?.status === 'empty'
|
|
598
|
+
? { status: 'complete', threads: 0, comments: 0, participants: 0, attachments: 0, bytes: 0, updatedAt: new Date().toISOString() }
|
|
599
|
+
: commentInventory?.status === 'failed'
|
|
600
|
+
? {
|
|
601
|
+
status: 'failed', threads: 0, comments: 0, participants: 0, attachments: 0, bytes: 0,
|
|
602
|
+
...(commentInventory.error ? { lastError: commentInventory.error } : {}), updatedAt: new Date().toISOString(),
|
|
603
|
+
}
|
|
604
|
+
: commentInventory
|
|
605
|
+
? {
|
|
606
|
+
status: 'planned', ...(commentInventory.fingerprint ? { fingerprint: commentInventory.fingerprint } : {}),
|
|
607
|
+
threads: commentInventory.threads.length, comments: commentInventory.comments,
|
|
608
|
+
participants: commentInventory.participants, attachments: commentInventory.attachments,
|
|
609
|
+
bytes: commentInventory.knownBytes, updatedAt: new Date().toISOString(),
|
|
610
|
+
}
|
|
611
|
+
: undefined;
|
|
612
|
+
const resourceStates = Object.fromEntries((resourcesByPage.get(page.canonicalId) ?? [])
|
|
613
|
+
.filter((resource) => resource.kind !== 'grid')
|
|
614
|
+
.map((resource) => [resource.id, {
|
|
615
|
+
sourceId: resource.id,
|
|
616
|
+
sourceName: resource.name,
|
|
617
|
+
destinationName: preferredDestinationName(resource),
|
|
618
|
+
...(resource.size !== undefined ? { size: resource.size } : {}),
|
|
619
|
+
...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
|
|
620
|
+
status: 'planned',
|
|
621
|
+
updatedAt: new Date().toISOString(),
|
|
622
|
+
}]));
|
|
623
|
+
await store.setPage({
|
|
624
|
+
sourceId: page.id,
|
|
625
|
+
sourceCanonicalId: page.canonicalId,
|
|
626
|
+
sourceLocator: page.locator,
|
|
627
|
+
sourceTitle: page.title,
|
|
628
|
+
...(page.projectedParentId ? { projectedParentId: page.projectedParentId } : {}),
|
|
629
|
+
destinationTitle: page.title,
|
|
630
|
+
sourceFingerprint: sourceFingerprints.get(page.canonicalId),
|
|
631
|
+
status: 'discovered',
|
|
632
|
+
resources: resourceStates,
|
|
633
|
+
...(commentState ? { comments: commentState } : {}),
|
|
634
|
+
degradations: [],
|
|
635
|
+
updatedAt: new Date().toISOString(),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
429
639
|
const planSummary = `Plan ready: root + ${graph.selection.selectedDescendants}/${graph.selection.returnedDescendants} descendants (${graph.selection.excluded.length} excluded, depth ${graph.selection.maximumDepth}) · ${graph.pages.length} pages · ${inventory.resources} files (${inventory.bytes} known bytes${inventory.unknownSizes > 0 ? ` · ${inventory.unknownSizes} sizes unknown` : ''}) · ${inventory.links} links · ${inventory.images} images · ${inventory.tables} tables (${inventory.grids} grids) · ${inventory.codeBlocks} code blocks · ${commentsEnabled ? `${inventory.commentThreads} comment threads / ${inventory.comments} entries / ${inventory.commentAttachments} files (${inventory.commentBytes} known bytes)` : 'comments skipped'}`;
|
|
430
640
|
status(planSummary);
|
|
431
641
|
this.logger.info(planSummary);
|
|
@@ -436,6 +646,8 @@ export class Migrator {
|
|
|
436
646
|
const result = await this.exportFilesystem(source, graph, options.destination, options, stateLocation.id, store, report, sourceFingerprints, resourcesByPage, commentsByPage);
|
|
437
647
|
output = result.directory;
|
|
438
648
|
archive = result.archive;
|
|
649
|
+
const exportEntry = path.join(result.directory, options.destination.format === 'html' ? 'index.html' : 'index.md');
|
|
650
|
+
report.setDestinationUrl(pathToFileURL(exportEntry).toString());
|
|
439
651
|
report.output('directory', result.directory);
|
|
440
652
|
if (result.archive)
|
|
441
653
|
report.output('archive', result.archive);
|
|
@@ -460,14 +672,11 @@ export class Migrator {
|
|
|
460
672
|
}
|
|
461
673
|
report.finish('completed');
|
|
462
674
|
const finalStatus = runStatus(report.data.summary.errors, report.data.summary.warnings, report.data.summary.degradations);
|
|
675
|
+
await store.finalizeOutcomes(graph.pages.map((page) => page.canonicalId), finalStatus, options.dryRun);
|
|
463
676
|
report.finish(finalStatus);
|
|
464
677
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
465
678
|
if (!options.dryRun)
|
|
466
|
-
await store.finishRun(run.id, finalStatus,
|
|
467
|
-
pages: graph.pages.length,
|
|
468
|
-
warnings: attentionCount(report.data.summary),
|
|
469
|
-
errors: report.data.summary.errors,
|
|
470
|
-
});
|
|
679
|
+
await store.finishRun(run.id, finalStatus, runOutcomeCounts(report.data.summary, graph.pages.length));
|
|
471
680
|
status(`${finalStatus}: source ${report.data.summary.pages} pages · ${report.data.summary.complete} complete · ${report.data.summary.partial} partial · ${report.data.summary.failed + report.data.summary.blocked} failed/blocked · files ${report.data.summary.resourcesComplete}/${report.data.summary.resources} complete · ${report.data.summary.errors} errors · ${report.data.summary.warnings} warnings · ${report.data.summary.degradations} degradations`);
|
|
472
681
|
return {
|
|
473
682
|
status: finalStatus,
|
|
@@ -486,14 +695,11 @@ export class Migrator {
|
|
|
486
695
|
const finalStatus = cancelled ? 'cancelled' : 'failed';
|
|
487
696
|
const message = error instanceof Error ? error.message : String(error);
|
|
488
697
|
report.error(message);
|
|
698
|
+
await store.finalizeOutcomes(graph.pages.map((page) => page.canonicalId), finalStatus, options.dryRun);
|
|
489
699
|
report.finish(finalStatus);
|
|
490
700
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
491
701
|
if (!options.dryRun)
|
|
492
|
-
await store.finishRun(run.id, finalStatus,
|
|
493
|
-
pages: graph.pages.length,
|
|
494
|
-
warnings: attentionCount(report.data.summary),
|
|
495
|
-
errors: report.data.summary.errors,
|
|
496
|
-
});
|
|
702
|
+
await store.finishRun(run.id, finalStatus, runOutcomeCounts(report.data.summary, graph.pages.length));
|
|
497
703
|
this.logger.error(`${message}. Report: ${reportPaths.html}`);
|
|
498
704
|
throw new MigrationRunError(message, reportPaths, stateLocation.filename, { cause: error });
|
|
499
705
|
}
|
|
@@ -536,6 +742,7 @@ export class Migrator {
|
|
|
536
742
|
sourceName: sourceResource?.name ?? resource.name,
|
|
537
743
|
destinationId: resource.path,
|
|
538
744
|
destinationName: resource.name,
|
|
745
|
+
destinationUrl: pathToFileURL(path.join(destination.path, resource.path)).toString(),
|
|
539
746
|
...(resource.sha256 ? { sha256: resource.sha256 } : {}),
|
|
540
747
|
...(resource.size !== undefined ? { size: resource.size } : {}),
|
|
541
748
|
status: options.dryRun ? 'planned' : 'complete',
|
|
@@ -574,8 +781,16 @@ export class Migrator {
|
|
|
574
781
|
} } : {}),
|
|
575
782
|
updatedAt: new Date().toISOString(),
|
|
576
783
|
});
|
|
784
|
+
const parentManifestPage = manifestPage?.parentId
|
|
785
|
+
? result.manifest.pages.find((item) => item.sourceCanonicalId === manifestPage.parentId)
|
|
786
|
+
: undefined;
|
|
577
787
|
report.addPage(reportPage({
|
|
578
788
|
page,
|
|
789
|
+
...(manifestPage ? { destinationFile: {
|
|
790
|
+
id: manifestPage.path,
|
|
791
|
+
url: pathToFileURL(path.join(destination.path, manifestPage.path)).toString(),
|
|
792
|
+
...(parentManifestPage ? { parentId: parentManifestPage.path } : {}),
|
|
793
|
+
} } : {}),
|
|
579
794
|
status: pageStatus,
|
|
580
795
|
resources: sourceResources,
|
|
581
796
|
document,
|
|
@@ -780,6 +995,7 @@ export class Migrator {
|
|
|
780
995
|
const archiveState = zip ? {
|
|
781
996
|
...(archiveResource ? { archiveResourceId: archiveResource.id } : {}),
|
|
782
997
|
archiveName: archiveResource?.name ?? zip.name,
|
|
998
|
+
...(archiveResource?.url ? { archiveUrl: archiveResource.url } : {}),
|
|
783
999
|
archivePath: zip.path,
|
|
784
1000
|
archiveSha256: zip.sha256,
|
|
785
1001
|
} : {};
|
|
@@ -899,6 +1115,7 @@ export class Migrator {
|
|
|
899
1115
|
fingerprint,
|
|
900
1116
|
destinationId: verified.id,
|
|
901
1117
|
destinationFingerprint: verified.fingerprint,
|
|
1118
|
+
...(verified.url ? { destinationUrl: verified.url } : {}),
|
|
902
1119
|
...(previousIds?.length ? { previousDestinationIds: previousIds } : {}),
|
|
903
1120
|
...archiveState,
|
|
904
1121
|
...fullArchiveState,
|
|
@@ -966,15 +1183,47 @@ export class Migrator {
|
|
|
966
1183
|
const saved = store.value.pages[page.canonicalId];
|
|
967
1184
|
const parent = page.projectedParentId ? destinationPages.get(page.projectedParentId) : container;
|
|
968
1185
|
if (!parent) {
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
1186
|
+
const upstream = page.projectedParentId ? stagingFailures.get(page.projectedParentId) : undefined;
|
|
1187
|
+
const blockedByPageId = upstream?.blockedByPageId ?? page.projectedParentId ?? page.canonicalId;
|
|
1188
|
+
const blockedByCode = upstream?.blockedByCode ?? 'destination-parent-unavailable';
|
|
1189
|
+
const message = `Destination parent is unavailable for ${page.title}; blocked by ${blockedByPageId} (${blockedByCode})`;
|
|
1190
|
+
const cause = { blockedByPageId, blockedByCode, blockedReason: message };
|
|
1191
|
+
stagingFailures.set(page.canonicalId, { status: 'blocked', message, blockedByPageId, blockedByCode });
|
|
1192
|
+
const previous = store.value.pages[page.canonicalId];
|
|
1193
|
+
const resources = resourcesByPage.get(page.canonicalId) ?? [];
|
|
1194
|
+
const comments = (options.comments ?? true)
|
|
1195
|
+
? blockedCommentState(commentsByPage.get(page.canonicalId), previous?.comments, cause)
|
|
1196
|
+
: undefined;
|
|
1197
|
+
await store.setPage({
|
|
1198
|
+
sourceId: page.id,
|
|
1199
|
+
sourceCanonicalId: page.canonicalId,
|
|
1200
|
+
sourceLocator: page.locator,
|
|
1201
|
+
sourceTitle: page.title,
|
|
1202
|
+
...(page.projectedParentId ? { projectedParentId: page.projectedParentId } : {}),
|
|
1203
|
+
destinationTitle: previous?.destinationTitle ?? page.title,
|
|
1204
|
+
...(previous?.destinationId ? { destinationId: previous.destinationId } : {}),
|
|
1205
|
+
...(previous?.destinationCanonicalId ? { destinationCanonicalId: previous.destinationCanonicalId } : {}),
|
|
1206
|
+
...(previous?.destinationLocator ? { destinationLocator: previous.destinationLocator } : {}),
|
|
1207
|
+
...(previous?.previousDestinationIds ? { previousDestinationIds: previous.previousDestinationIds } : {}),
|
|
1208
|
+
sourceFingerprint: sourceFingerprints.get(page.canonicalId),
|
|
1209
|
+
...(previous?.destinationFingerprint ? { destinationFingerprint: previous.destinationFingerprint } : {}),
|
|
1210
|
+
status: 'blocked',
|
|
1211
|
+
resources: blockedResourceStates(resources, previous?.resources ?? {}, cause),
|
|
1212
|
+
...(comments ? { comments } : {}),
|
|
1213
|
+
degradations: previous?.degradations ?? [],
|
|
1214
|
+
...cause,
|
|
1215
|
+
lastError: message,
|
|
1216
|
+
updatedAt: new Date().toISOString(),
|
|
1217
|
+
});
|
|
1218
|
+
report.updatePage(page.canonicalId, { status: 'blocked', error: message, ...cause });
|
|
972
1219
|
report.issue({
|
|
973
1220
|
code: 'destination-parent-unavailable',
|
|
974
|
-
severity: '
|
|
1221
|
+
severity: 'blocked',
|
|
975
1222
|
phase: 'page',
|
|
976
1223
|
pageId: page.canonicalId,
|
|
977
|
-
retryable:
|
|
1224
|
+
retryable: false,
|
|
1225
|
+
blockedByPageId,
|
|
1226
|
+
blockedByCode,
|
|
978
1227
|
message,
|
|
979
1228
|
});
|
|
980
1229
|
progress.staged += 1;
|
|
@@ -985,14 +1234,20 @@ export class Migrator {
|
|
|
985
1234
|
const cachedPage = await loadCachedPage(options.cacheDirectory, page.canonicalId);
|
|
986
1235
|
const bootstrap = bootstrapBodies.get(page.canonicalId);
|
|
987
1236
|
let created;
|
|
988
|
-
|
|
1237
|
+
const title = saved?.destinationTitle ?? page.title;
|
|
989
1238
|
let resourcesForState = saved?.resources ?? {};
|
|
990
1239
|
let commentsForState = saved?.comments;
|
|
991
1240
|
let previousDestinationIds = saved?.previousDestinationIds ?? [];
|
|
992
1241
|
let mappingRecovered = false;
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
1242
|
+
const allocatePage = (preferredTitle) => createPageWithAllocatedTitle({
|
|
1243
|
+
destination,
|
|
1244
|
+
parent,
|
|
1245
|
+
sourcePage: cachedPage,
|
|
1246
|
+
bootstrap,
|
|
1247
|
+
...(preferredTitle ? { preferredTitle } : {}),
|
|
1248
|
+
consistencyOptions: this.consistencyOptions,
|
|
1249
|
+
report,
|
|
1250
|
+
});
|
|
996
1251
|
if (saved?.destinationId) {
|
|
997
1252
|
const mapped = destinationFromState(saved, destination);
|
|
998
1253
|
try {
|
|
@@ -1014,77 +1269,8 @@ export class Migrator {
|
|
|
1014
1269
|
if (page.projectedParentId) {
|
|
1015
1270
|
await verifyConfirmedDestinationPage(destination, parent, this.consistencyOptions);
|
|
1016
1271
|
}
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
: destination.findChild(parent, candidateTitle);
|
|
1020
|
-
let candidate;
|
|
1021
|
-
let recoveryLookupFailed = false;
|
|
1022
|
-
try {
|
|
1023
|
-
candidate = await findRecoveryChild(title);
|
|
1024
|
-
}
|
|
1025
|
-
catch (lookupError) {
|
|
1026
|
-
recoveryLookupFailed = true;
|
|
1027
|
-
report.issue({
|
|
1028
|
-
code: 'destination-recovery-lookup-failed',
|
|
1029
|
-
severity: 'warning',
|
|
1030
|
-
phase: 'page',
|
|
1031
|
-
pageId: page.canonicalId,
|
|
1032
|
-
retryable: true,
|
|
1033
|
-
message: `${page.title}: recovery lookup failed; bootstrap recreation will be attempted: ${lookupError instanceof Error ? lookupError.message : String(lookupError)}`,
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
if (candidate && sourceDerivedTextMatches(bootstrap.body, candidate.content, destination.kind, cachedPage)) {
|
|
1037
|
-
created = candidate;
|
|
1038
|
-
}
|
|
1039
|
-
else {
|
|
1040
|
-
let recoveryTitle = title;
|
|
1041
|
-
let titleConflict = candidate;
|
|
1042
|
-
if (!titleConflict && !recoveryLookupFailed) {
|
|
1043
|
-
try {
|
|
1044
|
-
titleConflict = await findTitleConflict(recoveryTitle);
|
|
1045
|
-
}
|
|
1046
|
-
catch (lookupError) {
|
|
1047
|
-
recoveryLookupFailed = true;
|
|
1048
|
-
report.issue({
|
|
1049
|
-
code: 'destination-title-lookup-failed',
|
|
1050
|
-
severity: 'warning',
|
|
1051
|
-
phase: 'page',
|
|
1052
|
-
pageId: page.canonicalId,
|
|
1053
|
-
retryable: true,
|
|
1054
|
-
message: `${page.title}: destination title lookup failed; bootstrap recreation will be attempted with a stable suffix: ${lookupError instanceof Error ? lookupError.message : String(lookupError)}`,
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
if (titleConflict || recoveryLookupFailed) {
|
|
1059
|
-
const base = collisionTitle(page);
|
|
1060
|
-
const hash = sha256(page.canonicalId).slice(0, 8);
|
|
1061
|
-
recoveryTitle = base;
|
|
1062
|
-
let sequence = 0;
|
|
1063
|
-
while (!recoveryLookupFailed) {
|
|
1064
|
-
try {
|
|
1065
|
-
if (!await findTitleConflict(recoveryTitle))
|
|
1066
|
-
break;
|
|
1067
|
-
sequence += 1;
|
|
1068
|
-
recoveryTitle = `${base} [${hash}${sequence > 1 ? `-${sequence}` : ''}]`;
|
|
1069
|
-
}
|
|
1070
|
-
catch (lookupError) {
|
|
1071
|
-
recoveryLookupFailed = true;
|
|
1072
|
-
recoveryTitle = `${base} [${hash}]`;
|
|
1073
|
-
report.issue({
|
|
1074
|
-
code: 'destination-recovery-lookup-failed',
|
|
1075
|
-
severity: 'warning',
|
|
1076
|
-
phase: 'page',
|
|
1077
|
-
pageId: page.canonicalId,
|
|
1078
|
-
retryable: true,
|
|
1079
|
-
message: `${page.title}: suffixed-title lookup failed; bootstrap recreation will be attempted: ${lookupError instanceof Error ? lookupError.message : String(lookupError)}`,
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
created = await destination.createPage({ container: parent, title: recoveryTitle, body: bootstrap.body });
|
|
1085
|
-
created = await verifyConfirmedDestinationPage(destination, created, this.consistencyOptions);
|
|
1086
|
-
assertDestinationContent(bootstrap.body, created.content, destination.kind, cachedPage);
|
|
1087
|
-
}
|
|
1272
|
+
const allocation = await allocatePage(title);
|
|
1273
|
+
created = allocation.page;
|
|
1088
1274
|
previousDestinationIds = [...new Set([...previousDestinationIds, mapped.id])];
|
|
1089
1275
|
resourcesForState = resetResourceDestinations(saved.resources);
|
|
1090
1276
|
commentsForState = undefined;
|
|
@@ -1155,27 +1341,7 @@ export class Migrator {
|
|
|
1155
1341
|
});
|
|
1156
1342
|
}
|
|
1157
1343
|
else {
|
|
1158
|
-
|
|
1159
|
-
const replacementMatches = replacement
|
|
1160
|
-
&& sourceDerivedTextMatches(bootstrap.body, replacement.content, destination.kind, cachedPage);
|
|
1161
|
-
const titleConflict = replacementMatches ? undefined : replacement ?? await findTitleConflict(title);
|
|
1162
|
-
if (titleConflict) {
|
|
1163
|
-
const base = collisionTitle(page);
|
|
1164
|
-
const hash = sha256(page.canonicalId).slice(0, 8);
|
|
1165
|
-
title = base;
|
|
1166
|
-
let sequence = 0;
|
|
1167
|
-
while (await findTitleConflict(title)) {
|
|
1168
|
-
sequence += 1;
|
|
1169
|
-
title = `${base} [${hash}${sequence > 1 ? `-${sequence}` : ''}]`;
|
|
1170
|
-
}
|
|
1171
|
-
replacement = undefined;
|
|
1172
|
-
}
|
|
1173
|
-
created = replacement ?? await destination.createPage({ container: parent, title, body: bootstrap.body });
|
|
1174
|
-
created = await verifyConfirmedDestinationPage(destination, created, this.consistencyOptions);
|
|
1175
|
-
assertDestinationContent(bootstrap.body, created.content, destination.kind, cachedPage);
|
|
1176
|
-
if (created.parentId !== parent.id) {
|
|
1177
|
-
throw new Error(`Destination hierarchy verification failed for recreated page ${page.title}: expected parent ${parent.id}, received ${created.parentId ?? 'none'}`);
|
|
1178
|
-
}
|
|
1344
|
+
created = (await allocatePage(title)).page;
|
|
1179
1345
|
previousDestinationIds = [...new Set([...previousDestinationIds, oldPage.id])];
|
|
1180
1346
|
resourcesForState = resetResourceDestinations(saved.resources);
|
|
1181
1347
|
commentsForState = undefined;
|
|
@@ -1216,9 +1382,9 @@ export class Migrator {
|
|
|
1216
1382
|
};
|
|
1217
1383
|
}
|
|
1218
1384
|
else {
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1385
|
+
const allocation = await allocatePage(title);
|
|
1386
|
+
created = allocation.page;
|
|
1387
|
+
if (allocation.reused) {
|
|
1222
1388
|
mappingRecovered = true;
|
|
1223
1389
|
report.issue({
|
|
1224
1390
|
code: 'destination-unmapped-page-recovered',
|
|
@@ -1229,22 +1395,6 @@ export class Migrator {
|
|
|
1229
1395
|
message: `${page.title}: adopted an existing source-derived page below the expected parent`,
|
|
1230
1396
|
});
|
|
1231
1397
|
}
|
|
1232
|
-
else {
|
|
1233
|
-
const titleConflict = existingChild ?? await findTitleConflict(title);
|
|
1234
|
-
if (titleConflict) {
|
|
1235
|
-
const base = collisionTitle(page);
|
|
1236
|
-
const hash = sha256(page.canonicalId).slice(0, 8);
|
|
1237
|
-
title = base;
|
|
1238
|
-
let sequence = 0;
|
|
1239
|
-
while (await findTitleConflict(title)) {
|
|
1240
|
-
sequence += 1;
|
|
1241
|
-
title = `${base} [${hash}${sequence > 1 ? `-${sequence}` : ''}]`;
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
created = await destination.createPage({ container: parent, title, body: bootstrap.body });
|
|
1245
|
-
created = await verifyConfirmedDestinationPage(destination, created, this.consistencyOptions);
|
|
1246
|
-
assertDestinationContent(bootstrap.body, created.content, destination.kind, cachedPage);
|
|
1247
|
-
}
|
|
1248
1398
|
}
|
|
1249
1399
|
if (!options.dryRun && created.parentId !== parent.id) {
|
|
1250
1400
|
throw new Error(`Destination hierarchy verification failed for ${page.title}: expected parent ${parent.id}, received ${created.parentId ?? 'none'}`);
|
|
@@ -1277,10 +1427,16 @@ export class Migrator {
|
|
|
1277
1427
|
throw options.signal.reason ?? error;
|
|
1278
1428
|
const message = `${page.title}: ${error instanceof Error ? error.message : String(error)}`;
|
|
1279
1429
|
const failureStatus = /conflict/i.test(message) ? 'conflict' : 'failed';
|
|
1280
|
-
|
|
1430
|
+
const failureCode = failureStatus === 'conflict' ? 'destination-conflict' : 'destination-page-staging-failed';
|
|
1431
|
+
stagingFailures.set(page.canonicalId, {
|
|
1432
|
+
status: failureStatus,
|
|
1433
|
+
message,
|
|
1434
|
+
blockedByPageId: page.canonicalId,
|
|
1435
|
+
blockedByCode: failureCode,
|
|
1436
|
+
});
|
|
1281
1437
|
report.updatePage(page.canonicalId, { status: failureStatus, error: message });
|
|
1282
1438
|
report.issue({
|
|
1283
|
-
code:
|
|
1439
|
+
code: failureCode,
|
|
1284
1440
|
severity: 'error',
|
|
1285
1441
|
phase: 'page',
|
|
1286
1442
|
pageId: page.canonicalId,
|
|
@@ -1288,8 +1444,29 @@ export class Migrator {
|
|
|
1288
1444
|
message,
|
|
1289
1445
|
});
|
|
1290
1446
|
const savedState = store.value.pages[page.canonicalId];
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
1447
|
+
if (!options.dryRun) {
|
|
1448
|
+
const cause = { blockedByPageId: page.canonicalId, blockedByCode: failureCode, blockedReason: message };
|
|
1449
|
+
const resources = resourcesByPage.get(page.canonicalId) ?? [];
|
|
1450
|
+
const comments = (options.comments ?? true)
|
|
1451
|
+
? blockedCommentState(commentsByPage.get(page.canonicalId), savedState?.comments, cause)
|
|
1452
|
+
: undefined;
|
|
1453
|
+
await store.setPage({
|
|
1454
|
+
...(savedState ?? {}),
|
|
1455
|
+
sourceId: page.id,
|
|
1456
|
+
sourceCanonicalId: page.canonicalId,
|
|
1457
|
+
sourceLocator: page.locator,
|
|
1458
|
+
sourceTitle: page.title,
|
|
1459
|
+
...(page.projectedParentId ? { projectedParentId: page.projectedParentId } : {}),
|
|
1460
|
+
destinationTitle: savedState?.destinationTitle ?? page.title,
|
|
1461
|
+
sourceFingerprint: sourceFingerprints.get(page.canonicalId),
|
|
1462
|
+
status: failureStatus,
|
|
1463
|
+
resources: blockedResourceStates(resources, savedState?.resources ?? {}, cause),
|
|
1464
|
+
...(comments ? { comments } : {}),
|
|
1465
|
+
degradations: savedState?.degradations ?? [],
|
|
1466
|
+
lastError: message,
|
|
1467
|
+
updatedAt: new Date().toISOString(),
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1293
1470
|
}
|
|
1294
1471
|
finally {
|
|
1295
1472
|
progress.staged += 1;
|
|
@@ -1307,16 +1484,25 @@ export class Migrator {
|
|
|
1307
1484
|
if (!destinationPage) {
|
|
1308
1485
|
const currentFailure = stagingFailures.get(page.canonicalId);
|
|
1309
1486
|
const failedState = store.value.pages[page.canonicalId];
|
|
1487
|
+
const blockedBy = {
|
|
1488
|
+
blockedByPageId: currentFailure?.blockedByPageId ?? page.canonicalId,
|
|
1489
|
+
blockedByCode: currentFailure?.blockedByCode ?? 'destination-page-unavailable',
|
|
1490
|
+
blockedReason: currentFailure?.message ?? failedState?.lastError ?? 'Destination page with source-derived content was not created',
|
|
1491
|
+
};
|
|
1492
|
+
const commentState = failedState?.comments ?? ((options.comments ?? true)
|
|
1493
|
+
? blockedCommentState(commentsByPage.get(page.canonicalId), undefined, blockedBy)
|
|
1494
|
+
: undefined);
|
|
1310
1495
|
report.addPage(reportPage({
|
|
1311
1496
|
page,
|
|
1312
1497
|
status: currentFailure?.status ?? 'blocked',
|
|
1313
1498
|
resources,
|
|
1314
1499
|
document,
|
|
1315
1500
|
...(failedState?.resources ? { resourceStates: failedState.resources } : {}),
|
|
1316
|
-
|
|
1501
|
+
resourceStatusOverride: options.dryRun ? 'planned' : 'blocked',
|
|
1317
1502
|
commentsEnabled: options.comments ?? true,
|
|
1318
1503
|
...(commentsByPage.get(page.canonicalId) ? { commentInventory: commentsByPage.get(page.canonicalId) } : {}),
|
|
1319
|
-
...(
|
|
1504
|
+
...(commentState ? { commentState } : {}),
|
|
1505
|
+
blockedBy,
|
|
1320
1506
|
error: currentFailure?.message ?? failedState?.lastError ?? 'Destination page with source-derived content was not created',
|
|
1321
1507
|
}));
|
|
1322
1508
|
progress.pages += 1;
|
|
@@ -1418,6 +1604,7 @@ export class Migrator {
|
|
|
1418
1604
|
sourceName: resource.name,
|
|
1419
1605
|
destinationId: mappedRemote.id,
|
|
1420
1606
|
destinationName: mappedRemote.name,
|
|
1607
|
+
...(mappedRemote.url ? { destinationUrl: mappedRemote.url } : {}),
|
|
1421
1608
|
sha256: downloaded.sha256,
|
|
1422
1609
|
size: downloaded.size,
|
|
1423
1610
|
cachePath: downloaded.path,
|
|
@@ -1441,6 +1628,7 @@ export class Migrator {
|
|
|
1441
1628
|
sourceName: resource.name,
|
|
1442
1629
|
destinationId: reconciled.id,
|
|
1443
1630
|
destinationName: reconciled.name,
|
|
1631
|
+
...(reconciled.url ? { destinationUrl: reconciled.url } : {}),
|
|
1444
1632
|
sha256: downloaded.sha256,
|
|
1445
1633
|
size: downloaded.size,
|
|
1446
1634
|
cachePath: downloaded.path,
|
|
@@ -1504,6 +1692,7 @@ export class Migrator {
|
|
|
1504
1692
|
sourceName: resource.name,
|
|
1505
1693
|
destinationId: result.id,
|
|
1506
1694
|
destinationName: result.name,
|
|
1695
|
+
...(result.url ? { destinationUrl: result.url } : {}),
|
|
1507
1696
|
sha256: downloaded.sha256,
|
|
1508
1697
|
size: downloaded.size,
|
|
1509
1698
|
cachePath: downloaded.path,
|