upstream-radar 0.40.0 → 0.42.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 (56) hide show
  1. package/README.md +72 -241
  2. package/README.zh-CN.md +89 -0
  3. package/dist/src/cli.js +11 -2
  4. package/dist/src/cli.js.map +1 -1
  5. package/dist/src/dsh-compatibility-ir.d.ts +99 -0
  6. package/dist/src/dsh-compatibility-ir.d.ts.map +1 -0
  7. package/dist/src/dsh-compatibility-ir.js +408 -0
  8. package/dist/src/dsh-compatibility-ir.js.map +1 -0
  9. package/dist/src/dsh-compatibility-issues.d.ts +46 -0
  10. package/dist/src/dsh-compatibility-issues.d.ts.map +1 -0
  11. package/dist/src/dsh-compatibility-issues.js +223 -0
  12. package/dist/src/dsh-compatibility-issues.js.map +1 -0
  13. package/dist/src/dsh-compatibility-ledger.d.ts +152 -0
  14. package/dist/src/dsh-compatibility-ledger.d.ts.map +1 -0
  15. package/dist/src/dsh-compatibility-ledger.js +678 -0
  16. package/dist/src/dsh-compatibility-ledger.js.map +1 -0
  17. package/dist/src/dsh-directory-feed.d.ts +108 -0
  18. package/dist/src/dsh-directory-feed.d.ts.map +1 -0
  19. package/dist/src/dsh-directory-feed.js +267 -0
  20. package/dist/src/dsh-directory-feed.js.map +1 -0
  21. package/dist/src/dsh-install-observation.d.ts +89 -1
  22. package/dist/src/dsh-install-observation.d.ts.map +1 -1
  23. package/dist/src/dsh-install-observation.js +658 -19
  24. package/dist/src/dsh-install-observation.js.map +1 -1
  25. package/dist/src/dsh-install-plan.d.ts +16 -6
  26. package/dist/src/dsh-install-plan.d.ts.map +1 -1
  27. package/dist/src/dsh-install-plan.js +273 -47
  28. package/dist/src/dsh-install-plan.js.map +1 -1
  29. package/dist/src/dsh-plugin.d.ts.map +1 -1
  30. package/dist/src/dsh-plugin.js +6 -2
  31. package/dist/src/dsh-plugin.js.map +1 -1
  32. package/dist/src/dsh-runtime.d.ts +6 -0
  33. package/dist/src/dsh-runtime.d.ts.map +1 -1
  34. package/dist/src/dsh-runtime.js +38 -0
  35. package/dist/src/dsh-runtime.js.map +1 -1
  36. package/dist/src/index.d.ts +8 -4
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/index.js +5 -1
  39. package/dist/src/index.js.map +1 -1
  40. package/dist/src/installed-graph.d.ts.map +1 -1
  41. package/dist/src/installed-graph.js +105 -26
  42. package/dist/src/installed-graph.js.map +1 -1
  43. package/dist/src/radar-types.d.ts +15 -0
  44. package/dist/src/radar-types.d.ts.map +1 -1
  45. package/dist/src/semver.d.ts.map +1 -1
  46. package/dist/src/semver.js +7 -1
  47. package/dist/src/semver.js.map +1 -1
  48. package/dist/src/tar.d.ts.map +1 -1
  49. package/dist/src/tar.js +28 -13
  50. package/dist/src/tar.js.map +1 -1
  51. package/dist/src/version.d.ts +1 -1
  52. package/dist/src/version.js +1 -1
  53. package/docs/README.zh-CN.md +25 -23
  54. package/package.json +3 -1
  55. package/schemas/dsh-compatibility-ir.schema.json +96 -0
  56. package/schemas/dsh-install-observation.schema.json +141 -2
@@ -1,9 +1,13 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { constants } from 'node:fs';
4
- import { lstat, mkdir, mkdtemp, open, readdir, rm, writeFile } from 'node:fs/promises';
4
+ import { lstat, mkdir, mkdtemp, open, readdir, realpath, rm, writeFile } from 'node:fs/promises';
5
5
  import { tmpdir } from 'node:os';
6
- import { basename, join, relative, resolve, sep } from 'node:path';
6
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { parsePnpmLockGraph } from './graph.js';
9
+ import { discoverDshRuntimeHostNodeModulesDirectory, discoverDshRuntimePackage, discoverDshRuntimePackageDirectory, } from './dsh-runtime.js';
10
+ import { parseInstalledNodeModulesGraph } from './installed-graph.js';
7
11
  import { parseNpmSpec } from './npm.js';
8
12
  import { satisfiesSemverRange } from './semver.js';
9
13
  import { parseNpmTarball } from './tar.js';
@@ -12,6 +16,7 @@ export const DSH_INSTALL_OBSERVATION_SCHEMA = 'upstream-radar.dsh-install-observ
12
16
  const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
13
17
  const DEFAULT_TIMEOUT_MS = 180_000;
14
18
  const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
19
+ const MAX_ARTIFACT_UNPACKED_BYTES = 192 * 1024 * 1024;
15
20
  const MAX_COMMAND_OUTPUT_BYTES = 64 * 1024;
16
21
  const MAX_REPORT_DETAIL_BYTES = 4 * 1024;
17
22
  const MAX_TRACE_BYTES = 16 * 1024 * 1024;
@@ -20,8 +25,19 @@ const MAX_TRACE_EVENTS = 512;
20
25
  const MAX_SNAPSHOT_ENTRIES = 25_000;
21
26
  const MAX_DIFF_PATHS = 512;
22
27
  const MAX_ALLOWED_BUILDS = 16;
28
+ const MAX_PROFILE_LOCKFILE_BYTES = 16 * 1024 * 1024;
29
+ const MAX_PROFILE_GRAPH_GAPS = 32;
30
+ const MAX_PLUGIN_PEERS = 64;
31
+ const MAX_STATIC_PEER_SCAN_BYTES = 8 * 1024 * 1024;
32
+ const MAX_RUNTIME_DISCOVERY_DIRECTORIES = 12_000;
33
+ const MAX_RUNTIME_DISCOVERY_DEPTH = 16;
23
34
  const PROFILE = 'headless';
24
35
  const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'];
36
+ const PROFILE_LOCKFILE_CANDIDATES = [
37
+ ['pnpm-lock.yaml'],
38
+ ['node_modules', '.pnpm', 'lock.yaml'],
39
+ ];
40
+ const SYNTHETIC_PROFILE_GRAPH_ROOT = { name: 'dsh-profile-headless', version: '0.0.0' };
25
41
  function normalizeAllowedBuilds(values) {
26
42
  if (values === undefined)
27
43
  return [];
@@ -37,6 +53,127 @@ function normalizeAllowedBuilds(values) {
37
53
  }
38
54
  return [...names].sort();
39
55
  }
56
+ function isNpmPackageName(value) {
57
+ return /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/.test(value);
58
+ }
59
+ /**
60
+ * pnpm 10+ deliberately stops before running dependency lifecycle scripts
61
+ * unless each package is approved. Keep that policy gate distinct from a
62
+ * plugin install defect, but only when pnpm supplied a bounded, exact list.
63
+ */
64
+ function requiredDependencyBuilds(output, artifactName) {
65
+ const normalized = output.replace(/\r\n?/g, '\n');
66
+ const marker = '[ERR_PNPM_IGNORED_BUILDS]';
67
+ const markerIndex = normalized.indexOf(marker);
68
+ if (markerIndex < 0)
69
+ return [];
70
+ const tail = normalized.slice(markerIndex, markerIndex + MAX_COMMAND_OUTPUT_BYTES);
71
+ const prefix = 'Ignored build scripts:';
72
+ const prefixIndex = tail.indexOf(prefix);
73
+ if (prefixIndex < marker.length || prefixIndex > 256)
74
+ return [];
75
+ let list = tail.slice(prefixIndex + prefix.length);
76
+ const terminator = list.search(/\n\s*\n|\n\s*Run\s+["']pnpm approve-builds["']/);
77
+ if (terminator >= 0)
78
+ list = list.slice(0, terminator);
79
+ const coordinates = list.split(',').map(value => value.trim()).filter(Boolean);
80
+ if (coordinates.length === 0 || coordinates.length > MAX_ALLOWED_BUILDS)
81
+ return [];
82
+ const names = new Set();
83
+ for (const coordinate of coordinates) {
84
+ try {
85
+ const parsed = parseNpmSpec(coordinate);
86
+ if (!EXACT_VERSION.test(parsed.version) || !isNpmPackageName(parsed.name))
87
+ return [];
88
+ names.add(parsed.name);
89
+ }
90
+ catch {
91
+ // When the exact reviewed tarball itself has a lifecycle script, pnpm
92
+ // prints its profile-relative file: coordinate alongside registry
93
+ // dependencies. Accept only the already-established artifact identity;
94
+ // an arbitrary local coordinate must not become an approval suggestion.
95
+ const prefix = `${artifactName}@file:`;
96
+ const localPath = coordinate.startsWith(prefix) ? coordinate.slice(prefix.length) : '';
97
+ if (localPath === '' || localPath.length > 4_096 || /[\u0000-\u0020\u007f]/.test(localPath))
98
+ return [];
99
+ names.add(artifactName);
100
+ }
101
+ }
102
+ return [...names].sort();
103
+ }
104
+ function staticPeerUsage(entries, requirements) {
105
+ const state = new Map(requirements.map(requirement => [requirement.name, 'no-literal-reference-observed']));
106
+ let scanned = 0;
107
+ let incomplete = false;
108
+ for (const entry of entries) {
109
+ if (entry.type !== 'file' || entry.contents === undefined
110
+ || !/\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(entry.path))
111
+ continue;
112
+ if (scanned + entry.contents.length > MAX_STATIC_PEER_SCAN_BYTES) {
113
+ incomplete = true;
114
+ continue;
115
+ }
116
+ scanned += entry.contents.length;
117
+ const text = entry.contents.toString('utf8');
118
+ const declarationFile = /\.d\.(?:[cm]?ts)$/i.test(entry.path);
119
+ for (const requirement of requirements) {
120
+ if (state.get(requirement.name) === 'runtime-import-observed')
121
+ continue;
122
+ const escaped = requirement.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
123
+ const literal = `['"]${escaped}(?:/[^'"]*)?['"]`;
124
+ // Stay within one statement line. A wider expression can start at an
125
+ // unrelated runtime import and accidentally consume a later `import
126
+ // type`, which would turn declaration-only evidence into a false runtime
127
+ // claim. Missing a heavily formatted import is safer than that claim.
128
+ const typeOnly = new RegExp(`(?:^|[;\\n])[\\t ]*import[\\t ]+type\\b[^;\\n]{0,1024}?\\bfrom[\\t ]*${literal}`);
129
+ const runtime = new RegExp([
130
+ `(?:^|[;\\n])[\\t ]*import[\\t ]+(?!type\\b)(?:[^;\\n]{0,1024}?[\\t ]+from[\\t ]+)?${literal}`,
131
+ `(?:^|[;\\n])[\\t ]*export[\\t ]+(?!type\\b)[^;\\n]{0,1024}?[\\t ]+from[\\t ]+${literal}`,
132
+ `\\b(?:require|import)\\s*\\(\\s*${literal}`,
133
+ ].join('|'));
134
+ const literalReference = new RegExp(literal);
135
+ if (!declarationFile && runtime.test(text)) {
136
+ state.set(requirement.name, 'runtime-import-observed');
137
+ }
138
+ else if (declarationFile ? literalReference.test(text) : typeOnly.test(text)) {
139
+ state.set(requirement.name, 'type-only-reference-observed');
140
+ }
141
+ }
142
+ }
143
+ return requirements.map(requirement => {
144
+ const observed = state.get(requirement.name) ?? 'no-literal-reference-observed';
145
+ return observed === 'no-literal-reference-observed' && incomplete ? 'scan-incomplete' : observed;
146
+ });
147
+ }
148
+ function requiredPeerDependencies(manifest, entries) {
149
+ const peers = typeof manifest.peerDependencies === 'object' && manifest.peerDependencies !== null && !Array.isArray(manifest.peerDependencies)
150
+ ? manifest.peerDependencies
151
+ : {};
152
+ const metadata = typeof manifest.peerDependenciesMeta === 'object' && manifest.peerDependenciesMeta !== null && !Array.isArray(manifest.peerDependenciesMeta)
153
+ ? manifest.peerDependenciesMeta
154
+ : {};
155
+ const requirements = [];
156
+ for (const [name, rawRange] of Object.entries(peers).sort(([left], [right]) => left.localeCompare(right))) {
157
+ if (!isNpmPackageName(name))
158
+ throw new Error(`packed artifact has an invalid peer dependency name: ${name}`);
159
+ if (typeof rawRange !== 'string' || rawRange.trim() === '' || rawRange.length > 512) {
160
+ throw new Error(`packed artifact has an invalid peer dependency range for ${name}`);
161
+ }
162
+ const peerMetadata = metadata[name];
163
+ const optional = typeof peerMetadata === 'object' && peerMetadata !== null && !Array.isArray(peerMetadata)
164
+ && peerMetadata.optional === true;
165
+ if (!optional)
166
+ requirements.push({ name, required: bounded(rawRange.trim(), 512) });
167
+ }
168
+ if (requirements.length > MAX_PLUGIN_PEERS) {
169
+ throw new Error(`packed artifact declares more than ${MAX_PLUGIN_PEERS} required peer dependencies`);
170
+ }
171
+ const usages = staticPeerUsage(entries, requirements);
172
+ return requirements.map((requirement, index) => ({
173
+ ...requirement,
174
+ staticUsage: usages[index] ?? 'scan-incomplete',
175
+ }));
176
+ }
40
177
  function bounded(value, maximum = MAX_REPORT_DETAIL_BYTES) {
41
178
  const normalized = value.replace(/[\u0000-\u001f\u007f-\u009f]/g, character => (`\\u${character.codePointAt(0)?.toString(16).padStart(4, '0') ?? '0000'}`));
42
179
  return normalized.length <= maximum ? normalized : `${normalized.slice(0, maximum - 1)}…`;
@@ -391,18 +528,30 @@ async function readRegularFileNoFollow(path, maximum) {
391
528
  async function parsePackedArtifact(result, artifactDirectory, expectedName, expectedVersion) {
392
529
  if (commandStage(result).status !== 'passed')
393
530
  throw new Error(commandStage(result).detail ?? 'npm pack failed');
394
- let output;
395
- try {
396
- output = JSON.parse(result.stdout.trim());
397
- }
398
- catch {
399
- throw new Error('npm pack did not return JSON');
531
+ const stdout = result.stdout.trim();
532
+ let filename;
533
+ let integrity;
534
+ if (stdout.startsWith('[')) {
535
+ let output;
536
+ try {
537
+ output = JSON.parse(stdout);
538
+ }
539
+ catch {
540
+ throw new Error('npm pack did not return valid JSON');
541
+ }
542
+ if (!Array.isArray(output) || output.length !== 1 || typeof output[0] !== 'object' || output[0] === null) {
543
+ throw new Error('npm pack returned an unexpected result');
544
+ }
545
+ const item = output[0];
546
+ filename = item.filename;
547
+ integrity = typeof item.integrity === 'string' ? bounded(item.integrity, 1_024) : undefined;
400
548
  }
401
- if (!Array.isArray(output) || output.length !== 1 || typeof output[0] !== 'object' || output[0] === null) {
402
- throw new Error('npm pack returned an unexpected result');
549
+ else {
550
+ const lines = stdout.split(/\r?\n/);
551
+ if (lines.length !== 1)
552
+ throw new Error('npm pack returned an unexpected result');
553
+ filename = lines[0];
403
554
  }
404
- const item = output[0];
405
- const filename = item.filename;
406
555
  if (typeof filename !== 'string' || filename === '' || filename !== basename(filename) || !filename.endsWith('.tgz')) {
407
556
  throw new Error('npm pack returned an unsafe artifact filename');
408
557
  }
@@ -410,7 +559,10 @@ async function parsePackedArtifact(result, artifactDirectory, expectedName, expe
410
559
  if (!path.startsWith(`${resolve(artifactDirectory)}${sep}`))
411
560
  throw new Error('npm pack artifact escaped its directory');
412
561
  const compressed = await readRegularFileNoFollow(path, MAX_ARTIFACT_BYTES);
413
- const parsed = parseNpmTarball(compressed);
562
+ const parsed = parseNpmTarball(compressed, {
563
+ maxFileBytes: MAX_ARTIFACT_BYTES,
564
+ maxUnpackedBytes: MAX_ARTIFACT_UNPACKED_BYTES,
565
+ });
414
566
  const manifestEntry = parsed.entries.find(entry => entry.path === 'package.json' && entry.type === 'file');
415
567
  if (manifestEntry?.contents === undefined)
416
568
  throw new Error('packed artifact has no package.json');
@@ -455,7 +607,7 @@ async function parsePackedArtifact(result, artifactDirectory, expectedName, expe
455
607
  throw new Error('packed artifact Node engine requirement exceeds 512 characters');
456
608
  }
457
609
  const nodeEngine = rawNodeEngine === undefined || rawNodeEngine === '' ? undefined : bounded(rawNodeEngine, 512);
458
- const integrity = typeof item.integrity === 'string' ? bounded(item.integrity, 1_024) : undefined;
610
+ const peerRequirements = requiredPeerDependencies(manifest, parsed.entries);
459
611
  return {
460
612
  path,
461
613
  filename,
@@ -465,6 +617,7 @@ async function parsePackedArtifact(result, artifactDirectory, expectedName, expe
465
617
  bundlePatch,
466
618
  ...(nodeEngine === undefined ? {} : { nodeEngine }),
467
619
  lifecycleScripts,
620
+ requiredPeerDependencies: peerRequirements,
468
621
  };
469
622
  }
470
623
  async function snapshotTree(root) {
@@ -602,16 +755,472 @@ async function registeredBundle(dshHome, packageName) {
602
755
  return false;
603
756
  }
604
757
  }
758
+ function isNotFound(error) {
759
+ return typeof error === 'object' && error !== null
760
+ && 'code' in error
761
+ && error.code === 'ENOENT';
762
+ }
763
+ /**
764
+ * DSH versions have emitted both a project lockfile and pnpm's virtual-store
765
+ * lockfile. Read only those fixed descendants, refuse every symlink in their
766
+ * path, and never retain the lockfile contents outside the disposable runner.
767
+ */
768
+ async function readProfileLockfile(dshHome) {
769
+ const profileDirectory = join(dshHome, 'profiles', PROFILE);
770
+ try {
771
+ const profileMetadata = await lstat(profileDirectory);
772
+ if (!profileMetadata.isDirectory() || profileMetadata.isSymbolicLink())
773
+ return undefined;
774
+ }
775
+ catch {
776
+ return undefined;
777
+ }
778
+ for (const segments of PROFILE_LOCKFILE_CANDIDATES) {
779
+ let current = profileDirectory;
780
+ try {
781
+ for (let index = 0; index < segments.length; index += 1) {
782
+ const segment = segments[index];
783
+ current = join(current, segment);
784
+ const metadata = await lstat(current);
785
+ if (metadata.isSymbolicLink())
786
+ return undefined;
787
+ if (index < segments.length - 1 && !metadata.isDirectory())
788
+ return undefined;
789
+ }
790
+ return await readRegularFileNoFollow(current, MAX_PROFILE_LOCKFILE_BYTES);
791
+ }
792
+ catch (error) {
793
+ if (isNotFound(error))
794
+ continue;
795
+ return undefined;
796
+ }
797
+ }
798
+ return undefined;
799
+ }
800
+ function profileGraphRoot(manifest) {
801
+ return typeof manifest.name === 'string' && manifest.name.length > 0
802
+ && typeof manifest.version === 'string' && EXACT_VERSION.test(manifest.version)
803
+ ? { name: manifest.name, version: manifest.version }
804
+ : SYNTHETIC_PROFILE_GRAPH_ROOT;
805
+ }
806
+ function graphGaps(graph) {
807
+ if (graph.unresolved === undefined || graph.unresolved.length === 0)
808
+ return undefined;
809
+ return graph.unresolved.slice(0, MAX_PROFILE_GRAPH_GAPS).map(gap => ({
810
+ from: bounded(gap.from, 512),
811
+ name: bounded(gap.name, 214),
812
+ spec: bounded(gap.spec, 512),
813
+ kind: gap.kind,
814
+ }));
815
+ }
816
+ function pluginPeerContractEvidence(contracts) {
817
+ const entries = [...(contracts ?? [])].sort((left, right) => left.name.localeCompare(right.name));
818
+ const issues = entries
819
+ .filter((entry) => entry.status !== 'satisfied')
820
+ .sort((left, right) => left.name.localeCompare(right.name));
821
+ return {
822
+ declared: entries.length,
823
+ satisfied: entries.filter(entry => entry.status === 'satisfied').length,
824
+ mismatched: entries.filter(entry => entry.status === 'mismatched').length,
825
+ indeterminate: entries.filter(entry => entry.status === 'indeterminate').length,
826
+ missing: entries.filter(entry => entry.status === 'missing').length,
827
+ relations: entries.map(entry => ({
828
+ name: bounded(entry.name, 214),
829
+ required: bounded(entry.required, 512),
830
+ status: entry.status,
831
+ staticUsage: entry.staticUsage,
832
+ ...(entry.resolvedVersion === undefined ? {} : { resolvedVersion: bounded(entry.resolvedVersion, 256) }),
833
+ })),
834
+ ...(issues.length === 0 ? {} : {
835
+ issues: issues.slice(0, MAX_PROFILE_GRAPH_GAPS).map(issue => ({
836
+ name: bounded(issue.name, 214),
837
+ required: bounded(issue.required, 512),
838
+ status: issue.status,
839
+ staticUsage: issue.staticUsage,
840
+ ...(issue.resolvedVersion === undefined ? {} : { resolvedVersion: bounded(issue.resolvedVersion, 256) }),
841
+ })),
842
+ }),
843
+ };
844
+ }
845
+ function graphGapPartitions(graph) {
846
+ const required = [];
847
+ const optional = [];
848
+ for (const gap of graph.unresolved ?? []) {
849
+ if (gap.kind === 'optional')
850
+ optional.push(gap);
851
+ else
852
+ required.push(gap);
853
+ }
854
+ return { required, optional };
855
+ }
856
+ function isLexicallyInside(root, candidate) {
857
+ const child = relative(resolve(root), resolve(candidate));
858
+ return child === '' || (!child.startsWith(`..${sep}`) && child !== '..');
859
+ }
860
+ const PROFILE_PEER_RESOLUTION_SCHEMA = 'upstream-radar.profile-peer-resolution/v1alpha1';
861
+ function indeterminatePeerContracts(requirements) {
862
+ return requirements.map(requirement => ({ ...requirement, status: 'indeterminate' }));
863
+ }
864
+ async function peerVersionFromResolvedModule(url, expectedName, sandboxRoot) {
865
+ let resolvedModule;
866
+ try {
867
+ const parsed = new URL(url);
868
+ if (parsed.protocol !== 'file:')
869
+ return undefined;
870
+ resolvedModule = await realpath(fileURLToPath(parsed));
871
+ }
872
+ catch {
873
+ return undefined;
874
+ }
875
+ let sandboxReal;
876
+ try {
877
+ sandboxReal = await realpath(sandboxRoot);
878
+ }
879
+ catch {
880
+ return undefined;
881
+ }
882
+ if (!isLexicallyInside(sandboxReal, resolvedModule))
883
+ return undefined;
884
+ let cursor = dirname(resolvedModule);
885
+ for (let depth = 0; depth < 32 && isLexicallyInside(sandboxReal, cursor); depth += 1) {
886
+ try {
887
+ const manifest = JSON.parse((await readRegularFileNoFollow(join(cursor, 'package.json'), 1 * 1024 * 1024)).toString('utf8'));
888
+ if (typeof manifest === 'object' && manifest !== null && !Array.isArray(manifest)) {
889
+ const item = manifest;
890
+ if (item.name === expectedName && typeof item.version === 'string' && EXACT_VERSION.test(item.version))
891
+ return item.version;
892
+ }
893
+ }
894
+ catch {
895
+ // Continue toward the package root; a module may live in a nested directory.
896
+ }
897
+ const parent = dirname(cursor);
898
+ if (parent === cursor)
899
+ break;
900
+ cursor = parent;
901
+ }
902
+ return undefined;
903
+ }
904
+ async function readProfilePeerContracts(path, requirements, sandboxRoot) {
905
+ if (requirements.length === 0)
906
+ return [];
907
+ let records;
908
+ try {
909
+ const raw = JSON.parse((await readRegularFileNoFollow(path, 64 * 1024)).toString('utf8'));
910
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
911
+ return indeterminatePeerContracts(requirements);
912
+ const item = raw;
913
+ if (item.schema !== PROFILE_PEER_RESOLUTION_SCHEMA || !Array.isArray(item.peers) || item.peers.length !== requirements.length) {
914
+ return indeterminatePeerContracts(requirements);
915
+ }
916
+ records = item.peers.map((value) => {
917
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
918
+ throw new Error('invalid peer resolver record');
919
+ const peer = value;
920
+ if (typeof peer.name !== 'string' || (peer.status !== 'resolved' && peer.status !== 'missing')) {
921
+ throw new Error('invalid peer resolver record');
922
+ }
923
+ if (peer.status === 'resolved' && (typeof peer.url !== 'string' || peer.url.length === 0 || peer.url.length > 4_096)) {
924
+ throw new Error('resolved peer has no bounded URL');
925
+ }
926
+ return {
927
+ name: peer.name,
928
+ status: peer.status,
929
+ ...(typeof peer.url === 'string' ? { url: peer.url } : {}),
930
+ };
931
+ });
932
+ }
933
+ catch {
934
+ return indeterminatePeerContracts(requirements);
935
+ }
936
+ const byName = new Map(records.map(record => [record.name, record]));
937
+ if (byName.size !== requirements.length || requirements.some(requirement => !byName.has(requirement.name))) {
938
+ return indeterminatePeerContracts(requirements);
939
+ }
940
+ const contracts = [];
941
+ for (const requirement of requirements) {
942
+ const record = byName.get(requirement.name);
943
+ if (record === undefined || record.status === 'missing') {
944
+ contracts.push({ ...requirement, status: 'missing' });
945
+ continue;
946
+ }
947
+ const resolvedVersion = record.url === undefined
948
+ ? undefined
949
+ : await peerVersionFromResolvedModule(record.url, requirement.name, sandboxRoot);
950
+ if (resolvedVersion === undefined) {
951
+ contracts.push({ ...requirement, status: 'indeterminate' });
952
+ continue;
953
+ }
954
+ const evaluation = satisfiesSemverRange(resolvedVersion, requirement.required);
955
+ contracts.push({
956
+ ...requirement,
957
+ status: evaluation === true ? 'satisfied' : evaluation === false ? 'mismatched' : 'indeterminate',
958
+ resolvedVersion,
959
+ });
960
+ }
961
+ return contracts;
962
+ }
963
+ /**
964
+ * Resolve direct peer contracts from the profile anchor, then boot the actual
965
+ * composed DSH profile. A DSH bundle does not have to export its package root:
966
+ * its patch may load one or more package subpaths (or a command adapter).
967
+ * Importing the root here would therefore test a contract DSH never declared.
968
+ */
969
+ async function writeProfileLoadProbe(dshHome, dshVersion, pnpmCommand, peerRequirements) {
970
+ const profileDirectory = join(dshHome, 'profiles', PROFILE);
971
+ const profileMetadata = await lstat(profileDirectory);
972
+ if (!profileMetadata.isDirectory() || profileMetadata.isSymbolicLink()) {
973
+ throw new Error('the DSH profile directory is not a regular directory for the load probe');
974
+ }
975
+ const [homeReal, profileReal] = await Promise.all([realpath(dshHome), realpath(profileDirectory)]);
976
+ if (!isLexicallyInside(homeReal, profileReal)) {
977
+ throw new Error('the DSH profile directory escaped the controlled DSH home');
978
+ }
979
+ const probePath = join(profileDirectory, '.upstream-radar-load-probe.mjs');
980
+ const peerResolutionPath = join(profileDirectory, '.upstream-radar-peer-resolution.json');
981
+ const dshBootArgs = dshArgs(dshVersion, ['--profile', PROFILE, '--help']);
982
+ const contents = [
983
+ "import { spawn } from 'node:child_process'",
984
+ "import { writeFile } from 'node:fs/promises'",
985
+ `const peerRequirements = ${JSON.stringify(peerRequirements)}`,
986
+ 'const peers = []',
987
+ 'for (const peer of peerRequirements) {',
988
+ ' try { peers.push({ name: peer.name, status: \'resolved\', url: import.meta.resolve(peer.name) }) }',
989
+ ' catch { peers.push({ name: peer.name, status: \'missing\' }) }',
990
+ '}',
991
+ `await writeFile(${JSON.stringify(peerResolutionPath)}, JSON.stringify({ schema: ${JSON.stringify(PROFILE_PEER_RESOLUTION_SCHEMA)}, peers }), { encoding: 'utf8', mode: 0o600, flag: 'wx' })`,
992
+ `const child = spawn(${JSON.stringify(pnpmCommand)}, ${JSON.stringify(dshBootArgs)}, { cwd: process.cwd(), env: process.env, stdio: 'inherit' })`,
993
+ "const exitCode = await new Promise(resolve => {",
994
+ " child.once('error', error => { console.error(error.message); resolve(1) })",
995
+ " child.once('close', code => resolve(code ?? 1))",
996
+ '})',
997
+ 'process.exitCode = exitCode',
998
+ '',
999
+ ].join('\n');
1000
+ // Never overwrite a path a target package may have planted in the profile.
1001
+ await writeFile(probePath, contents, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
1002
+ return { path: probePath, peerResolutionPath, profileDirectory };
1003
+ }
1004
+ /**
1005
+ * `pnpm dlx` owns the DSH host dependency plane, rather than a plugin-created
1006
+ * symlink in the profile. Discover the exact cached DSH package by manifest,
1007
+ * with bounded directory traversal and no symlink following.
1008
+ */
1009
+ async function discoverExactDshRuntime(cacheHome, dshVersion) {
1010
+ const root = resolve(cacheHome, 'pnpm', 'dlx');
1011
+ const rootReal = await realpath(root);
1012
+ const queue = [{ path: root, depth: 0 }];
1013
+ const candidates = new Set();
1014
+ let visited = 0;
1015
+ while (queue.length > 0) {
1016
+ const current = queue.shift();
1017
+ if (current === undefined)
1018
+ break;
1019
+ if (visited >= MAX_RUNTIME_DISCOVERY_DIRECTORIES) {
1020
+ throw new Error(`DSH runtime discovery exceeds ${MAX_RUNTIME_DISCOVERY_DIRECTORIES} directories`);
1021
+ }
1022
+ visited += 1;
1023
+ let entries;
1024
+ try {
1025
+ entries = await readdir(current.path, { withFileTypes: true });
1026
+ }
1027
+ catch (error) {
1028
+ if (isNotFound(error))
1029
+ continue;
1030
+ throw error;
1031
+ }
1032
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
1033
+ const child = join(current.path, entry.name);
1034
+ if (entry.isDirectory()) {
1035
+ if (current.depth < MAX_RUNTIME_DISCOVERY_DEPTH)
1036
+ queue.push({ path: child, depth: current.depth + 1 });
1037
+ continue;
1038
+ }
1039
+ if (!entry.isFile() || entry.name !== 'package.json')
1040
+ continue;
1041
+ let manifest;
1042
+ try {
1043
+ manifest = JSON.parse((await readRegularFileNoFollow(child, 1 * 1024 * 1024)).toString('utf8'));
1044
+ }
1045
+ catch {
1046
+ continue;
1047
+ }
1048
+ if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest))
1049
+ continue;
1050
+ const item = manifest;
1051
+ if (item.name === '@deepseek-ai/dsh' && item.version === dshVersion)
1052
+ candidates.add(child);
1053
+ }
1054
+ }
1055
+ if (candidates.size === 0)
1056
+ throw new Error(`exact @deepseek-ai/dsh@${dshVersion} was not found in the controlled pnpm dlx cache`);
1057
+ if (candidates.size > 1)
1058
+ throw new Error(`multiple exact @deepseek-ai/dsh@${dshVersion} packages were found in the controlled pnpm dlx cache`);
1059
+ const manifestPath = [...candidates][0];
1060
+ const packageDirectory = discoverDshRuntimePackageDirectory(manifestPath);
1061
+ const packageCoordinate = discoverDshRuntimePackage(manifestPath);
1062
+ const nodeModulesDirectory = discoverDshRuntimeHostNodeModulesDirectory(manifestPath);
1063
+ if (packageDirectory === undefined || packageCoordinate === undefined || nodeModulesDirectory === undefined) {
1064
+ throw new Error('the exact DSH package did not expose a usable dependency plane');
1065
+ }
1066
+ if (!isLexicallyInside(rootReal, packageDirectory) || !isLexicallyInside(rootReal, nodeModulesDirectory)) {
1067
+ throw new Error('the exact DSH dependency plane escaped the controlled pnpm dlx cache');
1068
+ }
1069
+ if (packageCoordinate.name !== '@deepseek-ai/dsh' || packageCoordinate.version !== dshVersion) {
1070
+ throw new Error('the discovered DSH package does not match the requested exact version');
1071
+ }
1072
+ return {
1073
+ packageDirectory,
1074
+ nodeModulesDirectory,
1075
+ package: { ecosystem: 'npm', name: '@deepseek-ai/dsh', version: packageCoordinate.version },
1076
+ };
1077
+ }
1078
+ async function profileResolutionEvidence(dshHome) {
1079
+ const profileDirectory = join(dshHome, 'profiles', PROFILE);
1080
+ const lockfile = await readProfileLockfile(dshHome);
1081
+ if (lockfile === undefined)
1082
+ return {};
1083
+ const profileLockfile = {
1084
+ sha256: createHash('sha256').update(lockfile).digest('hex'),
1085
+ bytes: lockfile.length,
1086
+ };
1087
+ try {
1088
+ const manifestBuffer = await readRegularFileNoFollow(join(profileDirectory, 'package.json'), 4 * 1024 * 1024);
1089
+ const manifest = JSON.parse(manifestBuffer.toString('utf8'));
1090
+ const graph = parsePnpmLockGraph(lockfile.toString('utf8'), profileGraphRoot(manifest));
1091
+ const gaps = graphGaps(graph);
1092
+ return {
1093
+ profileLockfile: {
1094
+ ...profileLockfile,
1095
+ ...(graph.digest === undefined ? {} : { graphDigest: graph.digest }),
1096
+ nodes: graph.nodes.length,
1097
+ edges: graph.edges.length,
1098
+ unresolved: graph.unresolved?.length ?? 0,
1099
+ ...(gaps === undefined ? {} : { unresolvedDependencies: gaps }),
1100
+ },
1101
+ };
1102
+ }
1103
+ catch {
1104
+ return { profileLockfile };
1105
+ }
1106
+ }
1107
+ async function runtimeGraphEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts) {
1108
+ try {
1109
+ const dshRuntime = await discoverExactDshRuntime(cacheHome, dshVersion);
1110
+ const graph = await parseInstalledNodeModulesGraph(join(dshHome, 'profiles', PROFILE), rootPackage, {
1111
+ hostNodeModulesDirectory: dshRuntime.nodeModulesDirectory,
1112
+ hostRuntimeSource: 'dsh-process',
1113
+ hostRuntimePackage: dshRuntime.package,
1114
+ hostRuntimePackageDirectory: dshRuntime.packageDirectory,
1115
+ });
1116
+ if (graph.digest === undefined)
1117
+ return {};
1118
+ const gaps = graphGapPartitions(graph);
1119
+ const contracts = profileResolvedPeerContracts ?? graph.rootPeerContracts?.map(contract => ({
1120
+ ...contract,
1121
+ staticUsage: 'scan-incomplete',
1122
+ }));
1123
+ const concretelyResolvedRootPeers = new Set((contracts ?? [])
1124
+ .filter(contract => contract.status === 'satisfied' || contract.status === 'mismatched')
1125
+ .map(contract => contract.name));
1126
+ const effectiveRequiredGaps = gaps.required.filter(gap => !(gap.from === graph.rootNodeId
1127
+ && gap.kind === 'peer'
1128
+ && concretelyResolvedRootPeers.has(gap.name)));
1129
+ const requiredGaps = graphGaps({ unresolved: effectiveRequiredGaps });
1130
+ const optionalGaps = graphGaps({ unresolved: gaps.optional });
1131
+ return {
1132
+ runtimeGraph: {
1133
+ digest: graph.digest,
1134
+ nodes: graph.nodes.length,
1135
+ edges: graph.edges.length,
1136
+ unresolved: effectiveRequiredGaps.length,
1137
+ ...(requiredGaps === undefined ? {} : { unresolvedDependencies: requiredGaps }),
1138
+ ...(gaps.optional.length === 0 ? {} : {
1139
+ optionalUnavailable: gaps.optional.length,
1140
+ ...(optionalGaps === undefined ? {} : { optionalUnavailableDependencies: optionalGaps }),
1141
+ }),
1142
+ pluginPeerContracts: pluginPeerContractEvidence(contracts),
1143
+ ...(graph.hostRuntime === undefined ? {} : {
1144
+ hostRuntime: {
1145
+ source: graph.hostRuntime.source,
1146
+ resolvedNodes: graph.hostRuntime.resolvedNodes,
1147
+ ...(graph.hostRuntime.package === undefined ? {} : { dshVersion: graph.hostRuntime.package.version }),
1148
+ },
1149
+ }),
1150
+ },
1151
+ };
1152
+ }
1153
+ catch (error) {
1154
+ return {
1155
+ runtimeGraphError: bounded(error instanceof Error ? error.message : String(error), 512),
1156
+ };
1157
+ }
1158
+ }
1159
+ async function resolutionEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts) {
1160
+ const [profile, runtime] = await Promise.all([
1161
+ profileResolutionEvidence(dshHome),
1162
+ runtimeGraphEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts),
1163
+ ]);
1164
+ return { ...profile, ...runtime };
1165
+ }
605
1166
  function finishReport(report, result, reason) {
606
1167
  report.completedAt = new Date().toISOString();
607
1168
  report.result = result;
608
1169
  report.reason = reason;
609
1170
  return report;
610
1171
  }
1172
+ /**
1173
+ * A load success is necessary but not enough for the compatibility claim. The
1174
+ * final verdict also requires a complete required-edge graph and a direct
1175
+ * plugin peer contract that the observer could actually evaluate.
1176
+ */
1177
+ function finalCompatibilityConclusion(resolution) {
1178
+ const graph = resolution.runtimeGraph;
1179
+ if (graph === undefined) {
1180
+ return {
1181
+ result: 'unknown',
1182
+ reason: resolution.runtimeGraphError === undefined
1183
+ ? 'the exact artifact installed and loaded, but the effective DSH runtime graph was not established'
1184
+ : `the exact artifact installed and loaded, but the effective DSH runtime graph could not be established: ${resolution.runtimeGraphError}`,
1185
+ };
1186
+ }
1187
+ const contracts = graph.pluginPeerContracts;
1188
+ const firstIssue = contracts.issues?.[0];
1189
+ if (contracts.mismatched > 0 || contracts.missing > 0) {
1190
+ const detail = firstIssue === undefined
1191
+ ? `${contracts.mismatched + contracts.missing} required plugin peer contract(s) do not match the DSH runtime`
1192
+ : firstIssue.status === 'missing'
1193
+ ? `${firstIssue.name}@${firstIssue.required} was not resolved by the DSH runtime (${firstIssue.staticUsage})`
1194
+ : `${firstIssue.name}@${firstIssue.resolvedVersion ?? 'unknown'} does not satisfy ${firstIssue.required} (${firstIssue.staticUsage})`;
1195
+ return {
1196
+ result: 'peer-contract-incompatible',
1197
+ reason: `the exact artifact installed and loaded, but ${detail}`,
1198
+ };
1199
+ }
1200
+ if (graph.unresolved > 0) {
1201
+ return {
1202
+ result: 'unknown',
1203
+ reason: `the exact artifact installed and loaded, but the effective DSH runtime graph has ${graph.unresolved} required unresolved edge(s)`,
1204
+ };
1205
+ }
1206
+ if (contracts.indeterminate > 0) {
1207
+ return {
1208
+ result: 'unknown',
1209
+ reason: `the exact artifact installed and loaded, but ${contracts.indeterminate} required plugin peer range(s) could not be evaluated safely`,
1210
+ };
1211
+ }
1212
+ return {
1213
+ result: 'compatible',
1214
+ reason: 'the exact artifact installed, registered, loaded, and satisfied its direct peer contracts under the requested DSH version',
1215
+ };
1216
+ }
611
1217
  export async function observeDshPluginInstall(options) {
612
1218
  const spec = parseNpmSpec(options.packageSpec);
613
1219
  if (!EXACT_VERSION.test(options.dshVersion))
614
1220
  throw new Error('DSH version must be an exact semantic version');
1221
+ if (options.caseId !== undefined && !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(options.caseId)) {
1222
+ throw new Error('DSH install observation caseId must be a short lowercase label');
1223
+ }
615
1224
  const allowedBuilds = normalizeAllowedBuilds(options.allowedBuilds);
616
1225
  if (!options.allowExecution)
617
1226
  throw new Error('DSH install observation requires explicit execution consent');
@@ -637,6 +1246,7 @@ export async function observeDshPluginInstall(options) {
637
1246
  scope: 'install-and-load-behavior',
638
1247
  startedAt,
639
1248
  completedAt: startedAt,
1249
+ ...(options.caseId === undefined ? {} : { caseId: options.caseId }),
640
1250
  dshVersion: options.dshVersion,
641
1251
  runtime: {
642
1252
  platform: process.platform,
@@ -660,6 +1270,7 @@ export async function observeDshPluginInstall(options) {
660
1270
  },
661
1271
  observations: { install: emptyTraceObservation(), load: emptyTraceObservation() },
662
1272
  filesystem: { install: emptyFilesystemDiff(), load: emptyFilesystemDiff() },
1273
+ resolution: {},
663
1274
  result: 'unknown',
664
1275
  reason: 'observation did not complete',
665
1276
  boundary: {
@@ -670,6 +1281,7 @@ export async function observeDshPluginInstall(options) {
670
1281
  pluginCodeMayExecuteDuringLoad: true,
671
1282
  inheritedHostSecrets: false,
672
1283
  approvedDependencyBuilds: allowedBuilds,
1284
+ requiredDependencyBuilds: [],
673
1285
  note: 'Radar scrubs the child environment and records Linux system-call evidence, but the caller provides and must verify the disposable isolation boundary. Same-container traces are best-effort evidence, not a malicious-code safety certificate.',
674
1286
  },
675
1287
  };
@@ -719,7 +1331,7 @@ export async function observeDshPluginInstall(options) {
719
1331
  const artifactResult = await runSafely(runner, {
720
1332
  phase: 'artifact',
721
1333
  command: npmCommand,
722
- args: ['pack', `${spec.name}@${spec.version}`, '--ignore-scripts', '--json', '--pack-destination', '.'],
1334
+ args: ['pack', `${spec.name}@${spec.version}`, '--ignore-scripts', '--pack-destination', '.', '--silent'],
723
1335
  cwd: artifactDirectory,
724
1336
  env: noScriptsEnvironment,
725
1337
  timeoutMs,
@@ -788,6 +1400,7 @@ export async function observeDshPluginInstall(options) {
788
1400
  const afterInstall = await snapshotSandbox(sandboxRoot, environment);
789
1401
  report.observations.install = await readTrace(installTracePath, sandboxRoot);
790
1402
  report.filesystem.install = diffSnapshots(beforeInstall, afterInstall);
1403
+ report.resolution = await resolutionEvidence(environment.DSH_HOME, spec, options.dshVersion, environment.XDG_CACHE_HOME);
791
1404
  report.stages.install = commandStage(installResult);
792
1405
  if (installResult.timedOut || installResult.outputExceeded || installResult.launchError !== undefined) {
793
1406
  return finishReport(report, 'unknown', 'the plugin install did not produce a bounded command result');
@@ -801,6 +1414,11 @@ export async function observeDshPluginInstall(options) {
801
1414
  return finishReport(report, 'unknown', 'the install command ran without readable trace evidence');
802
1415
  }
803
1416
  if (installResult.code !== 0) {
1417
+ const requiredBuilds = requiredDependencyBuilds(`${installResult.stderr}\n${installResult.stdout}`, spec.name);
1418
+ if (requiredBuilds.length > 0) {
1419
+ report.boundary.requiredDependencyBuilds = requiredBuilds;
1420
+ return finishReport(report, 'build-approval-required', `the DSH plugin install requires explicit approval for dependency builds: ${requiredBuilds.join(', ')}`);
1421
+ }
804
1422
  return finishReport(report, 'install-failed', 'the traced DSH plugin install command failed');
805
1423
  }
806
1424
  const registered = await registeredBundle(environment.DSH_HOME, spec.name);
@@ -809,12 +1427,13 @@ export async function observeDshPluginInstall(options) {
809
1427
  : { status: 'failed', detail: `the DSH profile did not register ${spec.name}` };
810
1428
  if (!registered)
811
1429
  return finishReport(report, 'install-failed', 'DSH accepted the install command but did not register the plugin bundle');
1430
+ const loadProbe = await writeProfileLoadProbe(environment.DSH_HOME, options.dshVersion, pnpmCommand, artifact.requiredPeerDependencies);
812
1431
  const loadTracePath = join(traceDirectory, 'load.strace');
813
1432
  const loadResult = await runSafely(runner, {
814
1433
  phase: 'load',
815
- command: pnpmCommand,
816
- args: dshArgs(options.dshVersion, ['--profile', PROFILE, '--dump-config']),
817
- cwd: artifactDirectory,
1434
+ command: process.execPath,
1435
+ args: [loadProbe.path],
1436
+ cwd: loadProbe.profileDirectory,
818
1437
  env: scriptsEnvironment,
819
1438
  timeoutMs,
820
1439
  sandboxRoot,
@@ -823,6 +1442,10 @@ export async function observeDshPluginInstall(options) {
823
1442
  const afterLoad = await snapshotSandbox(sandboxRoot, environment);
824
1443
  report.observations.load = await readTrace(loadTracePath, sandboxRoot);
825
1444
  report.filesystem.load = diffSnapshots(afterInstall, afterLoad);
1445
+ const profilePeerContracts = await readProfilePeerContracts(loadProbe.peerResolutionPath, artifact.requiredPeerDependencies, sandboxRoot);
1446
+ // Loading may trigger one final DSH profile reconciliation. Preserve the
1447
+ // final resolved graph rather than only the state immediately after add.
1448
+ report.resolution = await resolutionEvidence(environment.DSH_HOME, spec, options.dshVersion, environment.XDG_CACHE_HOME, profilePeerContracts);
826
1449
  report.stages.load = commandStage(loadResult);
827
1450
  if (loadResult.timedOut || loadResult.outputExceeded || loadResult.launchError !== undefined) {
828
1451
  return finishReport(report, 'unknown', 'the plugin load did not produce a bounded command result');
@@ -833,7 +1456,8 @@ export async function observeDshPluginInstall(options) {
833
1456
  }
834
1457
  if (loadResult.code !== 0)
835
1458
  return finishReport(report, 'load-failed', 'the traced DSH profile load command failed');
836
- return finishReport(report, 'compatible', 'the exact artifact installed, registered and loaded under the requested DSH version');
1459
+ const conclusion = finalCompatibilityConclusion(report.resolution);
1460
+ return finishReport(report, conclusion.result, conclusion.reason);
837
1461
  }
838
1462
  catch (error) {
839
1463
  const detail = bounded(error instanceof Error ? error.message : String(error));
@@ -854,23 +1478,38 @@ export function renderDshInstallObservation(report) {
854
1478
  const lifecycle = report.artifact.lifecycleScripts.length === 0 ? 'none' : report.artifact.lifecycleScripts.join(', ');
855
1479
  const lines = [
856
1480
  'DSH isolated install observation',
1481
+ ...(report.caseId === undefined ? [] : [`Case: ${report.caseId}`]),
857
1482
  `Artifact: ${report.artifact.name}@${report.artifact.version}${report.artifact.sha256 === undefined ? '' : ` (sha256:${report.artifact.sha256.slice(0, 12)}…)`}`,
858
1483
  `DSH: ${report.dshVersion}`,
859
1484
  `Runtime: Node ${report.runtime.nodeVersion} (${report.runtime.platform}/${report.runtime.architecture}), pnpm ${report.runtime.packageManager.version ?? 'unknown'}`,
860
1485
  `Plugin Node requirement: ${report.artifact.nodeEngine ?? 'not declared'}`,
861
1486
  `Isolation claim: ${report.boundary.isolationProviderClaim} (provided externally; not verified by Radar)`,
862
1487
  `Approved dependency builds: ${report.boundary.approvedDependencyBuilds.length === 0 ? 'none' : report.boundary.approvedDependencyBuilds.join(', ')}`,
1488
+ `Additional dependency builds required: ${report.boundary.requiredDependencyBuilds.length === 0 ? 'none' : report.boundary.requiredDependencyBuilds.join(', ')}`,
863
1489
  '',
864
1490
  `Result: ${report.result.toUpperCase()} — ${report.reason}`,
865
1491
  `Lifecycle scripts declared: ${lifecycle}`,
866
1492
  `Install evidence: ${install.processes.length} process, ${install.network.length} network, ${install.fileWrites.length} file-write event(s); trace ${install.coverage.status}`,
867
1493
  `Load evidence: ${load.processes.length} process, ${load.network.length} network, ${load.fileWrites.length} file-write event(s); trace ${load.coverage.status}`,
868
1494
  `Final filesystem delta: install +${report.filesystem.install.totals.created} ~${report.filesystem.install.totals.modified} -${report.filesystem.install.totals.deleted}; load +${report.filesystem.load.totals.created} ~${report.filesystem.load.totals.modified} -${report.filesystem.load.totals.deleted}`,
1495
+ `Resolved profile graph: ${report.resolution.profileLockfile?.graphDigest ?? 'not established'}${report.resolution.profileLockfile === undefined ? '' : ` (lock sha256:${report.resolution.profileLockfile.sha256.slice(0, 12)}…)`}`,
1496
+ `Effective DSH runtime graph: ${report.resolution.runtimeGraph?.digest ?? 'not established'}${report.resolution.runtimeGraph === undefined ? '' : ` (${report.resolution.runtimeGraph.nodes} nodes, ${report.resolution.runtimeGraph.edges} edges, ${report.resolution.runtimeGraph.unresolved} required unresolved${report.resolution.runtimeGraph.optionalUnavailable === undefined ? '' : `, ${report.resolution.runtimeGraph.optionalUnavailable} optional unavailable`})`}`,
1497
+ `Plugin peer contracts: ${report.resolution.runtimeGraph === undefined
1498
+ ? 'not established'
1499
+ : `${report.resolution.runtimeGraph.pluginPeerContracts.satisfied}/${report.resolution.runtimeGraph.pluginPeerContracts.declared} satisfied; ${report.resolution.runtimeGraph.pluginPeerContracts.mismatched} mismatched, ${report.resolution.runtimeGraph.pluginPeerContracts.missing} missing, ${report.resolution.runtimeGraph.pluginPeerContracts.indeterminate} indeterminate`}`,
1500
+ `Effective graph collector: ${report.resolution.runtimeGraphError ?? 'captured'}`,
869
1501
  '',
870
1502
  ];
871
1503
  for (const [name, stage] of Object.entries(report.stages)) {
872
1504
  lines.push(` ${name}: ${stage.status}${stage.detail === undefined ? '' : ` (${stage.detail})`}`);
873
1505
  }
1506
+ const peerIssues = report.resolution.runtimeGraph?.pluginPeerContracts.issues ?? [];
1507
+ if (peerIssues.length > 0) {
1508
+ lines.push('', 'Direct peer-contract findings:');
1509
+ for (const issue of peerIssues) {
1510
+ lines.push(` ${issue.name}: ${issue.status}; requires ${issue.required}${issue.resolvedVersion === undefined ? '' : `, resolved ${issue.resolvedVersion}`}; static use ${issue.staticUsage}`);
1511
+ }
1512
+ }
874
1513
  lines.push('', report.boundary.note);
875
1514
  return `${lines.join('\n')}\n`;
876
1515
  }