@iwonz/ywtc 3.4.0 → 3.4.1
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 +13 -0
- package/README.md +27 -0
- package/dist/adapters/confluence.js +28 -3
- package/dist/adapters/confluence.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 +5 -0
- package/dist/domain.js +10 -0
- package/dist/domain.js.map +1 -1
- package/dist/issues.d.ts +3 -1
- package/dist/issues.js.map +1 -1
- package/dist/migrator.js +269 -152
- 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.d.ts +17 -0
- package/dist/report.js +89 -9
- package/dist/report.js.map +1 -1
- package/dist/state.d.ts +21 -6
- package/dist/state.js +51 -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/package.json +1 -1
package/dist/domain.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.js","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"domain.js","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"AAiFA,MAAM,OAAO,6BAA8B,SAAQ,KAAK;IAE3C;IACA;IAFX,YACW,KAAa,EACb,MAAc,EACvB,OAAsB;QAEtB,KAAK,CAAC,+CAA+C,KAAK,GAAG,EAAE,OAAO,CAAC,CAAC;QAJ/D,UAAK,GAAL,KAAK,CAAQ;QACb,WAAM,GAAN,MAAM,CAAQ;QAIvB,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAC;IAC9C,CAAC;CACF;AAoPD,MAAM,UAAU,0BAA0B,CACxC,MAAqB,EACrB,WAA2C,EAC3C,KAAoB,EACpB,eAA6B,KAAK,EAClC,QAAQ,GAAG,IAAI;IAEf,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,SAAS;QAAE,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,cAAc,CAAC,CAAC;IACvF,IAAI,KAAK,KAAK,MAAM,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,aAAa;QAAE,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,kBAAkB,CAAC,CAAC;IACnH,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,aAAa;QAAE,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,kBAAkB,CAAC,CAAC;IAC/F,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,UAAU;QAAE,OAAO,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,eAAe,CAAC,CAAC;IAClH,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,cAAc;QAAE,OAAO,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,mBAAmB,CAAC,CAAC;IAC1H,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,YAAY;QAAE,OAAO,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,iBAAiB,CAAC,CAAC;IACtH,IAAI,QAAQ,IAAI,MAAM,CAAC,YAAY,CAAC,YAAY,KAAK,IAAI;QAAE,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,wCAAwC,CAAC,CAAC;IACxI,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClH,MAAM,oBAAoB,GAAG,MAAM,CAAC,oBAAoB,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,YAAY,KAAK,KAAK,CAAC;IAC1G,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,sCAAsC,YAAY,gBAAgB,KAAK,wDAAwD,CAAC,CAAC;IACzL,CAAC;AACH,CAAC"}
|
package/dist/issues.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type IssueSeverity = 'normalization' | 'degradation' | 'warning' | 'error';
|
|
1
|
+
export type IssueSeverity = 'normalization' | 'degradation' | 'warning' | 'blocked' | 'error';
|
|
2
2
|
type IssuePhase = 'discovery' | 'inventory' | 'page' | 'resource' | 'comment' | 'verification' | 'report';
|
|
3
3
|
export interface MigrationIssue {
|
|
4
4
|
code: string;
|
|
@@ -7,6 +7,8 @@ export interface MigrationIssue {
|
|
|
7
7
|
message: string;
|
|
8
8
|
pageId?: string;
|
|
9
9
|
resourceId?: string;
|
|
10
|
+
blockedByPageId?: string;
|
|
11
|
+
blockedByCode?: string;
|
|
10
12
|
retryable: boolean;
|
|
11
13
|
occurrences: number;
|
|
12
14
|
}
|
package/dist/issues.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issues.js","sourceRoot":"","sources":["../src/issues.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"issues.js","sourceRoot":"","sources":["../src/issues.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAoB7C,SAAS,iBAAiB,CAAC,OAAe;IACxC,OAAO,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACxD,CAAC;AAED,MAAM,OAAO,cAAc;IACR,MAAM,GAAG,IAAI,GAAG,EAA0B,CAAC;IAE5D,GAAG,CAAC,KAAiB;QACnB,MAAM,OAAO,GAAG,iBAAiB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,IAAI,EAAE,EAAE,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7G,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,WAAW,IAAI,CAAC,CAAC;YAC1B,IAAI,OAAO,IAAI,QAAQ,CAAC,OAAO,KAAK,OAAO;gBAAE,QAAQ,CAAC,OAAO,GAAG,OAAO,CAAC;YACxE,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,GAAmB,EAAC,GAAG,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,EAAC,CAAC;QAClE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,QAAuB;QAC3B,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,MAAM,CAAC;IAC3E,CAAC;IAED,sBAAsB;QACpB,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/H,CAAC;CACF"}
|
package/dist/migrator.js
CHANGED
|
@@ -3,16 +3,19 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import PQueue from 'p-queue';
|
|
5
5
|
import { createProviderAdapter } from './adapters/factory.js';
|
|
6
|
-
import { appendGridSnapshots, assertDestinationContent, isLegacyTransferPlaceholder, normalizePageContent, normalizedPreviewHtml, rawContentFallback, renderedPreviewHtml, renderConfluenceStorage, renderYfm, setSourceResourceBinding, sourceResourceBindings, verifyDestinationContent, } from './content-model.js';
|
|
6
|
+
import { appendGridSnapshots, assertDestinationContent, emptyRendererFallbackReason, isLegacyTransferPlaceholder, normalizePageContent, normalizedPreviewHtml, rawContentFallback, renderedPreviewHtml, renderConfluenceStorage, renderYfm, setSourceResourceBinding, sourceResourceBindings, verifyDestinationContent, } from './content-model.js';
|
|
7
7
|
import { commentAttachmentUses, createCommentArchiveZip, renderCommentArchive, renderCommentArchiveIndex, resolveCommentAttachmentReferences, verifyCommentArchive, } from './comments.js';
|
|
8
8
|
import { ConfirmedDestinationMissingError, verifyConfirmedDestinationPage } from './consistency.js';
|
|
9
|
+
import { DestinationTitleConflictError } from './domain.js';
|
|
9
10
|
import { assertTransferCapabilities } from './domain.js';
|
|
10
11
|
import { FilesystemExporter } from './filesystem-export.js';
|
|
11
12
|
import { discoverPageGraph } from './graph.js';
|
|
13
|
+
import { HttpRequestError } from './http.js';
|
|
12
14
|
import { safeFilename } from './input.js';
|
|
13
15
|
import { profileBaseUrl, sameRemoteInstance } from './locators.js';
|
|
14
16
|
import { TransferReport, } from './report.js';
|
|
15
17
|
import { DestinationNameAllocator, preferredDestinationName, verifiedCacheFile } from './resource-reconciliation.js';
|
|
18
|
+
import { destinationTitleCandidates } from './title-allocation.js';
|
|
16
19
|
import { resolveStateLocation, StateStore, } from './state.js';
|
|
17
20
|
function sourceIdentity(locator, root) {
|
|
18
21
|
return {
|
|
@@ -65,10 +68,6 @@ async function cacheDocument(cacheDirectory, canonicalId, document) {
|
|
|
65
68
|
async function loadCachedDocument(cacheDirectory, canonicalId) {
|
|
66
69
|
return JSON.parse(await readFile(cachedDocumentPath(cacheDirectory, canonicalId), 'utf8'));
|
|
67
70
|
}
|
|
68
|
-
function collisionTitle(page) {
|
|
69
|
-
const suffix = page.locator.split('/').filter(Boolean).at(-1) ?? page.id;
|
|
70
|
-
return `${page.title} [${suffix}]`;
|
|
71
|
-
}
|
|
72
71
|
function cachePath(cacheDirectory, page, resource) {
|
|
73
72
|
const pageKey = sha256(page.canonicalId).slice(0, 12);
|
|
74
73
|
const resourceKey = sha256(`${resource.id}:${resource.name}`).slice(0, 12);
|
|
@@ -104,6 +103,73 @@ function sourceDerivedTextMatches(expectedBody, actualBody, provider, page) {
|
|
|
104
103
|
const text = (value) => renderedPreviewHtml(value, provider).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
105
104
|
return text(expectedBody) === text(actualBody);
|
|
106
105
|
}
|
|
106
|
+
function ambiguousCreateOutcome(error) {
|
|
107
|
+
return error instanceof HttpRequestError
|
|
108
|
+
&& (error.status === undefined || error.status >= 500);
|
|
109
|
+
}
|
|
110
|
+
async function createPageWithAllocatedTitle(input) {
|
|
111
|
+
const candidates = destinationTitleCandidates(input.sourcePage, input.preferredTitle);
|
|
112
|
+
for (const [index, title] of candidates.entries()) {
|
|
113
|
+
let child;
|
|
114
|
+
try {
|
|
115
|
+
child = input.destination.findRecoverablePage
|
|
116
|
+
? await input.destination.findRecoverablePage(input.parent, title)
|
|
117
|
+
: await input.destination.findChild(input.parent, title);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
input.report.issue({
|
|
121
|
+
code: 'destination-recovery-lookup-failed',
|
|
122
|
+
severity: 'warning',
|
|
123
|
+
phase: 'page',
|
|
124
|
+
pageId: input.sourcePage.canonicalId,
|
|
125
|
+
retryable: true,
|
|
126
|
+
message: `${input.sourcePage.title}: parent-scoped recovery lookup failed for "${title}"; authoritative creation will be attempted: ${error instanceof Error ? error.message : String(error)}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (child && sourceDerivedTextMatches(input.bootstrap.body, child.content, input.destination.kind, input.sourcePage)) {
|
|
130
|
+
return { page: child, reused: true };
|
|
131
|
+
}
|
|
132
|
+
const occupied = child ?? (input.destination.findTitleConflict
|
|
133
|
+
? await input.destination.findTitleConflict(input.parent, title)
|
|
134
|
+
: undefined);
|
|
135
|
+
if (occupied)
|
|
136
|
+
continue;
|
|
137
|
+
let created;
|
|
138
|
+
try {
|
|
139
|
+
created = await input.destination.createPage({ container: input.parent, title, body: input.bootstrap.body });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof DestinationTitleConflictError)
|
|
143
|
+
continue;
|
|
144
|
+
if (ambiguousCreateOutcome(error)) {
|
|
145
|
+
const recovered = input.destination.findRecoverablePage
|
|
146
|
+
? await input.destination.findRecoverablePage(input.parent, title)
|
|
147
|
+
: await input.destination.findChild(input.parent, title);
|
|
148
|
+
if (recovered && sourceDerivedTextMatches(input.bootstrap.body, recovered.content, input.destination.kind, input.sourcePage)) {
|
|
149
|
+
return { page: recovered, reused: true };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
created = await verifyConfirmedDestinationPage(input.destination, created, input.consistencyOptions);
|
|
155
|
+
assertDestinationContent(input.bootstrap.body, created.content, input.destination.kind, input.sourcePage);
|
|
156
|
+
if (created.parentId !== input.parent.id) {
|
|
157
|
+
throw new Error(`Destination hierarchy verification failed for ${input.sourcePage.title}: expected parent ${input.parent.id}, received ${created.parentId ?? 'none'}`);
|
|
158
|
+
}
|
|
159
|
+
if (index > 0) {
|
|
160
|
+
input.report.issue({
|
|
161
|
+
code: 'destination-title-renamed',
|
|
162
|
+
severity: 'normalization',
|
|
163
|
+
phase: 'page',
|
|
164
|
+
pageId: input.sourcePage.canonicalId,
|
|
165
|
+
retryable: false,
|
|
166
|
+
message: `${input.sourcePage.title}: destination title was allocated as "${created.title}" because an earlier candidate was occupied`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return { page: created, reused: false };
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`Confluence rejected all ${candidates.length} deterministic destination titles for ${input.sourcePage.title} because they are occupied`);
|
|
172
|
+
}
|
|
107
173
|
function destinationFromState(state, adapter) {
|
|
108
174
|
if (!state.destinationId || !state.destinationCanonicalId || !state.destinationLocator) {
|
|
109
175
|
throw new Error(`Incomplete mapped destination for ${state.sourceTitle}`);
|
|
@@ -135,6 +201,7 @@ function reportPage(input) {
|
|
|
135
201
|
...(state?.destinationId ? { destinationId: state.destinationId } : {}),
|
|
136
202
|
...(state?.destinationName ? { destinationName: state.destinationName } : {}),
|
|
137
203
|
...(state?.lastError ? { error: state.lastError } : {}),
|
|
204
|
+
...(status === 'blocked' && input.blockedBy ? input.blockedBy : {}),
|
|
138
205
|
};
|
|
139
206
|
});
|
|
140
207
|
const knownBytes = resourceItems.reduce((total, resource) => total + (resource.sourceBytes ?? 0), 0);
|
|
@@ -176,6 +243,7 @@ function reportPage(input) {
|
|
|
176
243
|
warnings: [...new Set([...input.document.warnings, ...(input.warnings ?? [])])],
|
|
177
244
|
normalizations: [...new Set([...(input.document.normalizations ?? []), ...(input.normalizations ?? [])])],
|
|
178
245
|
...(input.error ? { error: input.error } : {}),
|
|
246
|
+
...(input.status === 'blocked' && input.blockedBy ? input.blockedBy : {}),
|
|
179
247
|
comments: {
|
|
180
248
|
status: commentStatus,
|
|
181
249
|
threads: commentState?.threads ?? inventory?.threads.length ?? 0,
|
|
@@ -189,9 +257,43 @@ function reportPage(input) {
|
|
|
189
257
|
...(commentState?.status === 'stale' ? { stale: true } : {}),
|
|
190
258
|
...(commentError ? { error: commentError } : {}),
|
|
191
259
|
...(commentState?.preview ? { preview: commentState.preview } : {}),
|
|
260
|
+
...(commentStatus === 'blocked' && input.blockedBy ? input.blockedBy : {}),
|
|
192
261
|
},
|
|
193
262
|
};
|
|
194
263
|
}
|
|
264
|
+
function blockedResourceStates(resources, previous, cause) {
|
|
265
|
+
return {
|
|
266
|
+
...previous,
|
|
267
|
+
...Object.fromEntries(resources.filter((resource) => resource.kind !== 'grid').map((resource) => {
|
|
268
|
+
const existing = previous[resource.id];
|
|
269
|
+
return [resource.id, {
|
|
270
|
+
...(existing ?? {}),
|
|
271
|
+
sourceId: resource.id,
|
|
272
|
+
sourceName: resource.name,
|
|
273
|
+
destinationName: existing?.destinationName ?? preferredDestinationName(resource),
|
|
274
|
+
...(existing?.size !== undefined ? { size: existing.size } : resource.size !== undefined ? { size: resource.size } : {}),
|
|
275
|
+
status: 'blocked',
|
|
276
|
+
...cause,
|
|
277
|
+
lastError: cause.blockedReason,
|
|
278
|
+
updatedAt: new Date().toISOString(),
|
|
279
|
+
}];
|
|
280
|
+
})),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function blockedCommentState(inventory, previous, cause) {
|
|
284
|
+
return {
|
|
285
|
+
...(previous ?? {}),
|
|
286
|
+
status: 'blocked',
|
|
287
|
+
threads: previous?.threads ?? inventory?.threads.length ?? 0,
|
|
288
|
+
comments: previous?.comments ?? inventory?.comments ?? 0,
|
|
289
|
+
participants: previous?.participants ?? inventory?.participants ?? 0,
|
|
290
|
+
attachments: previous?.attachments ?? inventory?.attachments ?? 0,
|
|
291
|
+
bytes: previous?.bytes ?? inventory?.knownBytes ?? 0,
|
|
292
|
+
...cause,
|
|
293
|
+
lastError: cause.blockedReason,
|
|
294
|
+
updatedAt: new Date().toISOString(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
195
297
|
function runStatus(errors, warnings, degradations) {
|
|
196
298
|
if (errors > 0)
|
|
197
299
|
return 'partial';
|
|
@@ -200,6 +302,17 @@ function runStatus(errors, warnings, degradations) {
|
|
|
200
302
|
function attentionCount(summary) {
|
|
201
303
|
return summary.warnings + summary.degradations;
|
|
202
304
|
}
|
|
305
|
+
function runOutcomeCounts(summary, pages) {
|
|
306
|
+
return {
|
|
307
|
+
pages,
|
|
308
|
+
warnings: attentionCount(summary),
|
|
309
|
+
errors: summary.errors,
|
|
310
|
+
rootErrors: summary.rootErrors,
|
|
311
|
+
dependentPages: summary.dependentPages,
|
|
312
|
+
dependentFiles: summary.dependentFiles,
|
|
313
|
+
dependentComments: summary.dependentComments,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
203
316
|
export class MigrationRunError extends Error {
|
|
204
317
|
reportPaths;
|
|
205
318
|
stateFilename;
|
|
@@ -216,7 +329,7 @@ async function renderRemoteDocument(input) {
|
|
|
216
329
|
? await renderConfluenceStorage(input.document, input.page, input.context, input.container.space ?? '')
|
|
217
330
|
: await renderYfm(input.document, input.context);
|
|
218
331
|
if (!rendered.body.trim())
|
|
219
|
-
return rawContentFallback(input.page, input.destination.kind,
|
|
332
|
+
return rawContentFallback(input.page, input.destination.kind, emptyRendererFallbackReason(input.document));
|
|
220
333
|
return rendered;
|
|
221
334
|
}
|
|
222
335
|
catch (error) {
|
|
@@ -329,11 +442,7 @@ export class Migrator {
|
|
|
329
442
|
report.finish(finalStatus);
|
|
330
443
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
331
444
|
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
|
-
});
|
|
445
|
+
await store.finishRun(run.id, finalStatus, runOutcomeCounts(report.data.summary, 0));
|
|
337
446
|
throw new MigrationRunError(message, reportPaths, stateLocation.filename, { cause: error });
|
|
338
447
|
}
|
|
339
448
|
const resourcesByPage = new Map();
|
|
@@ -419,13 +528,58 @@ export class Migrator {
|
|
|
419
528
|
report.finish(finalStatus);
|
|
420
529
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
421
530
|
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
|
-
});
|
|
531
|
+
await store.finishRun(run.id, finalStatus, runOutcomeCounts(report.data.summary, graph.pages.length));
|
|
427
532
|
throw new MigrationRunError('Resource preflight failed before destination mutation', reportPaths, stateLocation.filename);
|
|
428
533
|
}
|
|
534
|
+
if (!options.dryRun) {
|
|
535
|
+
for (const page of graph.pages) {
|
|
536
|
+
if (store.value.pages[page.canonicalId])
|
|
537
|
+
continue;
|
|
538
|
+
const commentInventory = commentsByPage.get(page.canonicalId);
|
|
539
|
+
const commentState = !commentsEnabled
|
|
540
|
+
? { status: 'skipped', threads: 0, comments: 0, participants: 0, attachments: 0, bytes: 0, updatedAt: new Date().toISOString() }
|
|
541
|
+
: commentInventory?.status === 'empty'
|
|
542
|
+
? { status: 'complete', threads: 0, comments: 0, participants: 0, attachments: 0, bytes: 0, updatedAt: new Date().toISOString() }
|
|
543
|
+
: commentInventory?.status === 'failed'
|
|
544
|
+
? {
|
|
545
|
+
status: 'failed', threads: 0, comments: 0, participants: 0, attachments: 0, bytes: 0,
|
|
546
|
+
...(commentInventory.error ? { lastError: commentInventory.error } : {}), updatedAt: new Date().toISOString(),
|
|
547
|
+
}
|
|
548
|
+
: commentInventory
|
|
549
|
+
? {
|
|
550
|
+
status: 'planned', ...(commentInventory.fingerprint ? { fingerprint: commentInventory.fingerprint } : {}),
|
|
551
|
+
threads: commentInventory.threads.length, comments: commentInventory.comments,
|
|
552
|
+
participants: commentInventory.participants, attachments: commentInventory.attachments,
|
|
553
|
+
bytes: commentInventory.knownBytes, updatedAt: new Date().toISOString(),
|
|
554
|
+
}
|
|
555
|
+
: undefined;
|
|
556
|
+
const resourceStates = Object.fromEntries((resourcesByPage.get(page.canonicalId) ?? [])
|
|
557
|
+
.filter((resource) => resource.kind !== 'grid')
|
|
558
|
+
.map((resource) => [resource.id, {
|
|
559
|
+
sourceId: resource.id,
|
|
560
|
+
sourceName: resource.name,
|
|
561
|
+
destinationName: preferredDestinationName(resource),
|
|
562
|
+
...(resource.size !== undefined ? { size: resource.size } : {}),
|
|
563
|
+
...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
|
|
564
|
+
status: 'planned',
|
|
565
|
+
updatedAt: new Date().toISOString(),
|
|
566
|
+
}]));
|
|
567
|
+
await store.setPage({
|
|
568
|
+
sourceId: page.id,
|
|
569
|
+
sourceCanonicalId: page.canonicalId,
|
|
570
|
+
sourceLocator: page.locator,
|
|
571
|
+
sourceTitle: page.title,
|
|
572
|
+
...(page.projectedParentId ? { projectedParentId: page.projectedParentId } : {}),
|
|
573
|
+
destinationTitle: page.title,
|
|
574
|
+
sourceFingerprint: sourceFingerprints.get(page.canonicalId),
|
|
575
|
+
status: 'discovered',
|
|
576
|
+
resources: resourceStates,
|
|
577
|
+
...(commentState ? { comments: commentState } : {}),
|
|
578
|
+
degradations: [],
|
|
579
|
+
updatedAt: new Date().toISOString(),
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
429
583
|
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
584
|
status(planSummary);
|
|
431
585
|
this.logger.info(planSummary);
|
|
@@ -460,14 +614,11 @@ export class Migrator {
|
|
|
460
614
|
}
|
|
461
615
|
report.finish('completed');
|
|
462
616
|
const finalStatus = runStatus(report.data.summary.errors, report.data.summary.warnings, report.data.summary.degradations);
|
|
617
|
+
await store.finalizeOutcomes(graph.pages.map((page) => page.canonicalId), finalStatus, options.dryRun);
|
|
463
618
|
report.finish(finalStatus);
|
|
464
619
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
465
620
|
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
|
-
});
|
|
621
|
+
await store.finishRun(run.id, finalStatus, runOutcomeCounts(report.data.summary, graph.pages.length));
|
|
471
622
|
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
623
|
return {
|
|
473
624
|
status: finalStatus,
|
|
@@ -486,14 +637,11 @@ export class Migrator {
|
|
|
486
637
|
const finalStatus = cancelled ? 'cancelled' : 'failed';
|
|
487
638
|
const message = error instanceof Error ? error.message : String(error);
|
|
488
639
|
report.error(message);
|
|
640
|
+
await store.finalizeOutcomes(graph.pages.map((page) => page.canonicalId), finalStatus, options.dryRun);
|
|
489
641
|
report.finish(finalStatus);
|
|
490
642
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
491
643
|
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
|
-
});
|
|
644
|
+
await store.finishRun(run.id, finalStatus, runOutcomeCounts(report.data.summary, graph.pages.length));
|
|
497
645
|
this.logger.error(`${message}. Report: ${reportPaths.html}`);
|
|
498
646
|
throw new MigrationRunError(message, reportPaths, stateLocation.filename, { cause: error });
|
|
499
647
|
}
|
|
@@ -966,15 +1114,47 @@ export class Migrator {
|
|
|
966
1114
|
const saved = store.value.pages[page.canonicalId];
|
|
967
1115
|
const parent = page.projectedParentId ? destinationPages.get(page.projectedParentId) : container;
|
|
968
1116
|
if (!parent) {
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
1117
|
+
const upstream = page.projectedParentId ? stagingFailures.get(page.projectedParentId) : undefined;
|
|
1118
|
+
const blockedByPageId = upstream?.blockedByPageId ?? page.projectedParentId ?? page.canonicalId;
|
|
1119
|
+
const blockedByCode = upstream?.blockedByCode ?? 'destination-parent-unavailable';
|
|
1120
|
+
const message = `Destination parent is unavailable for ${page.title}; blocked by ${blockedByPageId} (${blockedByCode})`;
|
|
1121
|
+
const cause = { blockedByPageId, blockedByCode, blockedReason: message };
|
|
1122
|
+
stagingFailures.set(page.canonicalId, { status: 'blocked', message, blockedByPageId, blockedByCode });
|
|
1123
|
+
const previous = store.value.pages[page.canonicalId];
|
|
1124
|
+
const resources = resourcesByPage.get(page.canonicalId) ?? [];
|
|
1125
|
+
const comments = (options.comments ?? true)
|
|
1126
|
+
? blockedCommentState(commentsByPage.get(page.canonicalId), previous?.comments, cause)
|
|
1127
|
+
: undefined;
|
|
1128
|
+
await store.setPage({
|
|
1129
|
+
sourceId: page.id,
|
|
1130
|
+
sourceCanonicalId: page.canonicalId,
|
|
1131
|
+
sourceLocator: page.locator,
|
|
1132
|
+
sourceTitle: page.title,
|
|
1133
|
+
...(page.projectedParentId ? { projectedParentId: page.projectedParentId } : {}),
|
|
1134
|
+
destinationTitle: previous?.destinationTitle ?? page.title,
|
|
1135
|
+
...(previous?.destinationId ? { destinationId: previous.destinationId } : {}),
|
|
1136
|
+
...(previous?.destinationCanonicalId ? { destinationCanonicalId: previous.destinationCanonicalId } : {}),
|
|
1137
|
+
...(previous?.destinationLocator ? { destinationLocator: previous.destinationLocator } : {}),
|
|
1138
|
+
...(previous?.previousDestinationIds ? { previousDestinationIds: previous.previousDestinationIds } : {}),
|
|
1139
|
+
sourceFingerprint: sourceFingerprints.get(page.canonicalId),
|
|
1140
|
+
...(previous?.destinationFingerprint ? { destinationFingerprint: previous.destinationFingerprint } : {}),
|
|
1141
|
+
status: 'blocked',
|
|
1142
|
+
resources: blockedResourceStates(resources, previous?.resources ?? {}, cause),
|
|
1143
|
+
...(comments ? { comments } : {}),
|
|
1144
|
+
degradations: previous?.degradations ?? [],
|
|
1145
|
+
...cause,
|
|
1146
|
+
lastError: message,
|
|
1147
|
+
updatedAt: new Date().toISOString(),
|
|
1148
|
+
});
|
|
1149
|
+
report.updatePage(page.canonicalId, { status: 'blocked', error: message, ...cause });
|
|
972
1150
|
report.issue({
|
|
973
1151
|
code: 'destination-parent-unavailable',
|
|
974
|
-
severity: '
|
|
1152
|
+
severity: 'blocked',
|
|
975
1153
|
phase: 'page',
|
|
976
1154
|
pageId: page.canonicalId,
|
|
977
|
-
retryable:
|
|
1155
|
+
retryable: false,
|
|
1156
|
+
blockedByPageId,
|
|
1157
|
+
blockedByCode,
|
|
978
1158
|
message,
|
|
979
1159
|
});
|
|
980
1160
|
progress.staged += 1;
|
|
@@ -985,14 +1165,20 @@ export class Migrator {
|
|
|
985
1165
|
const cachedPage = await loadCachedPage(options.cacheDirectory, page.canonicalId);
|
|
986
1166
|
const bootstrap = bootstrapBodies.get(page.canonicalId);
|
|
987
1167
|
let created;
|
|
988
|
-
|
|
1168
|
+
const title = saved?.destinationTitle ?? page.title;
|
|
989
1169
|
let resourcesForState = saved?.resources ?? {};
|
|
990
1170
|
let commentsForState = saved?.comments;
|
|
991
1171
|
let previousDestinationIds = saved?.previousDestinationIds ?? [];
|
|
992
1172
|
let mappingRecovered = false;
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
1173
|
+
const allocatePage = (preferredTitle) => createPageWithAllocatedTitle({
|
|
1174
|
+
destination,
|
|
1175
|
+
parent,
|
|
1176
|
+
sourcePage: cachedPage,
|
|
1177
|
+
bootstrap,
|
|
1178
|
+
...(preferredTitle ? { preferredTitle } : {}),
|
|
1179
|
+
consistencyOptions: this.consistencyOptions,
|
|
1180
|
+
report,
|
|
1181
|
+
});
|
|
996
1182
|
if (saved?.destinationId) {
|
|
997
1183
|
const mapped = destinationFromState(saved, destination);
|
|
998
1184
|
try {
|
|
@@ -1014,77 +1200,8 @@ export class Migrator {
|
|
|
1014
1200
|
if (page.projectedParentId) {
|
|
1015
1201
|
await verifyConfirmedDestinationPage(destination, parent, this.consistencyOptions);
|
|
1016
1202
|
}
|
|
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
|
-
}
|
|
1203
|
+
const allocation = await allocatePage(title);
|
|
1204
|
+
created = allocation.page;
|
|
1088
1205
|
previousDestinationIds = [...new Set([...previousDestinationIds, mapped.id])];
|
|
1089
1206
|
resourcesForState = resetResourceDestinations(saved.resources);
|
|
1090
1207
|
commentsForState = undefined;
|
|
@@ -1155,27 +1272,7 @@ export class Migrator {
|
|
|
1155
1272
|
});
|
|
1156
1273
|
}
|
|
1157
1274
|
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
|
-
}
|
|
1275
|
+
created = (await allocatePage(title)).page;
|
|
1179
1276
|
previousDestinationIds = [...new Set([...previousDestinationIds, oldPage.id])];
|
|
1180
1277
|
resourcesForState = resetResourceDestinations(saved.resources);
|
|
1181
1278
|
commentsForState = undefined;
|
|
@@ -1216,9 +1313,9 @@ export class Migrator {
|
|
|
1216
1313
|
};
|
|
1217
1314
|
}
|
|
1218
1315
|
else {
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1316
|
+
const allocation = await allocatePage(title);
|
|
1317
|
+
created = allocation.page;
|
|
1318
|
+
if (allocation.reused) {
|
|
1222
1319
|
mappingRecovered = true;
|
|
1223
1320
|
report.issue({
|
|
1224
1321
|
code: 'destination-unmapped-page-recovered',
|
|
@@ -1229,22 +1326,6 @@ export class Migrator {
|
|
|
1229
1326
|
message: `${page.title}: adopted an existing source-derived page below the expected parent`,
|
|
1230
1327
|
});
|
|
1231
1328
|
}
|
|
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
1329
|
}
|
|
1249
1330
|
if (!options.dryRun && created.parentId !== parent.id) {
|
|
1250
1331
|
throw new Error(`Destination hierarchy verification failed for ${page.title}: expected parent ${parent.id}, received ${created.parentId ?? 'none'}`);
|
|
@@ -1277,10 +1358,16 @@ export class Migrator {
|
|
|
1277
1358
|
throw options.signal.reason ?? error;
|
|
1278
1359
|
const message = `${page.title}: ${error instanceof Error ? error.message : String(error)}`;
|
|
1279
1360
|
const failureStatus = /conflict/i.test(message) ? 'conflict' : 'failed';
|
|
1280
|
-
|
|
1361
|
+
const failureCode = failureStatus === 'conflict' ? 'destination-conflict' : 'destination-page-staging-failed';
|
|
1362
|
+
stagingFailures.set(page.canonicalId, {
|
|
1363
|
+
status: failureStatus,
|
|
1364
|
+
message,
|
|
1365
|
+
blockedByPageId: page.canonicalId,
|
|
1366
|
+
blockedByCode: failureCode,
|
|
1367
|
+
});
|
|
1281
1368
|
report.updatePage(page.canonicalId, { status: failureStatus, error: message });
|
|
1282
1369
|
report.issue({
|
|
1283
|
-
code:
|
|
1370
|
+
code: failureCode,
|
|
1284
1371
|
severity: 'error',
|
|
1285
1372
|
phase: 'page',
|
|
1286
1373
|
pageId: page.canonicalId,
|
|
@@ -1288,8 +1375,29 @@ export class Migrator {
|
|
|
1288
1375
|
message,
|
|
1289
1376
|
});
|
|
1290
1377
|
const savedState = store.value.pages[page.canonicalId];
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
1378
|
+
if (!options.dryRun) {
|
|
1379
|
+
const cause = { blockedByPageId: page.canonicalId, blockedByCode: failureCode, blockedReason: message };
|
|
1380
|
+
const resources = resourcesByPage.get(page.canonicalId) ?? [];
|
|
1381
|
+
const comments = (options.comments ?? true)
|
|
1382
|
+
? blockedCommentState(commentsByPage.get(page.canonicalId), savedState?.comments, cause)
|
|
1383
|
+
: undefined;
|
|
1384
|
+
await store.setPage({
|
|
1385
|
+
...(savedState ?? {}),
|
|
1386
|
+
sourceId: page.id,
|
|
1387
|
+
sourceCanonicalId: page.canonicalId,
|
|
1388
|
+
sourceLocator: page.locator,
|
|
1389
|
+
sourceTitle: page.title,
|
|
1390
|
+
...(page.projectedParentId ? { projectedParentId: page.projectedParentId } : {}),
|
|
1391
|
+
destinationTitle: savedState?.destinationTitle ?? page.title,
|
|
1392
|
+
sourceFingerprint: sourceFingerprints.get(page.canonicalId),
|
|
1393
|
+
status: failureStatus,
|
|
1394
|
+
resources: blockedResourceStates(resources, savedState?.resources ?? {}, cause),
|
|
1395
|
+
...(comments ? { comments } : {}),
|
|
1396
|
+
degradations: savedState?.degradations ?? [],
|
|
1397
|
+
lastError: message,
|
|
1398
|
+
updatedAt: new Date().toISOString(),
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1293
1401
|
}
|
|
1294
1402
|
finally {
|
|
1295
1403
|
progress.staged += 1;
|
|
@@ -1307,16 +1415,25 @@ export class Migrator {
|
|
|
1307
1415
|
if (!destinationPage) {
|
|
1308
1416
|
const currentFailure = stagingFailures.get(page.canonicalId);
|
|
1309
1417
|
const failedState = store.value.pages[page.canonicalId];
|
|
1418
|
+
const blockedBy = {
|
|
1419
|
+
blockedByPageId: currentFailure?.blockedByPageId ?? page.canonicalId,
|
|
1420
|
+
blockedByCode: currentFailure?.blockedByCode ?? 'destination-page-unavailable',
|
|
1421
|
+
blockedReason: currentFailure?.message ?? failedState?.lastError ?? 'Destination page with source-derived content was not created',
|
|
1422
|
+
};
|
|
1423
|
+
const commentState = failedState?.comments ?? ((options.comments ?? true)
|
|
1424
|
+
? blockedCommentState(commentsByPage.get(page.canonicalId), undefined, blockedBy)
|
|
1425
|
+
: undefined);
|
|
1310
1426
|
report.addPage(reportPage({
|
|
1311
1427
|
page,
|
|
1312
1428
|
status: currentFailure?.status ?? 'blocked',
|
|
1313
1429
|
resources,
|
|
1314
1430
|
document,
|
|
1315
1431
|
...(failedState?.resources ? { resourceStates: failedState.resources } : {}),
|
|
1316
|
-
|
|
1432
|
+
resourceStatusOverride: options.dryRun ? 'planned' : 'blocked',
|
|
1317
1433
|
commentsEnabled: options.comments ?? true,
|
|
1318
1434
|
...(commentsByPage.get(page.canonicalId) ? { commentInventory: commentsByPage.get(page.canonicalId) } : {}),
|
|
1319
|
-
...(
|
|
1435
|
+
...(commentState ? { commentState } : {}),
|
|
1436
|
+
blockedBy,
|
|
1320
1437
|
error: currentFailure?.message ?? failedState?.lastError ?? 'Destination page with source-derived content was not created',
|
|
1321
1438
|
}));
|
|
1322
1439
|
progress.pages += 1;
|