@thallylabs/migrate 0.2.41 → 0.2.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,12 @@ ordered navigation and binary assets), or uses ordinary local fetching. Hosted
10
10
  callers must inject their own network-safe text/capture and binary boundaries.
11
11
  No browser, model, credentials, sandbox or publishing provider is bundled here.
12
12
 
13
+ Default page requests advertise HTML to retain rendered content and declared
14
+ alternates. An explicit same-origin, same-route Markdown alternate shares the
15
+ article's destination; the rendered article takes precedence even when the
16
+ Markdown URL was discovered first or restored from an interrupted checkpoint.
17
+ Independent pages still report destination collisions.
18
+
13
19
  Server applications should import `@thallylabs/migrate/svg-policy` for the passive
14
20
  SVG validator, or `@thallylabs/migrate/url-migration` for the URL runner, renderer
15
21
  and their types. These independent entries avoid tracing repository cloning and
@@ -99,8 +105,13 @@ Missing primary counterparts and projected path collisions require review; the
99
105
  engine does not invent translations. Fresh HTML captures also observe `html lang`; conflicting or unsupported
100
106
  language evidence requires review. Capture adapters can supply `locale` when
101
107
  no language is declared in HTML or identified by the scoped URL path.
102
- The `url-evidence-v6` converter rejects earlier journals: replay retained raw
108
+ The `url-evidence-v7` converter rejects earlier journals: replay retained raw
103
109
  captures into a new journal rather than relabelling previously hashed pages.
110
+ Distinct observed redirect routes can retain their own sidebar titles through
111
+ noindex page metadata and portable internal redirects to the captured article.
112
+ Conflicting labels on the same route still require review. Markdown image and
113
+ link destinations are localized by source spans, including escaped query strings,
114
+ without changing matching labels, titles, or fenced examples.
104
115
 
105
116
  `stopReason` distinguishes a resumable `capture-limit` from hard `page-limit`
106
117
  or `byte-limit` outcomes. Hosts must not automatically retry a hard limit as an
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MigrationPage, a as MigrationAsset, b as MigrationNavigationGroup, R as RenderedMigrationFile, c as MigrationWarning, d as MigrationFetcher, e as MigrationPlatform, f as MigrationBundle, g as MigrationDocsConfig, E as EndpointTabSlot } from './url-run-mSKFqHx_.js';
2
- export { h as MigrationCapture, i as MigrationCaptureProvider, j as MigrationCaptureState, k as MigrationCapturedAsset, l as MigrationEvidence, m as MigrationFetchRequest, n as MigrationFetchResponse, o as MigrationJournalAsset, p as MigrationNavigationOccurrence, q as MigrationNavigationTab, U as UrlMigrationCheckpoint, r as UrlMigrationCheckpointDelta, s as UrlMigrationIssue, t as UrlMigrationRecord, u as UrlMigrationRun, v as UrlMigrationRunOptions, w as UrlMigrationValidation, x as classifyMigrationResponse, y as mergeMigrationConfig, z as renderMigrationFiles, A as runUrlMigration, B as validateUrlMigration } from './url-run-mSKFqHx_.js';
1
+ import { M as MigrationPage, a as MigrationAsset, b as MigrationNavigationGroup, R as RenderedMigrationFile, c as MigrationWarning, d as MigrationFetcher, e as MigrationPlatform, f as MigrationBundle, g as MigrationDocsConfig, E as EndpointTabSlot } from './url-run-CPzQfjRW.js';
2
+ export { h as MigrationCapture, i as MigrationCaptureProvider, j as MigrationCaptureState, k as MigrationCapturedAsset, l as MigrationEvidence, m as MigrationFetchRequest, n as MigrationFetchResponse, o as MigrationJournalAsset, p as MigrationNavigationOccurrence, q as MigrationNavigationTab, U as UrlMigrationCheckpoint, r as UrlMigrationCheckpointDelta, s as UrlMigrationIssue, t as UrlMigrationRecord, u as UrlMigrationRun, v as UrlMigrationRunOptions, w as UrlMigrationValidation, x as classifyMigrationResponse, y as mergeMigrationConfig, z as renderMigrationFiles, A as runUrlMigration, B as validateUrlMigration } from './url-run-CPzQfjRW.js';
3
3
  export { isPassiveSvg } from './svg-policy.js';
4
4
 
5
5
  /**
package/dist/index.js CHANGED
@@ -7860,7 +7860,10 @@ var VIDEO_ATTRIBUTES = /* @__PURE__ */ new Set([
7860
7860
  "width",
7861
7861
  "height",
7862
7862
  "preload",
7863
- "aria-label"
7863
+ "aria-label",
7864
+ // Publisher asset provenance does not change playback and is never emitted.
7865
+ // Keep this explicit: arbitrary data attributes may drive custom controls.
7866
+ "data-path"
7864
7867
  ]);
7865
7868
  var YOUTUBE_ATTRIBUTES = /* @__PURE__ */ new Set([
7866
7869
  "src",
@@ -16119,6 +16122,7 @@ function renderMigrationFiles(bundle, options = {}) {
16119
16122
  // src/url-run.ts
16120
16123
  import { createHash as createHash6 } from "crypto";
16121
16124
  import { load as load8 } from "cheerio";
16125
+ import { parse as parse2, postprocess, preprocess } from "micromark";
16122
16126
  import remarkParse5 from "remark-parse";
16123
16127
  import remarkMdx5 from "remark-mdx";
16124
16128
  import { unified as unified5 } from "unified";
@@ -17197,9 +17201,9 @@ function apiOperationCodeControls(body, specification, operation) {
17197
17201
  }
17198
17202
 
17199
17203
  // src/url-run.ts
17200
- var CONVERTER_VERSION = "url-evidence-v6";
17204
+ var CONVERTER_VERSION = "url-evidence-v7";
17201
17205
  var parser3 = unified5().use(remarkParse5).use(remarkMdx5);
17202
- var ACCEPT = "text/markdown,text/html,application/xhtml+xml;q=0.9";
17206
+ var ACCEPT = "text/html,application/xhtml+xml";
17203
17207
  var MAX_RESPONSE_BYTES = 8e6;
17204
17208
  var MAX_ASSET_BYTES2 = 1e7;
17205
17209
  var MAX_RECORDS = 1e5;
@@ -17229,6 +17233,11 @@ function canonical(value, base) {
17229
17233
  return null;
17230
17234
  }
17231
17235
  }
17236
+ function sameRouteMarkdownRepresentation(alternate, primary) {
17237
+ const target2 = new URL(alternate), source = new URL(primary);
17238
+ const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
17239
+ return target2.origin === source.origin && target2.search === source.search && directory2(target2.pathname) === directory2(source.pathname) && /\.mdx?$/i.test(target2.pathname);
17240
+ }
17232
17241
  function classifyMigrationResponse(document) {
17233
17242
  if (document.status && document.status >= 400) return "unsupported";
17234
17243
  if (/\/(?:llms(?:-full)?|robots)\.txt$|\/sitemap[^/]*\.xml$|\/search(?:\.html)?$/i.test(document.finalUrl.pathname) || /\.(?:json|ya?ml)$/i.test(document.finalUrl.pathname) || /(?:xml|json)/i.test(document.contentType)) return "index";
@@ -17479,7 +17488,7 @@ function resolveMigrationRecord(records, record) {
17479
17488
  }
17480
17489
  return record;
17481
17490
  }
17482
- function navigationConfig(checkpoint, records) {
17491
+ function navigationConfig(checkpoint, records, occurrencePages = /* @__PURE__ */ new Map()) {
17483
17492
  const navigation = checkpoint.navigation;
17484
17493
  const shortcuts = [];
17485
17494
  const collectShortcuts = (nodes) => {
@@ -17504,7 +17513,7 @@ function navigationConfig(checkpoint, records) {
17504
17513
  const project2 = (nodes) => nodes.flatMap((node) => {
17505
17514
  const record = node.url ? records.get(canonical(node.url, node.url) ?? "") : void 0;
17506
17515
  const target2 = resolveMigrationRecord(records, record);
17507
- const page = target2?.page;
17516
+ const page = occurrencePages.get(node) ?? target2?.page;
17508
17517
  const children = project2(node.children ?? []);
17509
17518
  if (children.length) {
17510
17519
  const contains = (entries) => entries.some((entry) => typeof entry === "string" ? entry === (page && projection.navigationId(page)) : contains(entry.pages));
@@ -17804,8 +17813,19 @@ async function runUrlMigration(options) {
17804
17813
  if (!record.evidence) continue;
17805
17814
  const final = canonical(record.evidence.finalUrl, sourceUrl);
17806
17815
  if (!final) continue;
17807
- if (record.outcome === "imported") enqueueArticleLinks(record.evidence.links, final, record.url);
17808
- else if (record.outcome === "excluded") {
17816
+ if (record.outcome === "imported") {
17817
+ enqueueArticleLinks(record.evidence.links, final, record.url);
17818
+ for (const reference of record.evidence.markdownRepresentations ?? []) {
17819
+ const alternate = canonical(reference, final);
17820
+ if (!alternate || !sameRouteMarkdownRepresentation(alternate, final)) continue;
17821
+ enqueue(alternate, final, "declared-markdown-representation");
17822
+ const sibling = records.get(alternate);
17823
+ if (sibling && sibling.representationOf !== record.url) {
17824
+ sibling.representationOf = record.url;
17825
+ changed.push(sibling);
17826
+ }
17827
+ }
17828
+ } else if (record.outcome === "excluded") {
17809
17829
  if (classifyMigrationResponse({ finalUrl: new URL(final), contentType: record.evidence.contentType, body: "" }) === "index") {
17810
17830
  for (const link of record.evidence.links) enqueue(link, final, record.url);
17811
17831
  }
@@ -17888,6 +17908,7 @@ async function runUrlMigration(options) {
17888
17908
  let claimedId;
17889
17909
  let claimedFinal;
17890
17910
  let markdownRepresentation;
17911
+ const markdownRepresentations = [];
17891
17912
  let exactKey;
17892
17913
  try {
17893
17914
  delete record.reason;
@@ -17980,13 +18001,13 @@ async function runUrlMigration(options) {
17980
18001
  $('link[rel="alternate"][type="text/markdown"][href]').each((_index, node) => {
17981
18002
  const alternate = canonical($(node).attr("href"), final);
17982
18003
  if (!alternate) return;
17983
- const target2 = new URL(alternate), primary = new URL(final);
17984
- const directory2 = (path) => path.replace(/\.mdx?$/i, "").replace(/\/index\/?$/i, "").replace(/\/$/, "");
17985
- if (target2.origin !== primary.origin || target2.search !== primary.search || directory2(target2.pathname) !== directory2(primary.pathname) || !/\.mdx?$/i.test(target2.pathname)) return;
18004
+ if (!sameRouteMarkdownRepresentation(alternate, final)) return;
17986
18005
  enqueue(alternate, final, "declared-markdown-representation");
17987
18006
  const sibling = records.get(alternate);
18007
+ if (!sibling) return;
17988
18008
  sibling.representationOf = record.url;
17989
18009
  markdownRepresentation ??= alternate;
18010
+ markdownRepresentations.push(alternate);
17990
18011
  changed.push(sibling);
17991
18012
  });
17992
18013
  }
@@ -17999,7 +18020,8 @@ async function runUrlMigration(options) {
17999
18020
  }
18000
18021
  enqueueNavigation(observedNavigation, final);
18001
18022
  }
18002
- if (imported >= maxPages) {
18023
+ const replacesRepresentation = imported >= maxPages && markdownRepresentations.some((url) => records.get(url)?.page);
18024
+ if (imported >= maxPages && !replacesRepresentation) {
18003
18025
  record.outcome = "excluded";
18004
18026
  record.reason = "A discovery endpoint resolved to an article after the selected page limit.";
18005
18027
  return;
@@ -18018,14 +18040,17 @@ async function runUrlMigration(options) {
18018
18040
  const locale = language.locale;
18019
18041
  const navigationId = locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id;
18020
18042
  const identityId = locale ? `${locale}/${navigationId}` : id;
18021
- if (usedIds.has(identityId) && usedIds.get(identityId) !== record.url) {
18043
+ const owner = records.get(usedIds.get(identityId) ?? "");
18044
+ const sharesDeclaredRepresentation = owner && (owner.representationOf === record.url || record.representationOf === owner.url);
18045
+ if (owner && owner !== record && !sharesDeclaredRepresentation) {
18022
18046
  record.outcome = "failed";
18023
18047
  record.reason = `Destination collision for ${identityId}.`;
18024
18048
  return;
18025
18049
  }
18026
- usedIds.set(identityId, record.url);
18050
+ if (!owner || owner === record || owner.representationOf === record.url) usedIds.set(identityId, record.url);
18027
18051
  claimedId = identityId;
18028
18052
  const evidence = evidenceOf(document);
18053
+ if (markdownRepresentations.length) evidence.markdownRepresentations = markdownRepresentations;
18029
18054
  const page = convert(document, id);
18030
18055
  if (!page) {
18031
18056
  record.outcome = "failed";
@@ -18236,6 +18261,7 @@ ${difference}`;
18236
18261
  if (!primary || primary === record) continue;
18237
18262
  const winner = primary.outcome === "imported" ? primary : record.outcome === "imported" && primary.outcome === "failed" ? record : void 0;
18238
18263
  if (!winner) continue;
18264
+ if (winner.page) usedIds.set(winner.page.id, winner.url);
18239
18265
  const alternate = winner === primary ? record : primary;
18240
18266
  if (alternate.outcome === "alias" && alternate.aliasOf === winner.url) continue;
18241
18267
  if (alternate.outcome === "imported") {
@@ -18257,7 +18283,17 @@ ${difference}`;
18257
18283
  settleArticleIdentities();
18258
18284
  while (cursor < pending.length && !options.signal?.aborted && !budgetExhausted && cursor < (options.maxCaptures ?? Infinity)) {
18259
18285
  if (imported >= maxPages) {
18260
- const index = pending.findIndex((record, position) => position >= cursor && isDiscoveryIndex(new URL(record.url), source));
18286
+ const index = pending.findIndex((record, position) => {
18287
+ if (position < cursor || record.outcome !== "pending") return false;
18288
+ const url = new URL(record.url);
18289
+ if (isDiscoveryIndex(url, source)) return true;
18290
+ if (/\.mdx?$/i.test(url.pathname)) return false;
18291
+ return [".md", ".mdx", "/index.md", "/index.mdx"].some((extension) => {
18292
+ const alternate = new URL(url);
18293
+ alternate.pathname = alternate.pathname.replace(/\/index$/i, "") + extension;
18294
+ return records.get(alternate.href)?.outcome === "imported";
18295
+ });
18296
+ });
18261
18297
  if (index < 0) break;
18262
18298
  pending.splice(cursor, 0, pending.splice(index, 1)[0]);
18263
18299
  }
@@ -18281,19 +18317,60 @@ ${difference}`;
18281
18317
  }
18282
18318
  const navigationLabels = /* @__PURE__ */ new Map();
18283
18319
  const navigationIssues = [];
18320
+ const navigationOccurrences = /* @__PURE__ */ new Map();
18284
18321
  const labelPages = (nodes) => {
18285
18322
  for (const node of nodes) {
18286
18323
  const entry = node.url ? records.get(canonical(node.url, sourceUrl) ?? "") : void 0;
18287
18324
  const target2 = resolveMigrationRecord(records, entry);
18288
- if (target2?.page && node.label && !node.children?.length && !entry?.aliasAnchor) {
18289
- const previous = navigationLabels.get(target2.url);
18290
- if (previous && previous !== node.label) navigationIssues.push({ code: "unsupported-component", source: target2.url, message: `This page has multiple source navigation labels (${previous}, ${node.label}); review the shared sidebar title.` });
18291
- else navigationLabels.set(target2.url, node.label);
18325
+ if (entry && target2?.page && node.label && !node.children?.length && !entry.aliasAnchor) {
18326
+ const occurrences = navigationOccurrences.get(target2) ?? [];
18327
+ occurrences.push({ node, entry });
18328
+ navigationOccurrences.set(target2, occurrences);
18292
18329
  }
18293
18330
  labelPages(node.children ?? []);
18294
18331
  }
18295
18332
  };
18296
18333
  labelPages(checkpoint.navigation);
18334
+ const occurrencePages = /* @__PURE__ */ new Map();
18335
+ const navigationAliases = /* @__PURE__ */ new Map();
18336
+ const occupiedPageIds = new Set(checkpoint.records.flatMap((record) => record.page ? [projection.pageId(record.page)] : []));
18337
+ const navigationRedirects = [];
18338
+ for (const [target2, occurrences] of navigationOccurrences) {
18339
+ const final = target2.evidence && canonical(target2.evidence.finalUrl, sourceUrl);
18340
+ const primary = occurrences.find(({ entry }) => entry.url === final) ?? occurrences.find(({ entry }) => entry === target2) ?? occurrences[0];
18341
+ navigationLabels.set(target2.url, primary.node.label);
18342
+ for (const { node, entry } of occurrences) {
18343
+ if (node.label === primary.node.label) continue;
18344
+ const existing = navigationAliases.get(entry.url);
18345
+ if (existing?.page.navTitle === node.label && existing.target === target2) {
18346
+ occurrencePages.set(node, existing.page);
18347
+ continue;
18348
+ }
18349
+ const sourceRoute = new URL(entry.url);
18350
+ const id = inScope(sourceRoute, source, scopePath) ? idForUrl(sourceRoute, scopePath) : null;
18351
+ const locale = projection.canonicalLocale(target2.page);
18352
+ let navigationId = id && (locale ? id.split("/").filter((segment) => segment.toLowerCase() !== locale.toLowerCase()).join("/") || "introduction" : id);
18353
+ const legacyPrefix = locale && `${locale.toLowerCase()}-`;
18354
+ if (navigationId && legacyPrefix && projection.navigationId(target2.page) !== target2.page.navigationId && navigationId.toLowerCase().startsWith(legacyPrefix)) navigationId = navigationId.slice(legacyPrefix.length);
18355
+ const page = navigationId ? {
18356
+ ...target2.page,
18357
+ id: locale ? `${locale}/${navigationId}` : navigationId,
18358
+ navigationId,
18359
+ ...locale ? { locale } : {},
18360
+ navTitle: node.label,
18361
+ source: entry.url,
18362
+ noindex: true
18363
+ } : void 0;
18364
+ if (!page || existing || occupiedPageIds.has(projection.pageId(page))) {
18365
+ navigationIssues.push({ code: "unsupported-component", source: target2.url, message: `This page has multiple source navigation labels (${primary.node.label}, ${node.label}); their source routes cannot retain distinct sidebar titles safely.` });
18366
+ continue;
18367
+ }
18368
+ occupiedPageIds.add(projection.pageId(page));
18369
+ navigationAliases.set(entry.url, { page, target: target2 });
18370
+ occurrencePages.set(node, page);
18371
+ navigationRedirects.push({ source: projection.route(page), destination: projection.route(target2.page), permanent: true });
18372
+ }
18373
+ }
18297
18374
  const pages = checkpoint.records.flatMap((record) => record.page ? [{ ...record.page, id: projection.pageId(record.page), navigationId: projection.navigationId(record.page), ...projection.canonicalLocale(record.page) ? { locale: projection.canonicalLocale(record.page) } : {}, ...navigationLabels.has(record.url) ? { navTitle: navigationLabels.get(record.url) } : {}, body: rewriteMarkdownDestinations(record.page.body, (value) => {
18298
18375
  let url;
18299
18376
  try {
@@ -18314,6 +18391,10 @@ ${difference}`;
18314
18391
  const hasBrokenSourceAnchor = Boolean(url.hash && destination?.outcome === "imported" && destination.evidence && !destination.evidence.anchors.includes(anchor));
18315
18392
  return `${route}${hasBrokenSourceAnchor ? "" : effectiveHash}`;
18316
18393
  }) }] : []);
18394
+ const projectedPages = new Map(pages.map((page) => [page.id, page]));
18395
+ for (const { page, target: target2 } of navigationAliases.values()) {
18396
+ pages.push({ ...page, id: projection.pageId(page), body: projectedPages.get(projection.pageId(target2.page)).body });
18397
+ }
18317
18398
  const validation = validateUrlMigration(checkpoint, Boolean(options.signal?.aborted));
18318
18399
  validation.issues.push(...navigationIssues);
18319
18400
  if (navigationIssues.length && validation.status === "passed") validation.status = "partial";
@@ -18334,12 +18415,12 @@ ${difference}`;
18334
18415
  platform: options.platform ?? "generic",
18335
18416
  pages,
18336
18417
  assets: [...assets.values()],
18337
- docsConfig: navigationConfig(checkpoint, records) ?? buildNavigationFromPages(pages),
18418
+ docsConfig: navigationConfig(checkpoint, records, occurrencePages) ?? buildNavigationFromPages(pages),
18338
18419
  warnings: [...validation.issues, ...validation.warnings ?? []].map((issue) => ({ code: issue.code === "missing-page" ? "missing-page" : "unsupported-config", message: issue.message, source: issue.source })),
18339
18420
  urlMigrationValidation: validation,
18340
18421
  stats: { discovered: records.size, imported: pages.length, skipped: records.size - pages.length }
18341
18422
  };
18342
- if (projection.redirects.length) bundle.docsConfig.redirects = projection.redirects;
18423
+ if (projection.redirects.length || navigationRedirects.length) bundle.docsConfig.redirects = [...projection.redirects, ...navigationRedirects];
18343
18424
  if (projection.locales.length) {
18344
18425
  const locales = [.../* @__PURE__ */ new Set([projection.defaultLocale, ...projection.locales])];
18345
18426
  bundle.docsConfig.i18n = { ...bundle.docsConfig.i18n, defaultLocale: projection.defaultLocale, locales: locales.map((code) => ({ code, label: code === "en" ? "English" : code })) };
@@ -18357,9 +18438,11 @@ function rewriteMarkdownDestinations(body, rewrite) {
18357
18438
  const original = body.slice(start, end);
18358
18439
  const replacement = rewrite(node.url);
18359
18440
  if (replacement === node.url) return null;
18360
- const index = original.lastIndexOf(node.url);
18361
- if (index < 0) return null;
18362
- return { start: start + index, end: start + index + node.url.length, replacement };
18441
+ const destinations = postprocess(parse2().document().write(preprocess()(original, void 0, true))).filter(([event, token]) => event === "enter" && ["resourceDestinationString", "definitionDestinationString", "autolinkProtocol", "autolinkEmail"].includes(token.type));
18442
+ const destination = destinations.at(-1)?.[1];
18443
+ if (!destination) return null;
18444
+ const escaped = replacement.replace(/[\\&()<>]/g, "\\$&").replace(/[\t\n\r ]/g, (character) => encodeURIComponent(character));
18445
+ return { start: start + destination.start.offset, end: start + destination.end.offset, replacement: escaped };
18363
18446
  }).filter((edit) => edit !== null).sort((a, b) => b.start - a.start);
18364
18447
  for (const node of markdownNodes(body)) for (const attribute of node.attributes ?? []) {
18365
18448
  if (!attribute.name || !["src", "poster", "href", "srcSet", "srcset", "img", "primaryHref", "secondaryHref"].includes(attribute.name) || !attribute.position) continue;