upstream-radar 0.40.0 → 0.41.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.
- package/README.md +123 -225
- package/dist/src/cli.js +11 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/dsh-compatibility-ir.d.ts +98 -0
- package/dist/src/dsh-compatibility-ir.d.ts.map +1 -0
- package/dist/src/dsh-compatibility-ir.js +389 -0
- package/dist/src/dsh-compatibility-ir.js.map +1 -0
- package/dist/src/dsh-compatibility-issues.d.ts +44 -0
- package/dist/src/dsh-compatibility-issues.d.ts.map +1 -0
- package/dist/src/dsh-compatibility-issues.js +185 -0
- package/dist/src/dsh-compatibility-issues.js.map +1 -0
- package/dist/src/dsh-compatibility-ledger.d.ts +150 -0
- package/dist/src/dsh-compatibility-ledger.d.ts.map +1 -0
- package/dist/src/dsh-compatibility-ledger.js +645 -0
- package/dist/src/dsh-compatibility-ledger.js.map +1 -0
- package/dist/src/dsh-install-observation.d.ts +88 -1
- package/dist/src/dsh-install-observation.d.ts.map +1 -1
- package/dist/src/dsh-install-observation.js +579 -6
- package/dist/src/dsh-install-observation.js.map +1 -1
- package/dist/src/dsh-install-plan.d.ts +16 -6
- package/dist/src/dsh-install-plan.d.ts.map +1 -1
- package/dist/src/dsh-install-plan.js +272 -46
- package/dist/src/dsh-install-plan.js.map +1 -1
- package/dist/src/dsh-plugin.d.ts.map +1 -1
- package/dist/src/dsh-plugin.js +6 -2
- package/dist/src/dsh-plugin.js.map +1 -1
- package/dist/src/dsh-runtime.d.ts +6 -0
- package/dist/src/dsh-runtime.d.ts.map +1 -1
- package/dist/src/dsh-runtime.js +38 -0
- package/dist/src/dsh-runtime.js.map +1 -1
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/installed-graph.d.ts.map +1 -1
- package/dist/src/installed-graph.js +105 -26
- package/dist/src/installed-graph.js.map +1 -1
- package/dist/src/radar-types.d.ts +15 -0
- package/dist/src/radar-types.d.ts.map +1 -1
- package/dist/src/semver.d.ts.map +1 -1
- package/dist/src/semver.js +7 -1
- package/dist/src/semver.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/docs/README.zh-CN.md +25 -23
- package/package.json +1 -1
- package/schemas/dsh-compatibility-ir.schema.json +89 -0
- package/schemas/dsh-install-observation.schema.json +135 -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';
|
|
@@ -20,8 +24,19 @@ const MAX_TRACE_EVENTS = 512;
|
|
|
20
24
|
const MAX_SNAPSHOT_ENTRIES = 25_000;
|
|
21
25
|
const MAX_DIFF_PATHS = 512;
|
|
22
26
|
const MAX_ALLOWED_BUILDS = 16;
|
|
27
|
+
const MAX_PROFILE_LOCKFILE_BYTES = 16 * 1024 * 1024;
|
|
28
|
+
const MAX_PROFILE_GRAPH_GAPS = 32;
|
|
29
|
+
const MAX_PLUGIN_PEERS = 64;
|
|
30
|
+
const MAX_STATIC_PEER_SCAN_BYTES = 8 * 1024 * 1024;
|
|
31
|
+
const MAX_RUNTIME_DISCOVERY_DIRECTORIES = 12_000;
|
|
32
|
+
const MAX_RUNTIME_DISCOVERY_DEPTH = 16;
|
|
23
33
|
const PROFILE = 'headless';
|
|
24
34
|
const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'];
|
|
35
|
+
const PROFILE_LOCKFILE_CANDIDATES = [
|
|
36
|
+
['pnpm-lock.yaml'],
|
|
37
|
+
['node_modules', '.pnpm', 'lock.yaml'],
|
|
38
|
+
];
|
|
39
|
+
const SYNTHETIC_PROFILE_GRAPH_ROOT = { name: 'dsh-profile-headless', version: '0.0.0' };
|
|
25
40
|
function normalizeAllowedBuilds(values) {
|
|
26
41
|
if (values === undefined)
|
|
27
42
|
return [];
|
|
@@ -37,6 +52,82 @@ function normalizeAllowedBuilds(values) {
|
|
|
37
52
|
}
|
|
38
53
|
return [...names].sort();
|
|
39
54
|
}
|
|
55
|
+
function isNpmPackageName(value) {
|
|
56
|
+
return /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/.test(value);
|
|
57
|
+
}
|
|
58
|
+
function staticPeerUsage(entries, requirements) {
|
|
59
|
+
const state = new Map(requirements.map(requirement => [requirement.name, 'no-literal-reference-observed']));
|
|
60
|
+
let scanned = 0;
|
|
61
|
+
let incomplete = false;
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (entry.type !== 'file' || entry.contents === undefined
|
|
64
|
+
|| !/\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(entry.path))
|
|
65
|
+
continue;
|
|
66
|
+
if (scanned + entry.contents.length > MAX_STATIC_PEER_SCAN_BYTES) {
|
|
67
|
+
incomplete = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
scanned += entry.contents.length;
|
|
71
|
+
const text = entry.contents.toString('utf8');
|
|
72
|
+
const declarationFile = /\.d\.(?:[cm]?ts)$/i.test(entry.path);
|
|
73
|
+
for (const requirement of requirements) {
|
|
74
|
+
if (state.get(requirement.name) === 'runtime-import-observed')
|
|
75
|
+
continue;
|
|
76
|
+
const escaped = requirement.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
77
|
+
const literal = `['"]${escaped}(?:/[^'"]*)?['"]`;
|
|
78
|
+
// Stay within one statement line. A wider expression can start at an
|
|
79
|
+
// unrelated runtime import and accidentally consume a later `import
|
|
80
|
+
// type`, which would turn declaration-only evidence into a false runtime
|
|
81
|
+
// claim. Missing a heavily formatted import is safer than that claim.
|
|
82
|
+
const typeOnly = new RegExp(`(?:^|[;\\n])[\\t ]*import[\\t ]+type\\b[^;\\n]{0,1024}?\\bfrom[\\t ]*${literal}`);
|
|
83
|
+
const runtime = new RegExp([
|
|
84
|
+
`(?:^|[;\\n])[\\t ]*import[\\t ]+(?!type\\b)(?:[^;\\n]{0,1024}?[\\t ]+from[\\t ]+)?${literal}`,
|
|
85
|
+
`(?:^|[;\\n])[\\t ]*export[\\t ]+(?!type\\b)[^;\\n]{0,1024}?[\\t ]+from[\\t ]+${literal}`,
|
|
86
|
+
`\\b(?:require|import)\\s*\\(\\s*${literal}`,
|
|
87
|
+
].join('|'));
|
|
88
|
+
const literalReference = new RegExp(literal);
|
|
89
|
+
if (!declarationFile && runtime.test(text)) {
|
|
90
|
+
state.set(requirement.name, 'runtime-import-observed');
|
|
91
|
+
}
|
|
92
|
+
else if (declarationFile ? literalReference.test(text) : typeOnly.test(text)) {
|
|
93
|
+
state.set(requirement.name, 'type-only-reference-observed');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return requirements.map(requirement => {
|
|
98
|
+
const observed = state.get(requirement.name) ?? 'no-literal-reference-observed';
|
|
99
|
+
return observed === 'no-literal-reference-observed' && incomplete ? 'scan-incomplete' : observed;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function requiredPeerDependencies(manifest, entries) {
|
|
103
|
+
const peers = typeof manifest.peerDependencies === 'object' && manifest.peerDependencies !== null && !Array.isArray(manifest.peerDependencies)
|
|
104
|
+
? manifest.peerDependencies
|
|
105
|
+
: {};
|
|
106
|
+
const metadata = typeof manifest.peerDependenciesMeta === 'object' && manifest.peerDependenciesMeta !== null && !Array.isArray(manifest.peerDependenciesMeta)
|
|
107
|
+
? manifest.peerDependenciesMeta
|
|
108
|
+
: {};
|
|
109
|
+
const requirements = [];
|
|
110
|
+
for (const [name, rawRange] of Object.entries(peers).sort(([left], [right]) => left.localeCompare(right))) {
|
|
111
|
+
if (!isNpmPackageName(name))
|
|
112
|
+
throw new Error(`packed artifact has an invalid peer dependency name: ${name}`);
|
|
113
|
+
if (typeof rawRange !== 'string' || rawRange.trim() === '' || rawRange.length > 512) {
|
|
114
|
+
throw new Error(`packed artifact has an invalid peer dependency range for ${name}`);
|
|
115
|
+
}
|
|
116
|
+
const peerMetadata = metadata[name];
|
|
117
|
+
const optional = typeof peerMetadata === 'object' && peerMetadata !== null && !Array.isArray(peerMetadata)
|
|
118
|
+
&& peerMetadata.optional === true;
|
|
119
|
+
if (!optional)
|
|
120
|
+
requirements.push({ name, required: bounded(rawRange.trim(), 512) });
|
|
121
|
+
}
|
|
122
|
+
if (requirements.length > MAX_PLUGIN_PEERS) {
|
|
123
|
+
throw new Error(`packed artifact declares more than ${MAX_PLUGIN_PEERS} required peer dependencies`);
|
|
124
|
+
}
|
|
125
|
+
const usages = staticPeerUsage(entries, requirements);
|
|
126
|
+
return requirements.map((requirement, index) => ({
|
|
127
|
+
...requirement,
|
|
128
|
+
staticUsage: usages[index] ?? 'scan-incomplete',
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
40
131
|
function bounded(value, maximum = MAX_REPORT_DETAIL_BYTES) {
|
|
41
132
|
const normalized = value.replace(/[\u0000-\u001f\u007f-\u009f]/g, character => (`\\u${character.codePointAt(0)?.toString(16).padStart(4, '0') ?? '0000'}`));
|
|
42
133
|
return normalized.length <= maximum ? normalized : `${normalized.slice(0, maximum - 1)}…`;
|
|
@@ -456,6 +547,7 @@ async function parsePackedArtifact(result, artifactDirectory, expectedName, expe
|
|
|
456
547
|
}
|
|
457
548
|
const nodeEngine = rawNodeEngine === undefined || rawNodeEngine === '' ? undefined : bounded(rawNodeEngine, 512);
|
|
458
549
|
const integrity = typeof item.integrity === 'string' ? bounded(item.integrity, 1_024) : undefined;
|
|
550
|
+
const peerRequirements = requiredPeerDependencies(manifest, parsed.entries);
|
|
459
551
|
return {
|
|
460
552
|
path,
|
|
461
553
|
filename,
|
|
@@ -465,6 +557,7 @@ async function parsePackedArtifact(result, artifactDirectory, expectedName, expe
|
|
|
465
557
|
bundlePatch,
|
|
466
558
|
...(nodeEngine === undefined ? {} : { nodeEngine }),
|
|
467
559
|
lifecycleScripts,
|
|
560
|
+
requiredPeerDependencies: peerRequirements,
|
|
468
561
|
};
|
|
469
562
|
}
|
|
470
563
|
async function snapshotTree(root) {
|
|
@@ -602,16 +695,473 @@ async function registeredBundle(dshHome, packageName) {
|
|
|
602
695
|
return false;
|
|
603
696
|
}
|
|
604
697
|
}
|
|
698
|
+
function isNotFound(error) {
|
|
699
|
+
return typeof error === 'object' && error !== null
|
|
700
|
+
&& 'code' in error
|
|
701
|
+
&& error.code === 'ENOENT';
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* DSH versions have emitted both a project lockfile and pnpm's virtual-store
|
|
705
|
+
* lockfile. Read only those fixed descendants, refuse every symlink in their
|
|
706
|
+
* path, and never retain the lockfile contents outside the disposable runner.
|
|
707
|
+
*/
|
|
708
|
+
async function readProfileLockfile(dshHome) {
|
|
709
|
+
const profileDirectory = join(dshHome, 'profiles', PROFILE);
|
|
710
|
+
try {
|
|
711
|
+
const profileMetadata = await lstat(profileDirectory);
|
|
712
|
+
if (!profileMetadata.isDirectory() || profileMetadata.isSymbolicLink())
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
for (const segments of PROFILE_LOCKFILE_CANDIDATES) {
|
|
719
|
+
let current = profileDirectory;
|
|
720
|
+
try {
|
|
721
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
722
|
+
const segment = segments[index];
|
|
723
|
+
current = join(current, segment);
|
|
724
|
+
const metadata = await lstat(current);
|
|
725
|
+
if (metadata.isSymbolicLink())
|
|
726
|
+
return undefined;
|
|
727
|
+
if (index < segments.length - 1 && !metadata.isDirectory())
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
return await readRegularFileNoFollow(current, MAX_PROFILE_LOCKFILE_BYTES);
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
if (isNotFound(error))
|
|
734
|
+
continue;
|
|
735
|
+
return undefined;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return undefined;
|
|
739
|
+
}
|
|
740
|
+
function profileGraphRoot(manifest) {
|
|
741
|
+
return typeof manifest.name === 'string' && manifest.name.length > 0
|
|
742
|
+
&& typeof manifest.version === 'string' && EXACT_VERSION.test(manifest.version)
|
|
743
|
+
? { name: manifest.name, version: manifest.version }
|
|
744
|
+
: SYNTHETIC_PROFILE_GRAPH_ROOT;
|
|
745
|
+
}
|
|
746
|
+
function graphGaps(graph) {
|
|
747
|
+
if (graph.unresolved === undefined || graph.unresolved.length === 0)
|
|
748
|
+
return undefined;
|
|
749
|
+
return graph.unresolved.slice(0, MAX_PROFILE_GRAPH_GAPS).map(gap => ({
|
|
750
|
+
from: bounded(gap.from, 512),
|
|
751
|
+
name: bounded(gap.name, 214),
|
|
752
|
+
spec: bounded(gap.spec, 512),
|
|
753
|
+
kind: gap.kind,
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
function pluginPeerContractEvidence(contracts) {
|
|
757
|
+
const entries = [...(contracts ?? [])].sort((left, right) => left.name.localeCompare(right.name));
|
|
758
|
+
const issues = entries
|
|
759
|
+
.filter((entry) => entry.status !== 'satisfied')
|
|
760
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
761
|
+
return {
|
|
762
|
+
declared: entries.length,
|
|
763
|
+
satisfied: entries.filter(entry => entry.status === 'satisfied').length,
|
|
764
|
+
mismatched: entries.filter(entry => entry.status === 'mismatched').length,
|
|
765
|
+
indeterminate: entries.filter(entry => entry.status === 'indeterminate').length,
|
|
766
|
+
missing: entries.filter(entry => entry.status === 'missing').length,
|
|
767
|
+
relations: entries.map(entry => ({
|
|
768
|
+
name: bounded(entry.name, 214),
|
|
769
|
+
required: bounded(entry.required, 512),
|
|
770
|
+
status: entry.status,
|
|
771
|
+
staticUsage: entry.staticUsage,
|
|
772
|
+
...(entry.resolvedVersion === undefined ? {} : { resolvedVersion: bounded(entry.resolvedVersion, 256) }),
|
|
773
|
+
})),
|
|
774
|
+
...(issues.length === 0 ? {} : {
|
|
775
|
+
issues: issues.slice(0, MAX_PROFILE_GRAPH_GAPS).map(issue => ({
|
|
776
|
+
name: bounded(issue.name, 214),
|
|
777
|
+
required: bounded(issue.required, 512),
|
|
778
|
+
status: issue.status,
|
|
779
|
+
staticUsage: issue.staticUsage,
|
|
780
|
+
...(issue.resolvedVersion === undefined ? {} : { resolvedVersion: bounded(issue.resolvedVersion, 256) }),
|
|
781
|
+
})),
|
|
782
|
+
}),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function graphGapPartitions(graph) {
|
|
786
|
+
const required = [];
|
|
787
|
+
const optional = [];
|
|
788
|
+
for (const gap of graph.unresolved ?? []) {
|
|
789
|
+
if (gap.kind === 'optional')
|
|
790
|
+
optional.push(gap);
|
|
791
|
+
else
|
|
792
|
+
required.push(gap);
|
|
793
|
+
}
|
|
794
|
+
return { required, optional };
|
|
795
|
+
}
|
|
796
|
+
function isLexicallyInside(root, candidate) {
|
|
797
|
+
const child = relative(resolve(root), resolve(candidate));
|
|
798
|
+
return child === '' || (!child.startsWith(`..${sep}`) && child !== '..');
|
|
799
|
+
}
|
|
800
|
+
const PROFILE_PEER_RESOLUTION_SCHEMA = 'upstream-radar.profile-peer-resolution/v1alpha1';
|
|
801
|
+
function indeterminatePeerContracts(requirements) {
|
|
802
|
+
return requirements.map(requirement => ({ ...requirement, status: 'indeterminate' }));
|
|
803
|
+
}
|
|
804
|
+
async function peerVersionFromResolvedModule(url, expectedName, sandboxRoot) {
|
|
805
|
+
let resolvedModule;
|
|
806
|
+
try {
|
|
807
|
+
const parsed = new URL(url);
|
|
808
|
+
if (parsed.protocol !== 'file:')
|
|
809
|
+
return undefined;
|
|
810
|
+
resolvedModule = await realpath(fileURLToPath(parsed));
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
let sandboxReal;
|
|
816
|
+
try {
|
|
817
|
+
sandboxReal = await realpath(sandboxRoot);
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
return undefined;
|
|
821
|
+
}
|
|
822
|
+
if (!isLexicallyInside(sandboxReal, resolvedModule))
|
|
823
|
+
return undefined;
|
|
824
|
+
let cursor = dirname(resolvedModule);
|
|
825
|
+
for (let depth = 0; depth < 32 && isLexicallyInside(sandboxReal, cursor); depth += 1) {
|
|
826
|
+
try {
|
|
827
|
+
const manifest = JSON.parse((await readRegularFileNoFollow(join(cursor, 'package.json'), 1 * 1024 * 1024)).toString('utf8'));
|
|
828
|
+
if (typeof manifest === 'object' && manifest !== null && !Array.isArray(manifest)) {
|
|
829
|
+
const item = manifest;
|
|
830
|
+
if (item.name === expectedName && typeof item.version === 'string' && EXACT_VERSION.test(item.version))
|
|
831
|
+
return item.version;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
// Continue toward the package root; a module may live in a nested directory.
|
|
836
|
+
}
|
|
837
|
+
const parent = dirname(cursor);
|
|
838
|
+
if (parent === cursor)
|
|
839
|
+
break;
|
|
840
|
+
cursor = parent;
|
|
841
|
+
}
|
|
842
|
+
return undefined;
|
|
843
|
+
}
|
|
844
|
+
async function readProfilePeerContracts(path, requirements, sandboxRoot) {
|
|
845
|
+
if (requirements.length === 0)
|
|
846
|
+
return [];
|
|
847
|
+
let records;
|
|
848
|
+
try {
|
|
849
|
+
const raw = JSON.parse((await readRegularFileNoFollow(path, 64 * 1024)).toString('utf8'));
|
|
850
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
851
|
+
return indeterminatePeerContracts(requirements);
|
|
852
|
+
const item = raw;
|
|
853
|
+
if (item.schema !== PROFILE_PEER_RESOLUTION_SCHEMA || !Array.isArray(item.peers) || item.peers.length !== requirements.length) {
|
|
854
|
+
return indeterminatePeerContracts(requirements);
|
|
855
|
+
}
|
|
856
|
+
records = item.peers.map((value) => {
|
|
857
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
858
|
+
throw new Error('invalid peer resolver record');
|
|
859
|
+
const peer = value;
|
|
860
|
+
if (typeof peer.name !== 'string' || (peer.status !== 'resolved' && peer.status !== 'missing')) {
|
|
861
|
+
throw new Error('invalid peer resolver record');
|
|
862
|
+
}
|
|
863
|
+
if (peer.status === 'resolved' && (typeof peer.url !== 'string' || peer.url.length === 0 || peer.url.length > 4_096)) {
|
|
864
|
+
throw new Error('resolved peer has no bounded URL');
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
name: peer.name,
|
|
868
|
+
status: peer.status,
|
|
869
|
+
...(typeof peer.url === 'string' ? { url: peer.url } : {}),
|
|
870
|
+
};
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
return indeterminatePeerContracts(requirements);
|
|
875
|
+
}
|
|
876
|
+
const byName = new Map(records.map(record => [record.name, record]));
|
|
877
|
+
if (byName.size !== requirements.length || requirements.some(requirement => !byName.has(requirement.name))) {
|
|
878
|
+
return indeterminatePeerContracts(requirements);
|
|
879
|
+
}
|
|
880
|
+
const contracts = [];
|
|
881
|
+
for (const requirement of requirements) {
|
|
882
|
+
const record = byName.get(requirement.name);
|
|
883
|
+
if (record === undefined || record.status === 'missing') {
|
|
884
|
+
contracts.push({ ...requirement, status: 'missing' });
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const resolvedVersion = record.url === undefined
|
|
888
|
+
? undefined
|
|
889
|
+
: await peerVersionFromResolvedModule(record.url, requirement.name, sandboxRoot);
|
|
890
|
+
if (resolvedVersion === undefined) {
|
|
891
|
+
contracts.push({ ...requirement, status: 'indeterminate' });
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const evaluation = satisfiesSemverRange(resolvedVersion, requirement.required);
|
|
895
|
+
contracts.push({
|
|
896
|
+
...requirement,
|
|
897
|
+
status: evaluation === true ? 'satisfied' : evaluation === false ? 'mismatched' : 'indeterminate',
|
|
898
|
+
resolvedVersion,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
return contracts;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Run the plugin's real ESM entry from the profile resolution anchor, then
|
|
905
|
+
* boot DSH. The config dumper intentionally skips `!!js` and module imports;
|
|
906
|
+
* this tiny trusted wrapper proves that the installed plugin can resolve its
|
|
907
|
+
* own direct imports before the DSH headless app exits through `--help`.
|
|
908
|
+
*/
|
|
909
|
+
async function writeProfileLoadProbe(dshHome, pluginName, dshVersion, pnpmCommand, peerRequirements) {
|
|
910
|
+
const profileDirectory = join(dshHome, 'profiles', PROFILE);
|
|
911
|
+
const profileMetadata = await lstat(profileDirectory);
|
|
912
|
+
if (!profileMetadata.isDirectory() || profileMetadata.isSymbolicLink()) {
|
|
913
|
+
throw new Error('the DSH profile directory is not a regular directory for the load probe');
|
|
914
|
+
}
|
|
915
|
+
const [homeReal, profileReal] = await Promise.all([realpath(dshHome), realpath(profileDirectory)]);
|
|
916
|
+
if (!isLexicallyInside(homeReal, profileReal)) {
|
|
917
|
+
throw new Error('the DSH profile directory escaped the controlled DSH home');
|
|
918
|
+
}
|
|
919
|
+
const probePath = join(profileDirectory, '.upstream-radar-load-probe.mjs');
|
|
920
|
+
const peerResolutionPath = join(profileDirectory, '.upstream-radar-peer-resolution.json');
|
|
921
|
+
const dshBootArgs = dshArgs(dshVersion, ['--profile', PROFILE, '--help']);
|
|
922
|
+
const contents = [
|
|
923
|
+
"import { spawn } from 'node:child_process'",
|
|
924
|
+
"import { writeFile } from 'node:fs/promises'",
|
|
925
|
+
`const peerRequirements = ${JSON.stringify(peerRequirements)}`,
|
|
926
|
+
'const peers = []',
|
|
927
|
+
'for (const peer of peerRequirements) {',
|
|
928
|
+
' try { peers.push({ name: peer.name, status: \'resolved\', url: import.meta.resolve(peer.name) }) }',
|
|
929
|
+
' catch { peers.push({ name: peer.name, status: \'missing\' }) }',
|
|
930
|
+
'}',
|
|
931
|
+
`await writeFile(${JSON.stringify(peerResolutionPath)}, JSON.stringify({ schema: ${JSON.stringify(PROFILE_PEER_RESOLUTION_SCHEMA)}, peers }), { encoding: 'utf8', mode: 0o600, flag: 'wx' })`,
|
|
932
|
+
`await import(${JSON.stringify(pluginName)})`,
|
|
933
|
+
`const child = spawn(${JSON.stringify(pnpmCommand)}, ${JSON.stringify(dshBootArgs)}, { cwd: process.cwd(), env: process.env, stdio: 'inherit' })`,
|
|
934
|
+
"const exitCode = await new Promise(resolve => {",
|
|
935
|
+
" child.once('error', error => { console.error(error.message); resolve(1) })",
|
|
936
|
+
" child.once('close', code => resolve(code ?? 1))",
|
|
937
|
+
'})',
|
|
938
|
+
'process.exitCode = exitCode',
|
|
939
|
+
'',
|
|
940
|
+
].join('\n');
|
|
941
|
+
// Never overwrite a path a target package may have planted in the profile.
|
|
942
|
+
await writeFile(probePath, contents, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
943
|
+
return { path: probePath, peerResolutionPath, profileDirectory };
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* `pnpm dlx` owns the DSH host dependency plane, rather than a plugin-created
|
|
947
|
+
* symlink in the profile. Discover the exact cached DSH package by manifest,
|
|
948
|
+
* with bounded directory traversal and no symlink following.
|
|
949
|
+
*/
|
|
950
|
+
async function discoverExactDshRuntime(cacheHome, dshVersion) {
|
|
951
|
+
const root = resolve(cacheHome, 'pnpm', 'dlx');
|
|
952
|
+
const rootReal = await realpath(root);
|
|
953
|
+
const queue = [{ path: root, depth: 0 }];
|
|
954
|
+
const candidates = new Set();
|
|
955
|
+
let visited = 0;
|
|
956
|
+
while (queue.length > 0) {
|
|
957
|
+
const current = queue.shift();
|
|
958
|
+
if (current === undefined)
|
|
959
|
+
break;
|
|
960
|
+
if (visited >= MAX_RUNTIME_DISCOVERY_DIRECTORIES) {
|
|
961
|
+
throw new Error(`DSH runtime discovery exceeds ${MAX_RUNTIME_DISCOVERY_DIRECTORIES} directories`);
|
|
962
|
+
}
|
|
963
|
+
visited += 1;
|
|
964
|
+
let entries;
|
|
965
|
+
try {
|
|
966
|
+
entries = await readdir(current.path, { withFileTypes: true });
|
|
967
|
+
}
|
|
968
|
+
catch (error) {
|
|
969
|
+
if (isNotFound(error))
|
|
970
|
+
continue;
|
|
971
|
+
throw error;
|
|
972
|
+
}
|
|
973
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
974
|
+
const child = join(current.path, entry.name);
|
|
975
|
+
if (entry.isDirectory()) {
|
|
976
|
+
if (current.depth < MAX_RUNTIME_DISCOVERY_DEPTH)
|
|
977
|
+
queue.push({ path: child, depth: current.depth + 1 });
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (!entry.isFile() || entry.name !== 'package.json')
|
|
981
|
+
continue;
|
|
982
|
+
let manifest;
|
|
983
|
+
try {
|
|
984
|
+
manifest = JSON.parse((await readRegularFileNoFollow(child, 1 * 1024 * 1024)).toString('utf8'));
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest))
|
|
990
|
+
continue;
|
|
991
|
+
const item = manifest;
|
|
992
|
+
if (item.name === '@deepseek-ai/dsh' && item.version === dshVersion)
|
|
993
|
+
candidates.add(child);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (candidates.size === 0)
|
|
997
|
+
throw new Error(`exact @deepseek-ai/dsh@${dshVersion} was not found in the controlled pnpm dlx cache`);
|
|
998
|
+
if (candidates.size > 1)
|
|
999
|
+
throw new Error(`multiple exact @deepseek-ai/dsh@${dshVersion} packages were found in the controlled pnpm dlx cache`);
|
|
1000
|
+
const manifestPath = [...candidates][0];
|
|
1001
|
+
const packageDirectory = discoverDshRuntimePackageDirectory(manifestPath);
|
|
1002
|
+
const packageCoordinate = discoverDshRuntimePackage(manifestPath);
|
|
1003
|
+
const nodeModulesDirectory = discoverDshRuntimeHostNodeModulesDirectory(manifestPath);
|
|
1004
|
+
if (packageDirectory === undefined || packageCoordinate === undefined || nodeModulesDirectory === undefined) {
|
|
1005
|
+
throw new Error('the exact DSH package did not expose a usable dependency plane');
|
|
1006
|
+
}
|
|
1007
|
+
if (!isLexicallyInside(rootReal, packageDirectory) || !isLexicallyInside(rootReal, nodeModulesDirectory)) {
|
|
1008
|
+
throw new Error('the exact DSH dependency plane escaped the controlled pnpm dlx cache');
|
|
1009
|
+
}
|
|
1010
|
+
if (packageCoordinate.name !== '@deepseek-ai/dsh' || packageCoordinate.version !== dshVersion) {
|
|
1011
|
+
throw new Error('the discovered DSH package does not match the requested exact version');
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
packageDirectory,
|
|
1015
|
+
nodeModulesDirectory,
|
|
1016
|
+
package: { ecosystem: 'npm', name: '@deepseek-ai/dsh', version: packageCoordinate.version },
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
async function profileResolutionEvidence(dshHome) {
|
|
1020
|
+
const profileDirectory = join(dshHome, 'profiles', PROFILE);
|
|
1021
|
+
const lockfile = await readProfileLockfile(dshHome);
|
|
1022
|
+
if (lockfile === undefined)
|
|
1023
|
+
return {};
|
|
1024
|
+
const profileLockfile = {
|
|
1025
|
+
sha256: createHash('sha256').update(lockfile).digest('hex'),
|
|
1026
|
+
bytes: lockfile.length,
|
|
1027
|
+
};
|
|
1028
|
+
try {
|
|
1029
|
+
const manifestBuffer = await readRegularFileNoFollow(join(profileDirectory, 'package.json'), 4 * 1024 * 1024);
|
|
1030
|
+
const manifest = JSON.parse(manifestBuffer.toString('utf8'));
|
|
1031
|
+
const graph = parsePnpmLockGraph(lockfile.toString('utf8'), profileGraphRoot(manifest));
|
|
1032
|
+
const gaps = graphGaps(graph);
|
|
1033
|
+
return {
|
|
1034
|
+
profileLockfile: {
|
|
1035
|
+
...profileLockfile,
|
|
1036
|
+
...(graph.digest === undefined ? {} : { graphDigest: graph.digest }),
|
|
1037
|
+
nodes: graph.nodes.length,
|
|
1038
|
+
edges: graph.edges.length,
|
|
1039
|
+
unresolved: graph.unresolved?.length ?? 0,
|
|
1040
|
+
...(gaps === undefined ? {} : { unresolvedDependencies: gaps }),
|
|
1041
|
+
},
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
return { profileLockfile };
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
async function runtimeGraphEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts) {
|
|
1049
|
+
try {
|
|
1050
|
+
const dshRuntime = await discoverExactDshRuntime(cacheHome, dshVersion);
|
|
1051
|
+
const graph = await parseInstalledNodeModulesGraph(join(dshHome, 'profiles', PROFILE), rootPackage, {
|
|
1052
|
+
hostNodeModulesDirectory: dshRuntime.nodeModulesDirectory,
|
|
1053
|
+
hostRuntimeSource: 'dsh-process',
|
|
1054
|
+
hostRuntimePackage: dshRuntime.package,
|
|
1055
|
+
hostRuntimePackageDirectory: dshRuntime.packageDirectory,
|
|
1056
|
+
});
|
|
1057
|
+
if (graph.digest === undefined)
|
|
1058
|
+
return {};
|
|
1059
|
+
const gaps = graphGapPartitions(graph);
|
|
1060
|
+
const contracts = profileResolvedPeerContracts ?? graph.rootPeerContracts?.map(contract => ({
|
|
1061
|
+
...contract,
|
|
1062
|
+
staticUsage: 'scan-incomplete',
|
|
1063
|
+
}));
|
|
1064
|
+
const concretelyResolvedRootPeers = new Set((contracts ?? [])
|
|
1065
|
+
.filter(contract => contract.status === 'satisfied' || contract.status === 'mismatched')
|
|
1066
|
+
.map(contract => contract.name));
|
|
1067
|
+
const effectiveRequiredGaps = gaps.required.filter(gap => !(gap.from === graph.rootNodeId
|
|
1068
|
+
&& gap.kind === 'peer'
|
|
1069
|
+
&& concretelyResolvedRootPeers.has(gap.name)));
|
|
1070
|
+
const requiredGaps = graphGaps({ unresolved: effectiveRequiredGaps });
|
|
1071
|
+
const optionalGaps = graphGaps({ unresolved: gaps.optional });
|
|
1072
|
+
return {
|
|
1073
|
+
runtimeGraph: {
|
|
1074
|
+
digest: graph.digest,
|
|
1075
|
+
nodes: graph.nodes.length,
|
|
1076
|
+
edges: graph.edges.length,
|
|
1077
|
+
unresolved: effectiveRequiredGaps.length,
|
|
1078
|
+
...(requiredGaps === undefined ? {} : { unresolvedDependencies: requiredGaps }),
|
|
1079
|
+
...(gaps.optional.length === 0 ? {} : {
|
|
1080
|
+
optionalUnavailable: gaps.optional.length,
|
|
1081
|
+
...(optionalGaps === undefined ? {} : { optionalUnavailableDependencies: optionalGaps }),
|
|
1082
|
+
}),
|
|
1083
|
+
pluginPeerContracts: pluginPeerContractEvidence(contracts),
|
|
1084
|
+
...(graph.hostRuntime === undefined ? {} : {
|
|
1085
|
+
hostRuntime: {
|
|
1086
|
+
source: graph.hostRuntime.source,
|
|
1087
|
+
resolvedNodes: graph.hostRuntime.resolvedNodes,
|
|
1088
|
+
...(graph.hostRuntime.package === undefined ? {} : { dshVersion: graph.hostRuntime.package.version }),
|
|
1089
|
+
},
|
|
1090
|
+
}),
|
|
1091
|
+
},
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
return {
|
|
1096
|
+
runtimeGraphError: bounded(error instanceof Error ? error.message : String(error), 512),
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async function resolutionEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts) {
|
|
1101
|
+
const [profile, runtime] = await Promise.all([
|
|
1102
|
+
profileResolutionEvidence(dshHome),
|
|
1103
|
+
runtimeGraphEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts),
|
|
1104
|
+
]);
|
|
1105
|
+
return { ...profile, ...runtime };
|
|
1106
|
+
}
|
|
605
1107
|
function finishReport(report, result, reason) {
|
|
606
1108
|
report.completedAt = new Date().toISOString();
|
|
607
1109
|
report.result = result;
|
|
608
1110
|
report.reason = reason;
|
|
609
1111
|
return report;
|
|
610
1112
|
}
|
|
1113
|
+
/**
|
|
1114
|
+
* A load success is necessary but not enough for the compatibility claim. The
|
|
1115
|
+
* final verdict also requires a complete required-edge graph and a direct
|
|
1116
|
+
* plugin peer contract that the observer could actually evaluate.
|
|
1117
|
+
*/
|
|
1118
|
+
function finalCompatibilityConclusion(resolution) {
|
|
1119
|
+
const graph = resolution.runtimeGraph;
|
|
1120
|
+
if (graph === undefined) {
|
|
1121
|
+
return {
|
|
1122
|
+
result: 'unknown',
|
|
1123
|
+
reason: resolution.runtimeGraphError === undefined
|
|
1124
|
+
? 'the exact artifact installed and loaded, but the effective DSH runtime graph was not established'
|
|
1125
|
+
: `the exact artifact installed and loaded, but the effective DSH runtime graph could not be established: ${resolution.runtimeGraphError}`,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
const contracts = graph.pluginPeerContracts;
|
|
1129
|
+
const firstIssue = contracts.issues?.[0];
|
|
1130
|
+
if (contracts.mismatched > 0 || contracts.missing > 0) {
|
|
1131
|
+
const detail = firstIssue === undefined
|
|
1132
|
+
? `${contracts.mismatched + contracts.missing} required plugin peer contract(s) do not match the DSH runtime`
|
|
1133
|
+
: firstIssue.status === 'missing'
|
|
1134
|
+
? `${firstIssue.name}@${firstIssue.required} was not resolved by the DSH runtime (${firstIssue.staticUsage})`
|
|
1135
|
+
: `${firstIssue.name}@${firstIssue.resolvedVersion ?? 'unknown'} does not satisfy ${firstIssue.required} (${firstIssue.staticUsage})`;
|
|
1136
|
+
return {
|
|
1137
|
+
result: 'peer-contract-incompatible',
|
|
1138
|
+
reason: `the exact artifact installed and loaded, but ${detail}`,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
if (graph.unresolved > 0) {
|
|
1142
|
+
return {
|
|
1143
|
+
result: 'unknown',
|
|
1144
|
+
reason: `the exact artifact installed and loaded, but the effective DSH runtime graph has ${graph.unresolved} required unresolved edge(s)`,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
if (contracts.indeterminate > 0) {
|
|
1148
|
+
return {
|
|
1149
|
+
result: 'unknown',
|
|
1150
|
+
reason: `the exact artifact installed and loaded, but ${contracts.indeterminate} required plugin peer range(s) could not be evaluated safely`,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
result: 'compatible',
|
|
1155
|
+
reason: 'the exact artifact installed, registered, loaded, and satisfied its direct peer contracts under the requested DSH version',
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
611
1158
|
export async function observeDshPluginInstall(options) {
|
|
612
1159
|
const spec = parseNpmSpec(options.packageSpec);
|
|
613
1160
|
if (!EXACT_VERSION.test(options.dshVersion))
|
|
614
1161
|
throw new Error('DSH version must be an exact semantic version');
|
|
1162
|
+
if (options.caseId !== undefined && !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(options.caseId)) {
|
|
1163
|
+
throw new Error('DSH install observation caseId must be a short lowercase label');
|
|
1164
|
+
}
|
|
615
1165
|
const allowedBuilds = normalizeAllowedBuilds(options.allowedBuilds);
|
|
616
1166
|
if (!options.allowExecution)
|
|
617
1167
|
throw new Error('DSH install observation requires explicit execution consent');
|
|
@@ -637,6 +1187,7 @@ export async function observeDshPluginInstall(options) {
|
|
|
637
1187
|
scope: 'install-and-load-behavior',
|
|
638
1188
|
startedAt,
|
|
639
1189
|
completedAt: startedAt,
|
|
1190
|
+
...(options.caseId === undefined ? {} : { caseId: options.caseId }),
|
|
640
1191
|
dshVersion: options.dshVersion,
|
|
641
1192
|
runtime: {
|
|
642
1193
|
platform: process.platform,
|
|
@@ -660,6 +1211,7 @@ export async function observeDshPluginInstall(options) {
|
|
|
660
1211
|
},
|
|
661
1212
|
observations: { install: emptyTraceObservation(), load: emptyTraceObservation() },
|
|
662
1213
|
filesystem: { install: emptyFilesystemDiff(), load: emptyFilesystemDiff() },
|
|
1214
|
+
resolution: {},
|
|
663
1215
|
result: 'unknown',
|
|
664
1216
|
reason: 'observation did not complete',
|
|
665
1217
|
boundary: {
|
|
@@ -788,6 +1340,7 @@ export async function observeDshPluginInstall(options) {
|
|
|
788
1340
|
const afterInstall = await snapshotSandbox(sandboxRoot, environment);
|
|
789
1341
|
report.observations.install = await readTrace(installTracePath, sandboxRoot);
|
|
790
1342
|
report.filesystem.install = diffSnapshots(beforeInstall, afterInstall);
|
|
1343
|
+
report.resolution = await resolutionEvidence(environment.DSH_HOME, spec, options.dshVersion, environment.XDG_CACHE_HOME);
|
|
791
1344
|
report.stages.install = commandStage(installResult);
|
|
792
1345
|
if (installResult.timedOut || installResult.outputExceeded || installResult.launchError !== undefined) {
|
|
793
1346
|
return finishReport(report, 'unknown', 'the plugin install did not produce a bounded command result');
|
|
@@ -809,12 +1362,13 @@ export async function observeDshPluginInstall(options) {
|
|
|
809
1362
|
: { status: 'failed', detail: `the DSH profile did not register ${spec.name}` };
|
|
810
1363
|
if (!registered)
|
|
811
1364
|
return finishReport(report, 'install-failed', 'DSH accepted the install command but did not register the plugin bundle');
|
|
1365
|
+
const loadProbe = await writeProfileLoadProbe(environment.DSH_HOME, spec.name, options.dshVersion, pnpmCommand, artifact.requiredPeerDependencies);
|
|
812
1366
|
const loadTracePath = join(traceDirectory, 'load.strace');
|
|
813
1367
|
const loadResult = await runSafely(runner, {
|
|
814
1368
|
phase: 'load',
|
|
815
|
-
command:
|
|
816
|
-
args:
|
|
817
|
-
cwd:
|
|
1369
|
+
command: process.execPath,
|
|
1370
|
+
args: [loadProbe.path],
|
|
1371
|
+
cwd: loadProbe.profileDirectory,
|
|
818
1372
|
env: scriptsEnvironment,
|
|
819
1373
|
timeoutMs,
|
|
820
1374
|
sandboxRoot,
|
|
@@ -823,6 +1377,10 @@ export async function observeDshPluginInstall(options) {
|
|
|
823
1377
|
const afterLoad = await snapshotSandbox(sandboxRoot, environment);
|
|
824
1378
|
report.observations.load = await readTrace(loadTracePath, sandboxRoot);
|
|
825
1379
|
report.filesystem.load = diffSnapshots(afterInstall, afterLoad);
|
|
1380
|
+
const profilePeerContracts = await readProfilePeerContracts(loadProbe.peerResolutionPath, artifact.requiredPeerDependencies, sandboxRoot);
|
|
1381
|
+
// Loading may trigger one final DSH profile reconciliation. Preserve the
|
|
1382
|
+
// final resolved graph rather than only the state immediately after add.
|
|
1383
|
+
report.resolution = await resolutionEvidence(environment.DSH_HOME, spec, options.dshVersion, environment.XDG_CACHE_HOME, profilePeerContracts);
|
|
826
1384
|
report.stages.load = commandStage(loadResult);
|
|
827
1385
|
if (loadResult.timedOut || loadResult.outputExceeded || loadResult.launchError !== undefined) {
|
|
828
1386
|
return finishReport(report, 'unknown', 'the plugin load did not produce a bounded command result');
|
|
@@ -833,7 +1391,8 @@ export async function observeDshPluginInstall(options) {
|
|
|
833
1391
|
}
|
|
834
1392
|
if (loadResult.code !== 0)
|
|
835
1393
|
return finishReport(report, 'load-failed', 'the traced DSH profile load command failed');
|
|
836
|
-
|
|
1394
|
+
const conclusion = finalCompatibilityConclusion(report.resolution);
|
|
1395
|
+
return finishReport(report, conclusion.result, conclusion.reason);
|
|
837
1396
|
}
|
|
838
1397
|
catch (error) {
|
|
839
1398
|
const detail = bounded(error instanceof Error ? error.message : String(error));
|
|
@@ -854,6 +1413,7 @@ export function renderDshInstallObservation(report) {
|
|
|
854
1413
|
const lifecycle = report.artifact.lifecycleScripts.length === 0 ? 'none' : report.artifact.lifecycleScripts.join(', ');
|
|
855
1414
|
const lines = [
|
|
856
1415
|
'DSH isolated install observation',
|
|
1416
|
+
...(report.caseId === undefined ? [] : [`Case: ${report.caseId}`]),
|
|
857
1417
|
`Artifact: ${report.artifact.name}@${report.artifact.version}${report.artifact.sha256 === undefined ? '' : ` (sha256:${report.artifact.sha256.slice(0, 12)}…)`}`,
|
|
858
1418
|
`DSH: ${report.dshVersion}`,
|
|
859
1419
|
`Runtime: Node ${report.runtime.nodeVersion} (${report.runtime.platform}/${report.runtime.architecture}), pnpm ${report.runtime.packageManager.version ?? 'unknown'}`,
|
|
@@ -866,11 +1426,24 @@ export function renderDshInstallObservation(report) {
|
|
|
866
1426
|
`Install evidence: ${install.processes.length} process, ${install.network.length} network, ${install.fileWrites.length} file-write event(s); trace ${install.coverage.status}`,
|
|
867
1427
|
`Load evidence: ${load.processes.length} process, ${load.network.length} network, ${load.fileWrites.length} file-write event(s); trace ${load.coverage.status}`,
|
|
868
1428
|
`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}`,
|
|
1429
|
+
`Resolved profile graph: ${report.resolution.profileLockfile?.graphDigest ?? 'not established'}${report.resolution.profileLockfile === undefined ? '' : ` (lock sha256:${report.resolution.profileLockfile.sha256.slice(0, 12)}…)`}`,
|
|
1430
|
+
`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`})`}`,
|
|
1431
|
+
`Plugin peer contracts: ${report.resolution.runtimeGraph === undefined
|
|
1432
|
+
? 'not established'
|
|
1433
|
+
: `${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`}`,
|
|
1434
|
+
`Effective graph collector: ${report.resolution.runtimeGraphError ?? 'captured'}`,
|
|
869
1435
|
'',
|
|
870
1436
|
];
|
|
871
1437
|
for (const [name, stage] of Object.entries(report.stages)) {
|
|
872
1438
|
lines.push(` ${name}: ${stage.status}${stage.detail === undefined ? '' : ` (${stage.detail})`}`);
|
|
873
1439
|
}
|
|
1440
|
+
const peerIssues = report.resolution.runtimeGraph?.pluginPeerContracts.issues ?? [];
|
|
1441
|
+
if (peerIssues.length > 0) {
|
|
1442
|
+
lines.push('', 'Direct peer-contract findings:');
|
|
1443
|
+
for (const issue of peerIssues) {
|
|
1444
|
+
lines.push(` ${issue.name}: ${issue.status}; requires ${issue.required}${issue.resolvedVersion === undefined ? '' : `, resolved ${issue.resolvedVersion}`}; static use ${issue.staticUsage}`);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
874
1447
|
lines.push('', report.boundary.note);
|
|
875
1448
|
return `${lines.join('\n')}\n`;
|
|
876
1449
|
}
|