upstream-radar 0.43.4 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README-zh-CN.md +92 -77
  2. package/README.md +125 -94
  3. package/dist/src/cli.js +177 -5
  4. package/dist/src/cli.js.map +1 -1
  5. package/dist/src/dsh-compatibility-ledger.d.ts +2 -0
  6. package/dist/src/dsh-compatibility-ledger.d.ts.map +1 -1
  7. package/dist/src/dsh-compatibility-ledger.js +5 -0
  8. package/dist/src/dsh-compatibility-ledger.js.map +1 -1
  9. package/dist/src/dsh-directory-feed.d.ts +7 -3
  10. package/dist/src/dsh-directory-feed.d.ts.map +1 -1
  11. package/dist/src/dsh-directory-feed.js +59 -25
  12. package/dist/src/dsh-directory-feed.js.map +1 -1
  13. package/dist/src/dsh-headless-agent-plan.d.ts +7 -0
  14. package/dist/src/dsh-headless-agent-plan.d.ts.map +1 -1
  15. package/dist/src/dsh-headless-agent-plan.js +20 -1
  16. package/dist/src/dsh-headless-agent-plan.js.map +1 -1
  17. package/dist/src/dsh-install-observation.d.ts +6 -0
  18. package/dist/src/dsh-install-observation.d.ts.map +1 -1
  19. package/dist/src/dsh-install-observation.js +2 -2
  20. package/dist/src/dsh-install-observation.js.map +1 -1
  21. package/dist/src/dsh-install-plan.d.ts +7 -0
  22. package/dist/src/dsh-install-plan.d.ts.map +1 -1
  23. package/dist/src/dsh-install-plan.js +8 -2
  24. package/dist/src/dsh-install-plan.js.map +1 -1
  25. package/dist/src/dsh-surface-observation.d.ts +160 -0
  26. package/dist/src/dsh-surface-observation.d.ts.map +1 -0
  27. package/dist/src/dsh-surface-observation.js +1031 -0
  28. package/dist/src/dsh-surface-observation.js.map +1 -0
  29. package/dist/src/dsh-surface.d.ts +127 -0
  30. package/dist/src/dsh-surface.d.ts.map +1 -0
  31. package/dist/src/dsh-surface.js +667 -0
  32. package/dist/src/dsh-surface.js.map +1 -0
  33. package/dist/src/index.d.ts +2 -0
  34. package/dist/src/index.d.ts.map +1 -1
  35. package/dist/src/index.js +2 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/upstream-observer.d.ts +6 -0
  38. package/dist/src/upstream-observer.d.ts.map +1 -1
  39. package/dist/src/upstream-observer.js +206 -127
  40. package/dist/src/upstream-observer.js.map +1 -1
  41. package/dist/src/version.d.ts +1 -1
  42. package/dist/src/version.js +1 -1
  43. package/docs/README.zh-CN.md +19 -19
  44. package/package.json +1 -1
@@ -404,6 +404,13 @@ function parsePackageObservation(value, label) {
404
404
  const distTag = optionalBoundedString(source.distTag, `${label}.distTag`, 128);
405
405
  if (distTag !== undefined && !SAFE_NPM_DIST_TAG.test(distTag))
406
406
  throw new Error(`${label}.distTag is invalid`);
407
+ const rawMigration = source.migration === undefined ? undefined : asRecord(source.migration);
408
+ if (source.migration !== undefined && rawMigration === undefined)
409
+ throw new Error(`${label}.migration must be an object`);
410
+ const migrationTo = rawMigration === undefined ? undefined : boundedString(rawMigration.to, `${label}.migration.to`, 214);
411
+ if (migrationTo !== undefined && !EXACT_NPM_PACKAGE_NAME.test(migrationTo))
412
+ throw new Error(`${label}.migration.to is not an npm package name`);
413
+ const migrationSince = rawMigration === undefined ? undefined : optionalBoundedString(rawMigration.since, `${label}.migration.since`, 128);
407
414
  return {
408
415
  name,
409
416
  version,
@@ -412,6 +419,12 @@ function parsePackageObservation(value, label) {
412
419
  ...(optionalBoundedString(source.tarball, `${label}.tarball`, 4_096) === undefined ? {} : { tarball: source.tarball }),
413
420
  ...(optionalBoundedString(source.repository, `${label}.repository`, 4_096) === undefined ? {} : { repository: source.repository }),
414
421
  ...(optionalBoundedString(source.publishedAt, `${label}.publishedAt`, 128) === undefined ? {} : { publishedAt: source.publishedAt }),
422
+ ...(migrationTo === undefined ? {} : {
423
+ migration: {
424
+ to: migrationTo,
425
+ ...(migrationSince === undefined ? {} : { since: migrationSince }),
426
+ },
427
+ }),
415
428
  };
416
429
  }
417
430
  function parseSnapshot(value, label) {
@@ -836,7 +849,10 @@ export class UpstreamObserverClient {
836
849
  async fetchNpmObservation(name, distTag = 'latest') {
837
850
  const url = new URL(encodeURIComponent(name), this.registry);
838
851
  const response = await this.fetchWithRetry(url, {
839
- headers: { accept: 'application/vnd.npm.install-v1+json, application/json', 'user-agent': 'upstream-radar/upstream-observer' },
852
+ // The abbreviated install-v1 packument deliberately drops custom DSH
853
+ // metadata such as dsh.migrate. The observer needs the published package
854
+ // contract so package moves become explicit Agent evidence.
855
+ headers: { accept: 'application/json', 'user-agent': 'upstream-radar/upstream-observer' },
840
856
  redirect: 'follow',
841
857
  });
842
858
  if (response.status === 404)
@@ -854,6 +870,13 @@ export class UpstreamObserverClient {
854
870
  const repository = typeof manifest.repository === 'string'
855
871
  ? manifest.repository
856
872
  : typeof asRecord(manifest.repository)?.url === 'string' ? asRecord(manifest.repository)?.url : undefined;
873
+ const rawMigration = asRecord(asRecord(manifest.dsh)?.migrate);
874
+ const migrationTo = typeof rawMigration?.to === 'string' && EXACT_NPM_PACKAGE_NAME.test(rawMigration.to)
875
+ ? rawMigration.to
876
+ : undefined;
877
+ const migrationSince = typeof rawMigration?.since === 'string' && rawMigration.since.length <= 128
878
+ ? rawMigration.since
879
+ : undefined;
857
880
  return {
858
881
  name,
859
882
  version: selectedVersion,
@@ -862,6 +885,12 @@ export class UpstreamObserverClient {
862
885
  ...(typeof dist?.tarball === 'string' ? { tarball: dist.tarball } : {}),
863
886
  ...(repository === undefined ? {} : { repository: repository.slice(0, 4_096) }),
864
887
  ...(typeof times?.[selectedVersion] === 'string' ? { publishedAt: times[selectedVersion] } : {}),
888
+ ...(migrationTo === undefined ? {} : {
889
+ migration: {
890
+ to: migrationTo,
891
+ ...(migrationSince === undefined ? {} : { since: migrationSince }),
892
+ },
893
+ }),
865
894
  };
866
895
  }
867
896
  async observe(targetValue, now) {
@@ -1180,7 +1209,8 @@ function reverseDependencyChanges(before, after) {
1180
1209
  function packageChanged(before, after) {
1181
1210
  if (before.package?.name !== after.package?.name || before.package?.version !== after.package?.version)
1182
1211
  return true;
1183
- return before.package?.integrity !== after.package?.integrity;
1212
+ return before.package?.integrity !== after.package?.integrity
1213
+ || JSON.stringify(before.package?.migration) !== JSON.stringify(after.package?.migration);
1184
1214
  }
1185
1215
  function sourcePublishedVersionDrift(snapshot) {
1186
1216
  if (snapshot.package === undefined || snapshot.manifest.version === snapshot.package.version)
@@ -1203,8 +1233,14 @@ function graphChanged(before, after) {
1203
1233
  function meaningfulChange(sourceChange, before, after) {
1204
1234
  const reasons = [];
1205
1235
  const manifest = manifestChanges(before.manifest, after.manifest);
1206
- if (packageChanged(before, after))
1236
+ if (before.package?.name !== after.package?.name
1237
+ || before.package?.version !== after.package?.version
1238
+ || before.package?.integrity !== after.package?.integrity) {
1207
1239
  reasons.push('npm package version or integrity changed');
1240
+ }
1241
+ if (JSON.stringify(before.package?.migration) !== JSON.stringify(after.package?.migration) && after.package?.migration !== undefined) {
1242
+ reasons.push(`npm package migration declared: ${after.package.name} → ${after.package.migration.to}${after.package.migration.since === undefined ? '' : ` since ${after.package.migration.since}`}`);
1243
+ }
1208
1244
  if (graphChanged(before, after))
1209
1245
  reasons.push('dependency graph changed or became unavailable');
1210
1246
  if (manifest.fields.length > 0)
@@ -1490,55 +1526,96 @@ export async function runOpenAiCompatibleAgent(task, prompt, options) {
1490
1526
  };
1491
1527
  }
1492
1528
  const endpoints = llmCompletionEndpoints(baseUrl);
1493
- for (let index = 0; index < endpoints.length; index += 1) {
1529
+ const systemPrompt = [
1530
+ '只返回一个严格 JSON 对象。不要输出 Markdown、解释、前缀或后缀;输出必须以 { 开始并以 } 结束。',
1531
+ 'Every response must contain exactly the eight fields shown below and no others.',
1532
+ 'EXAMPLE JSON OUTPUT:',
1533
+ JSON.stringify({
1534
+ impact: 'unknown',
1535
+ confidence: 'low',
1536
+ evidence: ['State an exact repository path or explicitly name the missing evidence.'],
1537
+ breaking_change: 'unknown',
1538
+ dependency_risk: 'unknown',
1539
+ recommended_action: 'Review the changed runtime paths before upgrading.',
1540
+ urgency: 'monitor',
1541
+ reasoning_summary: 'Separate observed facts from model judgment.',
1542
+ }, null, 2),
1543
+ ].join('\n');
1544
+ endpointLoop: for (let index = 0; index < endpoints.length; index += 1) {
1494
1545
  const endpoint = endpoints[index];
1495
- try {
1496
- const response = await fetch(endpoint, {
1497
- method: 'POST',
1498
- headers: {
1499
- authorization: `Bearer ${apiKey}`,
1500
- 'content-type': 'application/json',
1501
- },
1502
- body: JSON.stringify({
1503
- model,
1504
- messages: [
1505
- { role: 'system', content: '只返回一个严格 JSON 对象。不要输出 Markdown、解释、前缀或后缀;输出必须以 { 开始并以 } 结束。' },
1506
- { role: 'user', content: prompt },
1507
- ],
1508
- temperature: 0,
1509
- // DeepSeek V4 enables thinking by default. For this machine-readable
1510
- // contract, disable it so the output budget is reserved for the JSON
1511
- // conclusion instead of an optional reasoning stream.
1512
- ...(isDeepSeekJsonModel(baseUrl, model) ? { thinking: { type: 'disabled' } } : {}),
1513
- response_format: { type: 'json_object' },
1514
- // Keep an explicit output budget so the JSON conclusion is not
1515
- // truncated by a provider default.
1516
- max_tokens: DEFAULT_LLM_MAX_TOKENS,
1517
- }),
1518
- signal: AbortSignal.timeout(timeoutMs),
1519
- });
1520
- if (response.status === 404)
1521
- continue;
1522
- if (!response.ok) {
1546
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1547
+ const repairAttempt = attempt === 1;
1548
+ try {
1549
+ const response = await fetch(endpoint, {
1550
+ method: 'POST',
1551
+ headers: {
1552
+ authorization: `Bearer ${apiKey}`,
1553
+ 'content-type': 'application/json',
1554
+ },
1555
+ body: JSON.stringify({
1556
+ model,
1557
+ messages: [
1558
+ {
1559
+ role: 'system',
1560
+ content: repairAttempt
1561
+ ? `The previous JSON-mode response was empty or invalid. Return the example shape now.\n\n${systemPrompt}`
1562
+ : systemPrompt,
1563
+ },
1564
+ { role: 'user', content: prompt },
1565
+ ],
1566
+ temperature: 0,
1567
+ // DeepSeek V4 enables thinking by default. For this machine-readable
1568
+ // contract, disable it so the output budget is reserved for the JSON
1569
+ // conclusion instead of an optional reasoning stream.
1570
+ ...(isDeepSeekJsonModel(baseUrl, model) ? { thinking: { type: 'disabled' } } : {}),
1571
+ // DeepSeek documents that JSON Output may occasionally return an
1572
+ // empty content field. Retry once without provider JSON mode; the
1573
+ // same exact local schema validator still gates the conclusion.
1574
+ ...(repairAttempt ? {} : { response_format: { type: 'json_object' } }),
1575
+ // Keep an explicit output budget so the JSON conclusion is not
1576
+ // truncated by a provider default.
1577
+ max_tokens: DEFAULT_LLM_MAX_TOKENS,
1578
+ }),
1579
+ signal: AbortSignal.timeout(timeoutMs),
1580
+ });
1581
+ if (response.status === 404)
1582
+ continue endpointLoop;
1583
+ if (!response.ok) {
1584
+ return {
1585
+ taskId: task.id,
1586
+ status: 'failed',
1587
+ error: `LLM endpoint returned HTTP ${response.status}: ${safeLlmEndpoint(endpoint)}`,
1588
+ };
1589
+ }
1590
+ const body = await response.json();
1591
+ const content = body.choices?.[0]?.message?.content;
1592
+ let conclusionError;
1593
+ if (typeof content !== 'string') {
1594
+ conclusionError = 'LLM response had no message content';
1595
+ }
1596
+ else {
1597
+ try {
1598
+ const parsed = parseAgentOutput(extractJsonObject(content));
1599
+ if (parsed.value !== undefined) {
1600
+ return { taskId: task.id, status: 'succeeded', parsedOutput: parsed.value };
1601
+ }
1602
+ conclusionError = parsed.error ?? 'LLM returned an invalid conclusion';
1603
+ }
1604
+ catch (error) {
1605
+ conclusionError = safeError(error);
1606
+ }
1607
+ }
1608
+ if (!repairAttempt)
1609
+ continue;
1523
1610
  return {
1524
1611
  taskId: task.id,
1525
1612
  status: 'failed',
1526
- error: `LLM endpoint returned HTTP ${response.status}: ${safeLlmEndpoint(endpoint)}`,
1613
+ error: `LLM returned no valid conclusion after JSON-mode and plain-text attempts at ${safeLlmEndpoint(endpoint)}: ${conclusionError}`,
1527
1614
  };
1528
1615
  }
1529
- const body = await response.json();
1530
- const content = body.choices?.[0]?.message?.content;
1531
- if (typeof content !== 'string') {
1532
- return { taskId: task.id, status: 'failed', error: `LLM response had no message content: ${safeLlmEndpoint(endpoint)}` };
1533
- }
1534
- const parsed = parseAgentOutput(extractJsonObject(content));
1535
- if (parsed.value === undefined) {
1536
- return { taskId: task.id, status: 'failed', error: parsed.error ?? 'LLM returned an invalid conclusion' };
1616
+ catch (error) {
1617
+ return { taskId: task.id, status: 'failed', error: `LLM request failed at ${safeLlmEndpoint(endpoint)}: ${safeError(error)}` };
1537
1618
  }
1538
- return { taskId: task.id, status: 'succeeded', parsedOutput: parsed.value };
1539
- }
1540
- catch (error) {
1541
- return { taskId: task.id, status: 'failed', error: `LLM request failed at ${safeLlmEndpoint(endpoint)}: ${safeError(error)}` };
1542
1619
  }
1543
1620
  }
1544
1621
  return {
@@ -1621,92 +1698,94 @@ export async function runObserver(config, previousState, options = {}) {
1621
1698
  const alignmentFindings = [];
1622
1699
  const errors = [];
1623
1700
  const newTasks = [];
1624
- for (const configuredTarget of config.targets) {
1625
- try {
1626
- const current = await source.observe(configuredTarget, checkedAt);
1627
- const before = previousState.targets[configuredTarget.id];
1628
- if (before === undefined) {
1629
- nextTargets[configuredTarget.id] = current;
1630
- baselineTargets.push(configuredTarget.id);
1631
- if (current.alignment !== undefined)
1632
- alignmentFindings.push({ targetId: configuredTarget.id, alignment: structuredClone(current.alignment) });
1633
- continue;
1634
- }
1635
- // checkedAt belongs in the run report. Keep the durable observation point
1636
- // byte-stable when its evidence did not change, so a no-op cron run does
1637
- // not manufacture a commit from a fresh timestamp alone.
1638
- nextTargets[configuredTarget.id] = snapshotEvidenceSignature(before) === snapshotEvidenceSignature(current)
1639
- ? before
1640
- : current;
1641
- if (before.alignment === undefined && current.alignment !== undefined) {
1642
- // Upgrade legacy state into the IR and show its first finding once.
1643
- // This is evidence for the author, not a fake upstream change and not
1644
- // a reason to wake the Agent on every subsequent scheduled run.
1645
- alignmentFindings.push({ targetId: configuredTarget.id, alignment: structuredClone(current.alignment) });
1646
- }
1647
- const sourceChange = before.source.commit === current.source.commit
1648
- ? {
1649
- beforeCommit: before.source.commit,
1650
- afterCommit: current.source.commit,
1651
- comparison: 'complete',
1652
- changedFiles: [],
1653
- runtimeFiles: [],
1654
- nonRuntimeFiles: [],
1701
+ if (options.retryPendingOnly !== true) {
1702
+ for (const configuredTarget of config.targets) {
1703
+ try {
1704
+ const current = await source.observe(configuredTarget, checkedAt);
1705
+ const before = previousState.targets[configuredTarget.id];
1706
+ if (before === undefined) {
1707
+ nextTargets[configuredTarget.id] = current;
1708
+ baselineTargets.push(configuredTarget.id);
1709
+ if (current.alignment !== undefined)
1710
+ alignmentFindings.push({ targetId: configuredTarget.id, alignment: structuredClone(current.alignment) });
1711
+ continue;
1655
1712
  }
1656
- : await source.compare(current.source.repository, before.source.commit, current.source.commit);
1657
- const change = createChange(configuredTarget, before, current, sourceChange, options.reverseDependencyIndex);
1658
- if (change.reasons.length === 0)
1659
- continue;
1660
- let reviewedChange = change;
1661
- if (change.meaningful && current.package !== undefined && options.artifactReviewer !== undefined) {
1662
- const packageSpec = `npm:${current.package.name}@${current.package.version}`;
1663
- try {
1664
- reviewedChange = {
1665
- ...change,
1666
- artifactReview: await options.artifactReviewer(packageSpec, configuredTarget),
1667
- };
1713
+ // checkedAt belongs in the run report. Keep the durable observation point
1714
+ // byte-stable when its evidence did not change, so a no-op cron run does
1715
+ // not manufacture a commit from a fresh timestamp alone.
1716
+ nextTargets[configuredTarget.id] = snapshotEvidenceSignature(before) === snapshotEvidenceSignature(current)
1717
+ ? before
1718
+ : current;
1719
+ if (before.alignment === undefined && current.alignment !== undefined) {
1720
+ // Upgrade legacy state into the IR and show its first finding once.
1721
+ // This is evidence for the author, not a fake upstream change and not
1722
+ // a reason to wake the Agent on every subsequent scheduled run.
1723
+ alignmentFindings.push({ targetId: configuredTarget.id, alignment: structuredClone(current.alignment) });
1668
1724
  }
1669
- catch (error) {
1670
- // Artifact review is useful evidence, but it must not turn a valid
1671
- // source observation into a source-observation error. Keep the
1672
- // change and make the incomplete review explicit for the author and
1673
- // for a later retry of the whole scheduled run.
1674
- reviewedChange = {
1675
- ...change,
1676
- artifactReview: {
1677
- spec: packageSpec,
1678
- verdict: 'review',
1679
- riskVerdict: 'review',
1680
- coverageVerdict: 'incomplete',
1681
- artifactIntegrity: 'not-checked',
1682
- registrySignature: 'not-checked',
1683
- provenance: 'not-checked',
1684
- dependencyResolution: 'manifest-only',
1685
- dependencyAuditStatus: 'failed',
1686
- packages: null,
1687
- unresolved: 0,
1688
- vulnerabilities: null,
1689
- installScriptPackages: [],
1690
- installScriptDetails: [],
1691
- findings: [],
1692
- error: safeError(error),
1693
- },
1694
- };
1725
+ const sourceChange = before.source.commit === current.source.commit
1726
+ ? {
1727
+ beforeCommit: before.source.commit,
1728
+ afterCommit: current.source.commit,
1729
+ comparison: 'complete',
1730
+ changedFiles: [],
1731
+ runtimeFiles: [],
1732
+ nonRuntimeFiles: [],
1733
+ }
1734
+ : await source.compare(current.source.repository, before.source.commit, current.source.commit);
1735
+ const change = createChange(configuredTarget, before, current, sourceChange, options.reverseDependencyIndex);
1736
+ if (change.reasons.length === 0)
1737
+ continue;
1738
+ let reviewedChange = change;
1739
+ if (change.meaningful && current.package !== undefined && options.artifactReviewer !== undefined) {
1740
+ const packageSpec = `npm:${current.package.name}@${current.package.version}`;
1741
+ try {
1742
+ reviewedChange = {
1743
+ ...change,
1744
+ artifactReview: await options.artifactReviewer(packageSpec, configuredTarget),
1745
+ };
1746
+ }
1747
+ catch (error) {
1748
+ // Artifact review is useful evidence, but it must not turn a valid
1749
+ // source observation into a source-observation error. Keep the
1750
+ // change and make the incomplete review explicit for the author and
1751
+ // for a later retry of the whole scheduled run.
1752
+ reviewedChange = {
1753
+ ...change,
1754
+ artifactReview: {
1755
+ spec: packageSpec,
1756
+ verdict: 'review',
1757
+ riskVerdict: 'review',
1758
+ coverageVerdict: 'incomplete',
1759
+ artifactIntegrity: 'not-checked',
1760
+ registrySignature: 'not-checked',
1761
+ provenance: 'not-checked',
1762
+ dependencyResolution: 'manifest-only',
1763
+ dependencyAuditStatus: 'failed',
1764
+ packages: null,
1765
+ unresolved: 0,
1766
+ vulnerabilities: null,
1767
+ installScriptPackages: [],
1768
+ installScriptDetails: [],
1769
+ findings: [],
1770
+ error: safeError(error),
1771
+ },
1772
+ };
1773
+ }
1695
1774
  }
1775
+ changes.push(reviewedChange);
1776
+ if (!reviewedChange.meaningful || reviewedChange.taskId === undefined)
1777
+ continue;
1778
+ const task = createUpstreamChangeTask(configuredTarget, reviewedChange, checkedAt);
1779
+ newTasks.push(task);
1780
+ if (!taskAlreadyPending(pendingTasks, task.id))
1781
+ pendingTasks.push(task);
1782
+ }
1783
+ catch (error) {
1784
+ errors.push({ targetId: configuredTarget.id, message: safeError(error) });
1696
1785
  }
1697
- changes.push(reviewedChange);
1698
- if (!reviewedChange.meaningful || reviewedChange.taskId === undefined)
1699
- continue;
1700
- const task = createUpstreamChangeTask(configuredTarget, reviewedChange, checkedAt);
1701
- newTasks.push(task);
1702
- if (!taskAlreadyPending(pendingTasks, task.id))
1703
- pendingTasks.push(task);
1704
- }
1705
- catch (error) {
1706
- errors.push({ targetId: configuredTarget.id, message: safeError(error) });
1707
1786
  }
1708
1787
  }
1709
- const pendingToRetry = options.retryPending === true
1788
+ const pendingToRetry = options.retryPending === true || options.retryPendingOnly === true
1710
1789
  ? pendingTasks.filter(task => !newTasks.some(current => current.id === task.id))
1711
1790
  : [];
1712
1791
  const tasksToInvoke = [...newTasks, ...pendingToRetry];
@@ -1721,7 +1800,7 @@ export async function runObserver(config, previousState, options = {}) {
1721
1800
  const report = {
1722
1801
  schema: OBSERVER_REPORT_SCHEMA,
1723
1802
  checkedAt,
1724
- targetsChecked: config.targets.length,
1803
+ targetsChecked: options.retryPendingOnly === true ? 0 : config.targets.length,
1725
1804
  baselineTargets,
1726
1805
  alignmentFindings,
1727
1806
  ...(options.reverseDependencyIndex === undefined ? {} : {
@@ -1764,7 +1843,7 @@ export function observerExitCode(report) {
1764
1843
  function displayPackage(value) {
1765
1844
  return value === undefined
1766
1845
  ? 'not observed'
1767
- : `${value.name}@${value.version}${value.distTag === undefined ? '' : ` via npm tag ${value.distTag}`}`;
1846
+ : `${value.name}@${value.version}${value.distTag === undefined ? '' : ` via npm tag ${value.distTag}`}${value.migration === undefined ? '' : `; migrates to ${value.migration.to}${value.migration.since === undefined ? '' : ` since ${value.migration.since}`}`}`;
1768
1847
  }
1769
1848
  function displayGraph(value) {
1770
1849
  return value === undefined ? 'not observed' : `${value.nodes} nodes, ${value.edges} edges${value.unresolved === 0 ? '' : `, ${value.unresolved} unresolved`}`;