dsh-composition-doctor 0.1.3 → 0.2.1

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.
@@ -0,0 +1,150 @@
1
+ import { lstat, readFile, readdir } from 'node:fs/promises';
2
+ import { delimiter, join, resolve } from 'node:path';
3
+ import { stat } from 'node:fs/promises';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { tmpdir } from 'node:os';
7
+ import { downloadNpmArtifact, fileDigest, readNpmCacheMetadata, tarManifest } from './npm-artifact.js';
8
+ const execFileAsync = promisify(execFile);
9
+ async function readableFile(path) {
10
+ try {
11
+ return (await stat(path)).isFile();
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ async function readableDirectory(path) {
18
+ try {
19
+ return (await stat(path)).isDirectory();
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ async function respondsToVersion(path) {
26
+ try {
27
+ const shim = process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(path);
28
+ const executable = shim ? (process.env.ComSpec ?? 'cmd.exe') : path;
29
+ const args = shim ? ['/d', '/s', '/c', `call "${path}" --version`] : ['--version'];
30
+ await execFileAsync(executable, args, { windowsHide: true, timeout: 5_000, maxBuffer: 128 * 1024 });
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ async function explicitPackage(path, targetDsh) {
38
+ if (await readableFile(path)) {
39
+ if (/\.(?:tgz|tar|tar\.gz)$/i.test(path)) {
40
+ try {
41
+ const bytes = await readFile(path);
42
+ const manifest = tarManifest(bytes);
43
+ const digest = await fileDigest(path);
44
+ const resolvedVersion = typeof manifest.version === 'string' ? manifest.version : undefined;
45
+ return { kind: 'dsh-tarball', path, detail: resolvedVersion === targetDsh ? `Using explicit local DSH ${targetDsh} tarball.` : `Using local DSH tarball candidate ${resolvedVersion ?? 'unknown'}; target ${targetDsh} is not verified.`, targetVersionVerified: resolvedVersion === targetDsh, source: 'explicit-local', requestedVersion: targetDsh, resolvedVersion, manifest, ...digest };
46
+ }
47
+ catch {
48
+ return { kind: 'unavailable', detail: `Explicit DSH tarball could not be inspected: ${path}`, targetVersionVerified: false, source: 'unavailable', requestedVersion: targetDsh };
49
+ }
50
+ }
51
+ return { kind: 'unavailable', detail: `Explicit DSH package path is a file but not a supported local tarball: ${path}`, targetVersionVerified: false, source: 'unavailable', requestedVersion: targetDsh };
52
+ }
53
+ if (!(await readableDirectory(path)))
54
+ return { kind: 'unavailable', detail: `Explicit DSH package path is not readable: ${path}`, targetVersionVerified: false, source: 'unavailable', requestedVersion: targetDsh };
55
+ try {
56
+ const manifest = JSON.parse(await readFile(join(path, 'package.json'), 'utf8'));
57
+ const version = typeof manifest.version === 'string' ? manifest.version : undefined;
58
+ const hasBin = typeof manifest.bin === 'string' || (manifest.bin !== null && typeof manifest.bin === 'object');
59
+ const digest = await fileDigest(join(path, 'package.json'));
60
+ if (!hasBin)
61
+ return { kind: 'unavailable', detail: `Local DSH package has no public bin entry: ${path}`, targetVersionVerified: false, source: 'unavailable', requestedVersion: targetDsh, resolvedVersion: version, manifest, ...digest };
62
+ return { kind: 'dsh-package', path, detail: version === targetDsh ? `Using local DSH package ${targetDsh}.` : `Using local DSH package candidate ${version ?? 'unknown'}; target ${targetDsh} is not verified.`, targetVersionVerified: version === targetDsh, source: 'explicit-local', requestedVersion: targetDsh, resolvedVersion: version, manifest, ...digest };
63
+ }
64
+ catch {
65
+ return { kind: 'unavailable', detail: `Local DSH package has no readable package.json: ${path}`, targetVersionVerified: false, source: 'unavailable', requestedVersion: targetDsh };
66
+ }
67
+ }
68
+ async function cachedTarball(targetDsh, cacheDir, source) {
69
+ try {
70
+ const names = await readdir(cacheDir);
71
+ const path = names.map((name) => resolve(cacheDir, name)).find((candidate) => candidate.endsWith('.tgz') && candidate.includes(targetDsh));
72
+ if (path === undefined)
73
+ return undefined;
74
+ if (!(await lstat(path)).isFile())
75
+ return undefined;
76
+ const bytes = await readFile(path);
77
+ const manifest = tarManifest(bytes);
78
+ const resolvedVersion = typeof manifest.version === 'string' ? manifest.version : undefined;
79
+ const digest = await fileDigest(path);
80
+ if (source === 'doctor-cache') {
81
+ const metadata = await readNpmCacheMetadata(path);
82
+ if (metadata === undefined || metadata.resolvedVersion !== resolvedVersion || metadata.sha512 !== digest.sha512 || metadata.integrity !== digest.integrity)
83
+ return undefined;
84
+ }
85
+ const label = source === 'doctor-cache' ? 'Doctor cache' : 'package-manager cache/store';
86
+ return { kind: 'dsh-tarball', path, detail: resolvedVersion === targetDsh ? `Using ${label} artifact for DSH ${targetDsh}.` : `${label} contains DSH ${resolvedVersion ?? 'unknown'}, not requested ${targetDsh}.`, targetVersionVerified: resolvedVersion === targetDsh, source, requestedVersion: targetDsh, resolvedVersion, manifest, ...digest };
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ }
92
+ async function installedDsh() {
93
+ const pathValue = process.env.PATH ?? '';
94
+ for (const directory of pathValue.split(delimiter).filter(Boolean)) {
95
+ const candidates = process.platform === 'win32' ? [join(directory, 'dsh.cmd'), join(directory, 'dsh.exe'), join(directory, 'dsh')] : [join(directory, 'dsh')];
96
+ for (const candidate of candidates)
97
+ if (await readableFile(candidate) && await respondsToVersion(candidate)) {
98
+ try {
99
+ const shim = process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(candidate);
100
+ const executable = shim ? (process.env.ComSpec ?? 'cmd.exe') : candidate;
101
+ const args = shim ? ['/d', '/s', '/c', `call "${candidate}" --version`] : ['--version'];
102
+ const result = await execFileAsync(executable, args, { windowsHide: true, timeout: 5_000, maxBuffer: 128 * 1024 });
103
+ return { path: candidate, version: `${result.stdout}\n${result.stderr}`.trim() };
104
+ }
105
+ catch {
106
+ return { path: candidate };
107
+ }
108
+ }
109
+ }
110
+ return undefined;
111
+ }
112
+ /** Offline-first target artifact discovery. No registry, Git, cache install, or lifecycle is performed. */
113
+ export async function resolveDshArtifact(options) {
114
+ if (options.dshBin !== undefined) {
115
+ if (await readableFile(options.dshBin))
116
+ return { kind: 'dsh-bin', path: options.dshBin, detail: `Using explicit local DSH binary candidate for ${options.targetDsh}; release version is not verified until --version is checked.`, targetVersionVerified: false, source: 'explicit-local', requestedVersion: options.targetDsh, ...(await fileDigest(options.dshBin)) };
117
+ return { kind: 'unavailable', detail: `Explicit DSH binary path is not readable: ${options.dshBin}`, targetVersionVerified: false, source: 'unavailable', requestedVersion: options.targetDsh };
118
+ }
119
+ if (options.dshPackage !== undefined)
120
+ return explicitPackage(options.dshPackage, options.targetDsh);
121
+ const installed = await installedDsh();
122
+ if (installed !== undefined)
123
+ return { kind: 'dsh-bin', path: installed.path, detail: `Using installed local dsh candidate for ${options.targetDsh}; release version is not verified.`, targetVersionVerified: installed.version?.includes(options.targetDsh) === true, source: 'installed', requestedVersion: options.targetDsh, resolvedVersion: installed.version, ...(await fileDigest(installed.path)) };
124
+ for (const directory of options.packageManagerCacheDirs ?? []) {
125
+ const cached = await cachedTarball(options.targetDsh, directory, 'package-manager-cache');
126
+ if (cached !== undefined && cached.targetVersionVerified)
127
+ return cached;
128
+ }
129
+ if (options.cacheDir !== undefined) {
130
+ const cached = await cachedTarball(options.targetDsh, options.cacheDir, 'doctor-cache');
131
+ if (cached !== undefined && cached.targetVersionVerified)
132
+ return cached;
133
+ }
134
+ if (options.online) {
135
+ try {
136
+ const downloaded = await downloadNpmArtifact(`@deepseek-ai/dsh@${options.targetDsh}`, options.cacheDir ?? join(tmpdir(), 'dsh-composition-doctor-cache'));
137
+ return { kind: 'dsh-tarball', path: downloaded.path, detail: `Downloaded official DSH ${downloaded.resolvedVersion} artifact from npm registry into Doctor-owned cache.`, targetVersionVerified: downloaded.resolvedVersion === options.targetDsh, source: downloaded.source, requestedVersion: options.targetDsh, resolvedVersion: downloaded.resolvedVersion, manifest: downloaded.manifest, sha512: downloaded.sha512, integrity: downloaded.integrity };
138
+ }
139
+ catch (error) {
140
+ return { kind: 'unavailable', detail: `Official npm artifact download failed for DSH ${options.targetDsh}: ${error instanceof Error ? error.message : String(error)}`, targetVersionVerified: false, source: 'unavailable', requestedVersion: options.targetDsh };
141
+ }
142
+ }
143
+ return {
144
+ kind: 'unavailable',
145
+ detail: `Target DSH ${options.targetDsh} artifact unavailable offline; no permitted local package-manager cache/store or Doctor cache contained the exact release.`,
146
+ targetVersionVerified: false,
147
+ source: 'unavailable',
148
+ requestedVersion: options.targetDsh
149
+ };
150
+ }
@@ -0,0 +1,61 @@
1
+ import { lstat, readFile, readdir } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { downloadNpmArtifact, fileDigest, tarManifest } from './npm-artifact.js';
5
+ function candidateParts(candidate) {
6
+ const at = candidate.lastIndexOf('@');
7
+ return { name: candidate.slice(0, at), version: candidate.slice(at + 1) };
8
+ }
9
+ function packagePath(profileDir, name) {
10
+ return resolve(profileDir, 'node_modules', ...name.split('/'), 'package.json');
11
+ }
12
+ /** Reads only an already-installed candidate manifest; it never invokes a package manager. */
13
+ export async function resolveCandidateArtifact(profileDir, candidate, online, cacheDir, packageManagerCacheDirs) {
14
+ const { name, version } = candidateParts(candidate);
15
+ const packageJsonPath = packagePath(profileDir, name);
16
+ try {
17
+ const manifest = JSON.parse(await readFile(packageJsonPath, 'utf8'));
18
+ const installedVersion = typeof manifest.version === 'string' ? manifest.version : 'unknown';
19
+ if (installedVersion !== version)
20
+ return { status: 'unavailable', candidate, detail: `Local candidate manifest version ${installedVersion} does not match requested ${version}.` };
21
+ const scripts = manifest.scripts ?? {};
22
+ const lifecycleScripts = ['preinstall', 'install', 'postinstall', 'prepare'].filter((key) => typeof scripts[key] === 'string');
23
+ return { status: 'resolved', candidate, detail: 'Inspected an existing local candidate manifest; no install or lifecycle was executed.', artifact: { name, requestedVersion: version, installedVersion, packageJsonPath, lifecycleScripts, source: 'profile-node_modules', manifest } };
24
+ }
25
+ catch {
26
+ for (const directory of packageManagerCacheDirs ?? []) {
27
+ try {
28
+ const nameInFilename = name.replaceAll('/', '-');
29
+ const filename = (await readdir(directory)).find((entry) => entry.endsWith('.tgz') && entry.includes(nameInFilename) && entry.includes(version));
30
+ if (filename === undefined)
31
+ continue;
32
+ const tarballPath = resolve(directory, filename);
33
+ if (!(await lstat(tarballPath)).isFile())
34
+ continue;
35
+ const manifest = tarManifest(await readFile(tarballPath));
36
+ if (manifest.name !== name || manifest.version !== version)
37
+ continue;
38
+ const scripts = manifest.scripts !== null && typeof manifest.scripts === 'object' ? manifest.scripts : {};
39
+ const lifecycleScripts = ['preinstall', 'install', 'postinstall', 'prepare'].filter((key) => typeof scripts[key] === 'string');
40
+ const digest = await fileDigest(tarballPath);
41
+ return { status: 'resolved', candidate, detail: 'Inspected an exact package-manager cache/store tarball; no install or lifecycle was executed.', artifact: { name, requestedVersion: version, installedVersion: version, packageJsonPath: tarballPath, lifecycleScripts, source: 'package-manager-cache', manifest, integrity: digest.integrity, sha512: digest.sha512 } };
42
+ }
43
+ catch {
44
+ // An unreadable cache entry is not evidence of an unavailable release.
45
+ }
46
+ }
47
+ if (!online)
48
+ return { status: 'unavailable', candidate, detail: `${candidate} is not available as an exact local profile node_modules artifact offline.` };
49
+ try {
50
+ const downloaded = await downloadNpmArtifact(candidate, cacheDir ?? resolve(tmpdir(), 'dsh-composition-doctor-cache'));
51
+ if (downloaded.resolvedVersion !== version)
52
+ return { status: 'unavailable', candidate, detail: `npm resolved ${downloaded.resolvedVersion}, not requested ${version}; the downloaded artifact was not accepted.` };
53
+ const scripts = downloaded.manifest.scripts !== null && typeof downloaded.manifest.scripts === 'object' ? downloaded.manifest.scripts : {};
54
+ const lifecycleScripts = ['preinstall', 'install', 'postinstall', 'prepare'].filter((key) => typeof scripts[key] === 'string');
55
+ return { status: 'resolved', candidate, detail: `Downloaded and inspected the exact npm artifact into Doctor-owned cache; no install or lifecycle was executed.`, artifact: { name, requestedVersion: version, installedVersion: downloaded.resolvedVersion, packageJsonPath: downloaded.path, lifecycleScripts, source: 'npm-registry', manifest: downloaded.manifest, integrity: downloaded.integrity, sha512: downloaded.sha512, registry: downloaded.registry } };
56
+ }
57
+ catch (error) {
58
+ return { status: 'unavailable', candidate, detail: `Candidate npm artifact download failed for ${candidate}: ${error instanceof Error ? error.message : String(error)}` };
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,132 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { basename, resolve } from 'node:path';
4
+ import { gunzipSync } from 'node:zlib';
5
+ function text(bytes) {
6
+ return new TextDecoder().decode(bytes).replace(/\0+$/g, '');
7
+ }
8
+ export function tarManifest(bytes) {
9
+ // npm publishes gzip-compressed tar archives (.tgz). Keep accepting a
10
+ // plain tar too because explicit local fixtures are useful in unit tests.
11
+ const archive = bytes[0] === 0x1f && bytes[1] === 0x8b ? gunzipSync(bytes) : bytes;
12
+ let manifest;
13
+ for (let offset = 0; offset + 512 <= archive.length;) {
14
+ const name = text(archive.slice(offset, offset + 100));
15
+ if (name.length === 0)
16
+ break;
17
+ const normalizedName = name.replaceAll('\\', '/');
18
+ const pathParts = normalizedName.split('/');
19
+ const type = archive[offset + 156];
20
+ if (normalizedName.startsWith('/') || /^[A-Za-z]:\//.test(normalizedName) || name.includes('\\') || pathParts.includes('..')) {
21
+ throw new Error(`npm tarball contains an unsafe path: ${name}`);
22
+ }
23
+ if (type === 49 || type === 50)
24
+ throw new Error(`npm tarball contains a link entry: ${name}`);
25
+ const sizeText = text(archive.slice(offset + 124, offset + 136)).trim();
26
+ const size = Number.parseInt(sizeText || '0', 8);
27
+ if (!Number.isSafeInteger(size) || size < 0)
28
+ throw new Error(`npm tarball contains an invalid entry size: ${name}`);
29
+ const dataStart = offset + 512;
30
+ if (dataStart + size > archive.length)
31
+ throw new Error(`npm tarball entry is truncated: ${name}`);
32
+ if (normalizedName === 'package/package.json')
33
+ manifest = JSON.parse(text(archive.slice(dataStart, dataStart + size)));
34
+ offset = dataStart + Math.ceil(size / 512) * 512;
35
+ }
36
+ if (manifest === undefined)
37
+ throw new Error('npm tarball has no package/package.json');
38
+ return manifest;
39
+ }
40
+ function digest(bytes) {
41
+ const hash = createHash('sha512').update(bytes);
42
+ const base64 = hash.digest('base64');
43
+ const sha512 = createHash('sha512').update(bytes).digest('hex');
44
+ return { sha512, integrity: `sha512-${base64}` };
45
+ }
46
+ function exactPackageSpec(requestedSpec) {
47
+ const separator = requestedSpec.lastIndexOf('@');
48
+ if (separator <= 0 || separator === requestedSpec.length - 1)
49
+ throw new Error(`Only exact npm package specs are supported: ${requestedSpec}`);
50
+ return { name: requestedSpec.slice(0, separator), version: requestedSpec.slice(separator + 1) };
51
+ }
52
+ function metadataPath(path) {
53
+ return `${path}.metadata.json`;
54
+ }
55
+ async function atomicWrite(path, bytes) {
56
+ const temporaryPath = `${path}.${randomBytes(12).toString('hex')}.partial`;
57
+ try {
58
+ await writeFile(temporaryPath, bytes, { flag: 'wx' });
59
+ await rename(temporaryPath, path);
60
+ }
61
+ finally {
62
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
63
+ }
64
+ }
65
+ export async function readNpmCacheMetadata(path) {
66
+ try {
67
+ const value = JSON.parse(await readFile(metadataPath(path), 'utf8'));
68
+ if (value === null || typeof value !== 'object')
69
+ return undefined;
70
+ const metadata = value;
71
+ if (typeof metadata.requestedSpec !== 'string' || typeof metadata.resolvedVersion !== 'string' || metadata.registry !== 'https://registry.npmjs.org' || typeof metadata.sha512 !== 'string' || typeof metadata.integrity !== 'string')
72
+ return undefined;
73
+ return metadata;
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ }
79
+ async function registryJson(url) {
80
+ const response = await fetch(url, { headers: { accept: 'application/json' } });
81
+ if (!response.ok)
82
+ throw new Error(`npm registry returned HTTP ${response.status} for ${url}`);
83
+ const value = await response.json();
84
+ if (value === null || typeof value !== 'object')
85
+ throw new Error(`npm registry returned invalid JSON for ${url}`);
86
+ return value;
87
+ }
88
+ /** Downloads one exact npm package tarball into Doctor-owned cache storage without install or lifecycle execution. */
89
+ export async function downloadNpmArtifact(requestedSpec, cacheDir) {
90
+ const directory = resolve(cacheDir);
91
+ await mkdir(directory, { recursive: true });
92
+ const { name, version } = exactPackageSpec(requestedSpec);
93
+ const metadataUrl = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
94
+ const packageMetadata = await registryJson(metadataUrl);
95
+ const versions = packageMetadata.versions;
96
+ const versionMetadata = versions !== null && typeof versions === 'object'
97
+ ? versions[version]
98
+ : undefined;
99
+ if (versionMetadata === null || typeof versionMetadata !== 'object')
100
+ throw new Error(`npm registry has no exact version ${version} for ${name}`);
101
+ const metadata = versionMetadata;
102
+ const dist = metadata.dist;
103
+ if (dist === undefined)
104
+ throw new Error(`npm registry metadata has no dist information for ${requestedSpec}`);
105
+ const tarballUrl = dist?.tarball;
106
+ if (typeof tarballUrl !== 'string')
107
+ throw new Error(`npm registry metadata has no tarball URL for ${requestedSpec}`);
108
+ const response = await fetch(tarballUrl, { headers: { accept: 'application/octet-stream' } });
109
+ if (!response.ok)
110
+ throw new Error(`npm registry returned HTTP ${response.status} for ${tarballUrl}`);
111
+ const bytes = new Uint8Array(await response.arrayBuffer());
112
+ const filename = basename(new URL(tarballUrl).pathname) || `${name.replaceAll('/', '-')}-${version}.tgz`;
113
+ const path = resolve(directory, filename);
114
+ const manifest = tarManifest(bytes);
115
+ const resolvedVersion = typeof manifest.version === 'string' ? manifest.version : undefined;
116
+ if (resolvedVersion === undefined)
117
+ throw new Error(`npm tarball has no resolved version for ${requestedSpec}`);
118
+ if (manifest.name !== name || resolvedVersion !== version)
119
+ throw new Error(`npm tarball manifest does not match exact request ${requestedSpec}`);
120
+ const computed = digest(bytes);
121
+ if (typeof dist.integrity === 'string' && dist.integrity !== computed.integrity)
122
+ throw new Error(`npm registry integrity mismatch for ${requestedSpec}`);
123
+ const integrity = typeof dist.integrity === 'string' ? dist.integrity : computed.integrity;
124
+ const cacheMetadata = { requestedSpec, resolvedVersion, registry: 'https://registry.npmjs.org', sha512: computed.sha512, integrity };
125
+ await atomicWrite(metadataPath(path), Buffer.from(`${JSON.stringify(cacheMetadata)}\n`, 'utf8'));
126
+ await atomicWrite(path, bytes);
127
+ return { path, requestedSpec, resolvedVersion, manifest, source: 'npm-registry', registry: 'https://registry.npmjs.org', ...computed, integrity };
128
+ }
129
+ export async function fileDigest(path) {
130
+ const bytes = await readFile(path);
131
+ return digest(bytes);
132
+ }
@@ -0,0 +1,4 @@
1
+ /** Describes the only package-manager policy accepted by the preflight runner. */
2
+ export function packageManagerPolicy(online, allowBuild) {
3
+ return { online, allowBuild, lifecycleScripts: allowBuild ? ['preinstall', 'install', 'postinstall', 'prepare'] : [] };
4
+ }
@@ -0,0 +1,66 @@
1
+ import { satisfies } from 'semver';
2
+ function evidence(source, subject, detail, severity) {
3
+ return { source, subject, detail, evidenceKind: 'static', ...(severity === undefined ? {} : { severity }) };
4
+ }
5
+ function rangeCheck(range, actual, subject, source, label, evidenceItems, checks) {
6
+ if (typeof range !== 'string')
7
+ return;
8
+ if (actual === undefined) {
9
+ checks[label] = 'unknown';
10
+ evidenceItems.push(evidence(source, subject, `${label} requires ${range}, but the selected runtime fact is unavailable.`, 'warning'));
11
+ return;
12
+ }
13
+ try {
14
+ const matches = satisfies(actual, range, { includePrerelease: true });
15
+ checks[label] = matches ? 'pass' : 'fail';
16
+ evidenceItems.push(evidence(source, subject, `${label} requires ${range}; observed ${actual}.`, matches ? undefined : 'warning'));
17
+ }
18
+ catch {
19
+ checks[label] = 'unknown';
20
+ evidenceItems.push(evidence(source, subject, `${label} declares an unsupported range ${range}.`, 'warning'));
21
+ }
22
+ }
23
+ function listConstraint(value, actual, source, subject, label, evidenceItems, checks) {
24
+ if (value === undefined)
25
+ return;
26
+ const values = Array.isArray(value) ? value.filter((item) => typeof item === 'string') : typeof value === 'string' ? [value] : [];
27
+ if (values.length === 0) {
28
+ checks[label] = 'unknown';
29
+ evidenceItems.push(evidence(source, subject, `${label} constraint could not be interpreted.`, 'warning'));
30
+ return;
31
+ }
32
+ const excluded = values.some((item) => item.startsWith('!') && item.slice(1) === actual);
33
+ const positive = values.filter((item) => !item.startsWith('!'));
34
+ const matches = !excluded && (positive.length === 0 || positive.includes(actual));
35
+ checks[label] = matches ? 'pass' : 'fail';
36
+ evidenceItems.push(evidence(source, subject, `${label} declares ${values.join(', ')}; observed ${actual}.`, matches ? undefined : 'warning'));
37
+ }
38
+ export function evaluatePackageResolution(input) {
39
+ const evidenceItems = [];
40
+ const checks = {};
41
+ const manifest = input.manifest;
42
+ if (manifest === undefined || input.resolvedVersion === undefined) {
43
+ return { packageName: input.packageName, requestedVersion: input.requestedVersion, resolvedVersion: input.resolvedVersion, status: 'unavailable', source: input.source, integrity: input.integrity, sha512: input.sha512, checks: { artifact: 'unknown' }, evidence: [evidence(input.source, input.packageName, 'Package metadata was not available for resolution.', 'warning')] };
44
+ }
45
+ checks.version = input.resolvedVersion === input.requestedVersion ? 'pass' : 'fail';
46
+ evidenceItems.push(evidence(input.source, input.packageName, `Requested ${input.requestedVersion}; resolved ${input.resolvedVersion}.`, checks.version === 'pass' ? undefined : 'error'));
47
+ const peers = manifest.peerDependencies !== null && typeof manifest.peerDependencies === 'object' ? manifest.peerDependencies : {};
48
+ rangeCheck(peers['@deepseek-ai/dsh'], input.runtime.dsh, input.packageName, input.source, 'DSH peer', evidenceItems, checks);
49
+ rangeCheck(peers['@deepseek-ai/cordis'], input.runtime.cordis, input.packageName, input.source, 'Cordis peer', evidenceItems, checks);
50
+ const engines = manifest.engines !== null && typeof manifest.engines === 'object' ? manifest.engines : {};
51
+ rangeCheck(engines.node, input.runtime.node, input.packageName, input.source, 'Node engine', evidenceItems, checks);
52
+ listConstraint(manifest.os, input.runtime.platform ?? process.platform, input.source, input.packageName, 'OS', evidenceItems, checks);
53
+ listConstraint(manifest.cpu, process.arch, input.source, input.packageName, 'CPU', evidenceItems, checks);
54
+ const repository = manifest.repository;
55
+ const gitRef = typeof manifest.gitHead === 'string' ? manifest.gitHead : undefined;
56
+ if (input.integrity !== undefined || gitRef !== undefined || repository !== undefined) {
57
+ evidenceItems.push(evidence(input.source, input.packageName, `Package provenance: ${input.integrity === undefined ? 'no registry integrity' : input.integrity}${gitRef === undefined ? '' : `; git ref ${gitRef}`}.`));
58
+ }
59
+ else {
60
+ checks.provenance = 'unknown';
61
+ evidenceItems.push(evidence(input.source, input.packageName, 'No registry integrity or fixed Git ref was supplied with this local artifact.', 'warning'));
62
+ }
63
+ const values = Object.values(checks);
64
+ const status = values.includes('fail') ? 'incompatible' : values.includes('unknown') ? 'unknown' : 'compatible';
65
+ return { packageName: input.packageName, requestedVersion: input.requestedVersion, resolvedVersion: input.resolvedVersion, status, source: input.source, integrity: input.integrity, sha512: input.sha512, checks, evidence: evidenceItems };
66
+ }
@@ -0,0 +1,6 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ /** Creates only the directory boundary; callers must copy allow-listed files through AST redaction. */
3
+ export async function createRehearsalDirectory(directory) {
4
+ await mkdir(directory, { recursive: true });
5
+ return directory;
6
+ }
@@ -0,0 +1 @@
1
+ export const runtimeNotRun = { outcome: 'not-run', detail: 'Third-party runtime execution requires an explicit supported isolation backend; a temporary directory is not a security sandbox.' };
@@ -2,17 +2,20 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
- import { parse, stringify } from 'yaml';
6
5
  import { resolveComposition } from './composition-adapter.js';
6
+ import { tryDshDump } from './dsh-dump-provider.js';
7
7
  import { readProfile } from './profile-reader.js';
8
- import { redact } from './redaction.js';
8
+ import { redact, redactYamlPreservingTags } from './redaction.js';
9
9
  import { analyseComposition } from './rules.js';
10
+ import { resolveDshArtifact } from './preflight/artifact-resolver.js';
11
+ import { resolveCandidateArtifact } from './preflight/candidate-artifact-resolver.js';
12
+ import { evaluatePackageResolution } from './preflight/package-resolution.js';
10
13
  const yamlFiles = new Set(['cordis.yml', 'cordis.patch.yml']);
11
14
  const lockFiles = new Set(['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock']);
12
15
  const packageNamePattern = /^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+$/i;
13
16
  const versionPattern = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i;
14
17
  function evidence(source, subject, detail, severity) {
15
- return { source, subject, detail, ...(severity === undefined ? {} : { severity }) };
18
+ return { source, subject, detail, evidenceKind: 'static', ...(severity === undefined ? {} : { severity }) };
16
19
  }
17
20
  /** A digest of the allow-listed metadata surface; file contents are never returned. */
18
21
  export function profileFingerprint(input) {
@@ -40,7 +43,7 @@ function safeMetadataText(relativePath, text) {
40
43
  return `${JSON.stringify(redact(JSON.parse(text)), null, 2)}\n`;
41
44
  }
42
45
  if (yamlFiles.has(relativePath)) {
43
- return stringify(redact(parse(text)));
46
+ return redactYamlPreservingTags(text);
44
47
  }
45
48
  }
46
49
  catch {
@@ -114,6 +117,9 @@ export async function runPreflight(options) {
114
117
  let tempDirectory;
115
118
  let smokeOutcome = 'pass';
116
119
  let smokeDetail = 'No isolated smoke test was run.';
120
+ let packageResolutionOutcome = 'not-run';
121
+ let packageResolutionDetail = 'No candidate package was supplied.';
122
+ const packageResolutions = [];
117
123
  let diagnostics = [];
118
124
  let profileDir = requestedProfile;
119
125
  let extraWarning = false;
@@ -122,7 +128,11 @@ export async function runPreflight(options) {
122
128
  evidenceItems.push(evidence(requestedProfile, 'target-dsh-invalid', 'Target DSH version must be an explicit semantic version.', 'error'));
123
129
  return {
124
130
  schemaVersion: 1, outcome: 'fail', profileDir: requestedProfile, targetDsh: options.targetDsh,
125
- candidates: [], evidence: evidenceItems, diagnostics, smokeTest: { outcome: 'fail', detail: 'The target DSH version was rejected before rehearsal.' }
131
+ candidates: [], evidence: evidenceItems, diagnostics, smokeTest: { outcome: 'fail', detail: 'The target DSH version was rejected before rehearsal.' },
132
+ compositionSmoke: { outcome: 'fail', detail: 'The target DSH version was rejected before rehearsal.' },
133
+ packageResolutionSmoke: { outcome: 'not-run', detail: 'Target version validation failed before package resolution.' },
134
+ packageResolutions: [],
135
+ runtimeSmoke: { outcome: 'not-run', detail: 'Runtime execution was not attempted.' }
126
136
  };
127
137
  }
128
138
  const input = await readProfile({ profileDir: requestedProfile });
@@ -135,27 +145,66 @@ export async function runPreflight(options) {
135
145
  evidenceItems.push(evidence(profileDir, 'candidate-validation', `${invalidCandidates.length} candidate value(s) were omitted because they are not a simple package@version reference.`, 'warning'));
136
146
  }
137
147
  const candidates = options.candidates.filter(safeCandidate);
148
+ const artifact = await resolveDshArtifact({ targetDsh: options.targetDsh, dshBin: options.dshBin, dshPackage: options.dshPackage, online: options.online, cacheDir: options.artifactCacheDir, packageManagerCacheDirs: options.packageManagerCacheDirs });
149
+ const artifactProvenance = [artifact.detail, `source=${artifact.source}`, `requested=${artifact.requestedVersion}`, ...(artifact.resolvedVersion === undefined ? [] : [`resolved=${artifact.resolvedVersion}`]), ...(artifact.integrity === undefined ? [] : [`integrity=${artifact.integrity}`]), ...(artifact.sha512 === undefined ? [] : [`sha512=${artifact.sha512}`])].join(' ');
150
+ evidenceItems.push(evidence(profileDir, 'target-artifact', artifactProvenance, artifact.kind === 'unavailable' || !artifact.targetVersionVerified ? 'warning' : undefined));
151
+ packageResolutions.push(evaluatePackageResolution({ packageName: '@deepseek-ai/dsh', requestedVersion: options.targetDsh, resolvedVersion: artifact.resolvedVersion, manifest: artifact.manifest, source: artifact.source, integrity: artifact.integrity, sha512: artifact.sha512, runtime: { dsh: options.targetDsh, node: process.versions.node, platform: process.platform } }));
152
+ const artifactDiagnostics = artifact.kind === 'unavailable'
153
+ ? [{ id: 'artifact-unavailable', severity: 'warning', title: 'Target DSH artifact unavailable', evidence: [{ source: profileDir, subject: 'target-artifact', detail: artifact.detail, evidenceKind: 'static' }], explanation: 'The requested target DSH release was not available from the permitted offline artifact sources.', remediation: 'Provide --dsh-bin, --dsh-package, a local tarball, or an installed target artifact; use --online only when network resolution is explicitly intended.' }]
154
+ : !artifact.targetVersionVerified
155
+ ? [{ id: 'artifact-version-unverified', severity: 'warning', title: 'Target DSH release is unverified', evidence: [{ source: artifact.path ?? profileDir, subject: 'target-artifact', detail: artifact.detail, evidenceKind: 'static' }], explanation: 'A local artifact candidate was found, but its version was not verified as the requested release.', remediation: 'Run the harness with a locally available exact release and verify its public --version output.' }]
156
+ : [];
157
+ if (artifactDiagnostics.length > 0 || artifact.kind === 'unavailable')
158
+ extraWarning = true;
138
159
  evidenceItems.push(evidence(profileDir, 'target-dsh', `Rehearsal target is DSH ${options.targetDsh}.`));
139
160
  evidenceItems.push(evidence(profileDir, 'candidate-plugins', `${candidates.length} validated candidate plugin reference(s) supplied; values are not copied into the real profile.`));
161
+ const candidateResolutions = [];
140
162
  for (const candidate of candidates) {
141
163
  evidenceItems.push(evidence(profileDir, 'candidate-accepted', `${candidate} was accepted as an exact package@version reference.`));
142
- evidenceItems.push(evidence(profileDir, 'candidate-metadata-inspected', `${candidate} was injected into the isolated package metadata and included in static manifest analysis.`));
164
+ const resolution = await resolveCandidateArtifact(requestedProfile, candidate, options.online, options.artifactCacheDir, options.packageManagerCacheDirs);
165
+ candidateResolutions.push(resolution);
166
+ evidenceItems.push(evidence(profileDir, resolution.status === 'resolved' ? 'candidate-artifact-resolved' : 'candidate-declaration-recorded', resolution.detail, resolution.status === 'resolved' ? undefined : 'warning'));
167
+ packageResolutions.push(resolution.status === 'resolved' && resolution.artifact !== undefined
168
+ ? evaluatePackageResolution({ packageName: resolution.artifact.name, requestedVersion: resolution.artifact.requestedVersion, resolvedVersion: resolution.artifact.installedVersion, manifest: resolution.artifact.manifest, source: resolution.artifact.source, integrity: resolution.artifact.integrity, sha512: resolution.artifact.sha512, runtime: { dsh: options.targetDsh, node: process.versions.node, platform: process.platform } })
169
+ : { packageName: candidateParts(candidate).name, requestedVersion: candidateParts(candidate).version, status: 'unavailable', source: 'unavailable', checks: { artifact: 'unknown' }, evidence: [{ source: profileDir, subject: candidate, detail: resolution.detail, evidenceKind: 'static', severity: 'warning' }] });
143
170
  evidenceItems.push(evidence(profileDir, 'candidate-package-not-installed', `${candidate} was not downloaded or installed.`));
144
171
  evidenceItems.push(evidence(profileDir, 'candidate-runtime-unverified', `${candidate} was not loaded, so runtime compatibility is unverified.`, 'warning'));
145
172
  }
173
+ const nonCompatible = packageResolutions.filter((resolution) => resolution.status !== 'compatible');
174
+ packageResolutionOutcome = nonCompatible.some((resolution) => resolution.status === 'incompatible') ? 'fail' : nonCompatible.length > 0 ? 'warning' : 'pass';
175
+ packageResolutionDetail = packageResolutions.length === 0
176
+ ? 'No target or candidate package artifact was available for package-resolution checks.'
177
+ : `${nonCompatible.length} of ${packageResolutions.length} package artifact resolution(s) are not proven compatible; no package-manager install or lifecycle was executed.`;
146
178
  if (candidates.length > 0)
147
179
  extraWarning = true;
148
- evidenceItems.push(evidence(profileDir, 'network-policy', options.online ? 'Online mode was requested, but this adapter performs no network operation.' : 'Offline mode enforced; no network operation was attempted.'));
149
- if (options.online)
150
- extraWarning = true;
180
+ evidenceItems.push(evidence(profileDir, 'network-policy', options.online ? 'Online mode was explicitly requested; exact registry artifacts may be downloaded into Doctor-owned cache only.' : 'Offline mode enforced; no network operation was attempted.'));
151
181
  tempDirectory = await mkdtemp(join(tmpdir(), 'dsh-doctor-'));
152
182
  await prepareIsolatedProfile(input, tempDirectory, candidates, evidenceItems);
153
183
  evidenceItems.push(evidence(tempDirectory, 'isolated-profile', 'Only redacted, allow-listed composition metadata was copied into the temporary profile.'));
154
184
  try {
155
185
  const isolatedInput = await readProfile({ profileDir: tempDirectory });
156
- const isolatedModel = runtimeForTarget(await resolveComposition(isolatedInput), options.targetDsh);
186
+ const staticModel = runtimeForTarget(await resolveComposition(isolatedInput), options.targetDsh);
187
+ const dumped = artifact.kind === 'dsh-bin' && artifact.path !== undefined ? await tryDshDump(isolatedInput, { dshBin: artifact.path }) : undefined;
188
+ const isolatedModel = dumped === undefined ? staticModel : { ...staticModel, rows: dumped.rows, adapterDiagnostics: [...staticModel.adapterDiagnostics.filter((item) => item.id !== 'runtime-composition-unavailable'), ...dumped.diagnostics], evidenceMode: 'composed' };
157
189
  const report = analyseComposition(isolatedModel);
158
- diagnostics = report.diagnostics;
190
+ const candidateDiagnostics = candidateResolutions.filter((resolution) => resolution.status === 'unavailable').map((resolution) => {
191
+ const candidate = resolution.candidate;
192
+ const { name, version } = candidateParts(candidate);
193
+ return {
194
+ id: 'candidate-artifact-unavailable', severity: 'warning', title: 'Candidate artifact was not resolved',
195
+ evidence: [{ source: tempDirectory ?? profileDir, subject: candidate, packageName: name, version, detail: resolution.detail, evidenceKind: 'static' }],
196
+ explanation: 'A package declaration is not evidence that the candidate package manifest or runtime can be loaded.',
197
+ remediation: 'Provide a local exact artifact or use an explicit supported online resolver, then re-run the isolated rehearsal.'
198
+ };
199
+ });
200
+ const packageDiagnostics = packageResolutions.filter((resolution) => resolution.status !== 'compatible').map((resolution) => ({
201
+ id: `package-resolution-${resolution.status}`, severity: resolution.status === 'incompatible' ? 'error' : 'warning',
202
+ title: `Package resolution is ${resolution.status}`,
203
+ evidence: [...resolution.evidence],
204
+ explanation: `Package metadata resolution for ${resolution.packageName}@${resolution.requestedVersion} is ${resolution.status}.`,
205
+ remediation: resolution.status === 'unavailable' ? 'Provide an exact local artifact or use explicit --online registry resolution.' : 'Supply the missing runtime facts or use a compatible package version.'
206
+ }));
207
+ diagnostics = [...artifactDiagnostics, ...report.diagnostics, ...candidateDiagnostics, ...packageDiagnostics];
159
208
  const errors = diagnostics.filter((item) => item.severity === 'error').length;
160
209
  const warnings = diagnostics.filter((item) => item.severity === 'warning').length;
161
210
  smokeOutcome = errors > 0 ? 'fail' : warnings > 0 ? 'warning' : 'pass';
@@ -206,6 +255,10 @@ export async function runPreflight(options) {
206
255
  evidence: evidenceItems,
207
256
  diagnostics,
208
257
  smokeTest: { outcome: smokeOutcome, detail: smokeDetail },
258
+ compositionSmoke: { outcome: smokeOutcome, detail: smokeDetail },
259
+ packageResolutionSmoke: { outcome: packageResolutionOutcome, detail: packageResolutionDetail },
260
+ packageResolutions,
261
+ runtimeSmoke: { outcome: 'not-run', detail: 'Third-party runtime execution requires an explicit supported isolation backend; the temporary directory is not a security sandbox.' },
209
262
  ...(before === undefined ? {} : { realProfileFingerprintBefore: before }),
210
263
  ...(after === undefined ? {} : { realProfileFingerprintAfter: after }),
211
264
  ...(tempDirectory === undefined ? {} : { tempDirectory })
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { lstat, readFile, realpath, stat } from 'node:fs/promises';
3
3
  import { resolve } from 'node:path';
4
+ import { inspectBundleInventory } from './bundle-inventory.js';
4
5
  const allowedFiles = [
5
6
  'cordis.yml',
6
7
  'cordis.patch.yml',
@@ -52,13 +53,19 @@ export async function readProfile({ profileDir }) {
52
53
  throw error;
53
54
  }
54
55
  }
56
+ const packageFile = files.find((file) => file.relativePath === 'package.json');
57
+ const inventory = packageFile === undefined
58
+ ? { packages: [], diagnostics: [] }
59
+ : await inspectBundleInventory(resolvedRoot, packageFile.text);
55
60
  return {
56
61
  profileDir: resolvedRoot,
57
62
  files,
63
+ installedPackages: inventory.packages,
64
+ inventoryDiagnostics: inventory.diagnostics,
58
65
  metadataCoverage: {
59
66
  mode: 'allow-listed-root-metadata',
60
67
  scannedFiles: files.map((file) => file.relativePath).sort(),
61
- unscannedSurfaces: ['nested plugin manifests and bundle metadata', 'environment files and values', 'private keys, tokens, sessions, and workspace source files']
68
+ unscannedSurfaces: ['non-declared nested plugin manifests and non-selected package metadata', 'environment files and values', 'private keys, tokens, sessions, and workspace source files']
62
69
  }
63
70
  };
64
71
  }