@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.
@@ -1 +1 @@
1
- {"version":3,"file":"domain.js","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"AAmUA,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"}
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"issues.js","sourceRoot":"","sources":["../src/issues.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAkB7C,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"}
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, 'renderer returned an empty body');
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 message = `Destination parent is unavailable for ${page.title}`;
970
- stagingFailures.set(page.canonicalId, { status: 'blocked', message });
971
- report.updatePage(page.canonicalId, { status: 'blocked', error: message });
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: 'error',
1152
+ severity: 'blocked',
975
1153
  phase: 'page',
976
1154
  pageId: page.canonicalId,
977
- retryable: true,
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
- let title = saved?.destinationTitle ?? page.title;
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 findTitleConflict = (candidateTitle) => destination.findTitleConflict
994
- ? destination.findTitleConflict(parent, candidateTitle)
995
- : destination.findChild(parent, candidateTitle);
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 findRecoveryChild = (candidateTitle) => destination.findRecoverablePage
1018
- ? destination.findRecoverablePage(parent, candidateTitle)
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
- let replacement = await destination.findChild(parent, title);
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 existingChild = await destination.findChild(parent, title);
1220
- if (existingChild && sourceDerivedTextMatches(bootstrap.body, existingChild.content, destination.kind, cachedPage)) {
1221
- created = existingChild;
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
- stagingFailures.set(page.canonicalId, { status: failureStatus, message });
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: failureStatus === 'conflict' ? 'destination-conflict' : 'destination-page-staging-failed',
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 (savedState)
1292
- await store.setPage({ ...savedState, status: failureStatus, lastError: message, updatedAt: new Date().toISOString() });
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
- ...(options.dryRun ? { resourceStatusOverride: 'planned' } : {}),
1432
+ resourceStatusOverride: options.dryRun ? 'planned' : 'blocked',
1317
1433
  commentsEnabled: options.comments ?? true,
1318
1434
  ...(commentsByPage.get(page.canonicalId) ? { commentInventory: commentsByPage.get(page.canonicalId) } : {}),
1319
- ...(failedState?.comments ? { commentState: failedState.comments } : {}),
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;