feat: enforce published dependency policy

This commit is contained in:
imccyu 2026-08-26 15:49:42 +08:00
parent ee309c0794
commit de256e8bc1
14 changed files with 1804 additions and 450 deletions

View file

@ -50,6 +50,8 @@
"test:web:perf": "npm run build && npm run test:web:perf:built", "test:web:perf": "npm run build && npm run test:web:perf:built",
"test:web:perf:built": "DSH_SNAPSHOT=replay vitest run --config vitest.web.perf.config.ts", "test:web:perf:built": "DSH_SNAPSHOT=replay vitest run --config vitest.web.perf.config.ts",
"test:web:stress": "npm run build && vitest run --config vitest.web-stress.config.ts", "test:web:stress": "npm run build && vitest run --config vitest.web-stress.config.ts",
"benchmark:npm-resolution": "tsx scripts/benchmark-npm-resolution.ts",
"benchmark:npm-resolution:next": "tsx scripts/benchmark-next-package-dependency.ts",
"test:gui": "vitest run packages/client packages/host", "test:gui": "vitest run packages/client packages/host",
"check:all": "tsx scripts/run-gates.ts check-all", "check:all": "tsx scripts/run-gates.ts check-all",
"check:ci": "tsx scripts/run-gates.ts ci-primary", "check:ci": "tsx scripts/run-gates.ts ci-primary",
@ -104,6 +106,7 @@
"verify-optional-dependency-imports": "tsx scripts/verify-optional-dependency-imports.ts", "verify-optional-dependency-imports": "tsx scripts/verify-optional-dependency-imports.ts",
"verify-runtime-closure": "tsx scripts/verify-runtime-closure.ts", "verify-runtime-closure": "tsx scripts/verify-runtime-closure.ts",
"verify-application-entrypoints": "tsx scripts/verify-application-entrypoints.ts", "verify-application-entrypoints": "tsx scripts/verify-application-entrypoints.ts",
"verify-package-dependencies": "tsx scripts/verify-package-dependencies.ts",
"verify-client-packages": "tsx scripts/verify-client-packages.ts", "verify-client-packages": "tsx scripts/verify-client-packages.ts",
"verify-client-ui-i18n": "tsx scripts/verify-client-ui-i18n.ts", "verify-client-ui-i18n": "tsx scripts/verify-client-ui-i18n.ts",
"verify-vendored-links": "tsx scripts/verify-vendored-links.ts", "verify-vendored-links": "tsx scripts/verify-vendored-links.ts",

View file

@ -0,0 +1,114 @@
import { describe, expect, it } from 'vitest'
import {
applyFactsToRegistry,
discoverBenchmarkCandidates,
parseNextPackageBenchmarkOptions,
type MutableRegistryManifest,
} from './benchmark-next-package-dependency.ts'
import type {
PackageDependencyFacts,
PackageDependencyManifest,
WorkspacePackageManifest,
} from './verify-package-dependencies.ts'
import type { RegistryIndex } from './benchmark-npm-resolution.ts'
describe('next package benchmark options', () => {
it('parses candidate and repetition controls', () => {
expect(parseNextPackageBenchmarkOptions([
'--',
'--candidates=@f/a,@f/b',
'--runs=2',
'--finalist-runs=4',
'--finalists=3',
'--jobs=6',
'--timeout-ms=9000',
])).toEqual({
candidates: ['@f/a', '@f/b'],
coarseRuns: 2,
finalistRuns: 4,
finalists: 3,
jobs: 6,
timeoutMs: 9000,
})
})
it('rejects invalid positive integers', () => {
expect(() => parseNextPackageBenchmarkOptions(['--jobs=0'])).toThrow('--jobs must be a positive integer')
})
})
describe('next package benchmark graph', () => {
it('applies a source-derived candidate without changing the filesystem', () => {
const manifest: PackageDependencyManifest & { version: string } = {
name: '@f/probe',
version: '1.0.0',
peerDependencies: {
'@deepseek-ai/cordis': 'workspace:^',
'@f/runtime': 'workspace:^',
'@f/types': 'workspace:^',
},
devDependencies: {
'@deepseek-ai/cordis': 'workspace:^',
'@f/runtime': 'workspace:^',
'@f/types': 'workspace:^',
},
}
const facts: PackageDependencyFacts = {
manifestPath: 'packages/g/probe/package.json',
role: 'configured-host',
manifest,
workspaceNames: new Set(['@deepseek-ai/cordis', '@f/probe', '@f/runtime', '@f/types']),
allSourceUses: new Map([
['@f/runtime', ['packages/g/probe/src/index.ts']],
['@f/types', ['packages/g/probe/src/types.ts']],
]),
hostRuntimeSourceUses: new Map([['@f/runtime', ['packages/g/probe/src/index.ts']]]),
clientInject: new Set(),
}
const index = new Map<string, Map<string, MutableRegistryManifest>>([
['@f/probe', new Map([['1.0.0', structuredClone(manifest) as MutableRegistryManifest]])],
])
applyFactsToRegistry(index, facts, new Map([
['@deepseek-ai/cordis', '4.0.1'],
['@f/probe', '1.0.0'],
['@f/runtime', '2.0.0'],
['@f/types', '3.0.0'],
]))
expect(index.get('@f/probe')?.get('1.0.0')).toMatchObject({
dependencies: { '@f/runtime': '^2.0.0' },
peerDependencies: { '@deepseek-ai/cordis': '^4.0.1' },
})
expect(index.get('@f/probe')?.get('1.0.0')?.dependencies).not.toHaveProperty('@f/types')
})
it('finds reachable unconfigured packages with non-Cordis peers', () => {
const index = new Map([
['@deepseek-ai/dsh', new Map([['1.0.0', {
name: '@deepseek-ai/dsh', version: '1.0.0', dependencies: { '@f/a': '^1.0.0', '@f/b': '^1.0.0' },
}]])],
['@f/a', new Map([['1.0.0', {
name: '@f/a', version: '1.0.0', peerDependencies: { '@f/runtime': '^1.0.0' },
}]])],
['@f/b', new Map([['1.0.0', {
name: '@f/b', version: '1.0.0', peerDependencies: { '@deepseek-ai/cordis': '^4.0.0' },
}]])],
['@f/runtime', new Map([['1.0.0', { name: '@f/runtime', version: '1.0.0' }]])],
]) as RegistryIndex
const release = new Map<string, WorkspacePackageManifest>([
['@f/a', {
name: '@f/a', dir: 'packages/g/a', manifestPath: 'packages/g/a/package.json', manifest: { name: '@f/a' },
}],
['@f/b', {
name: '@f/b', dir: 'packages/g/b', manifestPath: 'packages/g/b/package.json', manifest: { name: '@f/b' },
}],
])
expect(discoverBenchmarkCandidates(
index,
new Map([['@deepseek-ai/dsh', '1.0.0'], ['@f/a', '1.0.0'], ['@f/b', '1.0.0']]),
release,
new Set(),
)).toEqual(['@f/a'])
})
})

View file

@ -0,0 +1,271 @@
/** Benchmark which additional Host package most reduces npm peer resolution. */
import { availableParallelism } from 'node:os'
import { resolve } from 'node:path'
import { parseArgs } from 'node:util'
import {
benchmarkNpmResolution,
buildRegistryIndex,
parsePositiveIntegerOption,
publishWorkspaceRange,
type RegistryIndex,
} from './benchmark-npm-resolution.ts'
import {
readPackageDependencyFacts,
readPackageDependencyState,
readWorkspacePackageManifests,
repairPackageDependencyManifest,
type PackageDependencyFacts,
type WorkspacePackageManifest,
} from './verify-package-dependencies.ts'
const TARGET_PACKAGE = '@deepseek-ai/dsh'
const CORDIS = '@deepseek-ai/cordis'
interface Options {
readonly candidates?: readonly string[]
readonly coarseRuns: number
readonly finalistRuns: number
readonly finalists: number
readonly jobs: number
readonly timeoutMs: number
}
export interface MutableRegistryManifest {
name: string
version: string
dependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
peerDependencies?: Record<string, string>
peerDependenciesMeta?: Record<string, { optional?: boolean }>
}
interface Measurement {
readonly package: string
readonly seconds: readonly number[]
readonly medianSeconds: number
}
/** Parse benchmark selection and repetition options. */
export function parseNextPackageBenchmarkOptions(args: readonly string[]): Options {
const normalized = args[0] === '--' ? args.slice(1) : args
const { values } = parseArgs({
args: [...normalized],
options: {
candidates: { type: 'string' },
runs: { type: 'string' },
'finalist-runs': { type: 'string' },
finalists: { type: 'string' },
jobs: { type: 'string' },
'timeout-ms': { type: 'string' },
},
allowPositionals: false,
})
return {
...(values.candidates === undefined
? {}
: { candidates: values.candidates.split(',').filter(Boolean) }),
coarseRuns: parsePositiveIntegerOption(values.runs, 1, '--runs'),
finalistRuns: parsePositiveIntegerOption(values['finalist-runs'], 3, '--finalist-runs'),
finalists: parsePositiveIntegerOption(values.finalists, 5, '--finalists'),
jobs: parsePositiveIntegerOption(values.jobs, Math.min(8, availableParallelism()), '--jobs'),
timeoutMs: parsePositiveIntegerOption(values['timeout-ms'], 120_000, '--timeout-ms'),
}
}
function median(values: readonly number[]): number {
const sorted = [...values].sort((left, right) => left - right)
const middle = Math.floor(sorted.length / 2)
return sorted.length % 2 === 0
? ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2
: sorted[middle] ?? 0
}
function cloneIndex(index: RegistryIndex): Map<string, Map<string, MutableRegistryManifest>> {
return new Map([...index].map(([name, versions]) => [
name,
new Map([...versions].map(([version, manifest]) => [
version,
structuredClone(manifest) as MutableRegistryManifest,
])),
]))
}
function publishedSection(
values: Readonly<Record<string, string>> | undefined,
workspaceVersions: ReadonlyMap<string, string>,
): Record<string, string> | undefined {
if (values === undefined) return undefined
return Object.fromEntries(Object.entries(values).map(([name, range]) => {
const version = workspaceVersions.get(name)
return [name, version === undefined ? range : publishWorkspaceRange(range, version)]
}))
}
/** Apply one source-derived policy result to an in-memory registry manifest. */
export function applyFactsToRegistry(
index: Map<string, Map<string, MutableRegistryManifest>>,
facts: PackageDependencyFacts,
workspaceVersions: ReadonlyMap<string, string>,
): void {
const source = structuredClone(facts.manifest)
repairPackageDependencyManifest({ ...facts, manifest: source })
const version = workspaceVersions.get(source.name ?? '')
const target = version === undefined ? undefined : index.get(source.name ?? '')?.get(version)
if (target === undefined) throw new Error(`local registry has no ${source.name ?? 'unnamed package'}@${version ?? 'unknown'}`)
for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies'] as const) {
const values = publishedSection(source[field], workspaceVersions)
if (values !== undefined) target[field] = values
else if (field === 'dependencies') delete target.dependencies
else if (field === 'optionalDependencies') delete target.optionalDependencies
else delete target.peerDependencies
}
if (source.peerDependenciesMeta === undefined) delete target.peerDependenciesMeta
else target.peerDependenciesMeta = structuredClone(source.peerDependenciesMeta) as Record<string, { optional?: boolean }>
}
function currentVersion(pkg: WorkspacePackageManifest): string {
const version = pkg.manifest.version
if (typeof version !== 'string') throw new Error(`${pkg.manifestPath}: missing package version`)
return version
}
/** Find reachable Host candidates whose published manifests still carry non-Cordis peers. */
export function discoverBenchmarkCandidates(
index: RegistryIndex,
workspaceVersions: ReadonlyMap<string, string>,
releasePackages: ReadonlyMap<string, WorkspacePackageManifest>,
policyPackages: ReadonlySet<string>,
): string[] {
const reached = new Set<string>()
const queue = [TARGET_PACKAGE]
for (let cursor = 0; cursor < queue.length; cursor += 1) {
const name = queue[cursor]
if (name === undefined || reached.has(name)) continue
const version = workspaceVersions.get(name)
const manifest = version === undefined ? undefined : index.get(name)?.get(version)
if (manifest === undefined) continue
reached.add(name)
const installed = {
...manifest.dependencies,
...manifest.optionalDependencies,
...Object.fromEntries(Object.entries(manifest.peerDependencies ?? {})
.filter(([peer]) => (manifest.peerDependenciesMeta?.[peer] as { optional?: boolean } | undefined)?.optional !== true)),
}
for (const dependency of Object.keys(installed).sort()) {
if (!reached.has(dependency)) queue.push(dependency)
}
}
return [...reached].filter((name) => {
if (policyPackages.has(name) || !releasePackages.has(name)) return false
const version = workspaceVersions.get(name)
const manifest = version === undefined ? undefined : index.get(name)?.get(version)
return Object.keys(manifest?.peerDependencies ?? {}).some(peer => peer !== CORDIS)
}).sort()
}
async function measure(
index: RegistryIndex,
targetVersion: string,
runs: number,
timeoutMs: number,
): Promise<number[]> {
const seconds: number[] = []
for (let run = 0; run < runs; run += 1) {
const result = await benchmarkNpmResolution(index, targetVersion, timeoutMs)
if (result.archiveRequests > 0) throw new Error('metadata-only benchmark requested package archives')
seconds.push(Number((result.durationMs / 1000).toFixed(2)))
}
return seconds
}
async function mapConcurrent<T, R>(
values: readonly T[],
jobs: number,
operation: (value: T) => Promise<R>,
): Promise<R[]> {
const results: R[] = []
let next = 0
await Promise.all(Array.from({ length: Math.min(jobs, values.length) }, async () => {
while (next < values.length) {
const index = next
next += 1
const value = values[index]
if (value === undefined) return
results[index] = await operation(value)
}
}))
return results
}
async function main(): Promise<void> {
const options = parseNextPackageBenchmarkOptions(process.argv.slice(2))
const root = resolve(import.meta.dirname, '..')
const packages = readWorkspacePackageManifests(root)
const workspaceVersions = new Map(packages.all.map(pkg => [pkg.name, currentVersion(pkg)]))
const releaseByName = new Map(packages.release.map(pkg => [pkg.name, pkg]))
const state = readPackageDependencyState(root)
if (state.policyViolations.length > 0) throw new Error(state.policyViolations.join('\n'))
const base = cloneIndex(buildRegistryIndex(root))
for (const facts of state.facts) applyFactsToRegistry(base, facts, workspaceVersions)
const targetVersion = workspaceVersions.get(TARGET_PACKAGE)
if (targetVersion === undefined) throw new Error(`workspace has no ${TARGET_PACKAGE}`)
const policyNames = new Set(state.facts.map(facts => facts.manifest.name).filter(name => name !== undefined))
const discovered = discoverBenchmarkCandidates(base, workspaceVersions, releaseByName, policyNames)
const candidates = options.candidates ?? discovered
for (const name of candidates) {
if (!discovered.includes(name)) throw new Error(`${name} is not a reachable unconfigured Host candidate`)
}
const candidateFacts = new Map(candidates.map((name) => {
const pkg = releaseByName.get(name)
if (pkg === undefined) throw new Error(`release set has no ${name}`)
return [name, readPackageDependencyFacts(root, pkg, 'configured-host', state.workspaceNames)]
}))
const baselineSeconds = await measure(base, targetVersion, options.finalistRuns, options.timeoutMs)
const baseline = median(baselineSeconds)
console.log(JSON.stringify({ type: 'baseline', seconds: baselineSeconds, medianSeconds: baseline }))
const coarse = await mapConcurrent(candidates, options.jobs, async (name): Promise<Measurement> => {
const index = cloneIndex(base)
const facts = candidateFacts.get(name)
if (facts === undefined) throw new Error(`missing source facts for ${name}`)
applyFactsToRegistry(index, facts, workspaceVersions)
const seconds = await measure(index, targetVersion, options.coarseRuns, options.timeoutMs)
const result = { package: name, seconds, medianSeconds: median(seconds) }
console.log(JSON.stringify({ type: 'coarse', ...result }))
return result
})
const finalists = coarse.sort((left, right) => left.medianSeconds - right.medianSeconds)
.slice(0, options.finalists)
const measured: Measurement[] = []
for (const finalist of finalists) {
const index = cloneIndex(base)
const facts = candidateFacts.get(finalist.package)
if (facts === undefined) throw new Error(`missing source facts for ${finalist.package}`)
applyFactsToRegistry(index, facts, workspaceVersions)
const seconds = await measure(index, targetVersion, options.finalistRuns, options.timeoutMs)
measured.push({ package: finalist.package, seconds, medianSeconds: median(seconds) })
}
const ranking = measured.sort((left, right) => left.medianSeconds - right.medianSeconds)
.map(result => ({
...result,
gainSeconds: Number((baseline - result.medianSeconds).toFixed(2)),
}))
console.log(JSON.stringify({
type: 'result',
baselineSeconds,
baselineMedianSeconds: baseline,
candidateCount: candidates.length,
ranking,
}, null, 2))
}
if (import.meta.main) {
try {
await main()
} catch (error) {
console.error(`benchmark-next-package-dependency: ${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
}
}

View file

@ -0,0 +1,84 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
benchmarkNpmResolution,
buildRegistryIndex,
parseBenchmarkOptions,
publishWorkspaceRange,
type RegistryIndex,
} from './benchmark-npm-resolution.ts'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function writeJson(root: string, path: string, value: unknown): void {
const absolute = join(root, path)
mkdirSync(dirname(absolute), { recursive: true })
writeFileSync(absolute, `${JSON.stringify(value, null, 2)}\n`)
}
describe('npm resolution benchmark', () => {
it('parses repeat, timeout, threshold, and ref options', () => {
expect(parseBenchmarkOptions([])).toEqual({ runs: 1, timeoutMs: 300_000 })
expect(parseBenchmarkOptions([
'--runs', '3', '--timeout-ms', '45000', '--max-ms', '20000', '--ref', 'master',
])).toEqual({ runs: 3, timeoutMs: 45_000, maxMs: 20_000, ref: 'master' })
expect(parseBenchmarkOptions(['--', '--runs', '2'])).toEqual({ runs: 2, timeoutMs: 300_000 })
expect(() => parseBenchmarkOptions(['--runs', '0'])).toThrow('--runs must be a positive integer')
})
it('projects workspace protocols to published ranges', () => {
expect(publishWorkspaceRange('workspace:^', '1.2.3')).toBe('^1.2.3')
expect(publishWorkspaceRange('workspace:~', '1.2.3')).toBe('~1.2.3')
expect(publishWorkspaceRange('workspace:*', '1.2.3')).toBe('1.2.3')
expect(publishWorkspaceRange('^4.0.0', '1.2.3')).toBe('^4.0.0')
})
it('combines installed metadata with current publishable workspace fields', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-npm-registry-index-'))
roots.push(root)
writeJson(root, 'node_modules/.pnpm/external@2.0.0/node_modules/external/package.json', {
name: 'external',
version: '2.0.0',
dependencies: { child: '^1.0.0' },
devDependencies: { ignored: '^1.0.0' },
})
writeJson(root, 'apps/cli/package.json', {
name: '@deepseek-ai/dsh',
version: '0.1.0',
dependencies: { '@deepseek-ai/dsh-child': 'workspace:^', external: '^2.0.0' },
devDependencies: { ignored: 'workspace:^' },
})
writeJson(root, 'packages/core/child/package.json', {
name: '@deepseek-ai/dsh-child',
version: '0.1.0',
})
const index = buildRegistryIndex(root)
expect(index.get('external')?.get('2.0.0')).toMatchObject({ dependencies: { child: '^1.0.0' } })
expect(index.get('@deepseek-ai/dsh')?.get('0.1.0')).toEqual({
name: '@deepseek-ai/dsh',
version: '0.1.0',
dependencies: { '@deepseek-ai/dsh-child': '^0.1.0', external: '^2.0.0' },
})
})
it('runs npm against the local registry without requesting an archive', async () => {
const index: RegistryIndex = new Map([[
'@deepseek-ai/dsh',
new Map([['0.1.0', { name: '@deepseek-ai/dsh', version: '0.1.0' }]]),
]])
const result = await benchmarkNpmResolution(index, '0.1.0', 10_000)
expect(result.durationMs).toBeGreaterThan(0)
expect(result.registryRequests).toBeGreaterThan(0)
expect(result.archiveRequests).toBe(0)
expect(result.unknownPackages).toEqual([])
})
})

View file

@ -0,0 +1,417 @@
/** Benchmark npm's dependency-tree resolution against an all-local registry. */
import { execFileSync, spawn } from 'node:child_process'
import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { createServer, type Server } from 'node:http'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { parseArgs } from 'node:util'
const TARGET_PACKAGE = '@deepseek-ai/dsh'
const DEFAULT_TIMEOUT_MS = 300_000
const WORKSPACE_MANIFEST_GLOBS = [
'apps/*/package.json',
'packages/*/*/package.json',
'vendor/*/package.json',
'native/landlock-run/package.json',
'native/landlock-run/packages/*/package.json',
]
const INSTALLED_MANIFEST_GLOBS = [
'node_modules/.pnpm/*/node_modules/*/package.json',
'node_modules/.pnpm/*/node_modules/@*/*/package.json',
]
const PUBLISHED_FIELDS = [
'dependencies',
'optionalDependencies',
'peerDependencies',
'peerDependenciesMeta',
'engines',
'os',
'cpu',
'bin',
] as const
interface PackageManifest {
readonly name?: unknown
readonly version?: unknown
readonly dependencies?: Record<string, string>
readonly optionalDependencies?: Record<string, string>
readonly peerDependencies?: Record<string, string>
readonly peerDependenciesMeta?: Record<string, unknown>
readonly engines?: unknown
readonly os?: unknown
readonly cpu?: unknown
readonly bin?: unknown
}
interface RegistryVersion extends PackageManifest {
readonly name: string
readonly version: string
}
/** Package versions served by the local benchmark registry. */
export type RegistryIndex = ReadonlyMap<string, ReadonlyMap<string, RegistryVersion>>
/** Parsed command-line options for one benchmark invocation. */
export interface BenchmarkOptions {
readonly ref?: string
readonly runs: number
readonly timeoutMs: number
readonly maxMs?: number
}
/** One measured npm resolution. */
export interface BenchmarkRun {
readonly durationMs: number
readonly registryRequests: number
readonly archiveRequests: number
readonly unknownPackages: readonly string[]
}
/** Parse one positive-integer command-line option or use its default. */
export function parsePositiveIntegerOption(raw: string | undefined, fallback: number, name: string): number {
if (raw === undefined) return fallback
const value = Number.parseInt(raw, 10)
if (!Number.isSafeInteger(value) || value < 1 || String(value) !== raw) {
throw new Error(`${name} must be a positive integer, got ${JSON.stringify(raw)}`)
}
return value
}
/**
* Parse supported benchmark arguments.
* @param args - Command-line arguments after the script path.
* @returns Validated benchmark options.
*/
export function parseBenchmarkOptions(args: readonly string[]): BenchmarkOptions {
const normalized = args[0] === '--' ? args.slice(1) : args
const { values } = parseArgs({
args: [...normalized],
options: {
ref: { type: 'string' },
runs: { type: 'string' },
'timeout-ms': { type: 'string' },
'max-ms': { type: 'string' },
},
allowPositionals: false,
})
const maxMs = values['max-ms'] === undefined
? undefined
: parsePositiveIntegerOption(values['max-ms'], 0, '--max-ms')
return {
runs: parsePositiveIntegerOption(values.runs, 1, '--runs'),
timeoutMs: parsePositiveIntegerOption(values['timeout-ms'], DEFAULT_TIMEOUT_MS, '--timeout-ms'),
...(values.ref === undefined ? {} : { ref: values.ref }),
...(maxMs === undefined ? {} : { maxMs }),
}
}
function workspaceManifestPath(path: string): boolean {
return /^(?:apps\/[^/]+|packages\/[^/]+\/[^/]+|vendor\/[^/]+|native\/landlock-run(?:\/packages\/[^/]+)?)\/package\.json$/.test(path)
}
function workspaceManifestPaths(root: string, ref: string | undefined): string[] {
if (ref === undefined) return globSync(WORKSPACE_MANIFEST_GLOBS, { cwd: root }).sort()
return execFileSync('git', ['ls-tree', '-r', '--name-only', ref, '--', 'apps', 'packages', 'vendor', 'native'], {
cwd: root,
encoding: 'utf8',
}).split('\n').filter(workspaceManifestPath).sort()
}
function readGitFiles(root: string, ref: string, paths: readonly string[]): ReadonlyMap<string, string> {
const output = execFileSync('git', ['cat-file', '--batch'], {
cwd: root,
input: paths.map(path => `${ref}:${path}\n`).join(''),
maxBuffer: 64 * 1024 * 1024,
})
const contents = new Map<string, string>()
let offset = 0
for (const path of paths) {
const headerEnd = output.indexOf(0x0a, offset)
if (headerEnd < 0) throw new Error(`git cat-file returned no header for ${ref}:${path}`)
const header = output.subarray(offset, headerEnd).toString('utf8')
if (header.endsWith(' missing')) throw new Error(`git ref ${ref} has no ${path}`)
const size = Number.parseInt(header.split(' ')[2] ?? '', 10)
if (!Number.isSafeInteger(size) || size < 0) {
throw new Error(`git cat-file returned an invalid size for ${ref}:${path}`)
}
const contentStart = headerEnd + 1
const contentEnd = contentStart + size
if (output[contentEnd] !== 0x0a) throw new Error(`git cat-file truncated ${ref}:${path}`)
contents.set(path, output.subarray(contentStart, contentEnd).toString('utf8'))
offset = contentEnd + 1
}
return contents
}
/**
* Convert a workspace protocol range to the range published by pnpm pack.
* @param range - Dependency range from a workspace manifest.
* @param targetVersion - Current version of the referenced workspace package.
* @returns The registry-facing semver range.
*/
export function publishWorkspaceRange(range: string, targetVersion: string): string {
if (range === 'workspace:*') return targetVersion
if (range === 'workspace:^') return `^${targetVersion}`
if (range === 'workspace:~') return `~${targetVersion}`
if (range.startsWith('workspace:')) return range.slice('workspace:'.length)
return range
}
function copyPublishedManifest(
source: PackageManifest,
workspaceVersions: ReadonlyMap<string, string>,
): RegistryVersion | undefined {
if (typeof source.name !== 'string' || typeof source.version !== 'string') return undefined
const output: Record<string, unknown> = { name: source.name, version: source.version }
for (const field of PUBLISHED_FIELDS) {
const value = source[field]
if (value === undefined) continue
if (field === 'dependencies' || field === 'optionalDependencies' || field === 'peerDependencies') {
output[field] = Object.fromEntries(Object.entries(value as Record<string, string>).map(([name, range]) => {
const targetVersion = workspaceVersions.get(name)
return [name, targetVersion === undefined ? range : publishWorkspaceRange(range, targetVersion)]
}))
} else {
output[field] = structuredClone(value)
}
}
return output as unknown as RegistryVersion
}
function addManifest(index: Map<string, Map<string, RegistryVersion>>, manifest: RegistryVersion): void {
const versions = index.get(manifest.name) ?? new Map<string, RegistryVersion>()
versions.set(manifest.version, manifest)
index.set(manifest.name, versions)
}
/**
* Build registry metadata from installed external packages and workspace manifests.
* @param root - Repository root containing the pnpm virtual store.
* @param ref - Optional Git ref used instead of working-tree workspace manifests.
* @returns Package metadata served by the benchmark registry.
*/
export function buildRegistryIndex(root: string, ref?: string): RegistryIndex {
const index = new Map<string, Map<string, RegistryVersion>>()
for (const path of globSync(INSTALLED_MANIFEST_GLOBS, { cwd: root }).sort()) {
const manifest = JSON.parse(readFileSync(resolve(root, path), 'utf8')) as PackageManifest
const copied = copyPublishedManifest(manifest, new Map())
if (copied !== undefined) addManifest(index, copied)
}
const paths = workspaceManifestPaths(root, ref)
const refContents = ref === undefined ? undefined : readGitFiles(root, ref, paths)
const workspace = paths.map(path =>
JSON.parse(refContents?.get(path) ?? readFileSync(resolve(root, path), 'utf8')) as PackageManifest)
const workspaceVersions = new Map(workspace.flatMap(manifest =>
typeof manifest.name === 'string' && typeof manifest.version === 'string'
? [[manifest.name, manifest.version] as const]
: []))
for (const manifest of workspace) {
const copied = copyPublishedManifest(manifest, workspaceVersions)
if (copied !== undefined) addManifest(index, copied)
}
return index
}
function latestVersion(versions: ReadonlyMap<string, RegistryVersion>): string {
const sorted = [...versions.keys()].sort((left, right) => left.localeCompare(right, 'en', { numeric: true }))
const latest = sorted.at(-1)
if (latest === undefined) throw new Error('local registry package has no versions')
return latest
}
function listen(server: Server): Promise<number> {
return new Promise((resolveListen, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', () => {
server.off('error', reject)
const address = server.address()
if (address === null || typeof address === 'string') {
reject(new Error('local registry did not expose a TCP port'))
return
}
resolveListen(address.port)
})
})
}
function close(server: Server): Promise<void> {
return new Promise((resolveClose, reject) => {
server.close((error) => {
if (error === undefined) resolveClose()
else reject(error)
})
})
}
function npmExecutable(): string {
return process.platform === 'win32' ? 'npm.cmd' : 'npm'
}
async function runNpm(
cwd: string,
registry: string,
timeoutMs: number,
): Promise<{ durationMs: number; output: string; timedOut: boolean }> {
const started = performance.now()
const child = spawn(npmExecutable(), [
'install',
'--package-lock-only',
'--ignore-scripts',
'--no-audit',
'--no-fund',
'--loglevel=error',
`--registry=${registry}`,
], {
cwd,
// Windows resolves npm through a .cmd shim, which spawn() refuses
// without a shell since the CVE-2024-27980 hardening.
shell: process.platform === 'win32',
env: {
...process.env,
npm_config_cache: join(cwd, '.npm-cache'),
npm_config_update_notifier: 'false',
},
stdio: ['ignore', 'pipe', 'pipe'],
})
let output = ''
child.stdout.setEncoding('utf8')
child.stderr.setEncoding('utf8')
child.stdout.on('data', (chunk) => { output += String(chunk) })
child.stderr.on('data', (chunk) => { output += String(chunk) })
const outcome = await new Promise<{ status: number | null; timedOut: boolean }>((resolveExit, reject) => {
let timeoutReached = false
const timer = setTimeout(() => {
timeoutReached = true
child.kill('SIGTERM')
}, timeoutMs)
child.once('error', reject)
child.once('exit', (status) => {
clearTimeout(timer)
resolveExit({ status, timedOut: timeoutReached })
})
})
const durationMs = performance.now() - started
if (outcome.timedOut) return { durationMs, output, timedOut: true }
if (outcome.status !== 0) {
throw new Error(`npm install exited ${String(outcome.status)} after ${durationMs.toFixed(0)} ms\n${output.trim()}`)
}
return { durationMs, output, timedOut: false }
}
/**
* Resolve the CLI install graph once without downloading package archives.
* @param index - Package metadata exposed through the local registry.
* @param targetVersion - Version of `@deepseek-ai/dsh` to install.
* @param timeoutMs - Hard wall-clock limit for the npm child process.
* @returns Timing and registry-request observations.
*/
export async function benchmarkNpmResolution(
index: RegistryIndex,
targetVersion: string,
timeoutMs: number,
): Promise<BenchmarkRun> {
let registryRequests = 0
let archiveRequests = 0
const unknownPackages = new Set<string>()
let registry = ''
const server = createServer((request, response) => {
registryRequests++
const pathname = new URL(request.url ?? '/', registry).pathname
if (pathname.startsWith('/tarballs/')) {
archiveRequests++
response.writeHead(500, { 'content-type': 'application/json' })
response.end(JSON.stringify({ error: 'package-lock-only benchmark requested an archive' }))
return
}
const name = decodeURIComponent(pathname.slice(1))
const versions = index.get(name)
if (versions === undefined) {
unknownPackages.add(name)
response.writeHead(404, { 'content-type': 'application/json' })
response.end(JSON.stringify({ error: 'not_found' }))
return
}
const materialized = Object.fromEntries([...versions].map(([version, manifest]) => [version, {
...manifest,
dist: { tarball: `${registry}tarballs/${encodeURIComponent(name)}-${version}.tgz` },
}]))
const body = JSON.stringify({
name,
'dist-tags': { latest: latestVersion(versions) },
versions: materialized,
})
response.writeHead(200, {
'content-type': 'application/json',
'content-length': Buffer.byteLength(body),
})
response.end(body)
})
const port = await listen(server)
registry = `http://127.0.0.1:${String(port)}/`
const consumer = mkdtempSync(join(tmpdir(), 'dsh-npm-resolution-'))
try {
writeFileSync(join(consumer, 'package.json'), `${JSON.stringify({
name: 'dsh-npm-resolution-benchmark',
version: '0.0.0',
private: true,
dependencies: { [TARGET_PACKAGE]: targetVersion },
}, null, 2)}\n`)
const result = await runNpm(consumer, registry, timeoutMs)
if (result.timedOut) throw new Error(`npm resolution exceeded ${String(timeoutMs)} ms`)
return {
durationMs: result.durationMs,
registryRequests,
archiveRequests,
unknownPackages: [...unknownPackages].sort(),
}
} finally {
await close(server)
rmSync(consumer, { recursive: true, force: true })
}
}
async function main(): Promise<void> {
const options = parseBenchmarkOptions(process.argv.slice(2))
const root = resolve(import.meta.dirname, '..')
const started = performance.now()
const index = buildRegistryIndex(root, options.ref)
const targetVersions = index.get(TARGET_PACKAGE)
if (targetVersions === undefined) throw new Error(`local registry contains no ${TARGET_PACKAGE}`)
const targetVersion = latestVersion(targetVersions)
const npmVersion = execFileSync(npmExecutable(), ['--version'], { encoding: 'utf8' }).trim()
console.log(
`benchmark-npm-resolution: npm ${npmVersion}, ${options.ref === undefined ? 'working tree' : options.ref}, `
+ `${String(index.size)} package name(s), setup ${(performance.now() - started).toFixed(0)} ms.`,
)
const durations: number[] = []
for (let run = 1; run <= options.runs; run++) {
const result = await benchmarkNpmResolution(index, targetVersion, options.timeoutMs)
durations.push(result.durationMs)
console.log(
`benchmark-npm-resolution: run ${String(run)}/${String(options.runs)} resolved ${TARGET_PACKAGE}@${targetVersion}`
+ ` in ${(result.durationMs / 1000).toFixed(2)} s with ${String(result.registryRequests)} metadata request(s)`
+ ` and ${String(result.unknownPackages.length)} local 404 package name(s).`,
)
if (result.archiveRequests > 0) throw new Error('npm requested package archives during the metadata-only benchmark')
}
const minimum = Math.min(...durations)
const maximum = Math.max(...durations)
console.log(
`benchmark-npm-resolution: ${String(options.runs)} run(s), min ${(minimum / 1000).toFixed(2)} s, max ${(maximum / 1000).toFixed(2)} s.`,
)
if (options.maxMs !== undefined && maximum > options.maxMs) {
throw new Error(`npm resolution exceeded --max-ms=${String(options.maxMs)} (max ${maximum.toFixed(0)} ms)`)
}
}
if (import.meta.main) {
try {
await main()
} catch (error) {
console.error(`benchmark-npm-resolution: ${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
}
}

View file

@ -0,0 +1,52 @@
/** Explicit exceptions and Host packages for the published dependency policy. */
/** Packages treated as Client/Host packages without declaring `dsh.client`. */
const CLIENT_FACE_INCLUDE: readonly string[] = []
/** Packages exempted from automatic Client/Host treatment despite declaring `dsh.client`. */
const CLIENT_FACE_EXCLUDE: readonly string[] = [
'@deepseek-ai/dsh-api-session-controller',
]
/** Host-only packages whose peer relays are deliberately flattened. */
const HOST_DEPENDENCY_PACKAGES: readonly string[] = [
'@deepseek-ai/dsh-llm',
'@deepseek-ai/dsh-session',
]
/** Complete configurable input to package dependency classification. */
export interface PackageDependencyPolicy {
readonly clientFaceInclude: readonly string[]
readonly clientFaceExclude: readonly string[]
readonly hostPackages: readonly string[]
}
/** Repository dependency policy consumed by verification and benchmarking. */
export const PACKAGE_DEPENDENCY_POLICY: PackageDependencyPolicy = {
clientFaceInclude: CLIENT_FACE_INCLUDE,
clientFaceExclude: CLIENT_FACE_EXCLUDE,
hostPackages: HOST_DEPENDENCY_PACKAGES,
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/** Whether a package manifest declares a dynamically loaded Client entry. */
export function hasClientDeclaration(dshField: unknown): boolean {
return isRecord(dshField) && Object.hasOwn(dshField, 'client')
}
/** Whether the repository policy flattens one package's non-Cordis peers. */
export function usesFlattenedPackageDependencies(
manifestPath: string,
packageName: string,
dshField: unknown,
policy: PackageDependencyPolicy = PACKAGE_DEPENDENCY_POLICY,
): boolean {
if (!manifestPath.startsWith('packages/') || manifestPath.startsWith('packages/experimental/')) return false
if (policy.hostPackages.includes(packageName)) return true
if (manifestPath.startsWith('packages/client/')) return true
const included = hasClientDeclaration(dshField) || policy.clientFaceInclude.includes(packageName)
return included && !policy.clientFaceExclude.includes(packageName)
}

View file

@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { import {
collectPackageInvariantViolations, collectPackageInvariantViolations,
} from './package-invariants.ts' } from './package-invariants.ts'
import { usesFlattenedPackageDependencies } from './package-dependency-policy.ts'
const roots: string[] = [] const roots: string[] = []
@ -28,7 +29,10 @@ export const apply = (ctx: { invariants: { register(name: string, install: typeo
function fixture(options: { function fixture(options: {
packageName?: string packageName?: string
packageDirectory?: string
source?: string source?: string
clientDeclaration?: boolean
clientExport?: boolean
invariantExport?: boolean invariantExport?: boolean
invariantDependency?: boolean invariantDependency?: boolean
invariantReference?: boolean invariantReference?: boolean
@ -36,19 +40,34 @@ function fixture(options: {
} = {}): string { } = {}): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-invariants-')) const root = mkdtempSync(join(tmpdir(), 'dsh-package-invariants-'))
roots.push(root) roots.push(root)
const dir = join(root, 'packages/core/probe') const packageDirectory = options.packageDirectory ?? 'packages/core/probe'
const dir = join(root, packageDirectory)
mkdirSync(join(dir, 'src'), { recursive: true }) mkdirSync(join(dir, 'src'), { recursive: true })
const packageName = options.packageName ?? '@deepseek-ai/dsh-probe' const packageName = options.packageName ?? '@deepseek-ai/dsh-probe'
const exports = options.invariantExport === false ? {} : {
'./invariant': {
types: './lib/types/invariant.d.ts',
default: './lib/invariant.js',
},
...(options.clientExport === true ? {
'./client': {
types: './lib/types/client/index.d.ts',
default: './lib/client.js',
},
} : {}),
}
const dsh = options.clientDeclaration === true ? { client: {} } : undefined
const developmentOnlyInvariant = usesFlattenedPackageDependencies(
`${packageDirectory}/package.json`,
packageName,
dsh,
)
const manifest = { const manifest = {
name: packageName, name: packageName,
exports: options.invariantExport === false ? {} : { ...(dsh === undefined ? {} : { dsh }),
'./invariant': { exports,
types: './lib/types/invariant.d.ts',
default: './lib/invariant.js',
},
},
files: ['lib/index.js', 'lib/invariant.js'], files: ['lib/index.js', 'lib/invariant.js'],
peerDependencies: options.invariantDependency === false ? {} : { peerDependencies: options.invariantDependency === false || developmentOnlyInvariant ? {} : {
'@deepseek-ai/dsh-invariants': 'workspace:^', '@deepseek-ai/dsh-invariants': 'workspace:^',
}, },
devDependencies: options.invariantDependency === false ? {} : { devDependencies: options.invariantDependency === false ? {} : {
@ -72,6 +91,33 @@ describe('package invariant gate', () => {
expect(collectPackageInvariantViolations(fixture())).toEqual([]) expect(collectPackageInvariantViolations(fixture())).toEqual([])
}) })
it('accepts development-only invariants for configured Host dependencies', () => {
expect(collectPackageInvariantViolations(fixture({ packageName: '@deepseek-ai/dsh-llm' }))).toEqual([])
})
it('accepts development-only invariants for client packages', () => {
expect(collectPackageInvariantViolations(fixture({
packageName: '@deepseek-ai/dsh-client-probe',
packageDirectory: 'packages/client/probe',
}))).toEqual([])
})
it('accepts development-only invariants for packages with a dsh.client entry', () => {
expect(collectPackageInvariantViolations(fixture({ clientDeclaration: true, clientExport: true }))).toEqual([])
})
it('keeps invariant peers for packages that only export a Client API', () => {
expect(collectPackageInvariantViolations(fixture({ clientExport: true }))).toEqual([])
})
it('keeps invariant peers for experimental packages with a dsh.client entry', () => {
expect(collectPackageInvariantViolations(fixture({
packageDirectory: 'packages/experimental/probe',
clientDeclaration: true,
clientExport: true,
}))).toEqual([])
})
it('accepts an invariant reference owned by a package-local leaf project', () => { it('accepts an invariant reference owned by a package-local leaf project', () => {
const root = fixture({ invariantReference: false }) const root = fixture({ invariantReference: false })
const dir = join(root, 'packages/core/probe') const dir = join(root, 'packages/core/probe')

View file

@ -7,12 +7,14 @@
import { existsSync, globSync, readFileSync } from 'node:fs' import { existsSync, globSync, readFileSync } from 'node:fs'
import { dirname, relative, resolve, sep } from 'node:path' import { dirname, relative, resolve, sep } from 'node:path'
import ts from 'typescript' import ts from 'typescript'
import { usesFlattenedPackageDependencies } from './package-dependency-policy.ts'
/** Required explanation marker for an intentionally empty installer. */ /** Required explanation marker for an intentionally empty installer. */
const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:' const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:'
interface PackageManifest { interface PackageManifest {
name?: string name?: string
dsh?: unknown
exports?: Record<string, { types?: string; default?: string } | string | undefined> exports?: Record<string, { types?: string; default?: string } | string | undefined>
files?: string[] files?: string[]
peerDependencies?: Record<string, string> peerDependencies?: Record<string, string>
@ -96,18 +98,23 @@ function checkManifest(
addViolation(violations, owner.manifestPath, 'files must publish lib/invariant.js') addViolation(violations, owner.manifestPath, 'files must publish lib/invariant.js')
} }
if (owner.packageName === '@deepseek-ai/dsh-invariants') return if (owner.packageName === '@deepseek-ai/dsh-invariants') return
if (manifest.peerDependencies?.['@deepseek-ai/dsh-invariants'] !== 'workspace:^') { const developmentOnlyInvariant = usesFlattenedPackageDependencies(
addViolation( owner.manifestPath,
violations, owner.packageName,
owner.manifestPath, manifest.dsh,
'@deepseek-ai/dsh-invariants must be a workspace:^ peerDependency', )
) const expectedRange = 'workspace:^'
const peerRange = manifest.peerDependencies?.['@deepseek-ai/dsh-invariants']
if (developmentOnlyInvariant ? peerRange !== undefined : peerRange !== expectedRange) {
addViolation(violations, owner.manifestPath, developmentOnlyInvariant
? '@deepseek-ai/dsh-invariants must not be a peerDependency under this package dependency policy'
: '@deepseek-ai/dsh-invariants must be a workspace:^ peerDependency')
} }
if (manifest.devDependencies?.['@deepseek-ai/dsh-invariants'] !== 'workspace:^') { if (manifest.devDependencies?.['@deepseek-ai/dsh-invariants'] !== expectedRange) {
addViolation( addViolation(
violations, violations,
owner.manifestPath, owner.manifestPath,
'@deepseek-ai/dsh-invariants must also be a workspace:^ devDependency', `@deepseek-ai/dsh-invariants must be a ${expectedRange} devDependency`,
) )
} }
} }

View file

@ -102,7 +102,7 @@ describe('gate graph validation', () => {
const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id)) const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
expect(ids).toEqual([ expect(ids).toEqual([
'rescope-vendor', 'publint', 'constraints', 'application-entrypoints', 'rescope-vendor', 'publint', 'constraints', 'package-dependencies', 'application-entrypoints',
'dsh-package-licenses', 'package-invariants', 'built-package-invariants', 'node-next-types', 'dsh-package-licenses', 'package-invariants', 'built-package-invariants', 'node-next-types',
'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'cordis-config', 'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'cordis-config',
'runtime-closure', 'vendored-links', 'runtime-closure', 'vendored-links',
@ -141,6 +141,15 @@ describe('gate graph validation', () => {
}, },
) )
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
'keeps package dependency enforcement in %s',
(mode) => {
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
expect(ids).toContain('package-dependencies')
},
)
it.each(['ci-primary', 'ci-static', 'check-all'] as const)( it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
'keeps the client dependency policy in %s', 'keeps the client dependency policy in %s',
(mode) => { (mode) => {

View file

@ -278,6 +278,7 @@ function ciSharedStaticGates(): Gate[] {
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }), pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }), pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
pnpmScript('constraints', 'constraints'), pnpmScript('constraints', 'constraints'),
pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }), pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }), pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }), pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
@ -667,6 +668,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
pnpmScript('rescope-vendor', 'rescope-vendor:check', { label: 'vendor rescope' }), pnpmScript('rescope-vendor', 'rescope-vendor:check', { label: 'vendor rescope' }),
pnpmScript('publint', 'publint', artifactOptions), pnpmScript('publint', 'publint', artifactOptions),
pnpmScript('constraints', 'constraints'), pnpmScript('constraints', 'constraints'),
pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }), pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }), pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }), pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),

View file

@ -1,4 +1,4 @@
/** Tests for client package modes, dependency sections, and module requests. */ /** Tests for client package modes and module requests. */
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
@ -6,6 +6,7 @@ import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest' import { afterEach, describe, expect, it } from 'vitest'
import { import {
collectClientPackageViolations, collectClientPackageViolations,
collectLocalSourceSpecifiers,
collectRuntimeSourcePackageUses, collectRuntimeSourcePackageUses,
collectRuntimeSourceSpecifiers, collectRuntimeSourceSpecifiers,
collectSourcePackageUses, collectSourcePackageUses,
@ -106,6 +107,19 @@ describe('source package uses', () => {
'@deepseek-ai/dsh-b/remote', '@deepseek-ai/dsh-b/remote',
'react', 'react',
]) ])
expect([...collectLocalSourceSpecifiers('feature.ts', [
"import type { A } from './types.ts'",
"export { value } from './value.ts'",
"const load = () => import('./lazy.ts')",
"const legacy = require('./legacy.ts')",
"declare module './augmentation.ts' {}",
"import '@deepseek-ai/dsh-a'",
].join('\n'))].sort()).toEqual([
'./lazy.ts',
'./legacy.ts',
'./types.ts',
'./value.ts',
])
}) })
}) })
@ -152,109 +166,6 @@ describe('package modes', () => {
}) })
}) })
describe('dependency sections', () => {
it('accepts dynamic peer plus dev relationships, static dev inputs, and private dependencies', () => {
const slots = pkg('ui-slots', { dynamic: false, staticLinked: true })
const conversation = pkg('conversation', {
inject: ['@deepseek-ai/dsh-client-feature'],
sourceUses: {
'@deepseek-ai/dsh-agent': ['packages/client/conversation/src/index.ts'],
'@deepseek-ai/dsh-client-ui-slots': ['packages/client/conversation/src/client/slots.ts'],
react: ['packages/client/conversation/src/client/view.tsx'],
},
dependencies: { immer: '^10.1.1' },
peerDependencies: {
[CORDIS]: 'workspace:^',
'@deepseek-ai/dsh-agent': 'workspace:^',
'@deepseek-ai/dsh-client-feature': 'workspace:^',
},
devDependencies: {
[CORDIS]: 'workspace:^',
'@deepseek-ai/dsh-agent': 'workspace:^',
'@deepseek-ai/dsh-client-feature': 'workspace:^',
'@deepseek-ai/dsh-client-ui-slots': 'workspace:^',
react: '^18.2.0',
},
})
expect(collectClientPackageViolations(facts([slots, conversation], {
platformModules: ['react', slots.name],
}))).toEqual([])
})
it('rejects internal dependencies, static peers, and mismatched peer development ranges', () => {
const slots = pkg('ui-slots', { dynamic: false, staticLinked: true })
const subject = pkg('feature', {
sourceUses: {
'@deepseek-ai/dsh-agent': ['packages/client/feature/src/index.ts'],
[slots.name]: ['packages/client/feature/src/view.tsx'],
},
dependencies: { '@deepseek-ai/dsh-agent': 'workspace:^' },
peerDependencies: { [CORDIS]: 'workspace:^', [slots.name]: 'workspace:^' },
devDependencies: { [CORDIS]: 'workspace:^', [slots.name]: 'workspace:*' },
})
const found = collectClientPackageViolations(facts([slots, subject]))
expect(found).toHaveLength(2)
expect(found.join('\n')).toContain('peer-installed DSH relationship')
expect(found.join('\n')).toContain('static client input')
})
it('requires every peer to have the same development range', () => {
const subject = pkg('feature', {
peerDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/cordis-plugin-loader': 'workspace:^' },
})
expect(collectClientPackageViolations(facts([subject]))).toEqual([
'packages/client/feature/package.json: peerDependencies.@deepseek-ai/cordis-plugin-loader'
+ ' is workspace:^, so devDependencies.@deepseek-ai/cordis-plugin-loader must use the same range;'
+ ' found no declaration',
])
})
it('requires statically linked third-party runtime imports in dependencies', () => {
const primitives = pkg('ui-primitives', {
dynamic: false,
staticLinked: true,
runtimeSourceUses: { shiki: ['packages/client/ui-primitives/src/highlight.ts'] },
devDependencies: { [CORDIS]: 'workspace:^', shiki: '^4.3.1' },
})
const found = collectClientPackageViolations(facts([primitives]))
expect(found).toHaveLength(1)
expect(found[0]).toContain('runtime import retained by a statically linked artifact')
expect(found[0]).toContain('declare it only in dependencies')
const valid = { ...primitives, dependencies: { shiki: '^4.3.1' }, devDependencies: { [CORDIS]: 'workspace:^' } }
expect(collectClientPackageViolations(facts([valid]))).toEqual([])
})
it('keeps the web shell runtime inputs development-only', () => {
const web = pkg('web', {
dynamic: false,
staticLinked: true,
runtimeSourceUses: {
'@deepseek-ai/cordis-plugin-loader': ['packages/client/web/src/boot.ts'],
react: ['packages/client/web/src/seed.ts'],
},
devDependencies: {
[CORDIS]: 'workspace:^',
'@deepseek-ai/cordis-plugin-loader': 'workspace:^',
react: '^18.2.0',
},
})
expect(collectClientPackageViolations(facts([web]))).toEqual([])
})
it('allows npm dependency cycles', () => {
const a = pkg('a', {
peerDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-client-b': 'workspace:^' },
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-client-b': 'workspace:^' },
})
const b = pkg('b', {
peerDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-client-a': 'workspace:^' },
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-client-a': 'workspace:^' },
})
expect(collectClientPackageViolations(facts([a, b]))).toEqual([])
})
})
describe('module requests', () => { describe('module requests', () => {
it('rejects runtime requests from one client feature package to another dynamic row', () => { it('rejects runtime requests from one client feature package to another dynamic row', () => {
const ui = declaration('ui', { const ui = declaration('ui', {
@ -378,7 +289,7 @@ describe('manifest declarations', () => {
]) ])
}) })
it('fixes unambiguous dependency sections and declaration entries', () => { it('fixes malformed declaration entries without changing dependency sections', () => {
const root = mkdtempSync(join(tmpdir(), 'client-packages-fix-')) const root = mkdtempSync(join(tmpdir(), 'client-packages-fix-'))
roots.push(root) roots.push(root)
const subject = pkg('feature', { const subject = pkg('feature', {
@ -426,43 +337,8 @@ describe('manifest declarations', () => {
external: ['@deepseek-ai/dsh-missing'], external: ['@deepseek-ai/dsh-missing'],
inject: ['@deepseek-ai/dsh-agent'], inject: ['@deepseek-ai/dsh-agent'],
}) })
expect(fixed.dependencies).toBeUndefined() expect(fixed.dependencies).toEqual(subject.dependencies)
expect(fixed.peerDependencies).toEqual({ expect(fixed.peerDependencies).toEqual(subject.peerDependencies)
'@deepseek-ai/cordis-plugin-loader': 'workspace:^', expect(fixed.devDependencies).toEqual(subject.devDependencies)
[CORDIS]: 'workspace:^',
'@deepseek-ai/dsh-agent': 'workspace:*',
})
expect(fixed.devDependencies).toEqual({
'@deepseek-ai/dsh-client-ui-slots': 'workspace:^',
[CORDIS]: 'workspace:^',
'@deepseek-ai/dsh-agent': 'workspace:*',
'@deepseek-ai/cordis-plugin-loader': 'workspace:^',
})
})
it('fixes a statically linked runtime import into dependencies', () => {
const root = mkdtempSync(join(tmpdir(), 'client-packages-static-fix-'))
roots.push(root)
const subject = pkg('ui-primitives', {
dynamic: false,
staticLinked: true,
runtimeSourceUses: { shiki: ['packages/client/ui-primitives/src/highlight.ts'] },
devDependencies: { [CORDIS]: 'workspace:^', shiki: '^4.3.1' },
})
mkdirSync(dirname(join(root, subject.manifest)), { recursive: true })
writeFileSync(join(root, subject.manifest), JSON.stringify({
name: subject.name,
peerDependencies: subject.peerDependencies,
devDependencies: subject.devDependencies,
}))
writeFileSync(join(root, 'package.json'), JSON.stringify({ private: true }))
expect(fixClientPackageManifests(root, facts([subject]))).toEqual([subject.manifest])
const fixed = JSON.parse(readFileSync(join(root, subject.manifest), 'utf8')) as {
dependencies: Record<string, string>
devDependencies: Record<string, string>
}
expect(fixed.dependencies).toEqual({ shiki: '^4.3.1' })
expect(fixed.devDependencies).toEqual({ [CORDIS]: 'workspace:^' })
}) })
}) })

View file

@ -1,6 +1,5 @@
/** /**
* Verify client package modes, npm dependency sections, and the synchronous * Verify client package modes and the synchronous browser module-request graph.
* browser module-request graph.
*/ */
import { globSync, readFileSync, writeFileSync } from 'node:fs' import { globSync, readFileSync, writeFileSync } from 'node:fs'
@ -17,8 +16,6 @@ const PLATFORM_SOURCE = 'packages/client/web/src/platform.ts'
const PARSER_PRELOAD_SOURCE = 'packages/client/modules/src/index.ts' const PARSER_PRELOAD_SOURCE = 'packages/client/modules/src/index.ts'
const STATIC_PRESET_SOURCE = 'packages/client/tsdown.client.ts' const STATIC_PRESET_SOURCE = 'packages/client/tsdown.client.ts'
const CORDIS = '@deepseek-ai/cordis' const CORDIS = '@deepseek-ai/cordis'
const DSH_PREFIX = '@deepseek-ai/dsh-'
const CLIENT_WEB = '@deepseek-ai/dsh-client-web'
/** One workspace package's browser-module declaration. */ /** One workspace package's browser-module declaration. */
export interface ClientDeclaration { export interface ClientDeclaration {
@ -92,6 +89,17 @@ export function collectRuntimeSourceSpecifiers(path: string, source: string): Se
return collectSourceFileUses(sourceFile, true, 'specifier') return collectSourceFileUses(sourceFile, true, 'specifier')
} }
/**
* Collect relative module specifiers used to follow one source entry's local closure.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Relative imports, exports, requires, and import types.
*/
export function collectLocalSourceSpecifiers(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, false, 'local')
}
function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean { function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
const clause = node.importClause const clause = node.importClause
if (clause === undefined) return true if (clause === undefined) return true
@ -114,12 +122,17 @@ function exportCarriesRuntimeValue(node: ts.ExportDeclaration): boolean {
function collectSourceFileUses( function collectSourceFileUses(
sourceFile: ts.SourceFile, sourceFile: ts.SourceFile,
runtimeOnly: boolean, runtimeOnly: boolean,
key: 'package' | 'specifier', key: 'local' | 'package' | 'specifier',
): Set<string> { ): Set<string> {
const uses = new Set<string>() const uses = new Set<string>()
const add = (specifier: ts.Expression | undefined): void => { const add = (specifier: ts.Expression | undefined): void => {
if (specifier === undefined || !ts.isStringLiteral(specifier) || !isBareSpecifier(specifier.text)) return if (specifier === undefined || !ts.isStringLiteralLike(specifier)) return
if (key === 'local') {
if (specifier.text.startsWith('.')) uses.add(specifier.text)
return
}
if (!isBareSpecifier(specifier.text)) return
uses.add(key === 'package' ? packageNameOf(specifier.text) : specifier.text) uses.add(key === 'package' ? packageNameOf(specifier.text) : specifier.text)
} }
const visit = (node: ts.Node): void => { const visit = (node: ts.Node): void => {
@ -135,9 +148,10 @@ function collectSourceFileUses(
&& (node.expression.kind === ts.SyntaxKind.ImportKeyword && (node.expression.kind === ts.SyntaxKind.ImportKeyword
|| ts.isIdentifier(node.expression) && node.expression.text === 'require')) { || ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
add(node.arguments[0]) add(node.arguments[0])
} else if (!runtimeOnly && ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) { } else if (!runtimeOnly && key !== 'local' && ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
add(node.name) add(node.name)
} else if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) { } else if (key !== 'local'
&& (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node))) {
uses.add('react') uses.add('react')
} }
ts.forEachChild(node, visit) ts.forEachChild(node, visit)
@ -170,7 +184,6 @@ export function collectClientPackageViolations(facts: ClientPackageFacts): strin
return [ return [
...facts.malformed, ...facts.malformed,
...collectModeViolations(facts), ...collectModeViolations(facts),
...collectDependencyViolations(facts),
...collectModuleViolations(facts), ...collectModuleViolations(facts),
].sort((left, right) => left.localeCompare(right)) ].sort((left, right) => left.localeCompare(right))
} }
@ -181,10 +194,8 @@ interface ManifestDocument {
changed: boolean changed: boolean
} }
type DependencySection = 'dependencies' | 'peerDependencies' | 'devDependencies'
/** /**
* Repair manifest declarations whose intended result follows uniquely from the policy. * Repair malformed or redundant `dsh.client` declaration entries.
* @param root - Absolute repository root. * @param root - Absolute repository root.
* @param facts - Facts used by the verification pass. * @param facts - Facts used by the verification pass.
* @returns Repository-relative manifests written by the fixer. * @returns Repository-relative manifests written by the fixer.
@ -217,53 +228,6 @@ export function fixClientPackageManifests(root: string, facts: ClientPackageFact
) || target.changed ) || target.changed
} }
const staticInputs = new Set([
...facts.staticLinkedPackages,
...facts.platformModules.map(packageNameOf),
])
staticInputs.delete(CORDIS)
const inferredRanges = dependencyRangeCandidates(root)
for (const pkg of facts.packages) {
const target = document(pkg.manifest)
const expected = expectedSections(pkg, staticInputs)
for (const [name, rule] of expected) {
const range = preferredRange(target.manifest, name, rule.kind, inferredRanges)
if (range === undefined) continue
target.changed = rule.kind === 'dependency'
? ensureDependencyOnly(target.manifest, name, range) || target.changed
: rule.kind === 'dev'
? ensureDevOnly(target.manifest, name, range) || target.changed
: ensurePeerDev(target.manifest, name, range) || target.changed
}
if (pkg.dynamic) {
const productionNames = new Set([
...Object.keys(section(target.manifest, 'dependencies')),
...Object.keys(section(target.manifest, 'peerDependencies')),
])
for (const name of productionNames) {
if (expected.has(name)) continue
const range = preferredRange(
target.manifest,
name,
staticInputs.has(name) ? 'dev' : 'peer-dev',
inferredRanges,
)
if (range === undefined) continue
if (staticInputs.has(name)) {
target.changed = ensureDevOnly(target.manifest, name, range) || target.changed
} else if (section(target.manifest, 'dependencies')[name] !== undefined && isInternalDsh(name)) {
target.changed = ensurePeerDev(target.manifest, name, range) || target.changed
}
}
}
for (const [name, range] of Object.entries(section(target.manifest, 'peerDependencies'))) {
target.changed = setDependency(target.manifest, 'devDependencies', name, range) || target.changed
}
target.changed = deleteEmptySections(target.manifest) || target.changed
}
const changed = [...documents.values()].filter(target => target.changed).sort((left, right) => const changed = [...documents.values()].filter(target => target.changed).sort((left, right) =>
left.path.localeCompare(right.path)) left.path.localeCompare(right.path))
for (const target of changed) { for (const target of changed) {
@ -295,102 +259,6 @@ function normalizeClientArray(
return true return true
} }
function ensureDevOnly(manifest: Manifest, name: string, range: string): boolean {
let changed = deleteDependency(manifest, 'dependencies', name)
changed = deleteDependency(manifest, 'peerDependencies', name) || changed
return setDependency(manifest, 'devDependencies', name, range) || changed
}
function ensureDependencyOnly(manifest: Manifest, name: string, range: string): boolean {
let changed = deleteDependency(manifest, 'peerDependencies', name)
changed = deleteDependency(manifest, 'devDependencies', name) || changed
return setDependency(manifest, 'dependencies', name, range) || changed
}
function ensurePeerDev(manifest: Manifest, name: string, range: string): boolean {
let changed = deleteDependency(manifest, 'dependencies', name)
changed = setDependency(manifest, 'peerDependencies', name, range) || changed
return setDependency(manifest, 'devDependencies', name, range) || changed
}
function setDependency(manifest: Manifest, field: DependencySection, name: string, range: string): boolean {
const dependencies = mutableSection(manifest, field)
if (dependencies[name] === range) return false
dependencies[name] = range
return true
}
function deleteDependency(manifest: Manifest, field: DependencySection, name: string): boolean {
const dependencies = section(manifest, field)
if (dependencies[name] === undefined) return false
manifest[field] = Object.fromEntries(Object.entries(dependencies).filter(([key]) => key !== name))
return true
}
function deleteEmptySections(manifest: Manifest): boolean {
let changed = false
for (const field of ['dependencies', 'peerDependencies', 'devDependencies'] as const) {
if (manifest[field] === undefined || Object.keys(section(manifest, field)).length > 0) continue
if (field === 'dependencies') delete manifest.dependencies
else if (field === 'peerDependencies') delete manifest.peerDependencies
else delete manifest.devDependencies
changed = true
}
return changed
}
function preferredRange(
manifest: Manifest,
name: string,
kind: ExpectedRule['kind'],
inferred: ReadonlyMap<string, ReadonlySet<string>>,
): string | undefined {
const order: readonly DependencySection[] = kind === 'dependency'
? ['dependencies', 'devDependencies', 'peerDependencies']
: kind === 'dev'
? ['devDependencies', 'peerDependencies', 'dependencies']
: ['peerDependencies', 'devDependencies', 'dependencies']
for (const field of order) {
const range = section(manifest, field)[name]
if (range !== undefined) return range
}
if (isInternalDsh(name)) return 'workspace:^'
const candidates = inferred.get(name)
return candidates?.size === 1 ? [...candidates][0] : undefined
}
function dependencyRangeCandidates(root: string): Map<string, Set<string>> {
const candidates = new Map<string, Set<string>>()
const paths = globSync([
'package.json',
...MANIFEST_GLOBS,
'website/package.json',
], { cwd: root }).map(normalizePath)
for (const path of new Set(paths)) {
const manifest = JSON.parse(readFileSync(resolve(root, path), 'utf8')) as Manifest
for (const field of ['dependencies', 'peerDependencies', 'devDependencies'] as const) {
for (const [name, range] of Object.entries(section(manifest, field))) {
const ranges = candidates.get(name) ?? new Set<string>()
ranges.add(range)
candidates.set(name, ranges)
}
}
}
return candidates
}
function section(manifest: Manifest, field: DependencySection): Record<string, string> {
return manifest[field] ?? {}
}
function mutableSection(manifest: Manifest, field: DependencySection): Record<string, string> {
const value = manifest[field]
if (value !== undefined) return value
const created: Record<string, string> = {}
manifest[field] = created
return created
}
function collectModeViolations(facts: ClientPackageFacts): string[] { function collectModeViolations(facts: ClientPackageFacts): string[] {
const violations: string[] = [] const violations: string[] = []
for (const pkg of facts.packages) { for (const pkg of facts.packages) {
@ -435,114 +303,6 @@ function collectModeViolations(facts: ClientPackageFacts): string[] {
return violations return violations
} }
interface ExpectedRule {
readonly kind: 'dependency' | 'dev' | 'peer-dev'
readonly origins: Set<string>
}
function collectDependencyViolations(facts: ClientPackageFacts): string[] {
const violations: string[] = []
const staticInputs = new Set([
...facts.staticLinkedPackages,
...facts.platformModules.map(packageNameOf),
])
staticInputs.delete(CORDIS)
for (const pkg of [...facts.packages].sort((left, right) => left.manifest.localeCompare(right.manifest))) {
const expected = expectedSections(pkg, staticInputs)
for (const [name, rule] of [...expected].sort(([left], [right]) => left.localeCompare(right))) {
const actual = declaredSections(pkg, name)
if (rule.kind === 'dependency') {
if (actual.length === 1 && actual[0] === 'dependencies') continue
violations.push(
pkg.manifest + ': ' + name + ' (' + describeOrigins(rule.origins) + ') is a runtime import'
+ ' retained by a statically linked artifact; declare it only in dependencies, found '
+ describeSections(actual),
)
continue
}
if (rule.kind === 'dev') {
if (actual.length === 1 && actual[0] === 'devDependencies') continue
violations.push(
pkg.manifest + ': ' + name + ' (' + describeOrigins(rule.origins) + ') is a static client input;'
+ ' declare it only in devDependencies, found ' + describeSections(actual),
)
continue
}
const peerRange = pkg.peerDependencies[name]
const devRange = pkg.devDependencies[name]
if (actual.length === 2
&& actual.includes('peerDependencies')
&& actual.includes('devDependencies')
&& peerRange === devRange) continue
violations.push(
pkg.manifest + ': ' + name + ' (' + describeOrigins(rule.origins) + ')'
+ ' is a peer-installed DSH relationship; declare it in peerDependencies and devDependencies'
+ ' with matching ranges, not dependencies; found ' + describeSections(actual)
+ describeRangeMismatch(peerRange, devRange),
)
}
for (const [name, peerRange] of Object.entries(pkg.peerDependencies).sort(([left], [right]) => left.localeCompare(right))) {
if (expected.has(name)) continue
const devRange = pkg.devDependencies[name]
if (devRange === peerRange) continue
violations.push(
pkg.manifest + ': peerDependencies.' + name + ' is ' + peerRange + ', so devDependencies.' + name
+ ' must use the same range; found ' + (devRange ?? 'no declaration'),
)
}
if (!pkg.dynamic) continue
for (const section of ['dependencies', 'peerDependencies'] as const) {
for (const name of Object.keys(pkg[section]).sort()) {
if (expected.has(name)) continue
if (staticInputs.has(name)) {
violations.push(
pkg.manifest + ': dynamic package declares static input ' + name + ' in ' + section + ';'
+ ' move it to devDependencies or delete the stale declaration',
)
} else if (section === 'dependencies' && isInternalDsh(name)) {
violations.push(
pkg.manifest + ': dynamic package declares ' + name + ' in dependencies;'
+ ' dynamic DSH relationships are peer plus dev, and static client inputs are dev-only',
)
}
}
}
}
return violations
}
function expectedSections(pkg: ClientPackage, staticInputs: ReadonlySet<string>): Map<string, ExpectedRule> {
const expected = new Map<string, ExpectedRule>([
[CORDIS, { kind: 'peer-dev', origins: new Set(['client package baseline']) }],
])
if (!pkg.dynamic) {
if (pkg.name === CLIENT_WEB) return expected
for (const [name, locations] of Object.entries(pkg.runtimeSourceUses)) {
if (name === pkg.name || name === CORDIS || isInternalDsh(name)) continue
expected.set(name, { kind: 'dependency', origins: new Set(locations) })
}
return expected
}
const add = (name: string, origin: string): void => {
if (name === pkg.name) return
const kind = staticInputs.has(name) ? 'dev' : isInternalDsh(name) ? 'peer-dev' : undefined
if (kind === undefined) return
const current = expected.get(name)
if (current !== undefined) current.origins.add(origin)
else expected.set(name, { kind, origins: new Set([origin]) })
}
for (const [name, locations] of Object.entries(pkg.sourceUses)) {
for (const location of locations) add(name, location)
}
for (const name of pkg.inject) add(name, 'dsh.client.inject')
return expected
}
interface ModuleEdge { interface ModuleEdge {
readonly from: string readonly from: string
readonly to: string readonly to: string
@ -895,32 +655,6 @@ function rowPackageOf(specifier: string, rows: ReadonlySet<string>): string | un
return rows.has(stripped) ? stripped : undefined return rows.has(stripped) ? stripped : undefined
} }
function declaredSections(pkg: ClientPackage, name: string): string[] {
return (['dependencies', 'peerDependencies', 'devDependencies'] as const)
.filter(section => pkg[section][name] !== undefined)
}
function describeSections(sections: readonly string[]): string {
return sections.length === 0 ? 'no dependency declaration' : sections.join(' + ')
}
function describeRangeMismatch(peer: string | undefined, dev: string | undefined): string {
if (peer === undefined || dev === undefined || peer === dev) return ''
return ' (peer ' + peer + ', dev ' + dev + ')'
}
function describeOrigins(origins: ReadonlySet<string>): string {
const sorted = [...origins].sort()
const [first, second, ...rest] = sorted
if (first === undefined) return 'production use'
if (second === undefined) return first
return rest.length === 0 ? first + ', ' + second : first + ', ' + second + ', and ' + String(rest.length) + ' more'
}
function isInternalDsh(name: string): boolean {
return name === CORDIS || name.startsWith(DSH_PREFIX)
}
function isBareSpecifier(specifier: string): boolean { function isBareSpecifier(specifier: string): boolean {
return !specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.startsWith('#') return !specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.startsWith('#')
} }
@ -956,7 +690,7 @@ async function main(): Promise<void> {
const requests = facts.declarations.reduce((total, pkg) => total + pkg.external.length, 0) const requests = facts.declarations.reduce((total, pkg) => total + pkg.external.length, 0)
console.log( console.log(
GATE + ': ' + String(facts.packages.length) + ' client packages (' + String(dynamic) + ' dynamic, ' GATE + ': ' + String(facts.packages.length) + ' client packages (' + String(dynamic) + ' dynamic, '
+ String(facts.packages.length - dynamic) + ' statically linked) satisfy dependency and module-request rules; ' + String(facts.packages.length - dynamic) + ' statically linked) satisfy package-mode and module-request rules; '
+ String(requests) + ' explicit external request(s).', + String(requests) + ' explicit external request(s).',
) )
} }

View file

@ -0,0 +1,260 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
PACKAGE_DEPENDENCY_POLICY,
type PackageDependencyPolicy,
} from './package-dependency-policy.ts'
import {
collectPackageDependencyViolations,
discoverPackageDependencyScope,
fixPackageDependencies,
formatManagedRuntimeDependencies,
readPackageDependencyFacts,
repairPackageDependencyManifest,
type PackageDependencyFacts,
type PackageDependencyManifest,
type WorkspacePackageManifest,
} from './verify-package-dependencies.ts'
const CORDIS = '@deepseek-ai/cordis'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function pkg(
name: string,
manifestPath: string,
manifest: Partial<PackageDependencyManifest> = {},
): WorkspacePackageManifest {
return {
name,
manifestPath,
dir: dirname(manifestPath),
manifest: { name, ...manifest },
}
}
function policy(fields: Partial<PackageDependencyPolicy> = {}): PackageDependencyPolicy {
return {
clientFaceInclude: [],
clientFaceExclude: [],
hostPackages: [],
...fields,
}
}
function facts(manifest: PackageDependencyManifest): PackageDependencyFacts {
return {
manifestPath: 'packages/core/probe/package.json',
role: 'configured-host',
manifest,
workspaceNames: new Set([
CORDIS,
'@deepseek-ai/dsh-runtime',
'@deepseek-ai/dsh-types',
'@deepseek-ai/dsh-stale',
'@deepseek-ai/schemastery',
]),
allSourceUses: new Map([
['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
['@deepseek-ai/dsh-types', ['packages/core/probe/src/types.ts']],
]),
hostRuntimeSourceUses: new Map([
['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
]),
clientInject: new Set(),
}
}
describe('package dependency scope', () => {
it('keeps the measured Host relay roster explicit', () => {
expect(PACKAGE_DEPENDENCY_POLICY.clientFaceExclude).toEqual([
'@deepseek-ai/dsh-api-session-controller',
])
expect(PACKAGE_DEPENDENCY_POLICY.hostPackages).toEqual([
'@deepseek-ai/dsh-llm',
'@deepseek-ai/dsh-session',
])
})
it('discovers the Client directory, dsh.client declarations, and configured Host packages', () => {
const packages = [
pkg('@f/static', 'packages/client/static/package.json'),
pkg('@f/dynamic-client', 'packages/client/dynamic/package.json', { dsh: { client: {} } }),
pkg('@f/dual', 'packages/api/dual/package.json', { dsh: { client: {} } }),
pkg('@f/export-only', 'packages/api/export-only/package.json', { exports: { './client': './lib/client.js' } }),
pkg('@f/forced-client', 'packages/api/forced/package.json'),
pkg('@f/excluded', 'packages/api/excluded/package.json', { dsh: { client: {} } }),
pkg('@f/host', 'packages/core/host/package.json'),
]
const found = discoverPackageDependencyScope(packages, policy({
clientFaceInclude: ['@f/forced-client'],
clientFaceExclude: ['@f/excluded'],
hostPackages: ['@f/host'],
}))
expect(found.violations).toEqual([])
expect(found.selected.map(item => [item.name, item.role])).toEqual([
['@f/dual', 'client-host'],
['@f/forced-client', 'client-host'],
['@f/dynamic-client', 'client-host'],
['@f/static', 'client-host'],
['@f/host', 'configured-host'],
])
})
it('rejects stale, redundant, overlapping, and unknown configuration', () => {
const packages = [
pkg('@f/client', 'packages/client/client/package.json'),
pkg('@f/dual', 'packages/api/dual/package.json', { dsh: { client: {} } }),
pkg('@f/host', 'packages/core/host/package.json'),
]
const found = discoverPackageDependencyScope(packages, policy({
clientFaceInclude: ['@f/dual', '@f/missing', '@f/host'],
clientFaceExclude: ['@f/client', '@f/host', '@f/missing'],
hostPackages: ['@f/dual'],
}))
expect(found.violations).toEqual(expect.arrayContaining([
expect.stringContaining('clientFaceInclude redundantly names automatically discovered package @f/dual'),
expect.stringContaining('@f/host appears in both clientFaceInclude and clientFaceExclude'),
expect.stringContaining('clientFaceExclude cannot exempt packages/client package @f/client'),
expect.stringContaining('clientFaceExclude names @f/host, which declares no dsh.client entry'),
expect.stringContaining('hostPackages redundantly names Client-faced package @f/dual'),
expect.stringContaining('unknown release package @f/missing'),
]))
})
})
describe('face-aware source classification', () => {
it('counts Host values as dependencies and Client values as development inputs', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-faces-'))
roots.push(root)
const subject = pkg('@f/dual', 'packages/g/dual/package.json', {
dsh: { client: { inject: ['@f/injected'] } },
})
const files = {
'packages/g/dual/src/index.ts': [
"import { value } from '@f/runtime'",
"import type { Shared } from '@f/types'",
"export { nested } from './nested.ts'",
].join('\n'),
'packages/g/dual/src/nested.ts': "export { nested } from '@f/nested'",
'packages/g/dual/src/client/index.ts': "import { browser } from '@f/browser'",
}
for (const [path, source] of Object.entries(files)) {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), source)
}
const found = readPackageDependencyFacts(root, subject, 'client-host', new Set([
CORDIS, '@f/runtime', '@f/types', '@f/nested', '@f/browser', '@f/injected',
]))
expect([...found.hostRuntimeSourceUses.keys()].sort()).toEqual(['@f/nested', '@f/runtime'])
expect([...found.allSourceUses.keys()].sort()).toEqual(['@f/browser', '@f/nested', '@f/runtime', '@f/types'])
})
})
describe('dependency sections', () => {
it('accepts Host dependencies, development-only inputs, and shared Cordis', () => {
const manifest: PackageDependencyManifest = {
name: '@deepseek-ai/dsh-probe',
dependencies: {
'@deepseek-ai/dsh-runtime': 'workspace:^',
'@deepseek-ai/schemastery': 'workspace:^',
external: '^1.0.0',
},
devDependencies: {
'@deepseek-ai/dsh-types': 'workspace:^',
[CORDIS]: 'workspace:^',
},
peerDependencies: { [CORDIS]: 'workspace:^' },
}
expect(collectPackageDependencyViolations({
facts: [facts(manifest)], packages: [], policyViolations: [], workspaceNames: facts(manifest).workspaceNames,
})).toEqual([])
})
it('lists managed Host runtime dependencies for fix review', () => {
const subject = facts({ name: '@deepseek-ai/dsh-probe' })
expect(formatManagedRuntimeDependencies({
facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
})).toEqual([
'verify-package-dependencies: 1 managed Host runtime dependency edge(s) remain in dependencies across 1 package(s):',
' @deepseek-ai/dsh-probe: @deepseek-ai/dsh-runtime',
])
})
it('reports wrong sections, workspace ranges, and stale peer metadata', () => {
const manifest: PackageDependencyManifest = {
name: '@deepseek-ai/dsh-probe',
dependencies: { '@deepseek-ai/dsh-types': 'workspace:*' },
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
peerDependencies: { [CORDIS]: 'workspace:*', '@deepseek-ai/dsh-runtime': 'workspace:^' },
peerDependenciesMeta: { '@deepseek-ai/dsh-missing': { optional: true } },
}
const state = {
facts: [facts(manifest)], packages: [], policyViolations: [], workspaceNames: facts(manifest).workspaceNames,
}
const violations = collectPackageDependencyViolations(state)
expect(violations).toEqual(expect.arrayContaining([
expect.stringContaining('@deepseek-ai/dsh-runtime'),
expect.stringContaining('@deepseek-ai/dsh-types'),
expect.stringContaining(`${CORDIS} must be matching peerDependencies + devDependencies`),
expect.stringContaining('dependencies.@deepseek-ai/dsh-types must use workspace:^'),
expect.stringContaining('peerDependenciesMeta.@deepseek-ai/dsh-missing has no matching'),
]))
})
it('repairs owned relationships without changing unrelated dependencies', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-dependencies-'))
roots.push(root)
const manifestPath = 'package.json'
const manifest: PackageDependencyManifest = {
name: '@deepseek-ai/dsh-probe',
dependencies: { '@deepseek-ai/schemastery': 'workspace:*', external: '^1.0.0' },
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
peerDependencies: {
[CORDIS]: 'workspace:^',
'@deepseek-ai/dsh-runtime': 'workspace:^',
'@deepseek-ai/dsh-stale': 'workspace:^',
},
peerDependenciesMeta: { '@deepseek-ai/dsh-stale': { optional: true } },
}
writeFileSync(join(root, manifestPath), `${JSON.stringify(manifest, null, 2)}\n`)
const subject = { ...facts(manifest), manifestPath }
const state = { facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames }
expect(fixPackageDependencies(root, state)).toEqual([manifestPath])
const fixed = JSON.parse(readFileSync(join(root, manifestPath), 'utf8')) as PackageDependencyManifest
expect(fixed.dependencies).toEqual({
'@deepseek-ai/schemastery': 'workspace:^',
external: '^1.0.0',
'@deepseek-ai/dsh-runtime': 'workspace:^',
})
expect(fixed.devDependencies).toEqual({
[CORDIS]: 'workspace:^',
'@deepseek-ai/dsh-types': 'workspace:^',
'@deepseek-ai/dsh-stale': 'workspace:^',
})
expect(fixed.peerDependencies).toEqual({ [CORDIS]: 'workspace:^' })
expect(fixed.peerDependenciesMeta).toBeUndefined()
})
it('repairs an in-memory manifest for benchmark simulation', () => {
const manifest: PackageDependencyManifest = {
name: '@deepseek-ai/dsh-probe',
peerDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-runtime': 'workspace:^' },
}
repairPackageDependencyManifest(facts(manifest))
expect(manifest.dependencies).toEqual({ '@deepseek-ai/dsh-runtime': 'workspace:^' })
expect(manifest.peerDependencies).toEqual({ [CORDIS]: 'workspace:^' })
})
})

View file

@ -0,0 +1,479 @@
/** Verify and repair npm dependency sections from published Client and Host faces. */
import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, extname, join, normalize, relative, resolve, sep } from 'node:path'
import {
hasClientDeclaration,
PACKAGE_DEPENDENCY_POLICY,
type PackageDependencyPolicy,
} from './package-dependency-policy.ts'
import {
collectLocalSourceSpecifiers,
collectRuntimeSourcePackageUses,
collectSourcePackageUses,
} from './verify-client-packages.ts'
const GATE = 'verify-package-dependencies'
const CORDIS = '@deepseek-ai/cordis'
const WORKSPACE_RANGE = 'workspace:^'
const RELEASE_MANIFEST_GLOB = 'packages/!(experimental)/*/package.json'
const WORKSPACE_MANIFEST_GLOBS = [
'apps/*/package.json',
'packages/*/*/package.json',
'vendor/*/package.json',
]
type DependencySection = 'dependencies' | 'devDependencies' | 'optionalDependencies' | 'peerDependencies'
export type PackageDependencyRole = 'client-host' | 'configured-host'
/** Manifest fields read and repaired by the package dependency policy. */
export interface PackageDependencyManifest {
name?: string
version?: string
exports?: unknown
dependencies?: Record<string, string>
devDependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
peerDependencies?: Record<string, string>
peerDependenciesMeta?: Record<string, unknown>
dsh?: { client?: { inject?: string[] } }
}
/** One workspace package and its source location. */
export interface WorkspacePackageManifest {
readonly dir: string
readonly manifestPath: string
readonly manifest: PackageDependencyManifest
readonly name: string
}
/** Source and manifest facts for one package covered by the policy. */
export interface PackageDependencyFacts {
readonly manifestPath: string
readonly role: PackageDependencyRole
readonly manifest: PackageDependencyManifest
readonly workspaceNames: ReadonlySet<string>
readonly allSourceUses: ReadonlyMap<string, readonly string[]>
readonly hostRuntimeSourceUses: ReadonlyMap<string, readonly string[]>
readonly clientInject: ReadonlySet<string>
}
/** Complete policy input read from the repository. */
export interface PackageDependencyState {
readonly facts: readonly PackageDependencyFacts[]
readonly packages: readonly WorkspacePackageManifest[]
readonly policyViolations: readonly string[]
readonly workspaceNames: ReadonlySet<string>
}
export interface ExpectedPackageDependency {
readonly section: 'dependencies' | 'devDependencies' | 'peer-dev'
readonly origins: readonly string[]
}
function normalizePath(path: string): string {
return path.split(sep).join('/')
}
function packageNameOf(specifier: string): string | undefined {
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#') || specifier.includes(':')) {
return undefined
}
const parts = specifier.split('/')
return specifier.startsWith('@') ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined : parts[0]
}
/** Read package manifests used for scope discovery and workspace-name checks. */
export function readWorkspacePackageManifests(root: string): {
all: WorkspacePackageManifest[]
release: WorkspacePackageManifest[]
} {
const read = (manifestPath: string): WorkspacePackageManifest => {
const manifest = JSON.parse(readFileSync(resolve(root, manifestPath), 'utf8')) as PackageDependencyManifest
if (typeof manifest.name !== 'string') throw new Error(`${manifestPath}: missing package name`)
return {
dir: dirname(manifestPath),
manifestPath,
manifest,
name: manifest.name,
}
}
const all = globSync(WORKSPACE_MANIFEST_GLOBS, { cwd: root }).map(normalizePath).sort().map(read)
const releasePaths = new Set(globSync(RELEASE_MANIFEST_GLOB, { cwd: root }).map(normalizePath))
return { all, release: all.filter(pkg => releasePaths.has(pkg.manifestPath)) }
}
function duplicates(values: readonly string[]): string[] {
const seen = new Set<string>()
const duplicated = new Set<string>()
for (const value of values) {
if (seen.has(value)) duplicated.add(value)
seen.add(value)
}
return [...duplicated].sort()
}
/** Discover Client faces and configured Host packages, validating explicit overrides. */
export function discoverPackageDependencyScope(
packages: readonly WorkspacePackageManifest[],
policy: PackageDependencyPolicy,
): { selected: Array<WorkspacePackageManifest & { role: PackageDependencyRole }>; violations: string[] } {
const violations: string[] = []
const byName = new Map(packages.map(pkg => [pkg.name, pkg]))
const include = new Set(policy.clientFaceInclude)
const exclude = new Set(policy.clientFaceExclude)
const host = new Set(policy.hostPackages)
for (const [field, values] of [
['clientFaceInclude', policy.clientFaceInclude],
['clientFaceExclude', policy.clientFaceExclude],
['hostPackages', policy.hostPackages],
] as const) {
for (const name of duplicates(values)) violations.push(`${field} lists ${name} more than once`)
for (const name of values) {
if (!byName.has(name)) violations.push(`${field} names unknown release package ${name}`)
}
}
for (const name of include) {
if (exclude.has(name)) violations.push(`${name} appears in both clientFaceInclude and clientFaceExclude`)
const pkg = byName.get(name)
if (pkg !== undefined
&& (pkg.manifestPath.startsWith('packages/client/') || hasClientDeclaration(pkg.manifest.dsh))) {
violations.push(`clientFaceInclude redundantly names automatically discovered package ${name}`)
}
}
for (const name of exclude) {
const pkg = byName.get(name)
if (pkg !== undefined && pkg.manifestPath.startsWith('packages/client/')) {
violations.push(`clientFaceExclude cannot exempt packages/client package ${name}`)
} else if (pkg !== undefined && !hasClientDeclaration(pkg.manifest.dsh)) {
violations.push(`clientFaceExclude names ${name}, which declares no dsh.client entry`)
}
}
const selected: Array<WorkspacePackageManifest & { role: PackageDependencyRole }> = []
for (const pkg of packages) {
const clientDirectory = pkg.manifestPath.startsWith('packages/client/')
const clientHost = clientDirectory
|| ((hasClientDeclaration(pkg.manifest.dsh) || include.has(pkg.name)) && !exclude.has(pkg.name))
const configuredHost = host.has(pkg.name)
if (configuredHost && clientHost) {
violations.push(`hostPackages redundantly names Client-faced package ${pkg.name}`)
}
const role = clientHost ? 'client-host' : configuredHost ? 'configured-host' : undefined
if (role !== undefined) selected.push({ ...pkg, role })
}
return {
selected: selected.sort((left, right) => left.manifestPath.localeCompare(right.manifestPath)),
violations: [...new Set(violations)].sort(),
}
}
function addUse(target: Map<string, string[]>, name: string, path: string): void {
const paths = target.get(name) ?? []
if (!paths.includes(path)) paths.push(path)
target.set(name, paths)
}
function resolveLocal(importer: string, specifier: string): string | undefined {
const raw = resolve(dirname(importer), specifier)
const candidates = extname(raw) === ''
? [`${raw}.ts`, `${raw}.tsx`, `${raw}.mts`, `${raw}.cts`, join(raw, 'index.ts'), join(raw, 'index.tsx')]
: [raw, raw.replace(/\.js$/, '.ts'), raw.replace(/\.jsx$/, '.tsx'), raw.replace(/\.mjs$/, '.mts'), raw.replace(/\.cjs$/, '.cts')]
return candidates.find(candidate => existsSync(candidate))
}
function readHostRuntimeUses(root: string, pkg: WorkspacePackageManifest): Map<string, string[]> {
const uses = new Map<string, string[]>()
const seen = new Set<string>()
const visit = (path: string): void => {
const normalized = normalize(path)
if (seen.has(normalized) || !existsSync(normalized)) return
seen.add(normalized)
const source = readFileSync(normalized, 'utf8')
const displayPath = normalizePath(relative(root, normalized))
for (const name of collectRuntimeSourcePackageUses(normalized, source)) addUse(uses, name, displayPath)
for (const specifier of collectLocalSourceSpecifiers(normalized, source)) {
const target = resolveLocal(normalized, specifier)
if (target !== undefined) visit(target)
}
}
visit(resolve(root, pkg.dir, 'src/index.ts'))
return uses
}
function readAllSourceUses(root: string, pkg: WorkspacePackageManifest): Map<string, string[]> {
const uses = new Map<string, string[]>()
for (const sourcePath of globSync('src/**/*.{ts,tsx,mts,cts}', { cwd: resolve(root, pkg.dir) }).sort()) {
const source = readFileSync(resolve(root, pkg.dir, sourcePath), 'utf8')
const displayPath = `${pkg.dir}/${normalizePath(sourcePath)}`
for (const name of collectSourcePackageUses(sourcePath, source)) addUse(uses, name, displayPath)
}
return uses
}
/** Read source usage for one already-classified package. */
export function readPackageDependencyFacts(
root: string,
pkg: WorkspacePackageManifest,
role: PackageDependencyRole,
workspaceNames: ReadonlySet<string>,
): PackageDependencyFacts {
const inject = pkg.manifest.dsh?.client?.inject ?? []
return {
manifestPath: pkg.manifestPath,
role,
manifest: pkg.manifest,
workspaceNames,
allSourceUses: readAllSourceUses(root, pkg),
hostRuntimeSourceUses: readHostRuntimeUses(root, pkg),
clientInject: new Set(inject.map(packageNameOf).filter(name => name !== undefined)),
}
}
/** Read every package covered by the current dependency policy. */
export function readPackageDependencyState(
root: string,
policy: PackageDependencyPolicy = PACKAGE_DEPENDENCY_POLICY,
): PackageDependencyState {
const packages = readWorkspacePackageManifests(root)
const workspaceNames = new Set(packages.all.map(pkg => pkg.name))
const discovered = discoverPackageDependencyScope(packages.release, policy)
return {
facts: discovered.selected.map(pkg => readPackageDependencyFacts(root, pkg, pkg.role, workspaceNames)),
packages: packages.release,
policyViolations: discovered.violations,
workspaceNames,
}
}
/** Derive the required npm section for each relationship owned by the policy. */
export function expectedPackageDependencies(
facts: PackageDependencyFacts,
): ReadonlyMap<string, ExpectedPackageDependency> {
const expected = new Map<string, { section: ExpectedPackageDependency['section']; origins: Set<string> }>()
const add = (name: string, sectionName: ExpectedPackageDependency['section'], origin: string): void => {
if (name === facts.manifest.name || name === CORDIS) return
const current = expected.get(name)
const section = current?.section === 'dependencies' || sectionName === 'dependencies'
? 'dependencies'
: 'devDependencies'
expected.set(name, { section, origins: new Set([...(current?.origins ?? []), origin]) })
}
expected.set(CORDIS, { section: 'peer-dev', origins: new Set(['shared Cordis runtime']) })
for (const [name, paths] of facts.allSourceUses) {
if (!facts.workspaceNames.has(name)) continue
for (const path of paths) add(name, 'devDependencies', path)
}
for (const name of facts.clientInject) {
if (facts.workspaceNames.has(name)) add(name, 'devDependencies', 'dsh.client.inject')
}
for (const name of Object.keys(facts.manifest.peerDependencies ?? {})) {
if (name !== CORDIS) add(name, 'devDependencies', 'existing non-Cordis peer')
}
for (const [name, paths] of facts.hostRuntimeSourceUses) {
if (!facts.workspaceNames.has(name) && facts.manifest.peerDependencies?.[name] === undefined) continue
for (const path of paths) add(name, 'dependencies', path)
}
return new Map([...expected].map(([name, rule]) => [name, {
section: rule.section,
origins: [...rule.origins].sort(),
}]))
}
/** Format the managed Host runtime edges that remain ordinary dependencies. */
export function formatManagedRuntimeDependencies(state: PackageDependencyState): string[] {
const rows = state.facts.flatMap((facts) => {
const dependencies = [...expectedPackageDependencies(facts)]
.filter(([, rule]) => rule.section === 'dependencies')
.map(([name]) => name)
.sort()
if (dependencies.length === 0) return []
return [{
name: facts.manifest.name ?? facts.manifestPath,
dependencies,
}]
}).sort((left, right) => left.name.localeCompare(right.name))
const edges = rows.reduce((total, row) => total + row.dependencies.length, 0)
return [
`${GATE}: ${String(edges)} managed Host runtime dependency edge(s) remain in dependencies across ${String(rows.length)} package(s):`,
...rows.map(row => ` ${row.name}: ${row.dependencies.join(', ')}`),
]
}
function section(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
return manifest[name] ?? {}
}
function mutableSection(manifest: PackageDependencyManifest, name: DependencySection): Record<string, string> {
manifest[name] ??= {}
return manifest[name]
}
function declaredSections(manifest: PackageDependencyManifest, name: string): DependencySection[] {
return (['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const)
.filter(sectionName => section(manifest, sectionName)[name] !== undefined)
}
function describeSections(sections: readonly DependencySection[]): string {
return sections.length === 0 ? 'no dependency section' : sections.join(' + ')
}
/** Return all manifest and policy violations in stable order. */
export function collectPackageDependencyViolations(state: PackageDependencyState): string[] {
const violations = [...state.policyViolations]
for (const facts of state.facts) {
for (const [name, rule] of expectedPackageDependencies(facts)) {
const actual = declaredSections(facts.manifest, name)
if (rule.section === 'peer-dev') {
if (actual.length === 2
&& actual.includes('peerDependencies')
&& actual.includes('devDependencies')
&& section(facts.manifest, 'peerDependencies')[name] === WORKSPACE_RANGE
&& section(facts.manifest, 'devDependencies')[name] === WORKSPACE_RANGE
&& facts.manifest.peerDependenciesMeta?.[name] === undefined) continue
violations.push(
`${facts.manifestPath}: ${name} must be matching peerDependencies + devDependencies at ${WORKSPACE_RANGE}; found ${describeSections(actual)}`,
)
continue
}
const expectedSection = rule.section
const range = section(facts.manifest, expectedSection)[name]
if (actual.length === 1
&& actual[0] === expectedSection
&& (!facts.workspaceNames.has(name) || range === WORKSPACE_RANGE)) continue
violations.push(
`${facts.manifestPath}: ${name} (${rule.origins.join(', ')}) must be ${expectedSection}-only`
+ (facts.workspaceNames.has(name) ? ` at ${WORKSPACE_RANGE}` : '')
+ `; found ${describeSections(actual)}`,
)
}
for (const sectionName of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const) {
for (const [name, range] of Object.entries(section(facts.manifest, sectionName))) {
if (!facts.workspaceNames.has(name) || range === WORKSPACE_RANGE) continue
violations.push(`${facts.manifestPath}: ${sectionName}.${name} must use ${WORKSPACE_RANGE}, found ${range}`)
}
}
for (const name of Object.keys(facts.manifest.peerDependenciesMeta ?? {})) {
if (facts.manifest.peerDependencies?.[name] === undefined) {
violations.push(`${facts.manifestPath}: peerDependenciesMeta.${name} has no matching peerDependencies entry`)
}
}
}
return [...new Set(violations)].sort()
}
function deleteDependency(
manifest: PackageDependencyManifest,
sectionName: DependencySection,
name: string,
): void {
const dependencies = manifest[sectionName]
if (dependencies?.[name] === undefined) return
const retained = Object.fromEntries(Object.entries(dependencies).filter(([key]) => key !== name))
if (Object.keys(retained).length > 0) {
manifest[sectionName] = retained
return
}
switch (sectionName) {
case 'dependencies': delete manifest.dependencies; break
case 'devDependencies': delete manifest.devDependencies; break
case 'optionalDependencies': delete manifest.optionalDependencies; break
case 'peerDependencies': delete manifest.peerDependencies; break
}
}
function deletePeerMeta(manifest: PackageDependencyManifest, name: string): void {
if (manifest.peerDependenciesMeta?.[name] === undefined) return
const retained = Object.fromEntries(Object.entries(manifest.peerDependenciesMeta)
.filter(([key]) => key !== name))
if (Object.keys(retained).length > 0) manifest.peerDependenciesMeta = retained
else delete manifest.peerDependenciesMeta
}
function preferredRange(
facts: PackageDependencyFacts,
name: string,
target: ExpectedPackageDependency['section'],
): string | undefined {
if (facts.workspaceNames.has(name)) return WORKSPACE_RANGE
const order: readonly DependencySection[] = target === 'dependencies'
? ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
: ['devDependencies', 'peerDependencies', 'dependencies', 'optionalDependencies']
return order.map(sectionName => section(facts.manifest, sectionName)[name]).find(value => value !== undefined)
}
/** Apply the dependency policy to one in-memory manifest. */
export function repairPackageDependencyManifest(facts: PackageDependencyFacts): void {
for (const [name, rule] of expectedPackageDependencies(facts)) {
if (rule.section === 'peer-dev') {
for (const sectionName of ['dependencies', 'optionalDependencies'] as const) {
deleteDependency(facts.manifest, sectionName, name)
}
mutableSection(facts.manifest, 'peerDependencies')[name] = WORKSPACE_RANGE
mutableSection(facts.manifest, 'devDependencies')[name] = WORKSPACE_RANGE
deletePeerMeta(facts.manifest, name)
continue
}
const range = preferredRange(facts, name, rule.section)
if (range === undefined) continue
for (const sectionName of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const) {
if (sectionName !== rule.section) deleteDependency(facts.manifest, sectionName, name)
}
mutableSection(facts.manifest, rule.section)[name] = range
deletePeerMeta(facts.manifest, name)
}
for (const name of Object.keys(facts.manifest.peerDependenciesMeta ?? {})) {
if (facts.manifest.peerDependencies?.[name] === undefined) deletePeerMeta(facts.manifest, name)
}
for (const sectionName of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const) {
for (const name of Object.keys(section(facts.manifest, sectionName))) {
if (facts.workspaceNames.has(name)) mutableSection(facts.manifest, sectionName)[name] = WORKSPACE_RANGE
}
}
}
/** Repair every covered manifest and return repository-relative changed paths. */
export function fixPackageDependencies(root: string, state: PackageDependencyState): string[] {
if (state.policyViolations.length > 0) return []
const changed: string[] = []
for (const facts of state.facts) {
const before = `${JSON.stringify(facts.manifest, null, 2)}\n`
repairPackageDependencyManifest(facts)
const after = `${JSON.stringify(facts.manifest, null, 2)}\n`
if (after === before) continue
writeFileSync(resolve(root, facts.manifestPath), after)
changed.push(facts.manifestPath)
}
return changed.sort()
}
function main(): void {
const root = resolve(import.meta.dirname, '..')
let state = readPackageDependencyState(root)
const fix = process.argv.includes('--fix')
if (fix) {
const changed = fixPackageDependencies(root, state)
console.log(`${GATE}: fixed ${String(changed.length)} manifest(s).`)
state = readPackageDependencyState(root)
}
const violations = collectPackageDependencyViolations(state)
if (violations.length > 0) {
console.error(`${GATE}: ${String(violations.length)} violation(s):`)
for (const violation of violations) console.error(` ${violation}`)
process.exitCode = 1
return
}
const roles = Object.groupBy(state.facts, fact => fact.role)
console.log(
`${GATE}: ${String(state.facts.length)} package(s) match the published dependency policy`
+ ` (${String(roles['client-host']?.length ?? 0)} Client/Host,`
+ ` ${String(roles['configured-host']?.length ?? 0)} configured Host).`,
)
if (fix) {
for (const line of formatManagedRuntimeDependencies(state)) console.log(line)
}
}
if (import.meta.main) main()