upstream-radar 0.33.12 → 0.35.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 (76) hide show
  1. package/README.md +100 -1020
  2. package/dist/src/cli.js +365 -19
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/dependency-index.d.ts +76 -0
  5. package/dist/src/dependency-index.d.ts.map +1 -0
  6. package/dist/src/dependency-index.js +199 -0
  7. package/dist/src/dependency-index.js.map +1 -0
  8. package/dist/src/dsh-case.d.ts +50 -0
  9. package/dist/src/dsh-case.d.ts.map +1 -0
  10. package/dist/src/dsh-case.js +88 -0
  11. package/dist/src/dsh-case.js.map +1 -0
  12. package/dist/src/dsh-review.d.ts +46 -0
  13. package/dist/src/dsh-review.d.ts.map +1 -0
  14. package/dist/src/dsh-review.js +209 -0
  15. package/dist/src/dsh-review.js.map +1 -0
  16. package/dist/src/graph.d.ts +5 -1
  17. package/dist/src/graph.d.ts.map +1 -1
  18. package/dist/src/graph.js +56 -10
  19. package/dist/src/graph.js.map +1 -1
  20. package/dist/src/index.d.ts +5 -1
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/index.js +4 -0
  23. package/dist/src/index.js.map +1 -1
  24. package/dist/src/npm.d.ts +11 -0
  25. package/dist/src/npm.d.ts.map +1 -1
  26. package/dist/src/npm.js +122 -12
  27. package/dist/src/npm.js.map +1 -1
  28. package/dist/src/render.d.ts.map +1 -1
  29. package/dist/src/render.js +19 -0
  30. package/dist/src/render.js.map +1 -1
  31. package/dist/src/scan.d.ts.map +1 -1
  32. package/dist/src/scan.js +41 -0
  33. package/dist/src/scan.js.map +1 -1
  34. package/dist/src/types.d.ts +10 -0
  35. package/dist/src/types.d.ts.map +1 -1
  36. package/dist/src/upstream-alignment.d.ts +67 -0
  37. package/dist/src/upstream-alignment.d.ts.map +1 -0
  38. package/dist/src/upstream-alignment.js +263 -0
  39. package/dist/src/upstream-alignment.js.map +1 -0
  40. package/dist/src/upstream-observer.d.ts +70 -0
  41. package/dist/src/upstream-observer.d.ts.map +1 -1
  42. package/dist/src/upstream-observer.js +387 -22
  43. package/dist/src/upstream-observer.js.map +1 -1
  44. package/dist/src/version.d.ts +1 -1
  45. package/dist/src/version.d.ts.map +1 -1
  46. package/dist/src/version.js +1 -1
  47. package/dist/src/version.js.map +1 -1
  48. package/docs/README.zh-CN.md +114 -22
  49. package/examples/cases/dsh-web-ui-issue-71/before/cordis.patch.yml +4 -0
  50. package/examples/cases/dsh-web-ui-issue-71/before/node_modules/@linxin666/dsh-client-ui-skin-center/cordis.patch.yml +1 -0
  51. package/examples/cases/dsh-web-ui-issue-71/before/node_modules/@linxin666/dsh-client-ui-skin-center/package.json +5 -0
  52. package/examples/cases/dsh-web-ui-issue-71/before/node_modules/@linxin666/dsh-web-ui-all/cordis.patch.yml +1 -0
  53. package/examples/cases/dsh-web-ui-issue-71/before/node_modules/@linxin666/dsh-web-ui-all/package.json +5 -0
  54. package/examples/cases/dsh-web-ui-issue-71/before/package.json +12 -0
  55. package/examples/cases/dsh-web-ui-issue-71/before/pnpm-lock.yaml +21 -0
  56. package/examples/cases/dsh-web-ui-issue-71/before/pnpm-workspace.yaml +7 -0
  57. package/examples/cases/dsh-web-ui-issue-71/fixed/cordis.patch.yml +3 -0
  58. package/examples/cases/dsh-web-ui-issue-71/fixed/node_modules/@linxin666/dsh-client-ui-skin-center/cordis.patch.yml +1 -0
  59. package/examples/cases/dsh-web-ui-issue-71/fixed/node_modules/@linxin666/dsh-client-ui-skin-center/package.json +5 -0
  60. package/examples/cases/dsh-web-ui-issue-71/fixed/node_modules/@linxin666/dsh-web-ui-all/cordis.patch.yml +1 -0
  61. package/examples/cases/dsh-web-ui-issue-71/fixed/node_modules/@linxin666/dsh-web-ui-all/package.json +5 -0
  62. package/examples/cases/dsh-web-ui-issue-71/fixed/package.json +12 -0
  63. package/examples/cases/dsh-web-ui-issue-71/fixed/pnpm-lock.yaml +18 -0
  64. package/examples/cases/dsh-web-ui-issue-71/fixed/pnpm-workspace.yaml +8 -0
  65. package/examples/cases/dsh-web-ui-issue-71/manual-add/cordis.patch.yml +4 -0
  66. package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-client-ui-skin-center/cordis.patch.yml +1 -0
  67. package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-client-ui-skin-center/package.json +5 -0
  68. package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-client-ui-skin-qq98/cordis.patch.yml +3 -0
  69. package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-client-ui-skin-qq98/package.json +5 -0
  70. package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-web-ui-all/cordis.patch.yml +1 -0
  71. package/examples/cases/dsh-web-ui-issue-71/manual-add/node_modules/@linxin666/dsh-web-ui-all/package.json +5 -0
  72. package/examples/cases/dsh-web-ui-issue-71/manual-add/package.json +13 -0
  73. package/examples/cases/dsh-web-ui-issue-71/manual-add/pnpm-lock.yaml +21 -0
  74. package/examples/cases/dsh-web-ui-issue-71/manual-add/pnpm-workspace.yaml +5 -0
  75. package/package.json +2 -1
  76. package/schemas/upstream-downstream-ir.schema.json +87 -0
package/dist/src/cli.js CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import process from 'node:process';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { access, readFile, writeFile } from 'node:fs/promises';
4
+ import { access, readFile, readdir, stat, writeFile } from 'node:fs/promises';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { renderCompatibilityBenchmark, runCompatibilityBenchmark } from './compatibility-benchmark.js';
7
7
  import { assessCompatibilityChange } from './compatibility.js';
8
8
  import { probeDshLoad, probeDshLoadMatrix, renderDshLoadMatrix, renderDshLoadProbe } from './dsh-probe.js';
9
+ import { renderDshPluginReview, reviewDshPlugin } from './dsh-review.js';
9
10
  import { createAnalysisTask, renderAgentAnalysisPrompt } from './dsh-analysis.js';
11
+ import { createDshCaseReport, renderDshCase } from './dsh-case.js';
10
12
  import { createDoctorReport, renderDoctorReport } from './doctor.js';
11
13
  import { checkDshProfile, renderDshProfileCheck, renderDshProfileCheckSummary } from './dsh-profile-check.js';
12
14
  import { createDemoReport, renderDemo } from './demo.js';
13
15
  import { GitHubReleaseClient } from './github-release.js';
14
16
  import { parseNpmLockGraph, parsePnpmLockGraph } from './graph.js';
17
+ import { buildReverseDependencyIndex, findReverseDependencyEntry, parseReverseDependencyObservations } from './dependency-index.js';
15
18
  import { createRadarConfigFromDshProfile, createRadarConfigFromNpmLock, createRadarConfigFromPnpmLock, discoverDshProfiles, refreshRadarConfigFromConfiguredProfile, resolveDshProfileDirectory, writeDshPatch, writeRadarConfig } from './init.js';
16
19
  import { parsePackageManifestSnapshot, parseRadarConfig } from './inventory.js';
17
20
  import { inspectNpmPackage } from './npm.js';
@@ -32,7 +35,7 @@ import { renderTextReport } from './render.js';
32
35
  import { materializeGitHubRepository, parseGitHubRepositoryUrl } from './repository.js';
33
36
  import { scanDirectory } from './scan.js';
34
37
  import { CisaKevClient, EpssClient } from './threat-intel.js';
35
- import { loadObservationState, OBSERVER_TARGETS_SCHEMA, parseObserverConfig, parseObserverConfigText, renderObserverReport, runDshAgentCommand, runOpenAiCompatibleAgent, runObserver, saveObservationState, UpstreamObserverClient, } from './upstream-observer.js';
38
+ import { loadObservationState, OBSERVER_TARGETS_SCHEMA, parseObserverConfig, parseObserverConfigText, observerExitCode, renderObserverReport, runDshAgentCommand, runOpenAiCompatibleAgent, runObserver, saveObservationState, UpstreamObserverClient, } from './upstream-observer.js';
36
39
  import { TOOL_VERSION } from './version.js';
37
40
  import { eventsForRadarWebhookTarget, markRadarWebhookEventsDelivered, markRadarWebhookEventsDeliveredForRoute, normalizeRadarWebhookUrl, queueRadarWebhookEvents, queueRadarWebhookEventsForRoute, resolveRadarWebhookTargets, sendRadarWebhook, undeliveredRadarWebhookEvents, undeliveredRadarWebhookEventsForRoute, } from './webhook.js';
38
41
  const VALID_THRESHOLDS = new Set(['warn', 'review', 'block', 'never']);
@@ -198,7 +201,8 @@ should remain visible without failing CI.
198
201
 
199
202
  Usage:
200
203
  upstream-radar observe <targets.yml|github-url> [--state <observations.json>]
201
- [--report <report.md>] [--dsh-agent-command <executable>]
204
+ [--report <report.md>] [--dsh-version <v1>,<v2>,...]
205
+ [--dsh-agent-command <executable>]
202
206
  [--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending]
203
207
  [--ecosystem <dsh|codex|pi>] [--id <id>] [--package <name>]
204
208
  [--package-path <path>] [--lockfile <path>] [--lockfile-type <npm|pnpm>]
@@ -206,30 +210,48 @@ Usage:
206
210
 
207
211
  The first run creates a baseline. Later runs compare source commits, published
208
212
  npm metadata, package manifests, and an optional npm/pnpm dependency graph. A
209
- DSH Agent is called only for meaningful changes. Safety: does not install packages, run lifecycle scripts, load plugin code, or invoke a shell.
213
+ meaningful change with a published npm version also triggers a deep review of
214
+ that exact artifact: Radar resolves its reachable dependency graph with npm
215
+ scripts disabled, checks known vulnerabilities, and records install-time
216
+ scripts and incomplete coverage. If exact DSH versions are supplied, the same
217
+ changed artifact is loaded in disposable profiles and the compatibility matrix
218
+ is added to the same report. A DSH Agent is called only for meaningful
219
+ changes. Safety: Radar does not install the observed plugin, run its lifecycle
220
+ scripts, load plugin code, or invoke a shell.
210
221
 
211
222
  When the target is an HTTPS GitHub repository URL, Radar builds one target in
212
- memory and does not require a targets.yml file. The default package path is
213
- package.json; pass --package-path for a nested plugin and --lockfile to include
214
- the committed npm/pnpm graph.
223
+ memory and does not require a targets.yml file. For a DSH target with no
224
+ --package-path, it looks for one DSH bundle or one @deepseek-ai/dsh package up
225
+ to three directory levels deep; pass --package-path when the repository is
226
+ ambiguous. --lockfile remains available when you want to pin the graph file.
215
227
 
216
228
  The Agent executable receives one read-only task prompt on stdin and should
217
- return one JSON conclusion on stdout. If it is not configured, the task stays
218
- in observations.json for a later explicit retry. As a simpler alternative,
229
+ return one JSON conclusion on stdout. If it is not configured or temporarily
230
+ unavailable, the static observation still succeeds and the task stays in
231
+ observations.json for a later explicit retry. Source observation errors still
232
+ return a non-zero exit code. As a simpler alternative,
219
233
  --llm-env-file reads an OpenAI-compatible issue-locator/.env-style file for
220
234
  only the model call. It accepts ISSUE_LOCATOR_LLM_*, OPENAI_*, or MODEL/CODEX_MODEL
221
235
  keys; it never writes the key or endpoint to observations.json.
222
236
  If a ModelBest-style base URL ends in /llm/v1, a 404 also retries the known
223
237
  /llm/openai/v1 path.
238
+
239
+ The exact npm artifact review is independent of the DSH Agent/model. It runs
240
+ when a meaningful change has a published package, and its dependency findings
241
+ remain in the report even when no model is configured or the model endpoint is
242
+ unavailable.
224
243
  `,
225
244
  graph: `Upstream Radar — read a lockfile into the canonical dependency graph
226
245
 
227
246
  Usage:
228
247
  upstream-radar graph pnpm-lock <pnpm-lock.yaml> [--root <package>@<exact-version>] [--json]
229
248
  upstream-radar graph npm-lock <package-lock.json> [--root <package>@<exact-version>] [--json]
249
+ upstream-radar graph reverse <reports-directory> [--package <name>@<exact-version>] [--output <index.json>] [--json]
230
250
 
231
251
  This command is offline and does not install packages, run lifecycle scripts,
232
- load plugin code, or query vulnerability sources.
252
+ load plugin code, or query vulnerability sources. The reverse form reads saved
253
+ scan, exact-review, or Radar config JSON and builds a deterministic
254
+ dependency -> affected-plugin index.
233
255
  `,
234
256
  'profile-check': `Upstream Radar — check one DSH profile before starting it
235
257
 
@@ -251,6 +273,18 @@ Usage:
251
273
 
252
274
  The probe is bounded and isolated. It is a compatibility/load check, not a
253
275
  semantic safety review or a substitute for dependency monitoring.
276
+ `,
277
+ review: `Upstream Radar — review one exact published DSH plugin in one command
278
+
279
+ Usage:
280
+ upstream-radar review dsh-plugin [npm:]<package>@<exact-version>
281
+ --dsh-version <v1>,<v2>,... [--registry <https-url>] [--timeout <seconds>] [--json]
282
+
283
+ This combines exact npm artifact/dependency inspection with a bounded DSH load
284
+ matrix. It packs with lifecycle scripts disabled, uses a temporary profile for
285
+ each requested DSH version, and prints one result that an author can act on.
286
+ It does not execute plugin code or business actions and does not call an LLM.
287
+ The default command reports findings without turning them into an extra gate.
254
288
  `,
255
289
  demo: `Upstream Radar — show the exact-path-to-DSH handoff without side effects
256
290
 
@@ -259,6 +293,16 @@ Usage:
259
293
 
260
294
  The demo is network-free and uses only a local fixture. It does not inspect your
261
295
  repository, install a plugin, start DSH, or claim that its advisory is real.
296
+ `,
297
+ case: `Upstream Radar — replay a real DSH profile failure and its repair
298
+
299
+ Usage:
300
+ upstream-radar case dsh-web-ui [--json]
301
+
302
+ This network-free case checks a broken DSH web-ui profile, the tempting manual
303
+ package workaround, and the corrected bundled-carrier layout. It reads only
304
+ packaged fixtures; it does not install packages, start DSH, execute plugin code,
305
+ or call an Agent/model.
262
306
  `,
263
307
  quickstart: `Upstream Radar — choose the smallest honest first-use path
264
308
 
@@ -418,12 +462,15 @@ Usage:
418
462
  upstream-radar doctor [config.json] [options]
419
463
  upstream-radar scan <directory-or-github-url> [--json] [--fail-on <warn|review|block|never>]
420
464
  upstream-radar inspect [npm:]<package>@<exact-version> [--deep] [--json] [--fail-on <warn|review|block|never>]
421
- upstream-radar observe <targets.yml|github-url> [--state <observations.json>] [--report <report.md>] [--dsh-agent-command <executable>] [--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending] [--ecosystem <dsh|codex|pi>] [--id <id>] [--package <name>] [--package-path <path>] [--lockfile <path>] [--lockfile-type <npm|pnpm>] [--ref <branch>] [--json]
465
+ upstream-radar observe <targets.yml|github-url> [--state <observations.json>] [--report <report.md>] [--dsh-version <v1>,<v2>,...] [--dsh-agent-command <executable>] [--dsh-agent-arg <argument>] [--llm-env-file <path>] [--retry-pending] [--ecosystem <dsh|codex|pi>] [--id <id>] [--package <name>] [--package-path <path>] [--lockfile <path>] [--lockfile-type <npm|pnpm>] [--ref <branch>] [--json]
422
466
  upstream-radar graph <npm-lock|pnpm-lock> <lockfile> [--root <package>@<exact-version>] [--json]
467
+ upstream-radar graph reverse <reports-directory> [--package <name>@<exact-version>] [--output <index.json>] [--json]
423
468
  upstream-radar profile-check [profile-directory] [--patch <path>] [--report <path>] [--summary] [--json]
424
469
  upstream-radar probe dsh-load <package.tgz> [--dsh-version <exact-version>] [--timeout <seconds>] [--keep-profile] [--json]
425
470
  upstream-radar probe dsh-matrix <package.tgz> --dsh-version <v1>[,<v2>,...] [--timeout <seconds>] [--keep-profile] [--json]
471
+ upstream-radar review dsh-plugin [npm:]<package>@<exact-version> --dsh-version <v1>,<v2>,... [--json]
426
472
  upstream-radar demo [--json]
473
+ upstream-radar case dsh-web-ui [--json]
427
474
  upstream-radar quickstart [directory] [--json]
428
475
  upstream-radar benchmark compatibility [--json]
429
476
  upstream-radar radar check <config.json> [--state <state.json>] [--webhook <https-url>] [--threat-intel] [--frozen] [--fail-on <severity>] [--fail-on-compatibility <never|breaking|any>] [--json]
@@ -449,10 +496,12 @@ Commands:
449
496
  scan bounded, read-only inspection of a local directory or public GitHub repository
450
497
  inspect fetch and verify the exact npm artifact before inspecting its contents
451
498
  observe compare upstream DSH plugin repositories and route only meaningful changes to a DSH Agent
452
- graph read a lockfile into the canonical dependency graph without installing packages
499
+ graph read lockfiles or build the dependency -> affected-plugin index without installing packages
453
500
  profile-check check a DSH profile's lockfile and patch rows without starting DSH
454
501
  probe run a bounded DSH bundle-load check or version matrix in disposable profiles
502
+ review combine exact npm dependency review and DSH load compatibility in one command
455
503
  demo show the exact-path-to-DSH handoff without network, DSH, or plugin installation
504
+ case replay a real DSH profile failure and its repair without side effects
456
505
  quickstart choose the smallest first-use path without changing the environment
457
506
  benchmark run offline compatibility-rule contracts without network or plugin execution
458
507
  radar monitor vulnerability changes, watch continuously, find the next action, inspect status/history, or assess a candidate compatibility change
@@ -524,10 +573,141 @@ async function inferLockfileRoot(lockfile, kind) {
524
573
  throw error;
525
574
  }
526
575
  }
576
+ const MAX_REVERSE_INDEX_FILES = 2_000;
577
+ const MAX_REVERSE_INDEX_DEPTH = 8;
578
+ async function collectReverseIndexFiles(path, depth = 0) {
579
+ if (depth > MAX_REVERSE_INDEX_DEPTH)
580
+ throw new Error(`reverse index input exceeds the ${MAX_REVERSE_INDEX_DEPTH}-level directory limit: ${path}`);
581
+ const metadata = await stat(resolve(path));
582
+ if (metadata.isFile())
583
+ return path.endsWith('.json') ? [resolve(path)] : [];
584
+ if (!metadata.isDirectory())
585
+ return [];
586
+ const files = [];
587
+ const entries = await readdir(resolve(path), { withFileTypes: true });
588
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
589
+ if (entry.name === '.git' || entry.name === 'node_modules' || entry.name.startsWith('.'))
590
+ continue;
591
+ const child = join(path, entry.name);
592
+ if (entry.isDirectory())
593
+ files.push(...await collectReverseIndexFiles(child, depth + 1));
594
+ else if (entry.isFile() && entry.name.endsWith('.json'))
595
+ files.push(resolve(child));
596
+ if (files.length > MAX_REVERSE_INDEX_FILES)
597
+ throw new Error(`reverse index input exceeds the ${MAX_REVERSE_INDEX_FILES}-file limit`);
598
+ }
599
+ return files;
600
+ }
601
+ function renderReverseDependencyIndex(index, packageFilter) {
602
+ const selected = packageFilter === undefined
603
+ ? undefined
604
+ : findReverseDependencyEntry(index, { ecosystem: 'npm', ...packageFilter });
605
+ const dependencies = selected === undefined && packageFilter !== undefined
606
+ ? []
607
+ : selected === undefined
608
+ ? [...index.dependencies].sort((left, right) => (right.dependents.length - left.dependents.length
609
+ || `${left.dependency.name}@${left.dependency.version}`.localeCompare(`${right.dependency.name}@${right.dependency.version}`))).slice(0, 20)
610
+ : [selected];
611
+ const lines = [
612
+ 'Upstream Radar — reverse dependency index',
613
+ `Observations: ${index.observations}`,
614
+ `Plugins: ${index.plugins.length}`,
615
+ `Dependencies: ${index.dependencies.length}`,
616
+ `Coverage: ${index.coverage.completeObservations} complete, ${index.coverage.incompleteObservations} incomplete, ${index.coverage.unresolvedEdges} unresolved edge(s)`,
617
+ `Input files: ${index.inputs.files} (${index.inputs.loadedFiles} loaded, ${index.inputs.skipped.length} skipped)`,
618
+ ];
619
+ if (packageFilter !== undefined)
620
+ lines.push(`Query: ${packageFilter.name}@${packageFilter.version}`);
621
+ lines.push('', dependencies.length === 0
622
+ ? (packageFilter === undefined ? 'Dependencies:\n (none)' : 'Dependents:\n (none)')
623
+ : `${packageFilter === undefined ? 'Top dependencies by affected plugin count' : 'Dependents'}:`);
624
+ for (const dependency of dependencies) {
625
+ if (packageFilter === undefined)
626
+ lines.push(` ${dependency.dependency.name}@${dependency.dependency.version} ← ${dependency.dependents.length} plugin(s)`);
627
+ for (const dependent of dependency.dependents) {
628
+ const project = dependent.project === undefined ? '' : ` [${dependent.project.name}]`;
629
+ lines.push(` ${dependent.plugin.name}@${dependent.plugin.version}${project} (${dependent.coverage})`);
630
+ for (const path of dependent.paths.slice(0, 2))
631
+ lines.push(` ${path.nodes.join(' → ')}`);
632
+ if (dependent.paths.length > 2)
633
+ lines.push(` ... ${dependent.paths.length - 2} more path(s)`);
634
+ }
635
+ }
636
+ if (index.inputs.skipped.length > 0) {
637
+ lines.push('', 'Skipped input files:');
638
+ for (const item of index.inputs.skipped.slice(0, 20))
639
+ lines.push(` ${item.source}: ${item.reason}`);
640
+ }
641
+ return `${lines.join('\n')}\n`;
642
+ }
643
+ async function runReverseDependencyIndex(args) {
644
+ const input = args[0];
645
+ if (input === undefined || input.startsWith('-'))
646
+ throw new Error('graph reverse requires a reports directory or JSON file');
647
+ let json = false;
648
+ let output;
649
+ let packageFilter;
650
+ for (let index = 1; index < args.length; index += 1) {
651
+ const argument = args[index];
652
+ if (argument === '--json')
653
+ json = true;
654
+ else if (argument === '--output') {
655
+ const value = args[index + 1];
656
+ if (value === undefined || value.startsWith('-'))
657
+ throw new Error('--output requires a path');
658
+ output = value;
659
+ index += 1;
660
+ }
661
+ else if (argument === '--package') {
662
+ const value = args[index + 1];
663
+ if (value === undefined || value.startsWith('-'))
664
+ throw new Error('--package requires an exact package coordinate');
665
+ packageFilter = parseExactPackageCoordinate(value);
666
+ index += 1;
667
+ }
668
+ else {
669
+ throw new Error(`unknown option for graph reverse: ${argument}`);
670
+ }
671
+ }
672
+ const files = await collectReverseIndexFiles(input);
673
+ if (files.length === 0)
674
+ throw new Error(`graph reverse found no JSON reports under ${input}`);
675
+ const observations = [];
676
+ const skipped = [];
677
+ for (const file of files) {
678
+ try {
679
+ observations.push(...parseReverseDependencyObservations(await readJson(file), file));
680
+ }
681
+ catch (error) {
682
+ skipped.push({ source: file, reason: safeErrorMessage(error instanceof Error ? error.message : String(error)) });
683
+ }
684
+ }
685
+ if (observations.length === 0)
686
+ throw new Error(`graph reverse could not load a supported graph from ${files.length} JSON file(s)`);
687
+ const index = buildReverseDependencyIndex(observations, {
688
+ inputs: { files: files.length, loadedFiles: files.length - skipped.length, skipped },
689
+ });
690
+ if (output !== undefined)
691
+ await writeFile(resolve(output), `${JSON.stringify(index, null, 2)}\n`);
692
+ if (json) {
693
+ if (packageFilter === undefined)
694
+ process.stdout.write(`${JSON.stringify(index, null, 2)}\n`);
695
+ else
696
+ process.stdout.write(`${JSON.stringify({ ...index, dependencies: index.dependencies.filter(item => item.dependency.name === packageFilter.name && item.dependency.version === packageFilter.version) }, null, 2)}\n`);
697
+ }
698
+ else {
699
+ process.stdout.write(renderReverseDependencyIndex(index, packageFilter));
700
+ if (output !== undefined)
701
+ process.stdout.write(`Index written to ${resolve(output)}\n`);
702
+ }
703
+ return 0;
704
+ }
527
705
  async function runGraph(args) {
528
706
  const kind = args[0];
707
+ if (kind === 'reverse')
708
+ return runReverseDependencyIndex(args.slice(1));
529
709
  if (kind !== 'npm-lock' && kind !== 'pnpm-lock')
530
- throw new Error('graph requires the npm-lock or pnpm-lock subcommand');
710
+ throw new Error('graph requires the npm-lock, pnpm-lock, or reverse subcommand');
531
711
  const lockfile = args[1];
532
712
  if (lockfile === undefined || lockfile.startsWith('-'))
533
713
  throw new Error(`graph ${kind} requires a lockfile path`);
@@ -1005,6 +1185,21 @@ function runDemo(args) {
1005
1185
  process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDemo(report));
1006
1186
  return 0;
1007
1187
  }
1188
+ async function runDshCase(args) {
1189
+ const caseId = args[0];
1190
+ if (caseId !== 'dsh-web-ui')
1191
+ throw new Error('case requires dsh-web-ui');
1192
+ let json = false;
1193
+ for (const argument of args.slice(1)) {
1194
+ if (argument === '--json')
1195
+ json = true;
1196
+ else
1197
+ throw new Error(`unknown option for case: ${argument}`);
1198
+ }
1199
+ const report = await createDshCaseReport();
1200
+ process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDshCase(report));
1201
+ return 0;
1202
+ }
1008
1203
  async function runProbe(args) {
1009
1204
  const mode = args[0];
1010
1205
  if (mode !== 'dsh-load' && mode !== 'dsh-matrix')
@@ -1071,6 +1266,57 @@ async function runProbe(args) {
1071
1266
  process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDshLoadMatrix(report));
1072
1267
  return report.result === 'compatible' ? 0 : report.result === 'incompatible' ? 2 : 1;
1073
1268
  }
1269
+ async function runDshPluginReview(args) {
1270
+ if (args[0] !== 'dsh-plugin')
1271
+ throw new Error('review requires dsh-plugin');
1272
+ const target = args[1];
1273
+ if (target === undefined || target.startsWith('-'))
1274
+ throw new Error('review dsh-plugin requires an exact npm package');
1275
+ const dshVersions = [];
1276
+ let registry;
1277
+ let timeoutSeconds = 120;
1278
+ let json = false;
1279
+ for (let index = 2; index < args.length; index += 1) {
1280
+ const argument = args[index];
1281
+ if (argument === '--json') {
1282
+ json = true;
1283
+ }
1284
+ else if (argument === '--dsh-version' || argument === '--registry' || argument === '--timeout') {
1285
+ const value = args[index + 1];
1286
+ if (value === undefined || value.startsWith('-'))
1287
+ throw new Error(`${argument} requires a value`);
1288
+ if (argument === '--dsh-version') {
1289
+ dshVersions.push(...value.split(',').map(item => item.trim()).filter(item => item !== ''));
1290
+ }
1291
+ else if (argument === '--registry') {
1292
+ registry = value;
1293
+ }
1294
+ else {
1295
+ const parsed = Number(value);
1296
+ if (!Number.isSafeInteger(parsed) || parsed < 30 || parsed > 600) {
1297
+ throw new Error('--timeout must be an integer between 30 and 600 seconds');
1298
+ }
1299
+ timeoutSeconds = parsed;
1300
+ }
1301
+ index += 1;
1302
+ }
1303
+ else {
1304
+ throw new Error(`unknown option for review dsh-plugin: ${argument}`);
1305
+ }
1306
+ }
1307
+ if (dshVersions.length < 2)
1308
+ throw new Error('review dsh-plugin requires at least two exact DSH versions in --dsh-version');
1309
+ const report = await reviewDshPlugin(target, {
1310
+ dshVersions,
1311
+ ...(registry === undefined ? {} : { registry }),
1312
+ timeoutMs: timeoutSeconds * 1_000,
1313
+ });
1314
+ process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : renderDshPluginReview(report));
1315
+ // This command is intentionally report-first: a review result is useful even
1316
+ // when it says review, block, incompatible, or unknown. CI callers can gate
1317
+ // on the JSON status without hiding the evidence from the author.
1318
+ return 0;
1319
+ }
1074
1320
  async function fileExists(path) {
1075
1321
  try {
1076
1322
  await access(resolve(path));
@@ -1405,6 +1651,95 @@ async function runRadar(args) {
1405
1651
  : `${renderRadarEvents(events)}Prepared ${analysisTasks.length} DSH compatibility analysis task(s).\n`);
1406
1652
  return 0;
1407
1653
  }
1654
+ function summarizeObserverDshCompatibility(report) {
1655
+ return {
1656
+ result: report.result,
1657
+ versions: report.dshVersions.slice(0, 8),
1658
+ summary: { ...report.summary },
1659
+ reports: report.reports.slice(0, 8).map(item => ({
1660
+ dshVersion: item.dshVersion,
1661
+ result: item.result,
1662
+ reason: item.reason.slice(0, 2_048),
1663
+ })),
1664
+ };
1665
+ }
1666
+ function summarizeObserverArtifactReview(spec, report, dshCompatibility) {
1667
+ const npm = report.evidence.npm;
1668
+ const audit = npm?.dependencyAudit;
1669
+ const installScriptDetails = (audit?.installScriptDetails ?? []).slice(0, 32).map(detail => ({
1670
+ package: detail.package.slice(0, 512),
1671
+ scripts: detail.scripts.slice(0, 16).map(script => ({
1672
+ name: script.name,
1673
+ command: script.command.slice(0, 4_096),
1674
+ })),
1675
+ }));
1676
+ return {
1677
+ spec,
1678
+ verdict: report.verdict,
1679
+ riskVerdict: report.riskVerdict,
1680
+ coverageVerdict: report.coverageVerdict,
1681
+ artifactIntegrity: report.coverage.artifactIntegrity,
1682
+ registrySignature: npm?.registrySignature.status ?? report.coverage.registrySignature,
1683
+ provenance: npm?.provenance.status ?? report.coverage.provenance,
1684
+ dependencyResolution: report.coverage.dependencyResolution,
1685
+ dependencyAuditStatus: audit?.status ?? 'not-run',
1686
+ ...(audit?.resolutionMode === undefined ? {} : { resolutionMode: audit.resolutionMode }),
1687
+ ...(audit?.graphDigest === undefined ? {} : { graphDigest: audit.graphDigest }),
1688
+ packages: audit?.packages ?? null,
1689
+ unresolved: audit?.graph?.unresolved?.length ?? 0,
1690
+ vulnerabilities: audit?.vulnerabilities ?? null,
1691
+ installScriptPackages: (audit?.installScriptPackages ?? []).slice(0, 32),
1692
+ installScriptDetails,
1693
+ findings: report.findings.slice(0, 32).map(finding => ({
1694
+ code: finding.code,
1695
+ severity: finding.severity,
1696
+ summary: finding.summary.slice(0, 2_048),
1697
+ detail: finding.detail.slice(0, 4_096),
1698
+ ...(finding.remediation === undefined ? {} : { remediation: finding.remediation.slice(0, 4_096) }),
1699
+ })),
1700
+ ...(dshCompatibility === undefined ? {} : { dshCompatibility: summarizeObserverDshCompatibility(dshCompatibility) }),
1701
+ };
1702
+ }
1703
+ async function reviewObserverArtifact(spec, target, registry) {
1704
+ const inspection = await inspectNpmPackage(spec, {
1705
+ deep: true,
1706
+ ...(registry === undefined ? {} : { registry }),
1707
+ });
1708
+ if (target.dshVersions === undefined)
1709
+ return summarizeObserverArtifactReview(spec, inspection);
1710
+ try {
1711
+ const combined = await reviewDshPlugin(spec, {
1712
+ dshVersions: target.dshVersions,
1713
+ ...(registry === undefined ? {} : { registry }),
1714
+ inspect: async () => inspection,
1715
+ });
1716
+ const summary = summarizeObserverArtifactReview(spec, combined.inspection, combined.compatibility);
1717
+ if (combined.artifact.matched)
1718
+ return summary;
1719
+ return {
1720
+ ...summary,
1721
+ verdict: 'review',
1722
+ riskVerdict: 'review',
1723
+ coverageVerdict: 'incomplete',
1724
+ findings: [
1725
+ ...summary.findings,
1726
+ {
1727
+ code: 'inspection-probe-artifact-mismatch',
1728
+ severity: 'high',
1729
+ summary: 'The artifact inspected for dependency evidence differs from the artifact loaded by the DSH probe.',
1730
+ detail: 'Repeat the review against one exact registry artifact before treating the compatibility result as evidence for the inspected bytes.',
1731
+ remediation: 'Pin the exact package version and registry, then rerun the observer review until the artifact digests match.',
1732
+ },
1733
+ ].slice(0, 32),
1734
+ };
1735
+ }
1736
+ catch (error) {
1737
+ return {
1738
+ ...summarizeObserverArtifactReview(spec, inspection),
1739
+ error: safeErrorMessage(error instanceof Error ? error.message : String(error)),
1740
+ };
1741
+ }
1742
+ }
1408
1743
  async function runObserve(args) {
1409
1744
  const targetsPath = args[0];
1410
1745
  if (targetsPath === undefined || targetsPath.startsWith('-'))
@@ -1424,6 +1759,7 @@ async function runObserve(args) {
1424
1759
  let inlineLockfile;
1425
1760
  let inlineLockfileType;
1426
1761
  let inlineRef;
1762
+ let inlineDshVersions;
1427
1763
  let inlineOptionsUsed = false;
1428
1764
  for (let index = 1; index < args.length; index += 1) {
1429
1765
  const argument = args[index];
@@ -1433,7 +1769,7 @@ async function runObserve(args) {
1433
1769
  else if (argument === '--retry-pending') {
1434
1770
  retryPending = true;
1435
1771
  }
1436
- else if (argument === '--ecosystem' || argument === '--id' || argument === '--package' || argument === '--package-path' || argument === '--lockfile' || argument === '--lockfile-type' || argument === '--ref' || argument === '--state' || argument === '--report' || argument === '--dsh-agent-command' || argument === '--dsh-agent-arg' || argument === '--llm-env-file' || argument === '--registry') {
1772
+ else if (argument === '--ecosystem' || argument === '--id' || argument === '--package' || argument === '--package-path' || argument === '--lockfile' || argument === '--lockfile-type' || argument === '--ref' || argument === '--dsh-version' || argument === '--state' || argument === '--report' || argument === '--dsh-agent-command' || argument === '--dsh-agent-arg' || argument === '--llm-env-file' || argument === '--registry') {
1437
1773
  const value = args[index + 1];
1438
1774
  if (value === undefined || (value.startsWith('-') && argument !== '--dsh-agent-arg'))
1439
1775
  throw new Error(`${argument} requires a value`);
@@ -1451,6 +1787,8 @@ async function runObserve(args) {
1451
1787
  inlineLockfileType = value;
1452
1788
  else if (argument === '--ref')
1453
1789
  inlineRef = value;
1790
+ else if (argument === '--dsh-version')
1791
+ inlineDshVersions = value.split(',').map(item => item.trim()).filter(item => item !== '');
1454
1792
  else if (argument === '--state')
1455
1793
  statePath = value;
1456
1794
  else if (argument === '--report')
@@ -1485,9 +1823,10 @@ async function runObserve(args) {
1485
1823
  repository: `${githubTarget.owner}/${githubTarget.repository}`,
1486
1824
  ref: inlineRef ?? 'main',
1487
1825
  packageName: inlinePackage,
1488
- packagePath: inlinePackagePath ?? 'package.json',
1826
+ ...(inlinePackagePath === undefined ? {} : { packagePath: inlinePackagePath }),
1489
1827
  ...(inlineLockfile === undefined ? {} : { lockfile: inlineLockfile }),
1490
1828
  ...(inlineLockfileType === undefined ? {} : { lockfileType: inlineLockfileType }),
1829
+ ...(inlineDshVersions === undefined ? {} : { dshVersions: inlineDshVersions }),
1491
1830
  }],
1492
1831
  });
1493
1832
  }
@@ -1495,7 +1834,7 @@ async function runObserve(args) {
1495
1834
  if (/^https?:\/\//i.test(targetsPath))
1496
1835
  throw new Error('observe URL must be an HTTPS GitHub repository URL');
1497
1836
  if (inlineOptionsUsed)
1498
- throw new Error('--ecosystem, --id, --package, --package-path, --lockfile, --lockfile-type and --ref require a GitHub URL target');
1837
+ throw new Error('--ecosystem, --id, --package, --package-path, --lockfile, --lockfile-type, --ref and --dsh-version require a GitHub URL target');
1499
1838
  const targetText = await readBoundedFile(targetsPath, 256 * 1024);
1500
1839
  config = parseObserverConfigText(targetText);
1501
1840
  }
@@ -1517,6 +1856,7 @@ async function runObserve(args) {
1517
1856
  const result = await runObserver(config, previousState, {
1518
1857
  source,
1519
1858
  retryPending,
1859
+ artifactReviewer: async (spec, target) => reviewObserverArtifact(spec, target, registry),
1520
1860
  ...(agentOptions === undefined ? {} : {
1521
1861
  agent: (task, prompt) => runDshAgentCommand(task, prompt, agentOptions),
1522
1862
  }),
@@ -1530,9 +1870,11 @@ async function runObserve(args) {
1530
1870
  await writeFile(resolve(reportPath), reportPath.endsWith('.json') ? reportJson : renderObserverReport(result.report));
1531
1871
  }
1532
1872
  process.stdout.write(json ? reportJson : renderObserverReport(result.report));
1533
- if (result.report.errors.length > 0 || result.report.agent.failed > 0)
1534
- return 1;
1535
- return 0;
1873
+ const observerExit = observerExitCode(result.report);
1874
+ if (result.report.agent.failed > 0) {
1875
+ process.stderr.write('upstream-radar: static observation completed; DSH Agent/model analysis remains pending. See the report and rerun with --retry-pending.\n');
1876
+ }
1877
+ return observerExit;
1536
1878
  }
1537
1879
  async function runQuickstart(args) {
1538
1880
  let directory = process.cwd();
@@ -2001,8 +2343,12 @@ async function main(args) {
2001
2343
  return runDshProfileCheck(args.slice(1));
2002
2344
  if (command === 'probe')
2003
2345
  return runProbe(args.slice(1));
2346
+ if (command === 'review')
2347
+ return runDshPluginReview(args.slice(1));
2004
2348
  if (command === 'demo')
2005
2349
  return runDemo(args.slice(1));
2350
+ if (command === 'case')
2351
+ return runDshCase(args.slice(1));
2006
2352
  if (command === 'observe')
2007
2353
  return runObserve(args.slice(1));
2008
2354
  if (command === 'benchmark')