Merge remote-tracking branch 'origin/master' into xtr/durable-inbox-recovery

# Conflicts:
#	docs/config-catalog.i18n.yaml
#	docs/config-catalog.md
#	docs/config-catalog.zh.md
#	docs/event-producer-consumer.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/event-producer-consumer.zh.md
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	packages/api/session-controller/src/control.ts
#	packages/context/agent-instructions/tests/agent-instructions.spec.ts
#	packages/test-support/agent-loop-testkit/package.json
This commit is contained in:
_Kerman
2026-08-31 13:37:55 +08:00
1292 changed files with 21476 additions and 11274 deletions
+2
View File
@@ -1,3 +1,5 @@
# AGENTS.md — Repository scripts
Gate scripts invoke pnpm shell-free, normalize repository-relative glob paths to `/` at ingestion, and keep platform adaptation in the gate that needs it instead of a shared platform layer. Source-ownership gates use syntax-aware discovery, guard against an empty or narrowed corpus, and test every admitted/excluded form that changes their detection boundary.
Script specs run in forked workers beside the rest of the suite and beside the other gate processes in their job, so own every port, temporary path, and child process a spec acquires. A spec that passes only when it runs alone is a defect in the spec; [the testing policy](../docs/testing.md#how-specs-execute) states the execution model and [dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) owns the rules.
@@ -0,0 +1,125 @@
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']]]),
hostRuntimeExportUses: [{
packageName: '@f/runtime',
specifier: '@f/runtime',
exportName: 'runtimeValue',
sourcePath: 'packages/g/probe/src/index.ts',
line: 1,
column: 10,
sourceLine: "import { runtimeValue } from '@f/runtime'",
}],
peerRequiredHostDependencies: new Set(),
configurationOnlyDevDependencies: new Set(),
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'])
})
})
@@ -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
}
}
+200
View File
@@ -0,0 +1,200 @@
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 {
benchmarkNpmResolution,
buildRegistryIndex,
parseBenchmarkOptions,
publishWorkspaceRange,
resolveNpmPackageLock,
runCommandWithTimeout,
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`)
}
function processCanExecute(pid: number): boolean {
try {
process.kill(pid, 0)
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ESRCH') return false
throw error
}
if (process.platform !== 'linux') return true
try {
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8')
const state = stat.slice(stat.lastIndexOf(')') + 2).split(/\s+/, 1)[0]
return !/^[ZXx]$/.test(state ?? '')
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
throw error
}
}
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([])
})
it('returns npm placement for two aliased package versions without requesting archives', async () => {
const index: RegistryIndex = new Map([[
'@deepseek-ai/dsh',
new Map([
['0.1.0', { name: '@deepseek-ai/dsh', version: '0.1.0' }],
['0.2.0', { name: '@deepseek-ai/dsh', version: '0.2.0' }],
]),
]])
const result = await resolveNpmPackageLock(index, {
'@deepseek-ai/dsh': '0.2.0',
'dsh-previous': 'npm:@deepseek-ai/dsh@0.1.0',
}, 10_000)
expect(result.archiveRequests).toBe(0)
expect(result.packageLock.packages['node_modules/@deepseek-ai/dsh']?.version).toBe('0.2.0')
expect(result.packageLock.packages['node_modules/dsh-previous']).toMatchObject({
name: '@deepseek-ai/dsh',
version: '0.1.0',
})
})
it('isolates peer resolution from inherited npm configuration', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-hostile-npm-config-'))
roots.push(root)
const userConfig = join(root, 'user.npmrc')
writeFileSync(userConfig, '@deepseek-ai:registry=http://127.0.0.1:1/\nlegacy-peer-deps=true\nomit=peer\n')
const previous = {
userConfig: process.env.npm_config_userconfig,
legacyPeerDeps: process.env.npm_config_legacy_peer_deps,
omit: process.env.npm_config_omit,
}
process.env.npm_config_userconfig = userConfig
process.env.npm_config_legacy_peer_deps = 'true'
process.env.npm_config_omit = 'peer'
try {
const index: RegistryIndex = new Map([
['@deepseek-ai/dsh', new Map([['0.1.0', {
name: '@deepseek-ai/dsh',
version: '0.1.0',
peerDependencies: { '@deepseek-ai/dsh-peer': '1.0.0' },
}]])],
['@deepseek-ai/dsh-peer', new Map([['1.0.0', {
name: '@deepseek-ai/dsh-peer',
version: '1.0.0',
}]])],
])
const result = await resolveNpmPackageLock(index, { '@deepseek-ai/dsh': '0.1.0' }, 10_000)
expect(result.archiveRequests).toBe(0)
expect(result.packageLock.packages['node_modules/@deepseek-ai/dsh-peer']?.version).toBe('1.0.0')
} finally {
if (previous.userConfig === undefined) delete process.env.npm_config_userconfig
else process.env.npm_config_userconfig = previous.userConfig
if (previous.legacyPeerDeps === undefined) delete process.env.npm_config_legacy_peer_deps
else process.env.npm_config_legacy_peer_deps = previous.legacyPeerDeps
if (previous.omit === undefined) delete process.env.npm_config_omit
else process.env.npm_config_omit = previous.omit
}
})
it.skipIf(process.platform === 'win32')('force-kills a timed-out process tree', async () => {
const source = [
"const { spawn } = require('node:child_process')",
"process.on('SIGTERM', () => {})",
'const child = spawn(process.execPath, [\'-e\', "process.on(\'SIGTERM\', () => {}); setInterval(() => {}, 1000)"], { stdio: \'ignore\' })',
'console.log(child.pid)',
'setInterval(() => {}, 1000)',
].join(';')
let descendantPid: number | undefined
try {
const result = await runCommandWithTimeout(process.execPath, ['-e', source], {
cwd: process.cwd(),
env: process.env,
timeoutMs: 1_000,
terminationGraceMs: 100,
})
const reportedPid = Number.parseInt(result.output.trim(), 10)
if (!Number.isSafeInteger(reportedPid)) throw new Error(`child reported invalid pid ${result.output.trim()}`)
descendantPid = reportedPid
expect(result.timedOut).toBe(true)
expect(result.signal).toBe('SIGKILL')
await expect.poll(() => processCanExecute(reportedPid), { timeout: 5_000 }).toBe(false)
} finally {
if (descendantPid !== undefined && Number.isSafeInteger(descendantPid)) {
try {
process.kill(descendantPid, 'SIGKILL')
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error
}
}
}
})
})
+551
View File
@@ -0,0 +1,551 @@
/** Benchmark npm's dependency-tree resolution against an all-local registry. */
import { execFileSync, spawn, spawnSync, type ChildProcess } 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 TERMINATION_GRACE_MS = 1_000
const FORCED_EXIT_TIMEOUT_MS = 5_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[]
}
/** Published-package fields retained in npm's package-lock layout. */
export interface NpmLockPackage {
readonly name?: string
readonly version?: string
readonly dependencies?: Readonly<Record<string, string>>
readonly optionalDependencies?: Readonly<Record<string, string>>
readonly peerDependencies?: Readonly<Record<string, string>>
readonly peerDependenciesMeta?: Readonly<Record<string, { readonly optional?: boolean }>>
}
/** The installed paths selected by npm without materializing package archives. */
export interface NpmPackageLock {
readonly lockfileVersion: number
readonly packages: Readonly<Record<string, NpmLockPackage>>
}
/** npm resolution observations together with its computed install layout. */
export interface NpmPackageLockResolution extends BenchmarkRun {
readonly packageLock: NpmPackageLock
}
/** 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'
}
function delay(ms: number): Promise<void> {
return new Promise(resolveDelay => setTimeout(resolveDelay, ms))
}
function signalProcessTree(child: ChildProcess, signal: 'SIGTERM' | 'SIGKILL'): void {
if (child.pid === undefined) {
child.kill(signal)
return
}
if (process.platform === 'win32') {
const force = signal === 'SIGKILL' ? ['/F'] : []
const result = spawnSync('taskkill', ['/PID', String(child.pid), '/T', ...force], {
stdio: 'ignore',
windowsHide: true,
})
if (result.error !== undefined) throw result.error
if (result.status !== 0 && child.exitCode === null && child.signalCode === null) child.kill(signal)
return
}
try {
process.kill(-child.pid, signal)
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error
}
}
/**
* Run one command with bounded process-tree termination after its deadline.
* @param command - Executable path or name.
* @param args - Arguments passed without shell interpolation on POSIX.
* @param options - Working directory, environment, timeout, and termination grace.
* @returns Exit facts, captured output, duration, and whether timeout handling began.
*/
export async function runCommandWithTimeout(
command: string,
args: readonly string[],
options: {
readonly cwd: string
readonly env: NodeJS.ProcessEnv
readonly timeoutMs: number
readonly terminationGraceMs?: number
},
): Promise<{ status: number | null; signal: NodeJS.Signals | null; durationMs: number; output: string; timedOut: boolean }> {
const started = performance.now()
const child = spawn(command, [...args], {
cwd: options.cwd,
detached: process.platform !== 'win32',
env: options.env,
shell: process.platform === 'win32',
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 exited = new Promise<{ status: number | null; signal: NodeJS.Signals | null }>((resolveExit, reject) => {
child.once('error', reject)
child.once('close', (status, signal) => { resolveExit({ status, signal }) })
})
let timeout: NodeJS.Timeout | undefined
try {
const first = await Promise.race([
exited.then(outcome => ({ type: 'exit' as const, outcome })),
new Promise<{ type: 'timeout' }>((resolveTimeout) => {
timeout = setTimeout(() => { resolveTimeout({ type: 'timeout' }) }, options.timeoutMs)
}),
])
if (first.type === 'exit') {
return { ...first.outcome, durationMs: performance.now() - started, output, timedOut: false }
}
signalProcessTree(child, 'SIGTERM')
await delay(options.terminationGraceMs ?? TERMINATION_GRACE_MS)
signalProcessTree(child, 'SIGKILL')
const forced = await Promise.race([
exited,
delay(FORCED_EXIT_TIMEOUT_MS).then(() => undefined),
])
if (forced === undefined) throw new Error('timed-out process tree did not exit after SIGKILL')
return { ...forced, durationMs: performance.now() - started, output, timedOut: true }
} finally {
if (timeout !== undefined) clearTimeout(timeout)
}
}
function readNpmPackageLock(path: string): NpmPackageLock {
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('npm produced an invalid package-lock.json')
}
const { lockfileVersion, packages } = parsed as Record<string, unknown>
if (!Number.isSafeInteger(lockfileVersion) || packages === null
|| typeof packages !== 'object' || Array.isArray(packages)) {
throw new Error('npm produced an invalid package-lock.json')
}
return parsed as NpmPackageLock
}
async function runNpm(
cwd: string,
registry: string,
timeoutMs: number,
): Promise<{ durationMs: number; output: string; timedOut: boolean }> {
const npmrc = join(cwd, '.npmrc')
const globalNpmrc = join(cwd, '.npmrc-global')
writeFileSync(npmrc, `registry=${registry}\n@deepseek-ai:registry=${registry}\n`)
writeFileSync(globalNpmrc, '')
const inheritedEnvironment = Object.fromEntries(Object.entries(process.env)
.filter(([name]) => !name.toLowerCase().startsWith('npm_config_')))
const result = await runCommandWithTimeout(npmExecutable(), [
'install',
'--package-lock-only',
'--ignore-scripts',
'--no-audit',
'--no-fund',
'--loglevel=error',
'--include=peer',
'--install-strategy=hoisted',
'--legacy-peer-deps=false',
`--registry=${registry}`,
], {
cwd,
env: {
...inheritedEnvironment,
npm_config_cache: join(cwd, '.npm-cache'),
npm_config_globalconfig: globalNpmrc,
npm_config_userconfig: npmrc,
npm_config_update_notifier: 'false',
},
timeoutMs,
})
if (result.timedOut) return result
if (result.status !== 0) {
throw new Error(`npm install exited ${String(result.status)} after ${result.durationMs.toFixed(0)} ms\n${result.output.trim()}`)
}
return result
}
/**
* Ask npm to compute an install layout without downloading package archives.
* @param index - Package metadata exposed through the local registry.
* @param dependencies - Root dependencies whose install layout npm computes.
* @param timeoutMs - Hard wall-clock limit for the npm child process.
* @returns The package lock plus timing and registry-request observations.
*/
export async function resolveNpmPackageLock(
index: RegistryIndex,
dependencies: Readonly<Record<string, string>>,
timeoutMs: number,
): Promise<NpmPackageLockResolution> {
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,
}, 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(),
packageLock: readNpmPackageLock(join(consumer, 'package-lock.json')),
}
} finally {
server.closeAllConnections()
await close(server)
rmSync(consumer, { recursive: true, force: true })
}
}
/**
* 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> {
const result = await resolveNpmPackageLock(index, { [TARGET_PACKAGE]: targetVersion }, timeoutMs)
return {
durationMs: result.durationMs,
registryRequests: result.registryRequests,
archiveRequests: result.archiveRequests,
unknownPackages: result.unknownPackages,
}
}
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
}
}
+64 -1
View File
@@ -117,6 +117,35 @@ describe('CI workflow', () => {
))
expect(buildCommands.map(step => step.run)).toContain('pnpm run check:ci:windows-blocking')
// The four native Windows installs branch on the workspace filesystem:
// clone (ReFS block clone) only on ReFS, plain install elsewhere. This
// keeps the TS6231 store-path leak (see the Windows ReFS store note) out
// of the self-hosted pool without forcing clone onto hosted NTFS, which
// rejects copy-on-write. The branch must stay, or a hosted fallback would
// fail installs with ERR_PNPM_LINKING_FAILED.
for (const [jobName, job] of [['windows-build', windowsBuild], ['windows-coverage', windowsCoverage], ['windows-native-tests', windowsNativeTests], ['windows-observational', windowsObservational]] as const) {
const steps = job.steps as unknown[]
const install = steps.find((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && step.name === 'Install (immutable)' && typeof step.run === 'string'
))
expect(install, `${jobName} must define the filesystem-branched install`).toBeDefined()
expect(install!.run).toContain("$fs -eq 'ReFS'")
expect(install!.run).toContain('--package-import-method=clone')
expect(install!.run).toContain('corepack pnpm install')
// The else branch must keep the plain hosted install as a distinct line
// (not the corepack clone line, which contains the same substring);
// dropping it or making both branches clone would force clone onto
// NTFS, which rejects copy-on-write (ERR_PNPM_LINKING_FAILED). The
// YAML folded block keeps the first statement on line 1 and folds the
// rest with leading two-space indents.
const installLines = install!.run.split('\n').map(line => line.trim())
expect(installLines).toContain('} else {')
expect(installLines.some(line => line === 'pnpm install --frozen-lockfile'), `${jobName} else branch must keep the plain hosted install`).toBe(true)
// The ReFS branch must not use the interpolated empty-flag form, which
// passes a stray "" positional argument to pnpm.
expect(install!.run).not.toContain('$cloneFlag')
}
// windows-coverage uses the lower 4-partition profile.
expect(windowsCoverage.name).toBe('windows node 24 / coverage')
expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '4' })
@@ -150,6 +179,26 @@ describe('CI workflow', () => {
expect(serialWindows.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
expect(serialWindows['runs-on']).toEqual(['self-hosted', 'dsh-win-ci', 'windows'])
expect(serialWindows.name).toBe('serial / windows (self-hosted standby)')
// Its store must share the ReFS workspace volume for clone; the install
// must carry the same filesystem branch as the PR jobs.
const serialSteps = serialWindows.steps as unknown[]
const serialStore = serialSteps.find((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && step.name === 'Configure persistent pnpm store' && typeof step.run === 'string'
))
expect(serialStore).toBeDefined()
expect(serialStore!.run).toContain('F:\\.pnpm-store')
const serialInstall = serialSteps.find((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && step.name === 'Install (immutable)' && typeof step.run === 'string'
))
expect(serialInstall).toBeDefined()
expect(serialInstall!.run).toContain("$fs -eq 'ReFS'")
expect(serialInstall!.run).toContain('--package-import-method=clone')
expect(serialInstall!.run).toContain('corepack pnpm install')
// Distinct else-branch line, as for the PR jobs: the corepack clone line
// contains the plain-install substring too.
expect(serialInstall!.run.split('\n').map(line => line.trim())).toContain('} else {')
expect(serialInstall!.run.split('\n').map(line => line.trim())).toContain('pnpm install --frozen-lockfile')
expect(serialInstall!.run).not.toContain('$cloneFlag')
// Aggregate: Wine and the required split native jobs are needed;
// windows-coverage is temporarily non-blocking while Windows ACP
@@ -603,7 +652,7 @@ describe('npm release workflows', () => {
for (const file of ['release.yml', 'release-vendor.yml']) {
const workflow = loadWorkflow(`.github/workflows/${file}`)
if (!isRecord(workflow.jobs)) throw new TypeError(`${file} must define jobs`)
expect(Object.keys(workflow.jobs).sort()).toEqual(['pack'])
expect(Object.keys(workflow.jobs).sort()).toEqual(file === 'release.yml' ? ['dependencies', 'pack'] : ['pack'])
}
// publication is workflow_dispatch-only (never a PR check) and keeps the
@@ -618,6 +667,20 @@ describe('npm release workflows', () => {
expect(publish.concurrency).toMatchObject({ group: 'Release-publish' })
}
})
it('runs dependency policy and npm layout checks in the DSH release workflow', () => {
const workflow = loadWorkflow('.github/workflows/release.yml')
const dependencies = workflowJob(workflow, 'dependencies')
if (!isRecord(workflow.on) || !Array.isArray(dependencies.steps)) {
throw new TypeError('DSH release workflow must define triggers and dependency steps')
}
const commands = dependencies.steps.flatMap(step =>
isRecord(step) && typeof step.run === 'string' ? [step.run] : [])
expect(Object.keys(workflow.on).sort()).toEqual(['pull_request', 'push', 'workflow_dispatch'])
expect(commands).toContain('pnpm run verify-package-dependencies')
expect(commands).toContain('pnpm run verify-npm-install-layout')
})
})
describe('Documentation site publication', () => {
+3 -1
View File
@@ -91,9 +91,11 @@ describe('client bundle purity gate', () => {
expect(() => resolveId('@deepseek-ai/dsh-client-web-react/store')).toThrow(/purity/)
})
it('lets inline-safe wire layers inline', () => {
it('lets inline-safe libraries inline', () => {
expect(resolveId('@deepseek-ai/dsh-session/surface')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-brand')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-deque')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-util-values')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-token-meter/client')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-token-meter')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-token-meter/client/internal')).toThrow(/purity/)
+2 -1
View File
@@ -90,10 +90,11 @@ describe('coverage partition count', () => {
})
describe('coverage partition timeout', () => {
it('applies one configured timeout to tests and polling', () => {
it('applies one configured timeout to tests, polling, and hooks', () => {
expect(coverageTestTimeoutArgs('30000')).toEqual([
'--testTimeout=30000',
'--expect.poll.timeout=30000',
'--hookTimeout=30000',
])
})
+12 -3
View File
@@ -12,7 +12,7 @@ export const COVERAGE_PARTITIONS_ENV = 'DSH_COVERAGE_PARTITIONS'
/** Internal marker that suppresses reports and thresholds inside a partition process. */
export const COVERAGE_PARTITION_MODE_ENV = 'DSH_COVERAGE_PARTITION_MODE'
/** Environment variable overriding instrumented test and polling timeouts. */
/** Environment variable overriding instrumented test, polling, and hook timeouts. */
export const COVERAGE_TEST_TIMEOUT_ENV = 'DSH_COVERAGE_TEST_TIMEOUT_MS'
/** One child command owned by the coverage coordinator. */
@@ -76,14 +76,23 @@ export function parseCoveragePartitionCount(raw: string | undefined): number | u
return parsed
}
/** Resolve the paired Vitest timeout arguments used by coverage partitions. */
/**
* Resolve the paired Vitest timeout arguments used by coverage partitions.
* `--hookTimeout` travels with the test budget because setup and teardown pay
* the same host contention the raised test budget accounts for: fixtures that
* await child exit or retry Windows handle release spend that cost in
* `afterEach`, where Vitest's separate 10 s default would otherwise fail a
* suite whose cases all passed.
* @param raw - the configured millisecond budget, or undefined to keep Vitest's defaults.
* @returns the Vitest arguments applying that budget, empty when unset.
*/
export function coverageTestTimeoutArgs(raw: string | undefined): string[] {
if (raw === undefined || raw === '') return []
const parsed = Number.parseInt(raw, 10)
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
throw new Error(`${COVERAGE_TEST_TIMEOUT_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`)
}
return [`--testTimeout=${raw}`, `--expect.poll.timeout=${raw}`]
return [`--testTimeout=${raw}`, `--expect.poll.timeout=${raw}`, `--hookTimeout=${raw}`]
}
/** Remove pnpm's package-script separator before forwarding Vitest arguments. */
+2 -2
View File
@@ -4,7 +4,7 @@
"docs/architecture.md": 2400,
"docs/cordis-primer.md": 600,
"docs/defensive-patterns.md": 550,
"docs/testing.md": 1150,
"packages/AGENTS.md": 675,
"docs/testing.md": 1300,
"packages/AGENTS.md": 750,
"packages/README.md": 994
}
+4 -1
View File
@@ -76,13 +76,16 @@ const PACKAGE_LIBRARIES: Readonly<Record<string, string>> = {
'packages/typert/generator': 'Build-time generator run outside any agent runtime.',
'packages/typert/protocol': 'Compiler-independent protocol declarations.',
'packages/util/atomic-write': 'Zero-dependency filesystem write utility.',
'packages/util/brand': 'Type-only branding primitive erased at compile time.',
'packages/util/brand': 'Stateless nominal-string and canonical-key constructors.',
'packages/util/crypto': 'Zero-dependency identifier minting utility.',
'packages/util/deque': 'Zero-dependency circular deque utility.',
'packages/util/home-paths': 'Zero-dependency harness-home path resolver.',
'packages/util/launch-environment': 'Zero-dependency environment resolver.',
'packages/util/native-command': 'Host-side subprocess runner utility.',
'packages/util/output-retention': 'Zero-dependency retention utility.',
'packages/util/time': 'Zero-dependency time-zone canonicalization utility.',
'packages/util/timeout': 'Zero-dependency timeout utility.',
'packages/util/values': 'Stateless lossless-JSON and immutable-value helpers.',
'packages/util/workspace-path': 'Zero-dependency Workspace path formatter.',
}
+5
View File
@@ -494,7 +494,9 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
SubagentStartRequest: 'subagent.md',
AssembleContext: 'system-prompt.md',
PromptContext: 'system-prompt.md',
PromptContextOrderName: 'system-prompt.md',
PromptSection: 'system-prompt.md',
PromptSectionOrderName: 'system-prompt.md',
SystemPrompt: 'system-prompt.md',
ToolProviderResult: 'system-prompt.md',
JobDoneListener: 'jobs.md',
@@ -534,7 +536,9 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ToolRestriction: 'tools.md',
ToolSchema: 'tools.md',
SettingsNamespace: 'settings.md',
SettingsNamespaceInput: 'settings.md',
SettingsRegisterOptions: 'settings.md',
SettingsSectionHooks: 'settings.md',
SettingsScope: 'settings.md',
SettingsDescriptor: 'settings.md',
SettingsDescribeValue: 'settings.md',
@@ -657,6 +661,7 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
AgentPreset: 'discovered preset record is owned by packages/preset/agent-presets/README.md',
AgentPresetRoster: 'path-free preset roster is owned by packages/preset/agent-presets/README.md',
AgentPresetDocument: 'preset composition view is owned by packages/preset/agent-presets/README.md',
AgentPresetComposition: 'flattened composition rows are owned by packages/preset/agent-presets/README.md',
PresetMetadata: 'preset display text is owned by packages/preset/agent-presets/README.md',
BashEnvContributor: 'service-local extension type is owned by packages/shell/tool-bash/src/index.ts',
BashEnvVariableInfo: 'service-local metadata type is owned by packages/shell/tool-bash/src/index.ts',
+106 -48
View File
@@ -1,21 +1,21 @@
/**
* Generate `docs/module-graph.md` from in-repo `peerDependencies`, the canonical
* runtime edges. The deterministic output groups packages by directory and
* renders both Mermaid and a dependency table; `--check` verifies freshness.
*/
/** Generate the paired shared-instance package graph from workspace peer dependencies. */
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { readFileSync, writeFileSync } from 'node:fs'
import {
collectPackageGraph,
escapeMermaidLabel as escLabel,
graphNodeId as nodeId,
type PackageGraphNode,
} from './package-graph.ts'
import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts'
import { renderTranslationPairingRecord, translationPairPaths } from './translation-pairing-record.ts'
const root = resolve(import.meta.dirname, '..')
const OUT = 'docs/module-graph.md'
const SOURCE = 'docs/module-graph.md'
const PATHS = translationPairPaths(SOURCE)
type Pkg = PackageGraphNode
type Locale = 'en' | 'zh'
const GROUP_ORDER = [
'util',
@@ -46,42 +46,55 @@ function packageLink(pkg: Pkg): string {
return `[\`${pkg.short}\`](../${pkg.rel})`
}
/** Render the full docs/module-graph.md content (pure, deterministic). */
function render(pkgs: Pkg[]): string {
/**
* Render one locale of the complete deterministic package graph.
* @param pkgs - Dependency-first package nodes.
* @param locale - Output document language.
* @returns Complete generated Markdown.
*/
export function renderModuleGraph(pkgs: readonly Pkg[], locale: Locale): string {
const edges: string[] = []
for (const p of pkgs) {
for (const d of p.deps) edges.push(` ${nodeId('pkg', p.short)} --> ${nodeId('pkg', d)}`)
for (const pkg of pkgs) {
for (const dependency of pkg.deps) edges.push(` ${nodeId('pkg', pkg.short)} --> ${nodeId('pkg', dependency)}`)
}
const byShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((a, b) => {
const ia = GROUP_ORDER.indexOf(a)
const ib = GROUP_ORDER.indexOf(b)
const na = ia === -1 ? Number.MAX_SAFE_INTEGER : ia
const nb = ib === -1 ? Number.MAX_SAFE_INTEGER : ib
return na - nb || a.localeCompare(b)
const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((left, right) => {
const leftIndex = GROUP_ORDER.indexOf(left)
const rightIndex = GROUP_ORDER.indexOf(right)
const normalizedLeft = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex
const normalizedRight = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex
return normalizedLeft - normalizedRight || left.localeCompare(right)
})
const groupBlocks: string[] = []
for (const group of groups) {
groupBlocks.push(` subgraph ${nodeId('group', group)}["packages/${escLabel(group)}"]`)
for (const pkg of pkgs.filter(p => p.group === group).sort((a, b) => a.short.localeCompare(b.short))) {
for (const pkg of pkgs.filter(candidate => candidate.group === group)
.sort((left, right) => left.short.localeCompare(right.short))) {
groupBlocks.push(` ${nodeId('pkg', pkg.short)}["${escLabel(pkg.short)}"]`)
}
groupBlocks.push(' end')
}
const rows = pkgs.map((p) => {
const deps = p.deps.length ? p.deps.map((d) => {
const dep = byShort.get(d)
return dep ? packageLink(dep) : `\`${d}\``
}).join(', ') : '—'
return `| ${packageLink(p)} | \`${p.group}\` | ${deps} |`
const rows = pkgs.map((pkg) => {
const dependencies = pkg.deps.length > 0
? pkg.deps.map((dependency) => {
const target = byShort.get(dependency)
return target ? packageLink(target) : `\`${dependency}\``
}).join(', ')
: '—'
return `| ${packageLink(pkg)} | \`${pkg.group}\` | ${dependencies} |`
})
const chinese = locale === 'zh'
return [
'<!-- Generated by scripts/gen-module-graph.ts — do not edit by hand.',
' Run `pnpm run gen-module-graph` to regenerate. -->',
chinese
? '<!-- 由 scripts/gen-module-graph.ts 生成——请勿手工编辑。\n 运行 `pnpm run gen-module-graph` 重新生成。 -->'
: '<!-- Generated by scripts/gen-module-graph.ts — do not edit by hand.\n Run `pnpm run gen-module-graph` to regenerate. -->',
'',
'# Module dependency graph',
chinese ? '# 共享实例依赖关系图' : '# Shared-instance dependency graph',
'',
'Inter-package dependencies among the `@deepseek-ai/dsh-*` harness packages, derived from each package\'s `peerDependencies` (the canonical runtime-dependency signal) and grouped by the `packages/<group>/<pkg>` hierarchy. An edge `a --> b` means package `a` depends on package `b`. Names have the `@deepseek-ai/dsh-` prefix stripped.',
...(chinese ? ['[English](module-graph.md) | 中文', ''] : []),
chinese
? '`@deepseek-ai/dsh-*` harness 包之间的 peer 依赖关系。peer 表示消费端需要提供共享实例,不包括普通运行时 dependency 或仅开发期关系。该图按 `packages/<group>/<pkg>` 层级分组;边 `a --> b` 表示包 `a` peer 依赖包 `b`。名称中的 `@deepseek-ai/dsh-` 前缀已移除。'
: 'Peer dependencies among the `@deepseek-ai/dsh-*` harness packages. A peer means the consumer requires a shared instance; ordinary runtime dependencies and development-only relationships are not shown. The graph is grouped by the `packages/<group>/<pkg>` hierarchy. An edge `a --> b` means package `a` has package `b` as a peer. Names omit the `@deepseek-ai/dsh-` prefix.',
'',
'```mermaid',
'flowchart TD',
@@ -89,31 +102,76 @@ function render(pkgs: Pkg[]): string {
...edges,
'```',
'',
'| Package | Group | Depends on |',
chinese ? '| 包 | 分组 | Peer 依赖 |' : '| Package | Group | Peer dependencies |',
'| --- | --- | --- |',
...rows,
'',
].join('\n')
}
const content = render(collectPackageGraph(root, GROUP_ORDER, 'gen-module-graph'))
if (process.argv.includes('--check')) {
let committed: string | null = null
try {
committed = readFileSync(resolve(root, OUT), 'utf8')
} catch {
// A missing artifact is the expected read failure. Any read failure has the
// same remedy here—regenerate—so it is reported as stale below.
committed = null
}
if (committed === content) {
console.log(`gen-module-graph: ${OUT} is up to date.`)
process.exit(0)
}
console.error(`gen-module-graph: ${OUT} is stale. Run \`pnpm run gen-module-graph\` and commit ${OUT}.`)
process.exit(1)
/**
* Compute both localized graph documents from the current workspace manifests.
* @param scanRoot - Repository root containing packages and documentation.
* @returns Repository-relative output paths and exact generated content.
*/
export function computeModuleGraphOutputs(scanRoot: string = root): ReadonlyMap<string, string> {
const packages = collectPackageGraph(scanRoot, GROUP_ORDER, 'gen-module-graph')
return new Map([
[PATHS.source, renderModuleGraph(packages, 'en')],
[PATHS.zh, renderModuleGraph(packages, 'zh')],
])
}
writeFileSync(resolve(root, OUT), content)
console.log(`gen-module-graph: wrote ${OUT}.`)
/**
* Write both graph documents and their recovery record.
* @param scanRoot - Repository root containing packages and documentation.
* @returns Repository-relative paths whose content changed.
*/
export function writeModuleGraph(scanRoot: string = root): string[] {
const outputs = computeModuleGraphOutputs(scanRoot)
const changed: string[] = []
for (const [path, content] of outputs) {
const destination = resolve(scanRoot, path)
if (existsSync(destination) && readFileSync(destination, 'utf8') === content) continue
writeFileSync(destination, content)
changed.push(path)
}
const source = Buffer.from(outputs.get(PATHS.source) ?? '')
const zh = Buffer.from(outputs.get(PATHS.zh) ?? '')
const record = renderTranslationPairingRecord(PATHS, {
sourceHash: storeGitBlob(scanRoot, source),
zhHash: storeGitBlob(scanRoot, zh),
})
const recordPath = resolve(scanRoot, PATHS.meta)
if (!existsSync(recordPath) || readFileSync(recordPath, 'utf8') !== record) {
writeFileSync(recordPath, record)
changed.push(PATHS.meta)
}
return changed.sort()
}
/** CLI entry: regenerate by default, or verify all paired outputs with `--check`. @returns Nothing. */
export function main(): void {
const outputs = computeModuleGraphOutputs(root)
const record = renderTranslationPairingRecord(PATHS, {
sourceHash: gitBlobHash(Buffer.from(outputs.get(PATHS.source) ?? '')),
zhHash: gitBlobHash(Buffer.from(outputs.get(PATHS.zh) ?? '')),
})
const expected = new Map([...outputs, [PATHS.meta, record]])
if (process.argv.includes('--check')) {
const stale = [...expected].filter(([path, content]) => (
!existsSync(resolve(root, path)) || readFileSync(resolve(root, path), 'utf8') !== content
)).map(([path]) => path)
if (stale.length === 0) {
console.log(`gen-module-graph: ${expected.size} artifact(s) are up to date.`)
return
}
console.error(`gen-module-graph: stale — ${stale.join(', ')}. Run \`pnpm run gen-module-graph\` and commit the result.`)
process.exitCode = 1
return
}
const changed = writeModuleGraph(root)
console.log(`gen-module-graph: ${expected.size} artifact(s) computed, ${String(changed.length)} written.`)
}
if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) main()
+11 -7
View File
@@ -368,7 +368,7 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv
'',
'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/archived/process/2026-07-04-persistence-log-catalog.md).',
'',
'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.',
'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, the optional `ignorable` unknown-type skip marker, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](subsystems/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.',
'',
'## Event envelope',
'',
@@ -394,7 +394,7 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv
/**
* Render the runtime known-vocabulary module: every event type the packages in
* this repo can write, as a generated `ReadonlySet` the read path checks
* before reconstructing a stored session.
* unknown-type refusal against (`SessionEvent.ignorable` contract).
*/
export function renderKnownEventTypes(events: AnnotatedLogEventEntry[]): string {
const names = [...new Set(events.map(e => e.name))].sort()
@@ -409,12 +409,16 @@ export function renderKnownEventTypes(events: AnnotatedLogEventEntry[]): string
'/**',
' * Every `SessionEventMap` member declared in this repository — the event',
' * vocabulary this build understands. The persistence read path refuses to',
' * interpret a log containing a type outside this set: such a log was likely',
' * written by a newer harness, and silently skipping the event could',
' * reconstruct a wrong session.',
' * interpret a log containing a type outside this set unless the event',
' * carries the envelope\'s `ignorable` marker (see `SessionEvent.ignorable`',
' * in `./types.ts`): such a log was likely written by a newer harness, and',
' * silently skipping a required event would reconstruct a wrong session.',
' * Downstream (out-of-repo) plugin events are outside this list by',
' * construction; a registration surface for them is deferred until such a',
' * consumer exists.',
' * construction. The persisted `SessionEvent.ignorable` marker is the',
' * compatibility mechanism; event-name registration was rejected because',
' * it does not classify omission safety and would make reads',
' * composition-dependent. The rationale is in',
' * `.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md`.',
' */',
'export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([',
...names.map(name => ` '${name}',`),
+14 -9
View File
@@ -23,9 +23,6 @@ const pairingMergeDriver = 'scripts/merge-translation-pairing-driver.sh %O %A %B
const scriptsDirectory = fileURLToPath(new URL('.', import.meta.url))
const tsxPackageDirectory = dirname(fileURLToPath(import.meta.resolve('tsx/package.json')))
const fixtures: string[] = []
// Multi-worktree cases spawn several Git and Node subprocesses; native Windows
// coverage concurrency can delay them without changing installer behavior.
const MULTI_PROCESS_TEST_TIMEOUT_MS = 30_000
interface Fixture {
container: string
@@ -211,7 +208,15 @@ function runInstaller(
})
}
describe('worktree-local Lefthook installer', { timeout: 30_000 }, () => {
// Every case builds scratch worktrees and drives them through spawned Git and
// Node subprocesses, so the suite is bound by process creation rather than by
// its assertions. The value matches DSH_COVERAGE_TEST_TIMEOUT_MS, which the
// Windows coverage lane passes as --testTimeout: a describe value overrides that
// flag rather than yielding to it, so a smaller one here lowers what the lane
// grants every case in this file, none of which carries an allowance of its own.
// Rationale and the paired hook budget are in
// .agents/notes/implemented/testing/2026-08-29-windows-lane-hook-and-lefthook-budget.md.
describe('worktree-local Lefthook installer', { timeout: 90_000 }, () => {
for (const [label, extraEnv] of [
['CI', { CI: 'true' }],
['GitHub Actions', { GITHUB_ACTIONS: 'true' }],
@@ -290,7 +295,7 @@ describe('worktree-local Lefthook installer', { timeout: 30_000 }, () => {
git(fixture, fixture.main, ['worktree', 'remove', '--force', fixture.linked])
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBeforeRemoval)
expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy hook\n')
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
})
it('replaces the owned hook path Git copies into a newly added worktree', async () => {
const fixture = createFixture()
@@ -315,7 +320,7 @@ describe('worktree-local Lefthook installer', { timeout: 30_000 }, () => {
'# config=late-linked-worktree-config',
)
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBefore)
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
})
it('serializes concurrent installs and keeps repeated output stable', async () => {
const fixture = createFixture()
@@ -336,7 +341,7 @@ describe('worktree-local Lefthook installer', { timeout: 30_000 }, () => {
expect(readFileSync(mainHookPath, 'utf8')).toBe(initialHook)
expect(existsSync(join(commonDirectory(fixture), 'dsh-lefthook-install.lock'))).toBe(false)
expect(existsSync(join(hooksPath(fixture, fixture.main), '.fake-lefthook-running'))).toBe(false)
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
})
it('waits for a concurrent installer to finish publishing its lock record', async () => {
const fixture = createFixture()
@@ -374,7 +379,7 @@ describe('worktree-local Lefthook installer', { timeout: 30_000 }, () => {
expect(readFileSync(join(movedHooks, '.dsh-lefthook-owned'), 'utf8')).toContain(
JSON.stringify(movedHooks),
)
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
})
it.skipIf(process.platform === 'win32')('refuses a multiply linked ownership marker before relocation rewrites it', async () => {
const fixture = createFixture()
@@ -415,7 +420,7 @@ describe('worktree-local Lefthook installer', { timeout: 30_000 }, () => {
expect(result.stderr).toContain('non-regular or multiply linked hook entry')
expect(readFileSync(externalHook, 'utf8')).toBe(externalContent)
}
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
})
it('restores the marker-backed stale hook path when relocation reinstall fails', async () => {
const fixture = createFixture()
+102
View File
@@ -0,0 +1,102 @@
/** 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',
'@deepseek-ai/dsh-api-workspace-controller',
]
/** Host-only packages whose peer relays are deliberately flattened. */
const HOST_DEPENDENCY_PACKAGES: readonly string[] = [
'@deepseek-ai/dsh-llm',
'@deepseek-ai/dsh-session',
]
/** Development-only package relationships not represented by source imports. */
const CONFIGURATION_ONLY_DEV_DEPENDENCIES = {
'@deepseek-ai/dsh-client-locale': ['@deepseek-ai/dsh-api-remotes'],
'@deepseek-ai/dsh-client-ui-conversation': [
'@deepseek-ai/dsh-api-remotes',
'@deepseek-ai/dsh-client-ui-workspace',
],
'@deepseek-ai/dsh-client-ui-model-selection': ['@deepseek-ai/dsh-client-ui-input-trigger'],
'@deepseek-ai/dsh-client-ui-sidebar': ['@deepseek-ai/dsh-client-ui-workspace'],
'@deepseek-ai/dsh-client-ui-subagent': ['@deepseek-ai/dsh-client-ui-input-trigger'],
'@deepseek-ai/dsh-client-ui-theme': ['@deepseek-ai/dsh-api-remotes'],
'@deepseek-ai/dsh-client-ui-tool': ['@deepseek-ai/dsh-api-remotes'],
} as const satisfies Readonly<Record<string, readonly string[]>>
/** Workspace packages whose complete runtime surface is safe across duplicate installations. */
const DUPLICATE_SAFE_PACKAGES: readonly string[] = [
'@deepseek-ai/dsh-brand',
'@deepseek-ai/dsh-typert-protocol',
'@deepseek-ai/dsh-util-crypto',
'@deepseek-ai/dsh-util-values',
]
/**
* Runtime exports whose values remain valid when npm installs another package copy.
*/
const SAFE_HOST_DEPENDENCY_EXPORTS = {
'@deepseek-ai/dsh-credentials': ['credentialKey'],
'@deepseek-ai/dsh-deque': ['Deque'],
'@deepseek-ai/dsh-llm': ['callConfigEquals'],
'@deepseek-ai/dsh-timeout': ['MAX_TIMER_DELAY_MS'],
'@deepseek-ai/schemastery': ['default'],
} as const satisfies HostDependencyExports
/** Runtime exports that require every consumer to resolve the provider's shared peer instance. */
const PEER_REQUIRED_HOST_EXPORTS = {
'@deepseek-ai/dsh-scope': ['carrierKeyOf', 'scopeOf', 'scopeTarget'],
} as const satisfies HostDependencyExports
/** Exact import specifier to reviewed runtime exports. */
type HostDependencyExports = Readonly<Record<string, readonly string[]>>
/** Complete configurable input to package dependency classification. */
export interface PackageDependencyPolicy {
readonly clientFaceInclude: readonly string[]
readonly clientFaceExclude: readonly string[]
readonly hostPackages: readonly string[]
readonly configurationOnlyDevDependencies: Readonly<Record<string, readonly string[]>>
readonly duplicateSafePackages?: readonly string[]
readonly safeHostDependencyExports: HostDependencyExports
readonly peerRequiredHostExports: HostDependencyExports
}
/** 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,
configurationOnlyDevDependencies: CONFIGURATION_ONLY_DEV_DEPENDENCIES,
duplicateSafePackages: DUPLICATE_SAFE_PACKAGES,
safeHostDependencyExports: SAFE_HOST_DEPENDENCY_EXPORTS,
peerRequiredHostExports: PEER_REQUIRED_HOST_EXPORTS,
}
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)
}
+21
View File
@@ -2,6 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { renderModuleGraph } from './gen-module-graph.ts'
import { collectPackageGraph } from './package-graph.ts'
const roots: string[] = []
@@ -49,3 +50,23 @@ describe('collectPackageGraph', () => {
.toThrow('fixture: @deepseek-ai/dsh-consumer references missing in-repo peer @deepseek-ai/dsh-missing')
})
})
describe('renderModuleGraph', () => {
it('renders the same peer edge in both generated languages', () => {
const packages = [
{ short: 'provider', name: '@deepseek-ai/dsh-provider', group: 'core', rel: 'packages/core/provider', deps: [] },
{ short: 'consumer', name: '@deepseek-ai/dsh-consumer', group: 'core', rel: 'packages/core/consumer', deps: ['provider'] },
]
const english = renderModuleGraph(packages, 'en')
const chinese = renderModuleGraph(packages, 'zh')
expect(english).toContain('# Shared-instance dependency graph')
expect(chinese).toContain('# 共享实例依赖关系图')
expect(chinese).toContain('[English](module-graph.md) | 中文')
for (const output of [english, chinese]) {
expect(output).toContain('pkg_consumer --> pkg_provider')
expect(output).toContain('| [`consumer`](../packages/core/consumer) | `core` | [`provider`](../packages/core/provider) |')
}
})
})
+54 -8
View File
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import {
collectPackageInvariantViolations,
} from './package-invariants.ts'
import { usesFlattenedPackageDependencies } from './package-dependency-policy.ts'
const roots: string[] = []
@@ -28,7 +29,10 @@ export const apply = (ctx: { invariants: { register(name: string, install: typeo
function fixture(options: {
packageName?: string
packageDirectory?: string
source?: string
clientDeclaration?: boolean
clientExport?: boolean
invariantExport?: boolean
invariantDependency?: boolean
invariantReference?: boolean
@@ -36,19 +40,34 @@ function fixture(options: {
} = {}): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-invariants-'))
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 })
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 = {
name: packageName,
exports: options.invariantExport === false ? {} : {
'./invariant': {
types: './lib/types/invariant.d.ts',
default: './lib/invariant.js',
},
},
...(dsh === undefined ? {} : { dsh }),
exports,
files: ['lib/index.js', 'lib/invariant.js'],
peerDependencies: options.invariantDependency === false ? {} : {
peerDependencies: options.invariantDependency === false || developmentOnlyInvariant ? {} : {
'@deepseek-ai/dsh-invariants': 'workspace:^',
},
devDependencies: options.invariantDependency === false ? {} : {
@@ -72,6 +91,33 @@ describe('package invariant gate', () => {
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', () => {
const root = fixture({ invariantReference: false })
const dir = join(root, 'packages/core/probe')
+15 -8
View File
@@ -7,12 +7,14 @@
import { existsSync, globSync, readFileSync } from 'node:fs'
import { dirname, relative, resolve, sep } from 'node:path'
import ts from 'typescript'
import { usesFlattenedPackageDependencies } from './package-dependency-policy.ts'
/** Required explanation marker for an intentionally empty installer. */
const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:'
interface PackageManifest {
name?: string
dsh?: unknown
exports?: Record<string, { types?: string; default?: string } | string | undefined>
files?: string[]
peerDependencies?: Record<string, string>
@@ -96,18 +98,23 @@ function checkManifest(
addViolation(violations, owner.manifestPath, 'files must publish lib/invariant.js')
}
if (owner.packageName === '@deepseek-ai/dsh-invariants') return
if (manifest.peerDependencies?.['@deepseek-ai/dsh-invariants'] !== 'workspace:^') {
addViolation(
violations,
owner.manifestPath,
'@deepseek-ai/dsh-invariants must be a workspace:^ peerDependency',
)
const developmentOnlyInvariant = usesFlattenedPackageDependencies(
owner.manifestPath,
owner.packageName,
manifest.dsh,
)
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(
violations,
owner.manifestPath,
'@deepseek-ai/dsh-invariants must also be a workspace:^ devDependency',
`@deepseek-ai/dsh-invariants must be a ${expectedRange} devDependency`,
)
}
}
+34
View File
@@ -67,6 +67,22 @@ describe('release families', () => {
])
})
it.each(['0.0.2-alpha.1', '0.0.2-canary.1', '0.0.2-rc.1'])(
'accepts the explicit dsh prerelease version %s',
(version) => {
const root = mkdtempSync(join(tmpdir(), 'dsh-release-prerelease-'))
roots.push(root)
write(join(root, 'package.json'), '{"version":"0.0.1"}\n')
const dsh = releaseFamily('dsh')
const published = member('packages/core/published', '@deepseek-ai/dsh-published')
const plan = planShared(dsh, root, [published], version)
expect(plan.version).toBe(version)
expect(plan.planned[1]?.tag).toBe(`dsh-v${version}`)
},
)
it('names one tag for the whole dsh family and one per vendored package', () => {
const dsh = releaseFamily('dsh')
const vendor = releaseFamily('vendor')
@@ -81,6 +97,18 @@ describe('release families', () => {
expect(vendor.tagFor({ ...cordis, version: '4.0.0-rc.7' })).toBe('vendor-cordis-v4.0.0-rc.7')
})
it('assigns alpha and canary dist-tags only to dsh releases', () => {
const dsh = releaseFamily('dsh')
const vendor = releaseFamily('vendor')
expect(dsh.distTagForVersion('0.0.2-alpha.1')).toBe('alpha')
expect(dsh.distTagForVersion('0.0.2-canary.1')).toBe('canary')
expect(dsh.distTagForVersion('0.0.2-rc.1')).toBe('next')
expect(dsh.distTagForVersion('0.0.2')).toBeUndefined()
expect(vendor.distTagForVersion('4.0.1-alpha.1')).toBe('next')
expect(vendor.distTagForVersion('4.0.1-canary.1')).toBe('next')
})
it('rejects a family whose members disagree on the shared version', () => {
const dsh = releaseFamily('dsh')
const members = [member('apps/cli', '@deepseek-ai/dsh'), { ...member('apps/web', '@deepseek-ai/dsh-web-frontend'), version: '0.0.2' }]
@@ -277,6 +305,12 @@ describe('vendored version baseline', () => {
})
describe('version precedence', () => {
it('orders alpha, canary, and release-candidate versions by semver precedence', () => {
expect(compareVersions('4.0.1-alpha.1', '4.0.1-canary.1')).toBeLessThan(0)
expect(compareVersions('4.0.1-canary.1', '4.0.1-rc.1')).toBeLessThan(0)
expect(compareVersions('4.0.1-rc.1', '4.0.1')).toBeLessThan(0)
})
it('ranks a release above the prerelease it follows', () => {
// git --sort=v:refname disagrees, placing 4.0.1-rc.1 above 4.0.1, which is
// why the newest published version is chosen here rather than by git.
+17
View File
@@ -285,6 +285,15 @@ export abstract class ReleaseFamily {
*/
abstract tagPrefixFor(member: ReleaseMember): string
/**
* The npm dist-tag assigned while publishing a version.
* @param version - package version from the packed manifest.
* @returns `next` for a prerelease, or undefined so npm uses `latest`.
*/
distTagForVersion(version: string): string | undefined {
return version.includes('-') ? 'next' : undefined
}
/**
* The tag a member publishes from.
* @param member - the member being published.
@@ -339,6 +348,14 @@ class DshFamily extends ReleaseFamily {
return this.tagPrefix
}
override distTagForVersion(version: string): string | undefined {
const separator = version.indexOf('-')
if (separator === -1) return undefined
const [channel] = version.slice(separator + 1).split('.')
if (channel === 'alpha' || channel === 'canary') return channel
return 'next'
}
/**
* Reject source and declaration-map members, the repository's publication policy.
* @param member - the packed member.
+9 -4
View File
@@ -93,10 +93,15 @@ function registryState(name: string, version: string): RegistryState {
* @param tarball - absolute tarball path.
* @param name - package name the tarball declares.
* @param version - package version the tarball declares.
* @param distTag - explicit npm dist-tag, or undefined for npm's `latest` default.
*/
async function publishTarball(tarball: string, name: string, version: string): Promise<void> {
// A prerelease version never takes the latest dist-tag.
const tagArgs = version.includes('-') ? ['--tag', 'next'] : []
async function publishTarball(
tarball: string,
name: string,
version: string,
distTag: string | undefined,
): Promise<void> {
const tagArgs = distTag === undefined ? [] : ['--tag', distTag]
for (let tries = 1; tries <= PUBLISH_ATTEMPTS; tries += 1) {
// No --access: the sequences do not share one access level, so a
// command-line flag could not serve both and would override the manifest
@@ -164,7 +169,7 @@ async function main(): Promise<void> {
// Space out the writes: the gap belongs between publishes, so a run that
// only skips does not wait at all.
if (published > 0) await sleep(PUBLISH_SPACING_MS)
await publishTarball(tarball, name, version)
await publishTarball(tarball, name, version, family.distTagForVersion(version))
console.log(`release publish: ${progress} ${name}@${version} published`)
published += 1
}
+13 -3
View File
@@ -102,7 +102,7 @@ describe('gate graph validation', () => {
const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
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',
'optional-dependency-imports', 'client-packages', 'client-ui-i18n', 'cordis-config',
'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)(
'keeps the client dependency policy in %s',
(mode) => {
@@ -208,7 +217,7 @@ describe('gate graph validation', () => {
expect(completeBuiltBin?.after).not.toContain('docs-site-build')
})
it('applies one configured test and polling timeout to both coverage gates', () => {
it('applies one configured test, polling, and hook timeout to both coverage gates', () => {
const gates = withEnv('DSH_COVERAGE_TEST_TIMEOUT_MS', '15000', () =>
withPnpmEntrypoint(() => gatesForMode('ci-windows-complete')))
@@ -216,6 +225,7 @@ describe('gate graph validation', () => {
expect(gates.find(subject => subject.id === id)?.args).toEqual(expect.arrayContaining([
'--testTimeout=15000',
'--expect.poll.timeout=15000',
'--hookTimeout=15000',
]))
}
})
@@ -226,7 +236,7 @@ describe('gate graph validation', () => {
for (const id of ['coverage', 'coverage-exempt-heavy']) {
expect(gates.find(subject => subject.id === id)?.args).not.toEqual(expect.arrayContaining([
expect.stringMatching(/^--(?:testTimeout|expect\.poll\.timeout)=/),
expect.stringMatching(/^--(?:testTimeout|expect\.poll\.timeout|hookTimeout)=/),
]))
}
})
+3 -1
View File
@@ -278,6 +278,7 @@ function ciSharedStaticGates(): Gate[] {
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
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('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
@@ -569,7 +570,7 @@ function lintGate(options: { needs?: string[] } = {}): Gate {
// small share. A budget of 1 gives each gate 1 worker; lanes that need a strict
// total of one (the serial reference jobs) also set DSH_GATE_CONCURRENCY=1,
// which keeps the gates from overlapping at all.
// DSH_COVERAGE_TEST_TIMEOUT_MS raises Vitest's per-test and expect.poll
// DSH_COVERAGE_TEST_TIMEOUT_MS raises Vitest's per-test, expect.poll, and hook
// defaults together for instrumented lanes whose scheduling overhead exceeds
// those defaults. Explicit fixture timeouts remain authoritative.
function coverageWorkerArgs(): { instrumented: string[]; exempt: string[] } {
@@ -667,6 +668,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
pnpmScript('rescope-vendor', 'rescope-vendor:check', { label: 'vendor rescope' }),
pnpmScript('publint', 'publint', artifactOptions),
pnpmScript('constraints', 'constraints'),
pnpmScript('package-dependencies', 'verify-package-dependencies', { label: 'package dependencies' }),
pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
+203
View File
@@ -0,0 +1,203 @@
import { EventEmitter } from 'node:events'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
const httpMock = vi.hoisted(() => ({ createServer: vi.fn() }))
vi.mock('node:http', () => ({ createServer: httpMock.createServer }))
// Snapshot plugins are plain runtime JavaScript loaded by cordis.yml.
// @ts-expect-error The fixture intentionally has no declaration artifact.
import * as searchFixtureModule from '../snapshots/session/web-search-endpoint-guidance/web-search-error-fixture.mjs'
// @ts-expect-error The fixture intentionally has no declaration artifact.
import * as loopbackFixtureModule from '../snapshots/session/loopback-fixture-server.mjs'
const RECORDED_ENDPOINT = 'http://127.0.0.1:43118/anthropic/v1/messages'
interface FixturePlugin {
readonly name: string
readonly inject?: readonly string[]
apply(ctx: Context): Promise<void>
}
interface LoopbackFixtureOptions {
readonly label: string
readonly onCleanup: () => void
readonly onListening: (address: { port: number }) => void
readonly requestListener: () => void
}
const searchFixture = searchFixtureModule as unknown as FixturePlugin
const typedLoopbackFixtureModule = loopbackFixtureModule as unknown as {
readonly applyLoopbackServerEffect: (ctx: Context, options: LoopbackFixtureOptions) => Promise<void>
}
const { applyLoopbackServerEffect } = typedLoopbackFixtureModule
const nativeFetch = globalThis.fetch
class FixtureServer extends EventEmitter {
readonly started = Promise.withResolvers<undefined>()
listening = false
closed = false
connectionsClosed = false
unreferenced = false
private listenCallback: (() => void) | undefined
private port = 0
listen(_port: number, _host: string, callback: () => void): this {
this.listenCallback = callback
this.started.resolve(undefined)
return this
}
finishListening(port = 54321): void {
this.port = port
this.listening = true
this.listenCallback?.()
}
address(): { address: string; family: string; port: number } | null {
return this.listening ? { address: '127.0.0.1', family: 'IPv4', port: this.port } : null
}
unref(): this {
this.unreferenced = true
return this
}
close(callback: (error?: Error) => void): this {
this.listening = false
this.closed = true
callback()
return this
}
closeAllConnections(): void {
this.connectionsClosed = true
}
}
function nextServer(): FixtureServer {
const server = new FixtureServer()
httpMock.createServer.mockReturnValueOnce(server)
return server
}
function captureErrors(ctx: Context): unknown[] {
const errors: unknown[] = []
ctx.logger.error = ((error: unknown) => { errors.push(error) }) as typeof ctx.logger.error
return errors
}
async function disposeWhileStarting(fiber: { dispose(): Promise<unknown> }, server: FixtureServer): Promise<void> {
await server.started.promise
const disposal = fiber.dispose()
const settled = vi.fn()
void disposal.then(settled)
await Promise.resolve()
expect(settled).not.toHaveBeenCalled()
server.finishListening()
await disposal
}
afterEach(() => {
globalThis.fetch = nativeFetch
httpMock.createServer.mockReset()
})
describe('snapshot HTTP fixture lifecycle', () => {
it('joins search listener setup and cleanup when disposal wins the startup race', async () => {
const server = nextServer()
const ctx = new Context()
const errors = captureErrors(ctx)
const fiber = ctx.plugin(searchFixture)
await disposeWhileStarting(fiber, server)
expect(server).toMatchObject({ closed: true, connectionsClosed: true, unreferenced: true })
expect(globalThis.fetch).toBe(nativeFetch)
expect(errors).toEqual([])
})
it('runs owner cleanup and closes the listener when disposal wins the startup race', async () => {
const server = nextServer()
const ctx = new Context()
const errors = captureErrors(ctx)
const onCleanup = vi.fn()
const onListening = vi.fn()
const fiber = ctx.plugin({
name: 'loopback-fixture-lifecycle-test',
apply: testCtx => applyLoopbackServerEffect(testCtx, {
label: 'loopback-fixture-lifecycle-test',
onCleanup,
onListening,
requestListener: () => {},
}),
})
await disposeWhileStarting(fiber, server)
expect(server).toMatchObject({ closed: true, connectionsClosed: true, unreferenced: true })
expect(onListening).toHaveBeenCalledWith(expect.objectContaining({ port: 54321 }))
expect(onCleanup).toHaveBeenCalledOnce()
expect(errors).toEqual([])
})
it('maps every fetch input form and rejects another path on the recorded authority', async () => {
const server = nextServer()
const fetchMock = vi.fn(async (_input: string | URL | Request, _init?: RequestInit) => new Response('{}'))
globalThis.fetch = fetchMock
const ctx = new Context()
const fiber = ctx.plugin(searchFixture)
await server.started.promise
server.finishListening(54322)
await fiber
try {
await globalThis.fetch(RECORDED_ENDPOINT)
expect(fetchMock.mock.calls.at(-1)?.[0]).toBe('http://127.0.0.1:54322/anthropic/v1/messages')
await globalThis.fetch(new URL(RECORDED_ENDPOINT))
expect(fetchMock.mock.calls.at(-1)?.[0]).toBe('http://127.0.0.1:54322/anthropic/v1/messages')
const request = new Request(RECORDED_ENDPOINT, { method: 'POST', headers: { 'x-fixture': 'request' } })
await globalThis.fetch(request)
const mappedRequest = fetchMock.mock.calls.at(-1)?.[0]
expect(mappedRequest).toBeInstanceOf(Request)
if (!(mappedRequest instanceof Request)) throw new TypeError('mapped fetch input must be a Request')
expect(mappedRequest.url).toBe('http://127.0.0.1:54322/anthropic/v1/messages')
expect(mappedRequest.method).toBe('POST')
expect(mappedRequest.headers.get('x-fixture')).toBe('request')
const unrelated = new URL('https://example.test/')
await globalThis.fetch(unrelated)
expect(fetchMock.mock.calls.at(-1)?.[0]).toBe(unrelated)
await expect(globalThis.fetch('http://127.0.0.1:43118/unexpected'))
.rejects.toThrow('web-search-error-fixture: unexpected URL for recorded authority')
} finally {
await fiber.dispose()
}
expect(globalThis.fetch).toBe(fetchMock)
expect(server.closed).toBe(true)
})
it('preserves a later fetch wrapper while still closing the listener and reporting the ownership error', async () => {
const server = nextServer()
const fetchMock = vi.fn(async (_input: string | URL | Request, _init?: RequestInit) => new Response('{}'))
globalThis.fetch = fetchMock
const ctx = new Context()
const errors = captureErrors(ctx)
const fiber = ctx.plugin(searchFixture)
await server.started.promise
server.finishListening()
await fiber
const fixtureFetch = globalThis.fetch
const laterFetch = vi.fn((input: string | URL | Request, init?: RequestInit) => fixtureFetch(input, init))
globalThis.fetch = laterFetch
await fiber.dispose()
expect(globalThis.fetch).toBe(laterFetch)
expect(server).toMatchObject({ closed: true, connectionsClosed: true })
expect(errors.map(String).join('\n')).toContain('web-search-error-fixture: global fetch owner changed before cleanup')
})
})
+9 -1
View File
@@ -261,7 +261,15 @@ function expectMergedPair(fixture: Fixture): void {
)
}
describe('translation pairing merge composition', { timeout: 15_000 }, () => {
// Every case in this suite drives real `git` invocations against a scratch
// repository, so it is bound by process creation rather than by its assertions.
// The value matches DSH_COVERAGE_TEST_TIMEOUT_MS, which the Windows coverage
// lane passes as --testTimeout: a describe value overrides that flag rather than
// yielding to it, so a smaller one here lowers what the lane grants every case
// in this file, none of which carries an allowance of its own. Measurements and
// the rejected alternatives are in
// .agents/notes/implemented/testing/2026-08-27-translation-pairing-merge-budget.md.
describe('translation pairing merge composition', { timeout: 90_000 }, () => {
it('rejects a pairing-record path outside the repository', () => {
const fixture = createFixture(false)
+19 -143
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 { tmpdir } from 'node:os'
@@ -6,6 +6,7 @@ import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
collectClientPackageViolations,
collectLocalSourceSpecifiers,
collectRuntimeSourcePackageUses,
collectRuntimeSourceSpecifiers,
collectSourcePackageUses,
@@ -106,6 +107,19 @@ describe('source package uses', () => {
'@deepseek-ai/dsh-b/remote',
'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', () => {
it('rejects runtime requests from one client feature package to another dynamic row', () => {
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-'))
roots.push(root)
const subject = pkg('feature', {
@@ -426,43 +337,8 @@ describe('manifest declarations', () => {
external: ['@deepseek-ai/dsh-missing'],
inject: ['@deepseek-ai/dsh-agent'],
})
expect(fixed.dependencies).toBeUndefined()
expect(fixed.peerDependencies).toEqual({
'@deepseek-ai/cordis-plugin-loader': 'workspace:^',
[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:^' })
expect(fixed.dependencies).toEqual(subject.dependencies)
expect(fixed.peerDependencies).toEqual(subject.peerDependencies)
expect(fixed.devDependencies).toEqual(subject.devDependencies)
})
})
+35 -290
View File
@@ -1,6 +1,5 @@
/**
* Verify client package modes, npm dependency sections, and the synchronous
* browser module-request graph.
* Verify client package modes and the synchronous browser module-request graph.
*/
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 STATIC_PRESET_SOURCE = 'packages/client/tsdown.client.ts'
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. */
export interface ClientDeclaration {
@@ -92,6 +89,28 @@ export function collectRuntimeSourceSpecifiers(path: string, source: string): Se
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')
}
/**
* Collect relative module specifiers retained by one production source file.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Relative imports, exports, and requires that survive compilation.
*/
export function collectRuntimeLocalSourceSpecifiers(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, true, 'local')
}
function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
const clause = node.importClause
if (clause === undefined) return true
@@ -114,12 +133,17 @@ function exportCarriesRuntimeValue(node: ts.ExportDeclaration): boolean {
function collectSourceFileUses(
sourceFile: ts.SourceFile,
runtimeOnly: boolean,
key: 'package' | 'specifier',
key: 'local' | 'package' | 'specifier',
): Set<string> {
const uses = new Set<string>()
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)
}
const visit = (node: ts.Node): void => {
@@ -135,9 +159,10 @@ function collectSourceFileUses(
&& (node.expression.kind === ts.SyntaxKind.ImportKeyword
|| ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
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)
} 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')
}
ts.forEachChild(node, visit)
@@ -170,7 +195,6 @@ export function collectClientPackageViolations(facts: ClientPackageFacts): strin
return [
...facts.malformed,
...collectModeViolations(facts),
...collectDependencyViolations(facts),
...collectModuleViolations(facts),
].sort((left, right) => left.localeCompare(right))
}
@@ -181,10 +205,8 @@ interface ManifestDocument {
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 facts - Facts used by the verification pass.
* @returns Repository-relative manifests written by the fixer.
@@ -217,53 +239,6 @@ export function fixClientPackageManifests(root: string, facts: ClientPackageFact
) || 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) =>
left.path.localeCompare(right.path))
for (const target of changed) {
@@ -295,102 +270,6 @@ function normalizeClientArray(
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[] {
const violations: string[] = []
for (const pkg of facts.packages) {
@@ -435,114 +314,6 @@ function collectModeViolations(facts: ClientPackageFacts): string[] {
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 {
readonly from: string
readonly to: string
@@ -895,32 +666,6 @@ function rowPackageOf(specifier: string, rows: ReadonlySet<string>): string | un
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 {
return !specifier.startsWith('.') && !specifier.startsWith('/') && !specifier.startsWith('#')
}
@@ -956,7 +701,7 @@ async function main(): Promise<void> {
const requests = facts.declarations.reduce((total, pkg) => total + pkg.external.length, 0)
console.log(
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).',
)
}
+102
View File
@@ -0,0 +1,102 @@
import { describe, expect, it } from 'vitest'
import type { NpmPackageLock, RegistryIndex } from './benchmark-npm-resolution.ts'
import {
assertDualDshInstallLayout,
buildDualDshRegistry,
} from './verify-npm-install-layout.ts'
function validLayout(): NpmPackageLock {
return {
lockfileVersion: 3,
packages: {
'': { dependencies: { '@deepseek-ai/dsh': '0.2.0', 'dsh-previous': 'npm:@deepseek-ai/dsh@0.1.0' } },
'node_modules/@deepseek-ai/cordis': { version: '4.0.1' },
'node_modules/@deepseek-ai/dsh': {
version: '0.2.0',
dependencies: { '@deepseek-ai/dsh-child': '^0.2.0' },
peerDependencies: { '@deepseek-ai/cordis': '^4.0.1' },
},
'node_modules/@deepseek-ai/dsh-child': {
version: '0.2.0',
dependencies: { '@deepseek-ai/dsh-leaf': '^0.2.0' },
},
'node_modules/@deepseek-ai/dsh-leaf': { version: '0.2.0' },
'node_modules/dsh-previous': {
name: '@deepseek-ai/dsh',
version: '0.1.0',
dependencies: { '@deepseek-ai/dsh-child': '^0.1.0' },
peerDependencies: { '@deepseek-ai/cordis': '^4.0.1' },
},
'node_modules/dsh-previous/node_modules/@deepseek-ai/dsh-child': {
version: '0.1.0',
dependencies: { '@deepseek-ai/dsh-leaf': '^0.1.0' },
},
'node_modules/dsh-previous/node_modules/@deepseek-ai/dsh-leaf': { version: '0.1.0' },
},
}
}
describe('npm install layout verifier', () => {
it('creates two incompatible versions of every DSH package', () => {
const index: RegistryIndex = new Map([
['@deepseek-ai/dsh', new Map([['0.1.1-rc.2', {
name: '@deepseek-ai/dsh',
version: '0.1.1-rc.2',
dependencies: { '@deepseek-ai/dsh-child': '^0.1.1-rc.2' },
peerDependencies: { '@deepseek-ai/cordis': '^4.0.1' },
}]])],
['@deepseek-ai/dsh-child', new Map([['0.1.1-rc.2', {
name: '@deepseek-ai/dsh-child',
version: '0.1.1-rc.2',
}]])],
['@deepseek-ai/cordis', new Map([['4.0.1', {
name: '@deepseek-ai/cordis',
version: '4.0.1',
}]])],
])
const dual = buildDualDshRegistry(index, '0.1.1-rc.2')
expect([...dual.get('@deepseek-ai/dsh')?.keys() ?? []]).toEqual(['0.1.0', '0.2.0'])
expect(dual.get('@deepseek-ai/dsh')?.get('0.1.0')).toMatchObject({
version: '0.1.0',
dependencies: { '@deepseek-ai/dsh-child': '^0.1.0' },
peerDependencies: { '@deepseek-ai/cordis': '^4.0.1' },
})
expect(dual.get('@deepseek-ai/dsh')?.get('0.2.0')).toMatchObject({
version: '0.2.0',
dependencies: { '@deepseek-ai/dsh-child': '^0.2.0' },
})
expect(dual.get('@deepseek-ai/cordis')).toBe(index.get('@deepseek-ai/cordis'))
})
it('accepts isolated DSH releases with one shared Cordis installation', () => {
expect(assertDualDshInstallLayout(validLayout())).toEqual({
dshPackagesPerVersion: 3,
checkedDshEdges: 4,
})
})
it('rejects an internal edge that crosses release versions', () => {
const layout = validLayout()
const packages = { ...layout.packages }
Reflect.deleteProperty(packages, 'node_modules/dsh-previous/node_modules/@deepseek-ai/dsh-leaf')
expect(() => assertDualDshInstallLayout({ ...layout, packages })).toThrow(
'node_modules/dsh-previous/node_modules/@deepseek-ai/dsh-child: dependencies '
+ '@deepseek-ai/dsh-leaf resolves to node_modules/@deepseek-ai/dsh-leaf@0.2.0, expected 0.1.0',
)
})
it('rejects a second Cordis installation', () => {
const layout = validLayout()
const packages = {
...layout.packages,
'node_modules/dsh-previous/node_modules/@deepseek-ai/cordis': { version: '4.0.1' },
}
expect(() => assertDualDshInstallLayout({ ...layout, packages })).toThrow(
'expected one shared @deepseek-ai/cordis',
)
})
})
+217
View File
@@ -0,0 +1,217 @@
/** Verify npm's physical package placement for two incompatible DSH releases. */
import { readFileSync } from 'node:fs'
import { posix, resolve } from 'node:path'
import {
buildRegistryIndex,
resolveNpmPackageLock,
type NpmLockPackage,
type NpmPackageLock,
type RegistryIndex,
} from './benchmark-npm-resolution.ts'
const DSH_PACKAGE = '@deepseek-ai/dsh'
const CORDIS_PACKAGE = '@deepseek-ai/cordis'
const NESTED_DSH_ALIAS = 'dsh-previous'
const NESTED_DSH_PATH = `node_modules/${NESTED_DSH_ALIAS}`
const DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
const TIMEOUT_MS = 300_000
/** Synthetic incompatible versions used to expose cross-release placement errors. */
export const SYNTHETIC_DSH_VERSIONS = ['0.1.0', '0.2.0'] as const
interface MutableRegistryManifest {
name: string
version: string
dependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
peerDependencies?: Record<string, string>
[key: string]: unknown
}
/** Summary of a verified two-release npm layout. */
export interface DshInstallLayoutSummary {
readonly dshPackagesPerVersion: number
readonly checkedDshEdges: number
}
function isDshPackage(name: string): boolean {
return name === DSH_PACKAGE || name.startsWith(`${DSH_PACKAGE}-`)
}
function cloneForVersion(manifest: object, version: string): MutableRegistryManifest {
const cloned = structuredClone(manifest) as MutableRegistryManifest
cloned.version = version
for (const field of DEPENDENCY_FIELDS) {
const dependencies = cloned[field]
if (dependencies === undefined) continue
for (const name of Object.keys(dependencies)) {
if (isDshPackage(name)) dependencies[name] = `^${version}`
}
}
return cloned
}
/**
* Replace the working release with two incompatible, internally consistent DSH releases.
* @param index - Registry metadata containing the working release.
* @param sourceVersion - Workspace version copied into each synthetic release.
* @returns Registry metadata containing both synthetic DSH releases and unchanged external packages.
*/
export function buildDualDshRegistry(index: RegistryIndex, sourceVersion: string): RegistryIndex {
const output = new Map(index)
let dshPackages = 0
for (const [name, versions] of index) {
if (!isDshPackage(name)) {
output.set(name, versions)
continue
}
const source = versions.get(sourceVersion)
if (source === undefined) throw new Error(`${name} has no workspace version ${sourceVersion}`)
dshPackages++
output.set(name, new Map(SYNTHETIC_DSH_VERSIONS.map(version => [
version,
cloneForVersion(source, version),
])))
}
if (dshPackages === 0) throw new Error('registry contains no DSH packages')
return output
}
function packageNameAtPath(path: string, manifest: NpmLockPackage): string | undefined {
if (manifest.name !== undefined) return manifest.name
const marker = 'node_modules/'
const markerIndex = path.lastIndexOf(marker)
if (markerIndex < 0) return undefined
const segments = path.slice(markerIndex + marker.length).split('/')
if (segments[0]?.startsWith('@')) {
return segments[1] === undefined ? undefined : `${segments[0]}/${segments[1]}`
}
return segments[0]
}
function resolvePackagePath(
packages: Readonly<Record<string, NpmLockPackage>>,
sourcePath: string,
dependency: string,
): string | undefined {
let directory = sourcePath
while (directory !== '.') {
const candidate = posix.join(directory, 'node_modules', dependency)
if (packages[candidate] !== undefined) return candidate
directory = posix.dirname(directory)
}
const rootCandidate = posix.join('node_modules', dependency)
return packages[rootCandidate] === undefined ? undefined : rootCandidate
}
function setDifference(left: ReadonlySet<string>, right: ReadonlySet<string>): string[] {
return [...left].filter(value => !right.has(value)).sort()
}
/**
* Assert that npm isolates both DSH releases while sharing the Cordis runtime.
* @param packageLock - Metadata-only package lock produced by npm.
* @returns Counts for the verified DSH packages and dependency edges.
*/
export function assertDualDshInstallLayout(packageLock: NpmPackageLock): DshInstallLayoutSummary {
const [nestedVersion, rootVersion] = SYNTHETIC_DSH_VERSIONS
const errors: string[] = []
const namesByVersion = new Map<string, Set<string>>([
[nestedVersion, new Set()],
[rootVersion, new Set()],
])
const installed = Object.entries(packageLock.packages)
let checkedDshEdges = 0
for (const [path, manifest] of installed) {
const name = packageNameAtPath(path, manifest)
if (name === undefined || !isDshPackage(name)) continue
const version = manifest.version
if (version !== nestedVersion && version !== rootVersion) {
errors.push(`${path}: expected DSH version ${nestedVersion} or ${rootVersion}, got ${String(version)}`)
continue
}
namesByVersion.get(version)?.add(name)
const expectedPath = version === rootVersion
? `node_modules/${name}`
: name === DSH_PACKAGE
? NESTED_DSH_PATH
: `${NESTED_DSH_PATH}/node_modules/${name}`
if (path !== expectedPath) {
errors.push(`${path}: expected ${name}@${version} at ${expectedPath}`)
}
for (const field of DEPENDENCY_FIELDS) {
for (const dependency of Object.keys(manifest[field] ?? {})) {
if (!isDshPackage(dependency)) continue
const targetPath = resolvePackagePath(packageLock.packages, path, dependency)
const optionalPeer = field === 'peerDependencies'
&& manifest.peerDependenciesMeta?.[dependency]?.optional === true
if (targetPath === undefined) {
if (field === 'optionalDependencies' || optionalPeer) continue
errors.push(`${path}: ${field} ${dependency} does not resolve`)
continue
}
checkedDshEdges++
const targetVersion = packageLock.packages[targetPath]?.version
if (targetVersion !== version) {
errors.push(
`${path}: ${field} ${dependency} resolves to ${targetPath}@${String(targetVersion)}, expected ${version}`,
)
}
}
}
}
const nestedNames = namesByVersion.get(nestedVersion) ?? new Set<string>()
const rootNames = namesByVersion.get(rootVersion) ?? new Set<string>()
if (!nestedNames.has(DSH_PACKAGE)) errors.push(`${NESTED_DSH_PATH}: missing ${DSH_PACKAGE}@${nestedVersion}`)
if (!rootNames.has(DSH_PACKAGE)) errors.push(`node_modules/${DSH_PACKAGE}: missing ${DSH_PACKAGE}@${rootVersion}`)
const onlyNested = setDifference(nestedNames, rootNames)
const onlyRoot = setDifference(rootNames, nestedNames)
if (onlyNested.length > 0) errors.push(`only ${nestedVersion} contains: ${onlyNested.join(', ')}`)
if (onlyRoot.length > 0) errors.push(`only ${rootVersion} contains: ${onlyRoot.join(', ')}`)
const cordisPaths = installed.flatMap(([path, manifest]) =>
packageNameAtPath(path, manifest) === CORDIS_PACKAGE ? [path] : [])
if (cordisPaths.length !== 1 || cordisPaths[0] !== `node_modules/${CORDIS_PACKAGE}`) {
errors.push(`expected one shared ${CORDIS_PACKAGE} at node_modules/${CORDIS_PACKAGE}, got ${cordisPaths.join(', ')}`)
}
if (errors.length > 0) throw new Error(`invalid npm install layout:\n${errors.map(error => ` - ${error}`).join('\n')}`)
return { dshPackagesPerVersion: rootNames.size, checkedDshEdges }
}
function workspaceVersion(root: string): string {
const manifest = JSON.parse(readFileSync(resolve(root, 'apps/cli/package.json'), 'utf8')) as { version?: unknown }
if (typeof manifest.version !== 'string') throw new Error('apps/cli/package.json has no string version')
return manifest.version
}
async function main(): Promise<void> {
const root = resolve(import.meta.dirname, '..')
const index = buildDualDshRegistry(buildRegistryIndex(root), workspaceVersion(root))
const [nestedVersion, rootVersion] = SYNTHETIC_DSH_VERSIONS
const result = await resolveNpmPackageLock(index, {
[DSH_PACKAGE]: rootVersion,
[NESTED_DSH_ALIAS]: `npm:${DSH_PACKAGE}@${nestedVersion}`,
}, TIMEOUT_MS)
if (result.archiveRequests !== 0) throw new Error(`npm requested ${String(result.archiveRequests)} package archive(s)`)
const summary = assertDualDshInstallLayout(result.packageLock)
console.log(
`verify-npm-install-layout: ${String(summary.dshPackagesPerVersion)} DSH package(s) per release and `
+ `${String(summary.checkedDshEdges)} internal edge(s) verified in ${(result.durationMs / 1000).toFixed(2)} s; `
+ `both releases share one Cordis installation; ${String(result.unknownPackages.length)} unavailable optional `
+ 'package name(s) ignored by npm.',
)
}
if (import.meta.main) {
try {
await main()
} catch (error) {
console.error(`verify-npm-install-layout: ${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
}
}
+563
View File
@@ -0,0 +1,563 @@
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 {
collectHostDependencyExportPolicyViolations,
collectPackageDependencyViolations,
collectRuntimeSourceExportUses,
discoverPackageDependencyScope,
fixPackageDependencies,
formatManagedRuntimeDependencies,
formatPeerRequiredRuntimeDependencies,
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: [],
configurationOnlyDevDependencies: {},
safeHostDependencyExports: {},
peerRequiredHostExports: {},
...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']],
]),
hostRuntimeExportUses: [{
packageName: '@deepseek-ai/dsh-runtime',
specifier: '@deepseek-ai/dsh-runtime',
exportName: 'runtimeValue',
sourcePath: 'packages/core/probe/src/index.ts',
line: 1,
column: 10,
sourceLine: "import { runtimeValue } from '@deepseek-ai/dsh-runtime'",
}],
peerRequiredHostDependencies: new Set(),
configurationOnlyDevDependencies: new Set(),
clientInject: new Set(),
}
}
function hostRuntimeFixture(): {
provider: WorkspacePackageManifest
workspaceNames: Set<string>
consumerFacts: PackageDependencyFacts
} {
const consumer = pkg('@f/consumer', 'packages/core/consumer/package.json')
const provider = pkg('@f/provider', 'packages/core/provider/package.json')
const sourcePath = 'packages/core/consumer/src/index.ts'
const specifier = `${provider.name}/api`
const workspaceNames = new Set([CORDIS, consumer.name, provider.name])
const consumerFacts: PackageDependencyFacts = {
manifestPath: consumer.manifestPath,
role: 'configured-host',
manifest: consumer.manifest,
workspaceNames,
allSourceUses: new Map(),
hostRuntimeSourceUses: new Map([[provider.name, [sourcePath]]]),
hostRuntimeExportUses: [{
packageName: provider.name,
specifier,
exportName: 'safeValue',
sourcePath,
line: 1,
column: 10,
sourceLine: `import { safeValue } from '${specifier}'`,
}],
peerRequiredHostDependencies: new Set(),
configurationOnlyDevDependencies: new Set(),
clientInject: new Set(),
}
return { provider, workspaceNames, consumerFacts }
}
describe('package dependency scope', () => {
it('keeps the measured Host relay roster explicit', () => {
expect(PACKAGE_DEPENDENCY_POLICY.clientFaceExclude).toEqual([
'@deepseek-ai/dsh-api-session-controller',
'@deepseek-ai/dsh-api-workspace-controller',
])
expect(PACKAGE_DEPENDENCY_POLICY.hostPackages).toEqual([
'@deepseek-ai/dsh-llm',
'@deepseek-ai/dsh-session',
])
expect(PACKAGE_DEPENDENCY_POLICY.configurationOnlyDevDependencies).toEqual({
'@deepseek-ai/dsh-client-locale': ['@deepseek-ai/dsh-api-remotes'],
'@deepseek-ai/dsh-client-ui-conversation': [
'@deepseek-ai/dsh-api-remotes',
'@deepseek-ai/dsh-client-ui-workspace',
],
'@deepseek-ai/dsh-client-ui-model-selection': ['@deepseek-ai/dsh-client-ui-input-trigger'],
'@deepseek-ai/dsh-client-ui-sidebar': ['@deepseek-ai/dsh-client-ui-workspace'],
'@deepseek-ai/dsh-client-ui-subagent': ['@deepseek-ai/dsh-client-ui-input-trigger'],
'@deepseek-ai/dsh-client-ui-theme': ['@deepseek-ai/dsh-api-remotes'],
'@deepseek-ai/dsh-client-ui-tool': ['@deepseek-ai/dsh-api-remotes'],
})
expect(PACKAGE_DEPENDENCY_POLICY.duplicateSafePackages).toEqual([
'@deepseek-ai/dsh-brand',
'@deepseek-ai/dsh-typert-protocol',
'@deepseek-ai/dsh-util-crypto',
'@deepseek-ai/dsh-util-values',
])
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-deque']).toEqual(['Deque'])
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/schemastery']).toEqual(['default'])
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-session/types']).toBeUndefined()
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-typert-protocol']).toBeUndefined()
expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-scope']).toEqual([
'carrierKeyOf', 'scopeOf', 'scopeTarget',
])
expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-typert-protocol']).toBeUndefined()
})
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-only'],
['@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'),
]))
})
it('rejects stale, duplicate, and unbounded safe Host export entries', () => {
const { provider, workspaceNames, consumerFacts } = hostRuntimeFixture()
expect(collectHostDependencyExportPolicyViolations(
[consumerFacts],
workspaceNames,
{
safeHostDependencyExports: {
[`${provider.name}/api`]: ['safeValue', 'safeValue', '*', 'staleValue'],
},
peerRequiredHostExports: {
[`${provider.name}/api`]: ['safeValue'],
},
},
)).toEqual(expect.arrayContaining([
expect.stringContaining('export safeValue more than once'),
expect.stringContaining('cannot classify unbounded'),
expect.stringContaining('unused @f/provider/api export staleValue'),
expect.stringContaining('appears in both Host export classifications'),
]))
})
it('applies a duplicate-safe package classification to its subpaths', () => {
const { provider, workspaceNames, consumerFacts } = hostRuntimeFixture()
expect(collectHostDependencyExportPolicyViolations(
[consumerFacts],
workspaceNames,
{
duplicateSafePackages: [provider.name],
safeHostDependencyExports: {},
peerRequiredHostExports: {},
},
)).toEqual([])
expect(collectHostDependencyExportPolicyViolations(
[consumerFacts],
workspaceNames,
{
duplicateSafePackages: [provider.name],
safeHostDependencyExports: { [`${provider.name}/api`]: ['safeValue'] },
peerRequiredHostExports: {},
},
)).toContain(`safeHostDependencyExports redundantly classifies duplicate-install-safe package ${provider.name}/api`)
})
})
describe('face-aware source classification', () => {
it('fails when a managed Host package has no Host entry', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-missing-host-'))
roots.push(root)
const subject = pkg('@f/host', 'packages/g/host/package.json')
expect(() => readPackageDependencyFacts(root, subject, 'configured-host', new Set([subject.name])))
.toThrow('packages/g/host/package.json: Host runtime entry packages/g/host/src/index.ts does not exist')
})
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'",
"import type { Hidden } from './types.ts'",
"export { nested } from './nested.ts'",
].join('\n'),
'packages/g/dual/src/nested.ts': "export { nested } from '@f/nested'",
'packages/g/dual/src/types.ts': "import { hidden } from '@f/hidden'; export type Hidden = typeof hidden",
'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/hidden', '@f/browser', '@f/injected',
]), policy({
configurationOnlyDevDependencies: { '@f/dual': ['@f/injected'] },
}))
expect([...found.hostRuntimeSourceUses.keys()].sort()).toEqual(['@f/nested', '@f/runtime'])
expect([...found.configurationOnlyDevDependencies]).toEqual(['@f/injected'])
expect(found.hostRuntimeExportUses).toEqual([
{
packageName: '@f/nested',
specifier: '@f/nested',
exportName: 'nested',
sourcePath: 'packages/g/dual/src/nested.ts',
line: 1,
column: 10,
sourceLine: "export { nested } from '@f/nested'",
},
{
packageName: '@f/runtime',
specifier: '@f/runtime',
exportName: 'value',
sourcePath: 'packages/g/dual/src/index.ts',
line: 1,
column: 10,
sourceLine: "import { value } from '@f/runtime'",
},
])
expect([...found.allSourceUses.keys()].sort()).toEqual([
'@f/browser', '@f/hidden', '@f/nested', '@f/runtime', '@f/types',
])
})
it('identifies exact runtime exports without treating type imports as values', () => {
const source = [
"import defaultValue, { value as local, type Kind } from '@f/root'",
"import * as namespace from '@f/namespace'",
"import '@f/effect'",
"import type { TypeOnly } from '@f/types'",
"export { source as renamed, type SourceType } from '@f/reexport'",
"export * from '@f/star'",
"void import('@f/dynamic')",
"void require('@f/required')",
'void defaultValue; void local; void namespace',
].join('\n')
const uses = collectRuntimeSourceExportUses('probe.ts', source)
expect(uses.map(({ specifier, exportName }) => ({ specifier, exportName }))).toEqual([
{ specifier: '@f/dynamic', exportName: '*' },
{ specifier: '@f/effect', exportName: '(side effect)' },
{ specifier: '@f/namespace', exportName: '*' },
{ specifier: '@f/reexport', exportName: 'source' },
{ specifier: '@f/required', exportName: '*' },
{ specifier: '@f/root', exportName: 'default' },
{ specifier: '@f/root', exportName: 'value' },
{ specifier: '@f/star', exportName: '*' },
])
expect(uses.find(use => use.specifier === '@f/root' && use.exportName === 'value')).toMatchObject({
line: 1,
column: 24,
sourceLine: "import defaultValue, { value as local, type Kind } from '@f/root'",
})
})
})
describe('dependency sections', () => {
it('does not leak repository configuration into captured dependency facts', () => {
const manifest: PackageDependencyManifest = {
name: '@deepseek-ai/dsh-client-locale',
dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
peerDependencies: { [CORDIS]: 'workspace:^' },
}
const base = facts(manifest)
const subject: PackageDependencyFacts = {
...base,
workspaceNames: new Set([...base.workspaceNames, '@deepseek-ai/dsh-api-remotes']),
}
expect(collectPackageDependencyViolations({
facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
})).toEqual([])
})
it('requires non-workspace Host runtime imports in dependencies', () => {
const manifest: PackageDependencyManifest = {
name: '@deepseek-ai/dsh-probe',
dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^', external: '^1.0.0' },
peerDependencies: { [CORDIS]: 'workspace:^' },
}
const subject: PackageDependencyFacts = {
...facts(manifest),
hostRuntimeSourceUses: new Map([
['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
['external', ['packages/core/probe/src/index.ts']],
]),
}
const state = {
facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
}
expect(collectPackageDependencyViolations(state)).toContain(
'packages/core/probe/package.json: external (packages/core/probe/src/index.ts) '
+ 'must be dependencies-only; found devDependencies',
)
repairPackageDependencyManifest(subject)
expect(manifest.dependencies?.external).toBe('^1.0.0')
expect(manifest.devDependencies?.external).toBeUndefined()
delete manifest.dependencies?.external
expect(collectPackageDependencyViolations(state)).toContain(
'packages/core/probe/package.json: external (packages/core/probe/src/index.ts) '
+ 'must be dependencies-only; found no dependency section',
)
})
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 edge(s) remain in dependencies across 1 package(s):',
' @deepseek-ai/dsh-probe -> @deepseek-ai/dsh-runtime: @deepseek-ai/dsh-runtime#runtimeValue',
])
})
it('reports an unapproved Host runtime export without rewriting its dependency section', () => {
const manifest: PackageDependencyManifest = {
name: '@deepseek-ai/dsh-probe',
dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
peerDependencies: { [CORDIS]: 'workspace:^' },
}
const subject = facts(manifest)
const safetyViolations = collectHostDependencyExportPolicyViolations(
[subject],
subject.workspaceNames,
{ safeHostDependencyExports: {}, peerRequiredHostExports: {} },
)
const state = {
facts: [subject], packages: [], policyViolations: safetyViolations, workspaceNames: subject.workspaceNames,
}
expect(safetyViolations).toEqual([
'packages/core/probe/src/index.ts:1:10: @deepseek-ai/dsh-runtime#runtimeValue is not classified as '
+ 'safe or peer-required — import { runtimeValue } from \'@deepseek-ai/dsh-runtime\'',
])
expect(fixPackageDependencies('/unused', state)).toEqual([])
expect(manifest.dependencies).toEqual({ '@deepseek-ai/dsh-runtime': 'workspace:^' })
})
it('keeps an edge as a peer when one imported export requires shared identity', () => {
const manifest: PackageDependencyManifest = {
name: '@deepseek-ai/dsh-probe',
dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
peerDependencies: { [CORDIS]: 'workspace:^' },
}
const subject: PackageDependencyFacts = {
...facts(manifest),
peerRequiredHostDependencies: new Set(['@deepseek-ai/dsh-runtime']),
}
expect(collectHostDependencyExportPolicyViolations(
[subject],
subject.workspaceNames,
{
safeHostDependencyExports: {},
peerRequiredHostExports: {
'@deepseek-ai/dsh-runtime': ['runtimeValue'],
},
},
)).toEqual([])
repairPackageDependencyManifest(subject)
expect(manifest.dependencies).toBeUndefined()
expect(manifest.peerDependencies).toMatchObject({
[CORDIS]: 'workspace:^',
'@deepseek-ai/dsh-runtime': 'workspace:^',
})
expect(manifest.devDependencies).toMatchObject({
[CORDIS]: 'workspace:^',
'@deepseek-ai/dsh-runtime': 'workspace:^',
})
expect(formatPeerRequiredRuntimeDependencies({
facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
})).toEqual([
'verify-package-dependencies: 1 Host runtime edge(s) remain in peerDependencies because their exports require shared identity across 1 package(s):',
' @deepseek-ai/dsh-probe -> @deepseek-ai/dsh-runtime: @deepseek-ai/dsh-runtime#runtimeValue',
])
})
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:^' })
})
})
+746
View File
@@ -0,0 +1,746 @@
/** Verify and repair npm dependency sections from published Client and Host faces. */
import { spawnSync } from 'node:child_process'
import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, extname, join, normalize, relative, resolve, sep } from 'node:path'
import ts from 'typescript'
import { writeModuleGraph } from './gen-module-graph.ts'
import {
hasClientDeclaration,
PACKAGE_DEPENDENCY_POLICY,
type PackageDependencyPolicy,
} from './package-dependency-policy.ts'
import {
collectRuntimeLocalSourceSpecifiers,
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-only' | '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 hostRuntimeExportUses: readonly HostRuntimeExportUse[]
readonly peerRequiredHostDependencies: ReadonlySet<string>
readonly configurationOnlyDevDependencies: ReadonlySet<string>
readonly clientInject: ReadonlySet<string>
}
/** One runtime export reached from a package's Host source closure. */
export interface HostRuntimeExportUse {
readonly packageName: string
readonly specifier: string
readonly exportName: string
readonly sourcePath: string
readonly line: number
readonly column: number
readonly sourceLine: 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 = (hasClientDeclaration(pkg.manifest.dsh) || include.has(pkg.name)) && !exclude.has(pkg.name)
const clientOnly = clientDirectory && !clientHost
const configuredHost = host.has(pkg.name)
if (configuredHost && (clientHost || clientOnly)) {
violations.push(`hostPackages redundantly names Client-faced package ${pkg.name}`)
}
const role = clientHost ? 'client-host' : clientOnly ? 'client-only' : 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)
}
const NAMESPACE_RUNTIME_EXPORT = '*'
const SIDE_EFFECT_RUNTIME_EXPORT = '(side effect)'
interface RuntimeSourceExportUse {
readonly specifier: string
readonly exportName: string
readonly line: number
readonly column: number
readonly sourceLine: string
}
/** Collect exact runtime exports imported or re-exported by one source file. */
export function collectRuntimeSourceExportUses(path: string, source: string): RuntimeSourceExportUse[] {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
const uses = new Map<string, RuntimeSourceExportUse>()
const sourceLines = source.split(/\r?\n/u)
const record = (specifier: string, exportName: string, locationNode: ts.Node): void => {
const key = `${specifier}\0${exportName}`
if (uses.has(key)) return
const position = sourceFile.getLineAndCharacterOfPosition(locationNode.getStart(sourceFile))
uses.set(key, {
specifier,
exportName,
line: position.line + 1,
column: position.character + 1,
sourceLine: sourceLines[position.line]?.trim() ?? '',
})
}
const add = (
specifierNode: ts.Expression | undefined,
exportName: string,
locationNode: ts.Node = specifierNode ?? sourceFile,
): void => {
if (specifierNode === undefined || !ts.isStringLiteralLike(specifierNode)) return
if (packageNameOf(specifierNode.text) === undefined) return
record(specifierNode.text, exportName, locationNode)
}
const visit = (node: ts.Node): void => {
if (ts.isImportDeclaration(node)) {
const clause = node.importClause
if (clause === undefined) {
add(node.moduleSpecifier, SIDE_EFFECT_RUNTIME_EXPORT)
} else if (clause.phaseModifier !== ts.SyntaxKind.TypeKeyword) {
if (clause.name !== undefined) add(node.moduleSpecifier, 'default', clause.name)
const bindings = clause.namedBindings
if (bindings !== undefined && ts.isNamespaceImport(bindings)) {
add(node.moduleSpecifier, NAMESPACE_RUNTIME_EXPORT, bindings.name)
} else if (bindings !== undefined && bindings.elements.length === 0) {
add(node.moduleSpecifier, SIDE_EFFECT_RUNTIME_EXPORT)
} else if (bindings !== undefined) {
for (const element of bindings.elements) {
const imported = element.propertyName ?? element.name
if (!element.isTypeOnly) add(node.moduleSpecifier, imported.text, imported)
}
}
}
} else if (ts.isExportDeclaration(node) && !node.isTypeOnly) {
const clause = node.exportClause
if (clause === undefined || ts.isNamespaceExport(clause)) {
add(node.moduleSpecifier, NAMESPACE_RUNTIME_EXPORT)
} else if (clause.elements.length === 0) {
add(node.moduleSpecifier, SIDE_EFFECT_RUNTIME_EXPORT)
} else {
for (const element of clause.elements) {
const imported = element.propertyName ?? element.name
if (!element.isTypeOnly) add(node.moduleSpecifier, imported.text, imported)
}
}
} else if (ts.isImportEqualsDeclaration(node)
&& !node.isTypeOnly
&& ts.isExternalModuleReference(node.moduleReference)) {
add(node.moduleReference.expression, NAMESPACE_RUNTIME_EXPORT, node.name)
} else if (ts.isCallExpression(node)
&& (node.expression.kind === ts.SyntaxKind.ImportKeyword
|| ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
add(node.arguments[0], NAMESPACE_RUNTIME_EXPORT)
} else if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
record('react/jsx-runtime', NAMESPACE_RUNTIME_EXPORT, node)
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return [...uses.values()].sort((left, right) =>
left.specifier.localeCompare(right.specifier)
|| left.exportName.localeCompare(right.exportName)
|| left.line - right.line
|| left.column - right.column)
}
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): {
packageUses: Map<string, string[]>
exportUses: HostRuntimeExportUse[]
} {
const packageUses = new Map<string, string[]>()
const exportUses = new Map<string, HostRuntimeExportUse>()
const seen = new Set<string>()
const visit = (path: string): void => {
const normalized = normalize(path)
if (seen.has(normalized)) return
seen.add(normalized)
const source = readFileSync(normalized, 'utf8')
const displayPath = normalizePath(relative(root, normalized))
for (const use of collectRuntimeSourceExportUses(normalized, source)) {
const name = packageNameOf(use.specifier)
if (name === undefined) continue
addUse(packageUses, name, displayPath)
const fact = { packageName: name, ...use, sourcePath: displayPath }
exportUses.set(`${use.specifier}\0${use.exportName}\0${displayPath}\0${String(use.line)}\0${String(use.column)}`, fact)
}
for (const specifier of collectRuntimeLocalSourceSpecifiers(normalized, source)) {
const target = resolveLocal(normalized, specifier)
if (target !== undefined) visit(target)
}
}
const entry = resolve(root, pkg.dir, 'src/index.ts')
if (!existsSync(entry)) {
throw new Error(`${pkg.manifestPath}: Host runtime entry ${normalizePath(relative(root, entry))} does not exist`)
}
visit(entry)
return {
packageUses,
exportUses: [...exportUses.values()].sort((left, right) =>
left.packageName.localeCompare(right.packageName)
|| left.specifier.localeCompare(right.specifier)
|| left.exportName.localeCompare(right.exportName)
|| left.sourcePath.localeCompare(right.sourcePath)
|| left.line - right.line
|| left.column - right.column),
}
}
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>,
policy: PackageDependencyPolicy = PACKAGE_DEPENDENCY_POLICY,
): PackageDependencyFacts {
const inject = pkg.manifest.dsh?.client?.inject ?? []
const hostRuntime = role === 'client-only'
? { packageUses: new Map<string, string[]>(), exportUses: [] }
: readHostRuntimeUses(root, pkg)
return {
manifestPath: pkg.manifestPath,
role,
manifest: pkg.manifest,
workspaceNames,
allSourceUses: readAllSourceUses(root, pkg),
hostRuntimeSourceUses: hostRuntime.packageUses,
hostRuntimeExportUses: hostRuntime.exportUses,
peerRequiredHostDependencies: new Set(hostRuntime.exportUses
.filter(use => policy.peerRequiredHostExports[use.specifier]?.includes(use.exportName) === true)
.map(use => use.packageName)),
configurationOnlyDevDependencies: new Set(
policy.configurationOnlyDevDependencies[pkg.manifest.name ?? ''] ?? [],
),
clientInject: new Set(inject.map(packageNameOf).filter(name => name !== undefined)),
}
}
/** Validate reviewed Host export classifications against current source facts. */
export function collectHostDependencyExportPolicyViolations(
facts: readonly PackageDependencyFacts[],
workspaceNames: ReadonlySet<string>,
policy: Pick<PackageDependencyPolicy, 'duplicateSafePackages' | 'peerRequiredHostExports' | 'safeHostDependencyExports'>,
): string[] {
const violations: string[] = []
const allRuntimeUses = facts.flatMap(fact => fact.hostRuntimeExportUses)
const duplicateSafePackages = new Set(policy.duplicateSafePackages ?? [])
for (const packageName of duplicates(policy.duplicateSafePackages ?? [])) {
violations.push(`duplicateSafePackages lists ${packageName} more than once`)
}
for (const packageName of duplicateSafePackages) {
if (!workspaceNames.has(packageName)) {
violations.push(`duplicateSafePackages names unknown workspace package ${packageName}`)
}
}
const classifications = [
['safeHostDependencyExports', policy.safeHostDependencyExports],
['peerRequiredHostExports', policy.peerRequiredHostExports],
] as const
for (const [field, entries] of classifications) {
for (const [specifier, exportNames] of Object.entries(entries)) {
const provider = packageNameOf(specifier)
if (provider === undefined || !workspaceNames.has(provider)) {
violations.push(`${field} specifier ${specifier} is not a workspace package`)
} else if (duplicateSafePackages.has(provider)) {
violations.push(`${field} redundantly classifies duplicate-install-safe package ${specifier}`)
}
if (exportNames.length === 0) {
violations.push(`${field} lists no exports for ${specifier}`)
}
for (const exportName of duplicates(exportNames)) {
violations.push(`${field} lists ${specifier} export ${exportName} more than once`)
}
for (const exportName of exportNames) {
if (exportName === '' || exportName === NAMESPACE_RUNTIME_EXPORT || exportName === SIDE_EFFECT_RUNTIME_EXPORT) {
violations.push(`${field} cannot classify unbounded ${specifier} export ${exportName}`)
continue
}
if (!allRuntimeUses.some(use => use.specifier === specifier && use.exportName === exportName)) {
violations.push(`${field} lists unused ${specifier} export ${exportName}`)
}
if (field === 'safeHostDependencyExports'
&& policy.peerRequiredHostExports[specifier]?.includes(exportName) === true) {
violations.push(`${specifier} export ${exportName} appears in both Host export classifications`)
}
}
}
}
for (const fact of facts) {
for (const use of fact.hostRuntimeExportUses) {
if (use.packageName === fact.manifest.name || use.packageName === CORDIS) continue
if (!workspaceNames.has(use.packageName)) continue
if (duplicateSafePackages.has(use.packageName)) continue
if (policy.safeHostDependencyExports[use.specifier]?.includes(use.exportName) === true) continue
if (policy.peerRequiredHostExports[use.specifier]?.includes(use.exportName) === true) continue
violations.push(
`${use.sourcePath}:${String(use.line)}:${String(use.column)}: `
+ `${use.specifier}#${use.exportName} is not classified as safe or peer-required — ${use.sourceLine}`,
)
}
}
return violations.sort()
}
/** 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)
const facts = discovered.selected.map(pkg =>
readPackageDependencyFacts(root, pkg, pkg.role, workspaceNames, policy))
const selectedNames = new Set(facts.map(fact => fact.manifest.name))
return {
facts,
packages: packages.release,
policyViolations: [
...discovered.violations,
...collectHostDependencyExportPolicyViolations(facts, workspaceNames, policy),
...Object.keys(policy.configurationOnlyDevDependencies)
.filter(name => !selectedNames.has(name))
.map(name => `configurationOnlyDevDependencies names unmanaged package ${name}`),
].sort(),
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 === 'peer-dev' || sectionName === 'peer-dev'
? 'peer-dev'
: 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 facts.configurationOnlyDevDependencies) {
if (facts.workspaceNames.has(name)) add(name, 'devDependencies', 'configured development-only relationship')
}
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) {
const expectedSection = facts.workspaceNames.has(name) && facts.peerRequiredHostDependencies.has(name)
? 'peer-dev'
: 'dependencies'
for (const path of paths) add(name, expectedSection, path)
}
return new Map([...expected].map(([name, rule]) => [name, {
section: rule.section,
origins: [...rule.origins].sort(),
}]))
}
interface ManagedRuntimeEdge {
readonly consumer: string
readonly dependency: string
readonly exports: readonly string[]
}
function managedRuntimeEdges(
state: PackageDependencyState,
expectedSection: 'dependencies' | 'peer-dev',
): ManagedRuntimeEdge[] {
return state.facts.flatMap(facts => [...expectedPackageDependencies(facts)]
.filter(([name, rule]) => name !== CORDIS && rule.section === expectedSection)
.map(([dependency]) => ({
consumer: facts.manifest.name ?? facts.manifestPath,
dependency,
exports: [...new Set(facts.hostRuntimeExportUses
.filter(use => use.packageName === dependency)
.map(use => `${use.specifier}#${use.exportName}`))].sort(),
})))
.sort((left, right) =>
left.consumer.localeCompare(right.consumer) || left.dependency.localeCompare(right.dependency))
}
/** Format Host runtime edges whose reviewed exports permit ordinary dependencies. */
export function formatManagedRuntimeDependencies(state: PackageDependencyState): string[] {
const rows = managedRuntimeEdges(state, 'dependencies')
const packages = new Set(rows.map(row => row.consumer)).size
return [
`${GATE}: ${String(rows.length)} managed Host runtime edge(s) remain in dependencies across ${String(packages)} package(s):`,
...rows.map(row => ` ${row.consumer} -> ${row.dependency}: ${row.exports.join(', ')}`),
]
}
/** Format Host runtime edges retained as peers by their imported export classification. */
export function formatPeerRequiredRuntimeDependencies(state: PackageDependencyState): string[] {
const rows = managedRuntimeEdges(state, 'peer-dev')
const packages = new Set(rows.map(row => row.consumer)).size
return [
`${GATE}: ${String(rows.length)} Host runtime edge(s) remain in peerDependencies because their exports require shared identity across ${String(packages)} package(s):`,
...rows.map(row => ` ${row.consumer} -> ${row.dependency}: ${row.exports.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]
if (violations.length > 0) return [...new Set(violations)].sort()
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 refreshPnpmLockfile(root: string): void {
const result = spawnSync(
'pnpm',
['install', '--lockfile-only', '--ignore-scripts', '--no-frozen-lockfile'],
{ cwd: root, shell: process.platform === 'win32', stdio: 'inherit' },
)
if (result.error !== undefined) throw new Error(`could not refresh pnpm-lock.yaml: ${result.error.message}`)
if (result.status !== 0) throw new Error(`pnpm lockfile refresh exited with status ${String(result.status)}`)
}
function main(): void {
const root = resolve(import.meta.dirname, '..')
let state = readPackageDependencyState(root)
const fix = process.argv.includes('--fix')
if (fix) {
if (state.policyViolations.length > 0) {
console.error(`${GATE}: --fix skipped because dependency policy review failed.`)
} else {
const changed = fixPackageDependencies(root, state)
console.log(`${GATE}: fixed ${String(changed.length)} manifest(s).`)
refreshPnpmLockfile(root)
const graphChanges = writeModuleGraph(root)
console.log(
`${GATE}: refreshed pnpm-lock.yaml and wrote ${String(graphChanges.length)} module-graph artifact(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-only']?.length ?? 0)} Client-only,`
+ ` ${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)
for (const line of formatPeerRequiredRuntimeDependencies(state)) console.log(line)
}
}
if (import.meta.main) main()
+1 -1
View File
@@ -16,7 +16,7 @@ const CANONICAL = '## Known Limitations and Deferred Work'
/** Packages audited as having no limitations section, keyed by repo-relative directory. */
const NO_LIMITATIONS: Readonly<Record<string, string>> = {
'packages/util/brand': 'Type-only nominal-branding primitive with no runtime behavior or deferred work.',
'packages/util/brand': 'Stateless nominal-string and canonical-key helpers have no deferred work.',
}
/** A heading that reads as a limitations section — canonical or drifted. */
@@ -31,10 +31,11 @@ interface SentenceContract {
*/
const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.',
'packages/util/brand': 'The package is a type-only primitive erased at compile time.',
'packages/util/brand': 'The package only constructs plain string values and registers nothing model-facing.',
'packages/util/home-paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
'packages/util/launch-environment': 'The package only resolves host environment values; model-facing consumers own any rendered use.',
'packages/util/workspace-path': 'The package only formats Workspace paths for browser UI; it never constructs model input.',
'packages/util/values': 'The package only validates, snapshots, compares, freezes, or rejects caller-owned values; consumers own every model-facing use.',
}
/**
@@ -55,6 +56,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/code-runtime/code-runtime-python': { kind: 'indirect', reason: 'The CPython subprocess backend delegates model rendering to PTC mode in dsh-tools.' },
'packages/client/ui-agent-preset': { kind: 'indirect', reason: 'Browser-side settings row; the preset it selects owns every model-facing effect.' },
'packages/util/crypto': { kind: 'indirect', reason: 'Pure identifier minting; the ids consumers mint with it never enter prompts as semantic content.' },
'packages/util/deque': { kind: 'none', reason: 'In-process collection primitive; registers nothing model-facing.' },
'packages/util/time': { kind: 'indirect', reason: 'Pure zone validation; the consumer that records a canonical zone owns the model-visible line derived from it.' },
'packages/core/agent-default-model': { kind: 'indirect', reason: 'The service supplies a ModelSelection; request assembly and adapters own the model-visible request.' },
'packages/llm/deepseek-llm-api-extensions': { kind: 'indirect', reason: 'The registry contributes model-hidden provider fields; dsh-llm-deepseek owns their wire placement.' },
'packages/preset/agent-presets': { kind: 'indirect', reason: 'The mount installs a preset\'s own plugins, which own every model-facing registration it makes visible.' },