Merge commit '0c177e17a23692d018a79bae59ac9a2db6eba59c' into codex/subprocess-native-containment

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
#	.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/input.json
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/bundle/acp-app/package.json
#	packages/examples/jsonrpc-demo/src/packaged-bin.ts
#	packages/subagent/subagent-acp/src/run.ts
#	packages/subagent/subagent-acp/tests/subagent-acp.spec.ts
#	packages/subprocess/subprocess-local/README.i18n.yaml
#	packages/subprocess/subprocess-local/README.md
#	packages/subprocess/subprocess-local/README.zh.md
#	pnpm-lock.yaml
#	scripts/smoke-python-runtime.py
This commit is contained in:
pku-xht committed 2026-08-25 23:22:25 +08:00
commit 678cfdfdb4
4260 files changed
+141534 -56369

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
# 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.
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.
@@ -0,0 +1,27 @@
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
const root = resolve(import.meta.dirname, '..')
const script = resolve(root, 'scripts/build-exe-for-python-sdk.ts')
describe('Python runtime executable assets', () => {
it('packages the dynamically resolved web frontend distribution', () => {
const result = spawnSync(process.execPath, [
'--import',
'tsx/esm',
script,
'--skip-build',
'--dry-run',
'--targets=node24-macos-arm64',
], {
cwd: root,
encoding: 'utf8',
env: { ...process.env, npm_execpath: 'C:\\tools\\pnpm.cjs' },
})
expect(result.status).toBe(0)
expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*')
expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*')
})
})
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { resolveLinuxNodePtyAddon } from './build-exe-for-python-sdk-native-pty.ts'
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
const roots: string[] = []
@@ -35,6 +35,24 @@ describe('resolveLinuxNodePtyAddon', () => {
})
})
describe('resolveWindowsNodePtyAddons', () => {
it('requires both ConPTY addons from the x64 prebuild', () => {
const root = temporaryPackage()
const conpty = createAddon(root, 'prebuilds', 'win32-x64', 'conpty.node')
const consoleList = createAddon(root, 'prebuilds', 'win32-x64', 'conpty_console_list.node')
expect(resolveWindowsNodePtyAddons(root, 'x64')).toEqual([conpty, consoleList])
})
it('names every missing Windows addon', () => {
const root = temporaryPackage()
expect(() => resolveWindowsNodePtyAddons(root, 'x64')).toThrow(
`Windows node-pty addons are missing: ${join(root, 'prebuilds', 'win32-x64', 'conpty.node')}, ${join(root, 'prebuilds', 'win32-x64', 'conpty_console_list.node')}`,
)
})
})
function temporaryPackage(): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-node-pty-addon-'))
roots.push(root)
@@ -21,3 +21,25 @@ export function resolveLinuxNodePtyAddon(
`build-exe-for-python-sdk: node-pty addon is absent from both ${built} and ${prebuilt}.`,
)
}
/**
* Require both node-pty addons used by the Windows ConPTY backend.
* @param packageDirectory - staged node-pty package directory.
* @param arch - Windows target architecture.
* @returns the existing addon paths in load order.
*/
export function resolveWindowsNodePtyAddons(
packageDirectory: string,
arch: 'x64',
): string[] {
const directory = join(packageDirectory, 'prebuilds', `win32-${arch}`)
const addons = [
join(directory, 'conpty.node'),
join(directory, 'conpty_console_list.node'),
]
const missing = addons.filter(path => !existsSync(path))
if (missing.length > 0) {
throw new Error(`build-exe-for-python-sdk: Windows node-pty addons are missing: ${missing.join(', ')}.`)
}
return addons
}
+81
View File
@@ -0,0 +1,81 @@
import { spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
const root = resolve(import.meta.dirname, '..')
const script = resolve(root, 'scripts/build-exe-for-python-sdk.ts')
const temporaryDirectories: string[] = []
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
function run(env: NodeJS.ProcessEnv, ...args: string[]) {
return spawnSync(process.execPath, ['--import', 'tsx/esm', script, ...args], {
cwd: root,
encoding: 'utf8',
env: isolatedPnpmEnvironment(env),
})
}
describe('Python runtime executable builder CLI', () => {
it('runs pnpm through its JavaScript entrypoint without a command shell', () => {
const result = run(
{ npm_execpath: 'C:\\tools\\pnpm.cjs' },
'--skip-build',
'--dry-run',
'--targets=node24-macos-arm64',
)
expect(result.status).toBe(0)
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs run verify-runtime-closure`)
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs --filter dsh-python-runtime-closure deploy`)
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs dlx @yao-pkg/[email protected]`)
expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
})
it('resolves the pnpm package behind a Windows command shim', () => {
const setup = mkdtempSync(join(tmpdir(), 'dsh-pnpm-home-'))
temporaryDirectories.push(setup)
const home = join(setup, 'node_modules', '.bin')
const entrypoint = join(setup, 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')
mkdirSync(home, { recursive: true })
mkdirSync(dirname(entrypoint), { recursive: true })
writeFileSync(entrypoint, '')
const result = run(
{ npm_execpath: 'C:\\tools\\pnpm.cmd', PNPM_HOME: home },
'--skip-build',
'--dry-run',
'--targets=node24-macos-arm64',
)
expect(result.status).toBe(0)
expect(result.stdout).toContain(`${process.execPath} ${entrypoint} run verify-runtime-closure`)
expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
})
it('rejects a Windows arm64 product before any build step', () => {
const result = run(
{ npm_execpath: 'C:\\tools\\pnpm.cjs' },
'--skip-build',
'--dry-run',
'--targets=node24-win-arm64',
)
expect(result.status).not.toBe(0)
expect(result.stderr).toContain('Windows supports x64 only')
expect(result.stdout).toBe('')
})
})
function isolatedPnpmEnvironment(overrides: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const environment = Object.fromEntries(
Object.entries(process.env).filter(([key]) => !['npm_execpath', 'pnpm_home'].includes(key.toLowerCase())),
)
return { ...environment, ...overrides }
}
+91 -27
View File
@@ -1,5 +1,5 @@
/**
* Build the SDK runtime executables and Python node carrier. The fixed
* Build the dsh executables and development Node carrier for the Python runtime wheel. The fixed
* `@yao-pkg/pkg --sea` route, deploy flags, and artifact layout are owned by
* .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
* The staged closure is symlink-free, and whole-tree assets cover Cordis's
@@ -9,17 +9,18 @@
import { spawn } from 'node:child_process'
import { existsSync, statSync } from 'node:fs'
import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
import { basename, dirname, join, resolve, sep } from 'node:path'
import { basename, dirname, extname, join, resolve, sep } from 'node:path'
import { parseArgs } from 'node:util'
import { resolveLinuxNodePtyAddon } from './build-exe-for-python-sdk-native-pty.ts'
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
const root = resolve(import.meta.dirname, '..')
/** The closure manifest whose dependencies define the executable. */
const DEPLOY_ROOT_PACKAGE = 'dsh-jsonrpc-agent-pkg'
/** The closed-runtime app entry inside the deployed closure. */
const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js'
const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg'
const DEPLOY_ROOT_PACKAGE = 'dsh-python-runtime-closure'
/** The sole application launcher inside the deployed closure. */
const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh/lib/bin.js'
/** Python-visible executable basename. */
const OUTPUT_BASENAME = 'deepseek-harness-sdk-runtime'
/** Default Node major; SEA mode requires at least Node 22. */
const DEFAULT_NODE_RANGE = 'node24'
/** Pinned for reproducible builds. */
@@ -46,11 +47,23 @@ const ASSET_GLOBS = [
'node_modules/**/*.mjs',
'node_modules/**/package.json',
'node_modules/**/*.json',
// Package-owned Markdown includes runtime skill instructions and badge content.
'node_modules/**/*.md',
'node_modules/**/*.dylib',
'node_modules/**/*.dll',
'node_modules/**/*.node',
'node_modules/**/*.so',
'node_modules/**/*.so.*',
'node_modules/**/*.wasm',
'node_modules/**/*.yaml',
'node_modules/**/*.yml',
// web-app builds this path dynamically, so pkg cannot discover the static frontend.
'node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*',
// skill-badge resolves both Markdown and image resources through import.meta.url.
'node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*',
]
const PLATFORMS = ['linux', 'macos'] as const
const PLATFORMS = ['linux', 'macos', 'win'] as const
const ARCHES = ['x64', 'arm64'] as const
type Platform = (typeof PLATFORMS)[number]
type Arch = (typeof ARCHES)[number]
@@ -70,10 +83,7 @@ class Target {
private constructor(
/** pkg Node range (`node<major>`). */
readonly nodeRange: string,
/**
* pkg platform tag. Windows is a documented non-goal
* (.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
*/
/** pkg platform tag. */
readonly platform: Platform,
/** pkg CPU tag. */
readonly arch: Arch,
@@ -104,6 +114,9 @@ class Target {
if (!isArch(arch)) {
throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: arch must be one of ${ARCHES.join(', ')}, got ${JSON.stringify(arch)}.`)
}
if (platform === 'win' && arch !== 'x64') {
throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: Windows supports x64 only.`)
}
return new Target(nodeRange, platform, arch)
}
@@ -112,7 +125,13 @@ class Target {
* @returns the host target; throws on an unsupported host platform or arch.
*/
static host(): Target {
const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'linux' ? 'linux' : undefined
const platform = process.platform === 'darwin'
? 'macos'
: process.platform === 'linux'
? 'linux'
: process.platform === 'win32'
? 'win'
: undefined
if (platform === undefined) {
throw new Error(`build-exe-for-python-sdk: unsupported host platform ${process.platform}; pass --targets explicitly.`)
}
@@ -120,6 +139,9 @@ class Target {
if (arch === undefined) {
throw new Error(`build-exe-for-python-sdk: unsupported host arch ${process.arch}; pass --targets explicitly.`)
}
if (platform === 'win' && arch !== 'x64') {
throw new Error('build-exe-for-python-sdk: Windows supports x64 only; use an x64 Node process.')
}
return new Target(DEFAULT_NODE_RANGE, platform, arch)
}
}
@@ -187,7 +209,7 @@ class BuildCli {
return [
'Usage: pnpm exec tsx scripts/build-exe-for-python-sdk.ts [flags]',
'',
' --targets=<t1,t2,...> pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64.',
' --targets=<t1,t2,...> pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64.',
' Default: the host platform only (on node24).',
' --skip-build skip `pnpm run build` (lib/ artifacts must already exist).',
' --dry-run print every command and config patch without executing.',
@@ -199,8 +221,27 @@ class BuildCli {
}
}
function pnpmBin(): string {
return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
function pnpmInvocation(args: string[]): [command: string, args: string[]] {
const entrypoint = process.env.npm_execpath?.trim()
if (entrypoint !== undefined && entrypoint !== '') {
const extension = extname(entrypoint).toLowerCase()
if (extension === '.js' || extension === '.cjs' || extension === '.mjs') {
return [process.execPath, [entrypoint, ...args]]
}
if (extension !== '.cmd') return [entrypoint, args]
}
const home = process.env.PNPM_HOME?.trim()
if (home !== undefined && home !== '') {
const packageBin = resolve(home, '..', 'pnpm', 'bin')
for (const filename of ['pnpm.mjs', 'pnpm.cjs']) {
const candidate = resolve(packageBin, filename)
if (existsSync(candidate)) return [process.execPath, [candidate, ...args]]
}
}
if (process.platform === 'win32') {
throw new Error('build-exe-for-python-sdk: pnpm must expose a JavaScript entrypoint through npm_execpath or PNPM_HOME on Windows.')
}
return ['pnpm', args]
}
/**
@@ -219,8 +260,7 @@ function formatCommand(command: string, args: string[]): string {
*/
class SingleExeBuild {
/**
* The cleared deploy target, pkg input, and Python node-mode carrier. The
* checked-in default `cordis.yml` remains in its parent directory.
* The cleared deploy target, pkg input, and Python node-mode carrier.
*/
readonly staging = resolve(root, PYTHON_RUNTIME_DIR, PYTHON_NODE_SUBDIR)
private readonly outDir = resolve(root, OUT_DIR)
@@ -229,7 +269,7 @@ class SingleExeBuild {
/** Verify the closure before compiling or packaging. */
async verifyClosure(): Promise<void> {
await this.run('runtime dependency closure', pnpmBin(), ['run', 'verify-runtime-closure'])
await this.runPnpm('runtime dependency closure', ['run', 'verify-runtime-closure'])
}
/** Build all package artifacts unless `--skip-build` was passed. */
@@ -238,7 +278,7 @@ class SingleExeBuild {
console.log('build-exe-for-python-sdk: skipping pnpm run build (--skip-build)')
return
}
await this.run('build', pnpmBin(), ['run', 'build'])
await this.runPnpm('build', ['run', 'build'])
}
/** Clear and deploy the runtime closure into the node carrier. */
@@ -248,7 +288,7 @@ class SingleExeBuild {
}
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${this.staging}`)
else await rm(this.staging, { recursive: true, force: true })
await this.run('deploy', pnpmBin(), [
await this.runPnpm('deploy', [
'--filter',
DEPLOY_ROOT_PACKAGE,
'deploy',
@@ -381,10 +421,11 @@ class SingleExeBuild {
* @returns the executable and ripgrep sidecar paths, plus the macOS spawn helper path when required.
*/
async pack(target: Target): Promise<string[]> {
const product = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
const productBase = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
const product = target.platform === 'win' ? `${productBase}.exe` : productBase
await this.prepareNativePty(target)
if (!this.cli.dryRun) await mkdir(this.outDir, { recursive: true })
await this.run(`pkg ${target.spec}`, pnpmBin(), [
await this.runPnpm(`pkg ${target.spec}`, [
'dlx',
PKG_SPEC,
this.staging,
@@ -412,16 +453,19 @@ class SingleExeBuild {
/** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */
private async copyRipgrepSidecar(target: Target, product: string): Promise<string> {
const platform = target.platform === 'macos' ? 'darwin' : target.platform
const platform = target.platform === 'macos' ? 'darwin' : target.platform === 'win' ? 'win32' : target.platform
const executable = target.platform === 'win' ? 'rg.exe' : 'rg'
const source = join(
this.staging,
'node_modules',
'@vscode',
`ripgrep-${platform}-${target.arch}`,
'bin',
'rg',
executable,
)
const destination = `${product}-rg`
const destination = target.platform === 'win'
? `${product.slice(0, -'.exe'.length)}-rg.exe`
: `${product}-rg`
if (this.cli.dryRun) {
console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
return destination
@@ -443,7 +487,6 @@ class SingleExeBuild {
const stagedBuild = join(this.staging, 'node_modules', 'node-pty', 'build')
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${stagedBuild}`)
else await rm(stagedBuild, { recursive: true, force: true })
if (target.platform !== 'linux') return
const packageDirectory = join(
root,
'packages',
@@ -452,6 +495,21 @@ class SingleExeBuild {
'node_modules',
'node-pty',
)
if (target.platform === 'win') {
if (target.arch !== 'x64') {
throw new Error('build-exe-for-python-sdk: Windows supports x64 only.')
}
const host = Target.host()
if (target.platform !== host.platform || target.arch !== host.arch) {
throw new Error(
'build-exe-for-python-sdk: build the Windows runtime under x64 Node on its target host; '
+ `target ${target.platform}-${target.arch} does not match host ${host.platform}-${host.arch}.`,
)
}
resolveWindowsNodePtyAddons(join(this.staging, 'node_modules', 'node-pty'), target.arch)
return
}
if (target.platform !== 'linux') return
const destination = join(stagedBuild, 'Release', 'pty.node')
const source = resolveLinuxNodePtyAddon(packageDirectory, target.arch)
if (this.cli.dryRun) {
@@ -541,6 +599,12 @@ class SingleExeBuild {
})
})
}
/** Run pnpm through its JavaScript entrypoint when the caller supplies one. */
private async runPnpm(label: string, args: string[]): Promise<void> {
const [command, invocationArgs] = pnpmInvocation(args)
await this.run(label, command, invocationArgs)
}
}
async function main(): Promise<void> {
+17 -9
View File
@@ -47,9 +47,12 @@ def load_platforms(path: Path = PLATFORM_MANIFEST) -> dict[str, tuple[str, str]]
PLATFORMS = load_platforms()
def runtime_suffixes(executable_name: str) -> tuple[str, ...]:
suffixes = ("", "-rg")
return (*suffixes, "-spawn-helper") if "-macos-" in executable_name else suffixes
def runtime_filenames(executable_name: str) -> tuple[str, ...]:
"""Return the exact platform payload names for one runtime executable."""
if executable_name.endswith(".exe"):
return (executable_name, f"{executable_name.removesuffix('.exe')}-rg.exe")
names = (executable_name, f"{executable_name}-rg")
return (*names, f"{executable_name}-spawn-helper") if "-macos-" in executable_name else names
def main() -> None:
@@ -150,7 +153,7 @@ def copy_package(source: Path, destination: Path) -> None:
"*.pyc",
"dist",
"node_modules",
"dsh-jsonrpc-agent-pkg-*",
"deepseek-harness-sdk-runtime-*",
),
)
@@ -205,13 +208,18 @@ def stage_sdk(destination: Path, version: str) -> None:
def stage_runtime(destination: Path, version: str, executable: Path, executable_name: str) -> None:
if executable.name != executable_name:
raise ValueError(
f"runtime executable must be named {executable_name}, got {executable.name}"
)
copy_package(ROOT / "python" / "sdk-runtime", destination)
stage_license_files(destination, include_notices=True)
rewrite_version(destination / "pyproject.toml", version)
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
runtime_dir.mkdir(parents=True, exist_ok=True)
for suffix in runtime_suffixes(executable_name):
shutil.copy2(Path(f"{executable}{suffix}"), runtime_dir / f"{executable_name}{suffix}")
source_directory = executable.parent
for filename in runtime_filenames(executable_name):
shutil.copy2(source_directory / filename, runtime_dir / filename)
def verify_wheel(
@@ -246,17 +254,17 @@ def verify_wheel(
f"{wheel} has license files {license_files}, expected {expected_license_files}"
)
runtime_files = [
name for name in archive.namelist() if "/runtime/dsh-jsonrpc-agent-pkg-" in name
name for name in archive.namelist() if "/runtime/deepseek-harness-sdk-runtime-" in name
]
if package == "runtime":
assert platform is not None
expected_files = [f"{platform[1]}{suffix}" for suffix in runtime_suffixes(platform[1])]
expected_files = sorted(runtime_filenames(platform[1]))
found_files = sorted(Path(name).name for name in runtime_files)
if found_files != expected_files:
raise RuntimeError(f"{wheel} runtime payload must be {expected_files}, found {found_files}")
for runtime_file in runtime_files:
mode = archive.getinfo(runtime_file).external_attr >> 16
if mode & stat.S_IXUSR == 0:
if platform[0] != "win_amd64" and mode & stat.S_IXUSR == 0:
raise RuntimeError(f"{wheel} runtime executable lost its executable bit: {runtime_file}")
elif runtime_files:
raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}")
+6 -7
View File
@@ -6,8 +6,9 @@ import { resolve } from 'node:path'
import { parseArgs } from 'node:util'
import {
CLIENT_BUILD_RECORD_PATH,
CLIENT_BUILD_PROFILE_SELECTOR,
clientBuildProcessEnvironment,
repositoryCommitHash,
repositoryClientBuildEnvironment,
resolveClientBuildEnvironment,
writeClientBuildRecord,
} from './client-build-environment.ts'
@@ -34,12 +35,10 @@ function main(): void {
allowPositionals: false,
})
const root = resolve(import.meta.dirname, '..')
const parentEnvironment = {
...process.env,
DSH_CLIENT_COMMIT_HASH: repositoryCommitHash(root, process.env),
}
const clientEnvironment = resolveClientBuildEnvironment(parentEnvironment, values.profile)
const buildEnvironment = clientBuildProcessEnvironment(parentEnvironment, clientEnvironment)
const repositoryEnvironment = repositoryClientBuildEnvironment(root, process.env)
const profile = values.profile ?? process.env[CLIENT_BUILD_PROFILE_SELECTOR]
const clientEnvironment = resolveClientBuildEnvironment(repositoryEnvironment, profile)
const buildEnvironment = clientBuildProcessEnvironment(process.env, clientEnvironment)
rmSync(resolve(root, CLIENT_BUILD_RECORD_PATH), { force: true })
runScript('build:lib', buildEnvironment)
+13 -7
View File
@@ -54,10 +54,9 @@ const experimentalPackageDirectory = /^packages\/experimental\/[^/]+$/
const experimentalPackageNamePrefix = '@deepseek-ai/dsh-experimental-'
/** Directories whose packages this repository publishes: one release member each. */
const releaseMemberDirectory = /^(?:packages\/(?!experimental\/)[^/]+\/[^/]+|apps\/[^/]+|vendor\/[^/]+)$/
const localArtifactDirs = new Set(['node_modules'])
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
'@deepseek-ai/dsh': ['lib/*.js', 'config'],
'@deepseek-ai/dsh': ['lib/*.js'],
// Sourcemaps stay out by payload policy; the worker-preview surface
// (dist/preview.html and dist/preview/) backs private experimental
// packages and is not published.
@@ -152,9 +151,11 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
// The CPython side ships as source .py files, published as-is rather than built.
'@deepseek-ai/dsh-code-runtime-python': ['py/**/*.py'],
// The Python runtime uses a distinct closed-resolution bin; the public CLI
// keeps config-owned bare-package resolution through lib/bin.js.
'@deepseek-ai/dsh-sdk-jsonrpc-demo': ['lib/packaged-bin.js'],
// The shipped preset compositions travel inside the roster package.
'@deepseek-ai/dsh-agent-presets': ['presets'],
// The Web Host mounts the default-off settings owner independently of each
// Agent-scoped delegation-tool instance.
'@deepseek-ai/dsh-tool-subagent': ['lib/model-selection-settings.js'],
// The argv-prefix runner entry ships beside the lib as its own bundle;
// sandbox-local resolves it through the package's ./runner export. tsdown
// also shares its generated FFI code through a hashed runtime chunk.
@@ -269,7 +270,12 @@ export function checkExperimentalManifest({ dir, manifest }: WorkspaceManifest):
return errors
}
function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
/**
* Check one workspace manifest against publication and dsh-package policy.
* @param workspace - package directory and parsed manifest.
* @returns path-qualified policy violations.
*/
export function checkWorkspaceManifest({ dir, manifest }: WorkspaceManifest): string[] {
const errors = checkExperimentalManifest({ dir, manifest })
const label = manifest.name ?? dir
const isLandlockPackageDir = dir.startsWith('native/landlock-run/packages/')
@@ -490,7 +496,7 @@ export function main(): void {
]
const errors = [
...checkRepositoryVersion(),
...manifests.flatMap(checkWorkspace),
...manifests.flatMap(checkWorkspaceManifest),
...checkWorkspaceProtocol(manifests),
...checkExperimentalDependencyIsolation(dependencyManifests),
...checkHierarchyShape(),
+152 -33
View File
@@ -27,21 +27,24 @@ describe('CI workflow', () => {
for (const { jobName, step } of setups) {
expect(step, `${jobName} must not share pnpm/action-setup's default destination`).toMatchObject({
with: {
dest: jobName === 'windows-native'
dest: jobName.startsWith('windows-')
? nativeWindowsPnpmDestination
: runnerPrivatePnpmDestination,
},
})
if (jobName === 'windows-native') expect(step).not.toMatchObject({ with: { standalone: true } })
if (jobName.startsWith('windows-')) expect(step).not.toMatchObject({ with: { standalone: true } })
}
})
it('keeps a required Wine Windows job, a non-blocking native Windows job with failover, and a master-only standby', () => {
it('keeps required Wine and split native Windows jobs with failover, plus a master-only standby', () => {
const workflow = loadWorkflow('.github/workflows/ci.yml')
const masterWorkflow = loadWorkflow('.github/workflows/ci-master.yml')
if (!isRecord(workflow.jobs)
|| !isRecord(workflow.jobs.windows)
|| !isRecord(workflow.jobs['windows-native'])
|| !isRecord(workflow.jobs['windows-build'])
|| !isRecord(workflow.jobs['windows-coverage'])
|| !isRecord(workflow.jobs['windows-native-tests'])
|| !isRecord(workflow.jobs['windows-observational'])
|| !isRecord(workflow.jobs['node-24'])
|| !isRecord(workflow.jobs['node-24-coverage'])
|| !isRecord(workflow.jobs['node-24-consumers'])
@@ -49,11 +52,14 @@ describe('CI workflow', () => {
|| !isRecord(masterWorkflow.jobs)
|| !isRecord(masterWorkflow.jobs['wine-apt-cache'])
|| !isRecord(masterWorkflow.jobs['serial-windows'])) {
throw new TypeError('CI workflow must define windows, windows-native, node-24, node-24-coverage, node-24-consumers, and all-checks-passed; ci-master must define wine-apt-cache and serial-windows')
throw new TypeError('CI workflow must define windows, windows-build, windows-coverage, windows-native-tests, windows-observational, node-24, node-24-coverage, node-24-consumers, and all-checks-passed; ci-master must define wine-apt-cache and serial-windows')
}
const windows = workflow.jobs.windows
const windowsNative = workflow.jobs['windows-native']
const windowsBuild = workflow.jobs['windows-build']
const windowsCoverage = workflow.jobs['windows-coverage']
const windowsNativeTests = workflow.jobs['windows-native-tests']
const windowsObservational = workflow.jobs['windows-observational']
const wineAptCache = masterWorkflow.jobs['wine-apt-cache']
const serialWindows = masterWorkflow.jobs['serial-windows']
const node24 = workflow.jobs['node-24']
@@ -73,24 +79,49 @@ describe('CI workflow', () => {
expect(windows.if).toBe("github.event_name == 'pull_request'")
expect(commandSteps.some(step => step.run.includes('wine-windows-gates.sh'))).toBe(true)
// windows-native: non-blocking native job with failover, runs windows-complete.
// Its pool is resolved by the Windows-specific switch.
expect(typeof windowsNative['runs-on']).toBe('string')
expect(windowsNative['runs-on']).toContain('DSH_CI_FAILOVER_WINDOWS')
expect(windowsNative['runs-on']).not.toContain('DSH_CI_FAILOVER_LINUX')
expect(windowsNative['runs-on']).toContain('self-hosted')
expect(windowsNative['runs-on']).toContain('dsh-win-ci')
expect(windowsNative['runs-on']).toContain('dsh-windows-2025-16core')
expect(windowsNative.name).toBe('windows node 24 / native complete')
expect(windowsNative.if).toBe("github.event_name == 'pull_request'")
expect(windowsNative.env).toMatchObject({
DSH_COVERAGE_TEST_TIMEOUT_MS: '30000',
})
const nativeSteps = windowsNative.steps as unknown[]
const nativeCommandSteps = nativeSteps.filter((step): step is Record<string, unknown> & { run: string } => (
// The split native jobs all resolve their pool through the Windows switch.
for (const [jobName, job] of [['windows-build', windowsBuild], ['windows-coverage', windowsCoverage], ['windows-native-tests', windowsNativeTests], ['windows-observational', windowsObservational]] as const) {
expect(typeof job['runs-on']).toBe('string')
expect(job['runs-on'], `${jobName} runs-on must use the Windows failover switch`).toContain('DSH_CI_FAILOVER_WINDOWS')
expect(job['runs-on'], `${jobName} runs-on must not use the Linux failover switch`).not.toContain('DSH_CI_FAILOVER_LINUX')
expect(job['runs-on']).toContain('self-hosted')
expect(job['runs-on']).toContain('dsh-win-ci')
expect(job['runs-on']).toContain('dsh-windows-2025-16core')
expect(job.if).toBe("github.event_name == 'pull_request'")
}
// windows-build runs the blocking build/site pair.
expect(windowsBuild.name).toBe('windows node 24 / build')
const buildSteps = windowsBuild.steps as unknown[]
const buildCommands = buildSteps.filter((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && typeof step.run === 'string'
))
expect(nativeCommandSteps.map(step => step.run)).toContain('pnpm run check:ci:windows-complete')
expect(buildCommands.map(step => step.run)).toContain('pnpm run check:ci:windows-blocking')
// windows-coverage uses the lower 4-partition profile.
expect(windowsCoverage.name).toBe('windows node 24 / coverage')
expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '4' })
const coverageSteps = windowsCoverage.steps as unknown[]
const coverageCommands = coverageSteps.filter((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && typeof step.run === 'string'
))
expect(coverageCommands.map(step => step.run)).toContain('pnpm run check:ci:coverage')
// windows-native-tests runs the Windows-specific specs.
expect(windowsNativeTests.name).toBe('windows node 24 / native tests')
const nativeTestSteps = windowsNativeTests.steps as unknown[]
const nativeTestCommands = nativeTestSteps.filter((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && typeof step.run === 'string'
))
const nativeTestCommand = nativeTestCommands.map(step => step.run).join('\n')
expect(nativeTestCommand).toContain('--no-file-parallelism')
expect(nativeTestCommand).toContain('--testTimeout 30000')
expect(nativeTestCommand).toContain('tool-pwsh/tests/loader.spec.ts')
expect(nativeTestCommand).toContain('workflow-worker-thread.spec.ts')
// windows-observational is non-blocking.
expect(windowsObservational.name).toBe('windows node 24 / observational')
expect(windowsObservational['continue-on-error']).toBe(true)
// wine-apt-cache: master-only, seeds the Wine apt cache, lives in ci-master.
expect(wineAptCache.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
@@ -101,9 +132,14 @@ describe('CI workflow', () => {
expect(serialWindows['runs-on']).toEqual(['self-hosted', 'dsh-win-ci', 'windows'])
expect(serialWindows.name).toBe('serial / windows (self-hosted standby)')
// Aggregate: Wine `windows` required, native `windows-native` excluded.
// Aggregate: Wine and the required split native jobs are needed;
// windows-coverage is temporarily non-blocking while Windows ACP
// half-close tests are stabilized; observational stays out too.
expect(aggregate.needs).toContain('windows')
expect(aggregate.needs).not.toContain('windows-native')
expect(aggregate.needs).toContain('windows-build')
expect(aggregate.needs).not.toContain('windows-coverage')
expect(aggregate.needs).toContain('windows-native-tests')
expect(aggregate.needs).not.toContain('windows-observational')
expect(aggregate.needs).not.toContain('serial-windows')
// Linux failover is a separate switch: the three required Linux workers
@@ -120,6 +156,14 @@ describe('CI workflow', () => {
expect(aggregate['runs-on']).toContain('vm-backup')
})
it('gives the Wine Host TypeScript compile the repository heap budget', () => {
const wineGates = readFileSync(resolve(root, 'scripts/wine-windows-gates.sh'), 'utf8')
expect(wineGates).toContain(
'wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=4096 "$tsc_js" -b tsconfig.host.json --pretty false',
)
})
it('exempts push from cancellation in ci-master, so one master merge does not cancel the running drill', () => {
const workflow = loadWorkflow('.github/workflows/ci-master.yml')
const prWorkflow = loadWorkflow('.github/workflows/ci.yml')
@@ -207,7 +251,7 @@ describe('CI workflow', () => {
expect(config).not.toContain('packages/lsp/lsp-stdio/src/instance.ts')
})
it('requires one release-shaped Python runtime target on every pull request', () => {
it('requires release-shaped Python runtime validation on every published target', () => {
const workflow = loadWorkflow('.github/workflows/ci.yml')
const pythonRuntime = workflowJob(workflow, 'python-runtime')
const aggregate = workflowJob(workflow, 'all-checks-passed')
@@ -217,12 +261,15 @@ describe('CI workflow', () => {
expect(pythonRuntime).toMatchObject({
if: "github.event_name == 'pull_request'",
name: 'python runtime / release-shaped Linux x64',
name: 'python runtime / release-shaped matrix',
uses: './.github/workflows/build-exe-for-python-sdk.yml',
with: {
targets: 'node24-linux-x64',
targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64',
ci: true,
},
secrets: {
DEEPSEEK_API_KEY_EXTERNAL: '${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}',
},
})
expect(aggregate.needs).toContain('python-runtime')
})
@@ -247,6 +294,15 @@ describe('DeepSeek e2e workflow', () => {
})
expect(JSON.stringify(steps)).not.toContain('apt-get')
})
it('bounds profile subprocess fan-out to the tested e2e default', () => {
const workflow = loadWorkflow('.github/workflows/e2e.yml')
const e2e = workflowJob(workflow, 'e2e')
if (!Array.isArray(e2e.steps)) throw new TypeError('DeepSeek e2e workflow must define steps')
const step = e2e.steps.filter(isRecord).find(candidate => candidate.name === 'E2E tests (real DeepSeek API)')
expect(step).toMatchObject({ env: { DSH_E2E_MAX_WORKERS: 4 } })
})
})
describe('E2B e2e workflow', () => {
@@ -280,7 +336,6 @@ describe('Python release workflows', () => {
it('keeps complete wheel validation separate from protected public publication', () => {
const workflow = loadWorkflow('.github/workflows/python-release.yml')
const dispatch = workflowEvent(workflow, 'workflow_dispatch')
const pullRequest = workflowEvent(workflow, 'pull_request')
const build = workflowJob(workflow, 'build')
const pythonCompat = workflowJob(workflow, 'python-compat')
const validate = workflowJob(workflow, 'validate')
@@ -296,12 +351,12 @@ describe('Python release workflows', () => {
}
expect(dispatch.inputs.publish).toMatchObject({ type: 'boolean', default: false })
expect(pullRequest).toEqual({ types: ['labeled'] })
if (!isRecord(workflow.on)) throw new TypeError('python-release workflow must define on')
expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch'])
expect(build).toMatchObject({
if: "github.event_name == 'workflow_dispatch' || github.event.label.name == 'python-release-dry-run'",
uses: './.github/workflows/build-exe-for-python-sdk.yml',
with: {
targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64',
targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64',
release: true,
},
})
@@ -363,7 +418,7 @@ describe('Python release workflows', () => {
const call = workflowEvent(workflow, 'workflow_call')
const plan = workflowJob(workflow, 'plan')
const build = workflowJob(workflow, 'build')
if (!isRecord(call.inputs) || !Array.isArray(plan.steps) || !Array.isArray(build.steps)) {
if (!isRecord(call.inputs) || !isRecord(call.secrets) || !Array.isArray(plan.steps) || !Array.isArray(build.steps)) {
throw new TypeError('Python wheel builder must define workflow_call inputs and plan steps')
}
@@ -371,35 +426,81 @@ describe('Python release workflows', () => {
const manylinuxAddon = buildSteps.find(step => isRecord(step) && step.name === 'Rebuild Linux node-pty against manylinux 2.28')
const macosCheck = buildSteps.find(step => isRecord(step) && step.name === 'Check macOS deployment target')
const manylinuxSmoke = buildSteps.find(step => isRecord(step) && step.name === 'Run wheel in a manylinux 2.28 container')
const cleanVenvPosix = buildSteps.find(step => isRecord(step) && step.name === 'Install local SDK and runtime wheels into a clean venv (POSIX)')
const cleanVenvWindows = buildSteps.find(step => isRecord(step) && step.name === 'Install local SDK and runtime wheels into a clean venv (Windows)')
const installedKeylessPosix = buildSteps.find(step => isRecord(step) && step.name === 'Run installed-wheel keyless black-box tests (POSIX)')
const installedKeylessWindows = buildSteps.find(step => isRecord(step) && step.name === 'Run installed-wheel keyless black-box tests (Windows)')
const realApiPreflightPosix = buildSteps.find(step => isRecord(step) && step.name === 'Preflight installed-wheel real API test (POSIX)')
const realApiPreflightWindows = buildSteps.find(step => isRecord(step) && step.name === 'Preflight installed-wheel real API test (Windows)')
const installedRealApiPosix = buildSteps.find(step => isRecord(step) && step.name === 'Run installed-wheel real API black-box test (POSIX)')
const installedRealApiWindows = buildSteps.find(step => isRecord(step) && step.name === 'Run installed-wheel real API black-box test (Windows)')
if (!isRecord(cleanVenvPosix) || !isRecord(cleanVenvWindows)
|| !isRecord(installedKeylessPosix) || !isRecord(installedKeylessWindows)
|| !isRecord(realApiPreflightPosix) || !isRecord(realApiPreflightWindows)
|| !isRecord(installedRealApiPosix) || !isRecord(installedRealApiWindows)) {
throw new TypeError('Python wheel builder must define native POSIX and Windows installed-wheel steps')
}
expect(call.inputs).toHaveProperty('targets')
expect(call.inputs).toMatchObject({
ci: { type: 'boolean', default: false },
release: { type: 'boolean', default: false },
})
expect(call.secrets).toMatchObject({
DEEPSEEK_API_KEY_EXTERNAL: { required: false },
})
expect(workflow.concurrency).toMatchObject({
group: 'build-single-exe-${{ github.workflow }}-${{ github.ref }}',
})
expect(build.defaults).toBeUndefined()
expect(plan.if).toContain('inputs.ci')
expect(plan.if).toContain('inputs.release')
expect(JSON.stringify(plan.steps)).toContain('pep440_version')
const workflowJson = JSON.stringify(workflow)
expect(workflowJson).toContain('macosx_14_0_arm64')
expect(workflowJson).toContain('win_amd64')
expect(workflowJson).toContain('node24-win-x64')
expect(workflowJson).toContain('windows-2025')
expect(workflowJson).toContain('dist-python/$SDK_WHEEL')
expect(workflowJson).toContain('dist-python/$RUNTIME_WHEEL')
expect(workflowJson).toContain('/work/dist-python/$SDK_WHEEL')
expect(workflowJson).toContain('/work/dist-python/$RUNTIME_WHEEL')
expect(workflowJson).not.toContain('--find-links dist-python')
expect(workflowJson).not.toContain('--find-links /work/dist-python')
expect(workflowJson).not.toContain('cygpath')
expect(manylinuxAddon).toMatchObject({ if: "runner.os == 'Linux'" })
expect(JSON.stringify(manylinuxAddon)).toContain('manylinux_2_28_x86_64')
expect(JSON.stringify(manylinuxAddon)).toContain('manylinux_2_28_aarch64')
expect(JSON.stringify(manylinuxAddon)).toContain('npm_config_build_from_source=true pnpm run install')
expect(JSON.stringify(manylinuxAddon)).toContain('$HOME/setup-pnpm:$HOME/setup-pnpm:ro')
expect(JSON.stringify(manylinuxAddon)).toContain('pnpm_setup_root')
expect(JSON.stringify(manylinuxAddon)).toContain('$pnpm_setup_root:$pnpm_setup_root:ro')
expect(JSON.stringify(manylinuxAddon)).toContain('node-pty-glibc-versions.txt')
expect(JSON.stringify(manylinuxAddon)).toContain('le 2.28')
expect(macosCheck).toMatchObject({ if: "runner.os == 'macOS'" })
expect(JSON.stringify(macosCheck)).toContain('scripts/check-macos-deployment-target.py')
expect(JSON.stringify(macosCheck)).toContain('$EXE-spawn-helper')
expect(JSON.stringify(installedKeylessPosix)).toContain('--scenario all')
expect(JSON.stringify(installedKeylessPosix)).toContain('env -u PYTHONPATH')
expect(JSON.stringify(installedKeylessWindows)).toContain('--scenario all --installed-wheel')
expect(installedKeylessWindows).toMatchObject({ if: "runner.os == 'Windows'", shell: 'pwsh' })
expect(cleanVenvWindows).toMatchObject({ if: "runner.os == 'Windows'", shell: 'pwsh' })
expect(JSON.stringify(cleanVenvWindows)).toContain('Scripts\\\\python.exe')
expect(realApiPreflightPosix).toMatchObject({
env: { DEEPSEEK_API_KEY: '${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}' },
})
expect(String(realApiPreflightPosix.if)).toContain('inputs.ci')
expect(String(realApiPreflightPosix.if)).toContain('head.repo.fork')
expect(String(realApiPreflightPosix.if)).toContain('dependabot[bot]')
expect(realApiPreflightWindows).toMatchObject({ shell: 'pwsh' })
expect(installedRealApiPosix).toMatchObject({
env: {
DEEPSEEK_API_KEY: '${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}',
DEEPSEEK_BASE_URL: 'https://api.deepseek.com',
},
})
expect(JSON.stringify(installedRealApiPosix)).toContain('--scenario sdk-live')
expect(JSON.stringify(installedRealApiPosix)).toContain('-u DSH_RUNTIME_MODE')
expect(installedRealApiWindows).toMatchObject({ shell: 'pwsh' })
expect(JSON.stringify(installedRealApiWindows)).toContain('--scenario sdk-live --installed-wheel')
expect(manylinuxSmoke).toMatchObject({ if: "runner.os == 'Linux'" })
expect(JSON.stringify(manylinuxSmoke)).toContain('-e DSH_TELEMETRY_DISABLED')
})
@@ -421,6 +522,24 @@ describe('Python release workflows', () => {
expect(macosCheck).toContain('scripts/check-macos-deployment-target.py')
expect(macosCheck).toContain('"$EXE" "$EXE-spawn-helper"')
})
it('builds and black-box tests the Windows x64 wheel in GitLab', () => {
const workflow = loadWorkflow('.gitlab-ci.yml')
const windows = workflow['runtime-windows-x64']
const publish = workflow['publish-python']
if (!isRecord(windows) || !Array.isArray(windows.before_script) || !Array.isArray(windows.script)
|| !isRecord(publish) || !Array.isArray(publish.needs)) {
throw new TypeError('GitLab CI must define the Windows runtime and aggregate publication jobs')
}
expect(windows.tags).toEqual(['windows-x64'])
expect(windows.variables).toMatchObject({ PKG_TARGET: 'node24-win-x64', PLATFORM: 'win-x64' })
expect(JSON.stringify(windows.before_script)).toContain('.ci-python\\\\Scripts')
expect(JSON.stringify(windows.before_script)).toContain('[IO.Path]::PathSeparator')
expect(JSON.stringify(windows.script)).toContain('win_amd64.whl')
expect(JSON.stringify(windows.script)).toContain('--scenario all --installed-wheel')
expect(publish.needs).toContainEqual({ job: 'runtime-windows-x64', artifacts: true })
})
})
describe('Issue lifecycle workflow', () => {
@@ -1,3 +1,4 @@
import { execFileSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
@@ -7,8 +8,12 @@ import {
assertClientBuildEnvironment,
clientBuildEnvironmentDefines,
clientBuildProcessEnvironment,
officialClientBuildEnvironment,
readClientBuildRecord,
repositoryClientBuildEnvironment,
repositoryCommitHash,
repositoryGitDirty,
repositoryVersion,
resolveClientBuildEnvironment,
writeClientBuildRecord,
} from './client-build-environment.ts'
@@ -51,12 +56,34 @@ function buildFixture(environment: Record<string, string>): string {
return fixtureRoot
}
function git(root: string, args: readonly string[]): string {
return execFileSync('git', [...args], {
cwd: root,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
}).trim()
}
function repositoryFixture(version = '1.2.3-rc.4'): string {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'dsh-client-build-repository-'))
roots.push(fixtureRoot)
write(join(fixtureRoot, 'package.json'), `${JSON.stringify({ version })}\n`)
write(join(fixtureRoot, 'tracked.txt'), 'committed\n')
git(fixtureRoot, ['init'])
git(fixtureRoot, ['config', 'user.name', 'DSH test'])
git(fixtureRoot, ['config', 'user.email', '[email protected]'])
git(fixtureRoot, ['add', 'package.json', 'tracked.txt'])
git(fixtureRoot, ['commit', '-m', 'fixture'])
return fixtureRoot
}
describe('client build environment', () => {
it('requires an exact public environment for a named artifact profile', () => {
const expected = {
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
} as const
expect(() => { assertClientBuildEnvironment({ PATH: '/bin', ...expected }, expected) }).not.toThrow()
@@ -73,7 +100,9 @@ describe('client build environment', () => {
DSH_BUILD_CLIENT_PROFILE: 'official',
DSH_CLIENT_BUILD_PROFILE: 'local',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_GIT_DIRTY: 'true',
DSH_CLIENT_TITLE: 'Local title',
DSH_CLIENT_VERSION: '1.2.3',
DSH_CLIENT_EXTRA: 'local-extra',
}
@@ -84,24 +113,108 @@ describe('client build environment', () => {
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
})
expect(() => {
resolveClientBuildEnvironment({ DSH_BUILD_CLIENT_PROFILE: 'official' })
}).toThrow(/DSH_CLIENT_COMMIT_HASH/)
expect(() => {
resolveClientBuildEnvironment({
DSH_BUILD_CLIENT_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
})
}).toThrow(/DSH_CLIENT_VERSION/)
expect(() => { resolveClientBuildEnvironment({}, 'unknown') }).toThrow(/unknown client build profile/)
expect(clientBuildProcessEnvironment(parent, {
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
})).toEqual({
PATH: '/bin',
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
})
expect(repositoryCommitHash('/unused', { DSH_CLIENT_COMMIT_HASH: COMMIT_HASH })).toBe(COMMIT_HASH.slice(0, 7))
})
it('owns repository version, commit, and dirty metadata for complete builds', () => {
const fixtureRoot = repositoryFixture()
const commit = git(fixtureRoot, ['rev-parse', '--short=7', 'HEAD'])
expect(repositoryVersion(fixtureRoot)).toBe('1.2.3-rc.4')
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
expect(repositoryClientBuildEnvironment(fixtureRoot, {
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH,
DSH_CLIENT_EXTRA: 'preserved',
DSH_CLIENT_GIT_DIRTY: 'true',
DSH_CLIENT_VERSION: 'spoofed',
})).toEqual({
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_EXTRA: 'preserved',
DSH_CLIENT_VERSION: '1.2.3-rc.4',
})
expect(officialClientBuildEnvironment(fixtureRoot)).toEqual({
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: commit,
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3-rc.4',
})
write(join(fixtureRoot, '.gitignore'), 'ignored.txt\n')
git(fixtureRoot, ['add', '.gitignore'])
git(fixtureRoot, ['commit', '-m', 'ignore fixture'])
write(join(fixtureRoot, 'ignored.txt'), 'ignored\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
rmSync(join(fixtureRoot, 'ignored.txt'))
write(join(fixtureRoot, 'tracked.txt'), 'unstaged\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(true)
write(join(fixtureRoot, 'tracked.txt'), 'committed\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
write(join(fixtureRoot, 'tracked.txt'), 'staged\n')
git(fixtureRoot, ['add', 'tracked.txt'])
expect(repositoryGitDirty(fixtureRoot)).toBe(true)
git(fixtureRoot, ['commit', '-m', 'staged fixture'])
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
write(join(fixtureRoot, 'untracked.txt'), 'untracked\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(true)
expect(repositoryClientBuildEnvironment(fixtureRoot, {
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH,
})).toEqual({
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_GIT_DIRTY: 'true',
DSH_CLIENT_VERSION: '1.2.3-rc.4',
})
rmSync(join(fixtureRoot, 'untracked.txt'))
const submoduleSource = repositoryFixture('9.8.7')
git(fixtureRoot, ['-c', 'protocol.file.allow=always', 'submodule', 'add', submoduleSource, 'submodule'])
git(fixtureRoot, ['commit', '-am', 'submodule fixture'])
expect(repositoryGitDirty(fixtureRoot)).toBe(false)
write(join(fixtureRoot, 'submodule/tracked.txt'), 'modified submodule\n')
expect(repositoryGitDirty(fixtureRoot)).toBe(true)
})
it('omits dirty metadata when repository metadata is unavailable', () => {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'dsh-client-build-no-git-'))
roots.push(fixtureRoot)
write(join(fixtureRoot, 'package.json'), '{"version":"2.0.0"}\n')
expect(repositoryGitDirty(fixtureRoot)).toBeUndefined()
expect(repositoryClientBuildEnvironment(fixtureRoot, {
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH,
DSH_CLIENT_GIT_DIRTY: 'true',
})).toEqual({
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_VERSION: '2.0.0',
})
})
it('defines only public client values over a non-enumerable fallback', () => {
expect(clientBuildEnvironmentDefines({
PATH: '/bin',
@@ -151,6 +264,7 @@ describe('client build environment', () => {
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: COMMIT_HASH.slice(0, 7),
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.3',
}
const official = buildFixture(officialEnvironment)
const defaultBuild = buildFixture({})
+84 -2
View File
@@ -1,5 +1,5 @@
import { createHash } from 'node:crypto'
import { execFileSync } from 'node:child_process'
import { execFileSync, spawnSync } from 'node:child_process'
import {
existsSync,
globSync,
@@ -25,6 +25,9 @@ const OFFICIAL_CLIENT_BUILD_ENVIRONMENT = {
/** Public variable carrying the source commit embedded in client artifacts. */
const CLIENT_COMMIT_HASH_VARIABLE = 'DSH_CLIENT_COMMIT_HASH'
/** Public variable carrying the repository package version embedded in client artifacts. */
const CLIENT_VERSION_VARIABLE = 'DSH_CLIENT_VERSION'
/** Repository-relative path of the complete client build record. */
export const CLIENT_BUILD_RECORD_PATH = '.dsh-build/client-build-environment.json'
@@ -57,6 +60,76 @@ export function repositoryCommitHash(root: string, environment: NodeJS.ProcessEn
return value.slice(0, 7).toLowerCase()
}
/**
* Resolve the repository package version used by browser build metadata.
* @param root - repository root containing the authoritative package.json.
* @returns the repository's semver-compatible package version.
*/
export function repositoryVersion(root: string): string {
const path = resolve(root, 'package.json')
let manifest: unknown
try {
manifest = JSON.parse(readFileSync(path, 'utf8'))
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
throw new Error(`cannot read repository version from ${path}: ${detail}`)
}
if (!isObject(manifest) || typeof manifest.version !== 'string'
|| !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version)) {
throw new Error(`repository package.json has an invalid version ${JSON.stringify(isObject(manifest) ? manifest.version : undefined)}`)
}
return manifest.version
}
/**
* Read whether Git reports any staged, unstaged, untracked, or submodule change.
* @param root - repository root whose worktree is inspected.
* @returns true or false inside a Git worktree; undefined without Git metadata.
*/
export function repositoryGitDirty(root: string): boolean | undefined {
const probe = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
cwd: root,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
})
if (probe.error !== undefined || probe.status !== 0 || probe.stdout.trim() !== 'true') return undefined
const status = spawnSync('git', ['status', '--porcelain=v1', '--untracked-files=normal'], {
cwd: root,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
})
if (status.error !== undefined) throw status.error
if (status.status !== 0) {
throw new Error(`git status failed in ${root}: ${status.stderr.trim() || String(status.status)}`)
}
return status.stdout !== ''
}
/**
* Resolve the public environment for a complete default build from one checkout.
* Repository-owned metadata replaces inherited values; other public values pass through.
* @param root - repository root supplying version and Git metadata.
* @param environment - caller environment supplying optional commit and public extensions.
* @returns complete public client environment for the default build.
*/
export function repositoryClientBuildEnvironment(
root: string,
environment: NodeJS.ProcessEnv = process.env,
): ClientBuildEnvironment {
const inherited = { ...clientBuildEnvironment(environment) }
delete inherited.DSH_CLIENT_COMMIT_HASH
delete inherited.DSH_CLIENT_GIT_DIRTY
delete inherited.DSH_CLIENT_VERSION
const dirty = repositoryGitDirty(root)
return {
...inherited,
DSH_CLIENT_COMMIT_HASH: repositoryCommitHash(root, environment),
...(dirty === true ? { DSH_CLIENT_GIT_DIRTY: 'true' } : {}),
DSH_CLIENT_VERSION: repositoryVersion(root),
}
}
/**
* Resolve the exact public values required by an official build at one commit.
* @param root - repository root whose HEAD must match the built source.
@@ -69,6 +142,7 @@ export function officialClientBuildEnvironment(
): Readonly<Record<`DSH_CLIENT_${string}`, string>> {
return {
DSH_CLIENT_COMMIT_HASH: repositoryCommitHash(root, environment),
DSH_CLIENT_VERSION: repositoryVersion(root),
...OFFICIAL_CLIENT_BUILD_ENVIRONMENT,
}
}
@@ -115,10 +189,18 @@ export function resolveClientBuildEnvironment(
if (profile === undefined) return clientBuildEnvironment(environment)
if (profile === 'official') {
const commitHash = environment[CLIENT_COMMIT_HASH_VARIABLE]
const version = environment[CLIENT_VERSION_VARIABLE]
if (commitHash === undefined) {
throw new Error(`${CLIENT_COMMIT_HASH_VARIABLE} is required for the official client build profile`)
}
return { DSH_CLIENT_COMMIT_HASH: commitHash, ...OFFICIAL_CLIENT_BUILD_ENVIRONMENT }
if (version === undefined) {
throw new Error(`${CLIENT_VERSION_VARIABLE} is required for the official client build profile`)
}
return {
DSH_CLIENT_COMMIT_HASH: commitHash,
DSH_CLIENT_VERSION: version,
...OFFICIAL_CLIENT_BUILD_ENVIRONMENT,
}
}
throw new Error(`unknown client build profile ${JSON.stringify(profile)}; expected "official"`)
}
+50 -9
View File
@@ -1,7 +1,10 @@
/**
* Pins shared client-bundle preset rules: the module-edge purity gate and
* the physical watch dependencies hidden behind virtual CSS Modules.
* Pins shared client-bundle preset rules: module-edge purity, source-map
* chaining, and physical watch dependencies hidden behind virtual CSS Modules.
*/
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it, vi } from 'vitest'
import { clientBundle, requestedExternals } from '../packages/client/tsdown.client.ts'
@@ -14,6 +17,11 @@ interface CssModulePlugin {
load?: (this: { addWatchFile: (id: string) => void }, id: string) => Promise<unknown>
}
interface SourceMapPlugin {
name: string
load?: (id: string) => Promise<unknown>
}
/** A representative dynamic bundle using the shared client baseline. */
const REQUESTING_PACKAGE = '@deepseek-ai/dsh-client-ui-conversation'
@@ -59,13 +67,21 @@ function cssModulePlugin(): CssModulePlugin {
return plugin
}
function sourceMapPlugin(): SourceMapPlugin {
const configs = clientConfigs()
const plugins = (configs[0] as { plugins: SourceMapPlugin[] }).plugins
const plugin = plugins.find(candidate => candidate.name === 'dsh-tsc-sourcemap')
if (plugin?.load === undefined) throw new Error('tsc sourcemap plugin missing from client config')
return plugin
}
describe('client bundle purity gate', () => {
const resolveId = purityResolveId()
it('leaves default externals and non-scoped specifiers alone', () => {
expect(resolveId('@deepseek-ai/dsh-client-store')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-client-ui-slots')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-client-ui-primitives')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-client-runtime/client')).toBeNull()
expect(resolveId('react')).toBeNull()
expect(resolveId('zod')).toBeNull()
})
@@ -79,6 +95,9 @@ describe('client bundle purity gate', () => {
expect(resolveId('@deepseek-ai/dsh-host-apiproxy/api')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-session/surface')).toBeNull()
expect(resolveId('@deepseek-ai/dsh-brand')).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/)
})
it('lets exact generated Remote contributions inline without admitting their package implementation', () => {
@@ -95,14 +114,14 @@ describe('client bundle purity gate', () => {
it('throws on cross-plugin value imports — bare plugin names and /client subpaths alike', () => {
expect(() => resolveId('@deepseek-ai/dsh-client-connection')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-runtime')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-ui-session')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-ui-layout/client')).toThrow(/purity/)
})
it('admits the parser-preloaded runtime for every dynamic bundle', () => {
expect(resolveId('@deepseek-ai/dsh-client-runtime/client')).toBeNull()
const withoutRequest = purityResolveId('@deepseek-ai/dsh-client-ui-goal')
expect(withoutRequest('@deepseek-ai/dsh-client-runtime/client')).toBeNull()
it('admits package-specific requests only for the declaring bundle', () => {
const requesting = purityResolveId('@deepseek-ai/dsh-api-session-controller')
expect(requesting('@deepseek-ai/dsh-api-gateway/client')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-api-gateway/client')).toThrow(/purity/)
})
it('externalizes the baseline independently of each package manifest', () => {
@@ -114,7 +133,7 @@ describe('client bundle purity gate', () => {
expect(requesting.neverBundle('react')).toBe(true)
expect(requesting.neverBundle('zod')).toBe(false)
expect(plain.neverBundle('react')).toBe(true)
expect(plain.neverBundle('@deepseek-ai/dsh-client-runtime/client')).toBe(true)
expect(plain.neverBundle('@deepseek-ai/dsh-client-store')).toBe(true)
})
})
@@ -143,6 +162,28 @@ describe('client bundle debug artifacts', () => {
it('emits source maps for plugin TS and TSX outside the Vite module graph', () => {
const configs = clientConfigs()
expect(configs[0]?.sourcemap).toBe(true)
expect(configs[0]?.outputOptions).toMatchObject({ sourcemapExcludeSources: false })
})
it('chains emitted tsc maps when the production Client build consumes lib/types', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-client-sourcemap-'))
try {
const entry = join(root, 'lib', 'types', 'client', 'index.js')
const source = join(root, 'src', 'client', 'index.ts')
const map = { version: 3, names: [], mappings: 'AAAA', sources: ['../../../src/client/index.ts'] }
mkdirSync(join(root, 'lib', 'types', 'client'), { recursive: true })
mkdirSync(join(root, 'src', 'client'), { recursive: true })
writeFileSync(entry, 'export const marker = true\n//# sourceMappingURL=index.js.map\n')
writeFileSync(`${entry}.map`, JSON.stringify(map))
writeFileSync(source, 'export const marker: true = true\n')
await expect(sourceMapPlugin().load!(entry)).resolves.toEqual({
code: 'export const marker = true',
map: { ...map, sourcesContent: ['export const marker: true = true\n'] },
})
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('maps first-party sources to their repository package paths', () => {
+5 -5
View File
@@ -14,14 +14,14 @@ describe('cordisConfigFiles', () => {
it('finds Loader YAML without treating translation records as configs', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-cordis-config-files-'))
roots.push(root)
for (const directory of ['.claude', 'docs', 'examples', 'node_modules/pkg', 'vendor/pkg']) {
for (const directory of ['.claude', 'apps/cli/config/examples', 'docs', 'node_modules/pkg', 'vendor/pkg']) {
mkdirSync(join(root, directory), { recursive: true })
}
for (const file of [
'.claude/hidden.cordis.yml',
'docs/cordis-primer.i18n.yaml',
'examples/agent.cordis.yaml',
'examples/headless.cordis.yml',
'apps/cli/config/examples/agent.cordis.yaml',
'apps/cli/config/examples/headless.cordis.yml',
'node_modules/pkg/hidden.cordis.yml',
'vendor/pkg/hidden.cordis.yml',
]) {
@@ -29,8 +29,8 @@ describe('cordisConfigFiles', () => {
}
expect(cordisConfigFiles(root)).toEqual([
join('examples', 'agent.cordis.yaml'),
join('examples', 'headless.cordis.yml'),
join('apps', 'cli', 'config', 'examples', 'agent.cordis.yaml'),
join('apps', 'cli', 'config', 'examples', 'headless.cordis.yml'),
])
})
})
-1
View File
@@ -17,7 +17,6 @@ const allSpecs = new Set([
...globSync('packages/*/*/tests/**/*.spec.ts', { cwd: root }),
...globSync('packages/*/*/tests/**/*.spec.tsx', { cwd: root }),
...globSync('apps/*/tests/**/*.spec.ts', { cwd: root }),
...globSync('examples/*/tests/**/*.spec.ts', { cwd: root }),
...globSync('scripts/**/*.spec.ts', { cwd: root }),
].map(path => path.replaceAll('\\', '/')))
+12 -10
View File
@@ -1,16 +1,18 @@
/** Boot the ACP Code Mode overlay. Requires a DeepSeek API key. */
/** Run one headless task through the shipped Code Mode composition. Requires a model credential. */
import { spawn } from 'node:child_process'
if (process.argv.length > 2) {
console.error('usage: pnpm run demo:code-mode')
process.exit(2)
}
const task = process.argv.slice(2).join(' ').trim()
|| 'Inspect this repository with Code Mode and report its top-level architecture.'
const child = spawn(process.execPath, [
'--import',
'tsx',
'packages/examples/acp-demo/src/bin.ts',
'--config',
'examples/acp-agent/code-mode.cordis.yml',
], { stdio: 'inherit' })
'tsx/esm',
'apps/cli/src/bin.ts',
'--profile',
'headless',
task,
], {
stdio: 'inherit',
env: { ...process.env, DSH_TOOLS_MODE: 'code' },
})
child.on('exit', (code, signal) => { process.exit(signal !== null ? 1 : code ?? 1) })
-22
View File
@@ -1,22 +0,0 @@
/**
* Boot the self-referential Cordis tools under Web or ACP, defaulting to Web. This is a repository demo wrapper, not a product CLI feature.
*/
import { spawn } from 'node:child_process'
const SURFACES = new Map([
// The browser surface with the cordis toolset layered on: `dsh web --config`
// applies this overlay over the shipped web composition; it owns port 3081.
['web', ['--import', 'tsx', 'apps/cli/src/bin.ts', 'web', '--patch', 'examples/web-cordis/cordis.yml']],
['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', 'examples/acp-agent/cordis-tools.cordis.yml']],
])
const surface = process.argv[2] ?? 'web'
const args = SURFACES.get(surface)
if (args === undefined || process.argv.length > 3) {
console.error('usage: pnpm run demo:cordis [web|acp]')
process.exit(2)
}
if (surface === 'web') console.log('Cordis Web: http://127.0.0.1:3081')
const child = spawn(process.execPath, args, { stdio: 'inherit' })
child.on('exit', (code, signal) => { process.exit(signal === null ? code ?? 1 : 1) })
+39 -1
View File
@@ -3,7 +3,45 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it } from 'vitest'
import type { TsdownBundle } from 'tsdown'
import { discoverLibraryDirs, discoverPluginDirs, watchClientPlugins } from './dev-web.ts'
import { writeClientBuildRecord } from './client-build-environment.ts'
import {
devWebBuildEnvironment,
discoverLibraryDirs,
discoverPluginDirs,
watchClientPlugins,
} from './dev-web.ts'
it('samples one local environment at startup without validating watcher outputs', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-dev-web-environment-'))
try {
await mkdir(join(root, 'apps/web/dist'), { recursive: true })
await mkdir(join(root, 'packages/client/example/lib'), { recursive: true })
await writeFile(join(root, 'package.json'), JSON.stringify({ version: '1.2.3' }))
await writeFile(join(root, 'apps/web/dist/index.html'), '<main></main>')
await writeFile(join(root, 'packages/client/example/lib/client.js'), 'module.exports = {}\n')
writeClientBuildRecord(root, {
DSH_CLIENT_BUILD_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: 'fffffff',
DSH_CLIENT_TITLE: 'DeepSeek Harness',
DSH_CLIENT_VERSION: '1.2.2',
})
await writeFile(join(root, 'packages/client/example/lib/client.js'), 'module.exports = { changed: true }\n')
expect(devWebBuildEnvironment(root, {
PATH: '/bin',
DSH_BUILD_CLIENT_PROFILE: 'official',
DSH_CLIENT_COMMIT_HASH: 'abc1234',
DSH_CLIENT_EXTRA: 'launch-value',
})).toEqual({
PATH: '/bin',
DSH_CLIENT_COMMIT_HASH: 'abc1234',
DSH_CLIENT_EXTRA: 'launch-value',
DSH_CLIENT_VERSION: '1.2.3',
})
} finally {
await rm(root, { recursive: true, force: true })
}
})
it('discovers dsh.client packages with sibling roles', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-dev-web-discovery-'))
+28
View File
@@ -34,6 +34,11 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
import { execa } from 'execa'
import { build } from 'tsdown'
import type { TsdownBundle } from 'tsdown'
import {
CLIENT_BUILD_PROFILE_SELECTOR,
clientBuildProcessEnvironment,
repositoryClientBuildEnvironment,
} from './client-build-environment.ts'
const repoRoot = fileURLToPath(new URL('..', import.meta.url))
@@ -49,6 +54,19 @@ const SHELL_PACKAGE = '@deepseek-ai/dsh-web-frontend'
*/
const TEST_INFRASTRUCTURE_PREFIX = 'packages/test-support/'
/**
* Sample one local public environment for every long-lived watcher stage.
* @param root - repository root supplying version and Git metadata.
* @param environment - watcher launch environment supplying public extensions.
* @returns process environment shared by tsdown and spawned watcher stages.
*/
export function devWebBuildEnvironment(
root: string,
environment: NodeJS.ProcessEnv = process.env,
): NodeJS.ProcessEnv {
return clientBuildProcessEnvironment(environment, repositoryClientBuildEnvironment(root, environment))
}
/**
* Discover the watch workspace by declaration: every packages/<group>/<name>
* whose package.json carries `dsh.client` with platform "web" is a client
@@ -175,6 +193,16 @@ interface StageHandle {
const invokedPath = process.argv[1]
const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href
if (isMain) {
const buildEnvironment = devWebBuildEnvironment(repoRoot, process.env)
for (const name of Object.keys(process.env)) {
if (name === CLIENT_BUILD_PROFILE_SELECTOR || name.startsWith('DSH_CLIENT_')) {
Reflect.deleteProperty(process.env, name)
}
}
for (const [name, value] of Object.entries(buildEnvironment)) {
if (name.startsWith('DSH_CLIENT_') && value !== undefined) process.env[name] = value
}
const pluginDirs = discoverPluginDirs()
const libraryDirs = discoverLibraryDirs()
if (pluginDirs.length === 0) {
-1
View File
@@ -5,7 +5,6 @@
"docs/cordis-primer.md": 600,
"docs/defensive-patterns.md": 550,
"docs/testing.md": 1150,
"examples/AGENTS.md": 310,
"packages/AGENTS.md": 675,
"packages/README.md": 994
}
@@ -0,0 +1,17 @@
# Agent Note: Consistent examples
Status: implemented
English | [中文](agent-note.zh.md)
## Problem
Similar examples used different headings.
## Decision
Use the same headings for similar examples.
## Consequences
Examples are easier to compare.
@@ -0,0 +1,17 @@
# Agent Note: 一致的示例
Status: implemented
[English](agent-note.md) | 中文
## 问题
相似示例使用了不同的标题。
## 决策
相似示例使用相同的标题。
## 后果
示例更易于比较。
@@ -0,0 +1,22 @@
# Acme Agent
English | [中文](product.zh.md)
Acme Agent is an open-source agent harness that automates repository chores.
This paragraph contains neutral placeholder text for the example.
## Install
Install Node.js 24, then run:
```sh
npx acme-agent setup
```
The command prints the setup URL, which is `http://127.0.0.1:3080` by default.
## Community
- Report bugs in the issue tracker.
- Add the `acme-agent` topic to your plugin repository.
@@ -0,0 +1,22 @@
# Acme Agent
[English](product.md) | 中文
Acme Agent 是一款开源 agent harness(智能体框架),用于自动化仓库日常事务。
本段包含供示例使用的中性占位文本。
## 安装
安装 Node.js 24,然后运行:
```sh
npx acme-agent setup
```
该命令会打印设置地址,默认地址为 `http://127.0.0.1:3080`。
## 社区
- 在 Issue 跟踪器中报告 bug。
- 为你的插件仓库添加 `acme-agent` 主题。
@@ -0,0 +1,15 @@
# Pairing rules
English | [中文](rules.zh.md)
These rules govern the Chinese counterpart of every documentation pair.
## Priority levels
| Level | Meaning |
|---|---|
| MUST | The pairing gate rejects a non-conforming pair. |
| SHOULD | Deviate only with a stated reason. |
- Preserve every proposition of the source.
- Keep code spans verbatim.
@@ -0,0 +1,15 @@
# 配对规则
[English](rules.md) | 中文
这些规则约束每个文档配对的中文对侧。
## 优先级
| 级别 | 含义 |
|---|---|
| MUST | 配对门禁会拒绝不符合要求的配对。 |
| SHOULD | 仅在说明理由后偏离。 |
- 保留源文的每个命题。
- 代码片段原样保留。
+1 -1
View File
@@ -42,7 +42,7 @@ const MAX_DECL_CHARS = 1200
/**
* Line budget for ONE slot's expanded report. The whole point of narrowing to a
* single slot is to spend less context, so a report a model cannot finish
* reading is a defect rather than a detail. Today's widest slot renders 60
* reading is a defect rather than a detail. The widest measured slot renders 60
* lines, so this leaves room to document a slot properly while catching the two
* ways a report runs away: an owner share that hands down a subsystem instead of
* a share, and prose that grew into a manual.
+75 -7
View File
@@ -71,6 +71,7 @@ export const SERVICE_PAGE: Record<string, string> = {
authorization: 'credentials.md',
credentials: 'credentials.md',
directoryPicker: 'workspace.md',
deepseekLlmApiExtensions: 'llm-streaming.md',
dynamicCordisRunner: 'extensions.md',
e2b: 'subprocess.md',
fileReferences: 'session-reference.md',
@@ -91,6 +92,7 @@ export const SERVICE_PAGE: Record<string, string> = {
sessionReferenceResolver: 'session-reference.md',
sessionProjectionCache: 'session-projection.md',
sessionProjections: 'session-projection.md',
sessionController: 'session.md',
sessions: 'session.md',
settings: 'settings.md',
sessionTitle: 'session-title.md',
@@ -98,6 +100,7 @@ export const SERVICE_PAGE: Record<string, string> = {
spillStore: 'spill.md',
storage: 'storage.md',
storageDomain: 'storage.md',
subagentModelSelection: 'subagent.md',
subagents: 'subagent.md',
subprocess: 'subprocess.md',
systemPrompt: 'system-prompt.md',
@@ -112,7 +115,9 @@ export const SERVICE_PAGE: Record<string, string> = {
userQuestions: 'user-questions.md',
web: 'web.md',
workflowEngine: 'workflow.md',
webhookRuntime: 'webhook.md',
workspaceRegistry: 'workspace.md',
workspaceController: 'workspace.md',
}
/**
@@ -135,6 +140,7 @@ export const SERVICE_PAGE: Record<string, string> = {
*/
export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
agent: 'not a service: the DX accessor field on Agent.ctx (root accessor defaulting to undefined) — docs/subsystems/core.md owns the Agent handle',
appReady: 'not a service: launcher-provided successful-startup signal — packages/boot/cmdline/README.md owns the launcher contract',
appExit: 'not a service: launcher-provided bounded process-exit callback — packages/boot/cmdline/README.md owns the launcher contract',
cmdlineArgs: 'not a service: launcher-provided immutable app argument accessor — packages/boot/cmdline/README.md owns the launcher contract',
configuredAgentIdentities: 'not a service: launcher-provided boot-context value (ConfiguredAgentIdentities | undefined) — packages/core/agent-loop/README.md owns this launcher contract',
@@ -143,13 +149,14 @@ export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
launchEnvironment: 'not a service: launcher-provided root accessor value (LaunchEnvironmentSnapshot | undefined) — packages/util/launch-environment/README.md owns this launcher contract',
connection: 'interface-typed (HostConnectionHandle); implementing class HostConnectionService is declared in rpc-host.ts — packages/client/connection/README.md owns the API',
uiRenderer: 'client-side interface-typed browser service — packages/client/ui-renderer/README.md owns the API',
uiSession: 'client-side Session source adapter — packages/client/ui-session/README.md owns the API',
uiConversation: 'client-side Conversation registries and assembler — packages/client/ui-conversation/README.md owns the API',
uiWorkspace: 'client-side Workspace navigation adapter — packages/client/ui-workspace/README.md owns the API',
settingsSchema: 'client-side schema introspection service — packages/client/ui-settings/README.md owns the API',
settingsScope: 'client-side settings-namespace transport service — packages/client/ui-settings/README.md owns the API',
chatFileMentions: 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-conversation/README.md owns the API',
chatFileMentions: 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-chat/README.md owns the API',
commandUi: 'client-side interface-typed browser service — packages/client/ui-commands/README.md owns the API',
conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the API',
conversationEvents: 'client-side interface-typed registry — packages/client/runtime/README.md owns the API',
conversationViews: 'client-side interface-typed registry — packages/client/runtime/README.md owns the API',
layout: 'client-side interface-typed browser service — packages/client/ui-layout/README.md owns the API',
locale: 'client-side interface-typed browser service — packages/client/locale/README.md owns the API',
modelDirectories: 'client-side interface-typed browser service — packages/client/ui-model-selection/README.md owns the API',
@@ -158,9 +165,9 @@ export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
sessionLogDownload: 'client-side browser download controller — packages/session-query/session-log-export/README.md owns the API',
inputTriggers: 'client-side interface-typed browser service — packages/client/ui-input-trigger/README.md owns the API',
timer: 'client-side dynamic-package timer service — packages/extensions/cordis-client-runner/README.md owns the API',
slots: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the API',
slots: 'client-side interface-typed browser service — packages/client/ui-renderer/README.md owns the API',
theme: 'client-side interface-typed browser service — packages/client/ui-theme/README.md owns the API',
workspaces: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the API',
workspaces: 'client-side interface-typed browser service — packages/api/workspace-controller/README.md owns the API',
}
/**
@@ -174,6 +181,7 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
'agent': 'core.md',
'agent-loop': 'core.md',
'agent-preset': 'core.md',
'api-session': 'session.md',
'approval': 'approval.md',
'commands': 'commands.md',
'cordis': 'extensions.md',
@@ -190,6 +198,7 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
'system-prompt': 'system-prompt.md',
'session-telemetry': 'session-telemetry.md',
'tools': 'tools.md',
'user-questions': 'user-questions.md',
'webserver': 'web-server.md',
'workflow': 'workflow.md',
}
@@ -206,13 +215,13 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
*/
export const EVENT_WALK_EXEMPTIONS: Record<string, string> = {
'command/executed': 'client-face local command acknowledgment — packages/client/ui-commands/README.md owns the API',
'connection/reset': 'client-face transport signal — packages/client/runtime/README.md owns the API',
'connection/reset': 'client-face transport signal — packages/api/session-controller/README.md owns the API',
'locale/change': 'client-face locale switch signal — packages/client/locale/README.md owns the API',
'slash/input-begin-command': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
'slash/input-consume-token': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
'slash/input-insert-reference': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
'slash/input-insert-text': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
'slots/changed': 'client-face slot invalidation signal — packages/client/runtime/README.md owns the API',
'slots/changed': 'client-face slot invalidation signal — packages/client/ui-renderer/README.md owns the API',
'theme/change': 'client-face theme switch signal — packages/client/ui-theme/README.md owns the API',
}
@@ -240,11 +249,15 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
SettleReason: 'core.md',
AdapterRegistrationHandle: 'llm-streaming.md',
DirectoryRegistrationHandle: 'llm-streaming.md',
DeepSeekLlmApiExtensionMap: 'llm-streaming.md',
DeepSeekLlmApiExtensionProvider: 'llm-streaming.md',
DeepSeekLlmApiExtensionRequest: 'llm-streaming.md',
LlmCallConfig: 'llm-streaming.md',
LlmModelContext: 'llm-streaming.md',
LlmModelReasoningInfo: 'llm-streaming.md',
LlmResolvedModelInfo: 'llm-streaming.md',
LlmFailure: 'llm-streaming.md',
LlmImageRequestPricing: 'llm-streaming.md',
LlmModelInfo: 'llm-streaming.md',
LlmProviderInfo: 'llm-streaming.md',
LlmConfigurableProvider: 'llm-streaming.md',
@@ -273,6 +286,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
MessageFeedbackVersion: 'feedback.md',
MessageFeedbackVersionConflict: 'feedback.md',
UserMessage: 'session.md',
ApiSessionAgentResult: 'session.md',
PreStepDecision: 'core.md',
PreStepContext: 'core.md',
RequestErrorAction: 'core.md',
@@ -282,16 +296,48 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
SessionReferenceCandidate: 'session-reference.md',
SessionReferenceMentionCandidate: 'session-reference.md',
SessionReferenceInput: 'session-reference.md',
SessionAttachmentRequest: 'session.md',
SessionAttachmentValue: 'session.md',
SessionCancelRequest: 'session.md',
SessionCancelValue: 'session.md',
SessionControlFrame: 'session.md',
SessionCreateRequest: 'session.md',
SessionCreateValue: 'session.md',
SessionEvent: 'session.md',
SessionFollowFrame: 'session.md',
SessionFollowRequest: 'session.md',
SessionForkRequest: 'session.md',
SessionForkValue: 'session.md',
SessionId: 'core.md',
SessionListRequest: 'session.md',
SessionListValue: 'session.md',
SessionModels: 'session.md',
SessionModelsRequest: 'session.md',
SessionPage: 'session.md',
SessionPageRequest: 'session.md',
SessionPromptRequest: 'session.md',
SessionPromptValue: 'session.md',
SessionRenameRequest: 'session.md',
SessionRenameValue: 'session.md',
SessionRespondReceipt: 'session.md',
SessionRespondRequest: 'session.md',
SessionSearchValue: 'session.md',
SessionSelectModelRequest: 'session.md',
SessionSelectModelValue: 'session.md',
SessionSummary: 'session.md',
SessionUpdateQueueRequest: 'session.md',
SessionUpdateQueueValue: 'session.md',
SessionStartSource: 'core.md',
SessionLogSnapshot: 'session-query.md',
SessionSurfaceSnapshot: 'session-query.md',
ApprovalOutcome: 'approval.md',
ApprovalPolicy: 'approval.md',
ApprovalRequest: 'approval.md',
ApprovalRequestEvent: 'approval.md',
ApprovalService: 'approval.md',
AskUserQuestionRequestEvent: 'user-questions.md',
EncodedImageAttachment: 'attachment.md',
ImageAttachmentAccess: 'llm-streaming.md',
ImageAttachmentRef: 'attachment.md',
ImageRequestPolicy: 'attachment.md',
RequestImageAttachment: 'attachment.md',
@@ -343,6 +389,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
LspQueryResult: 'lsp.md',
LlmAdapter: 'llm-streaming.md',
PreparedLlmCall: 'llm-streaming.md',
PreparedDeepSeekLlmApiExtensions: 'llm-streaming.md',
LlmRuntime: 'llm-streaming.md',
StreamChunk: 'llm-streaming.md',
SkillProviderControl: 'skills.md',
@@ -350,6 +397,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
PrepareSessionOptions: 'persistence.md',
SessionHeader: 'persistence.md',
SessionInspection: 'persistence.md',
BorrowedSessionSource: 'persistence.md',
SessionLocation: 'persistence.md',
SessionPreparation: 'persistence.md',
SessionPersistenceSnapshot: 'persistence.md',
@@ -389,6 +437,8 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
SessionEventTraceRequest: 'session-query.md',
SessionEventWindow: 'session-query.md',
SessionLineageTrace: 'session-query.md',
SessionObservation: 'session-query.md',
SessionObservationOptions: 'session-query.md',
SessionRecord: 'session-query.md',
SessionResultFilter: 'session-query.md',
SessionSearchExecContext: 'session-query.md',
@@ -500,6 +550,8 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
WebSearchRequest: 'web.md',
WebSearchResult: 'web.md',
WorkflowRun: 'workflow.md',
VerifiedWebhookDelivery: 'webhook.md',
WebhookRule: 'webhook.md',
PresetOption: 'permission-presets.md',
PresetSpec: 'permission-presets.md',
InvariantInstaller: 'invariants.md',
@@ -512,7 +564,20 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
DomainChanged: 'storage.md',
DomainFacility: 'storage.md',
Workspace: 'workspace.md',
WorkspaceArchiveSessionRequest: 'workspace.md',
WorkspaceArchiveValue: 'workspace.md',
WorkspaceCreateRequest: 'workspace.md',
WorkspaceCreateValue: 'workspace.md',
WorkspaceDeleteRequest: 'workspace.md',
WorkspaceDeleteValue: 'workspace.md',
WorkspaceFollowFrame: 'workspace.md',
WorkspaceId: 'workspace.md',
WorkspaceInsertBeforeRequest: 'workspace.md',
WorkspaceInsertSessionBeforeRequest: 'workspace.md',
WorkspaceOrderValue: 'workspace.md',
WorkspaceRenameRequest: 'workspace.md',
WorkspaceValue: 'workspace.md',
ClientArtifactBaseline: 'client-modules.md',
WebBootGraph: 'client-modules.md',
SessionTelemetryRecord: 'session-telemetry.md',
WorkflowRunInfo: 'workflow.md',
@@ -525,6 +590,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ProjectionCheckpoint: 'session-projection.md',
DirectoryPickerCapability: 'workspace.md',
TypertContribution: 'invariants.md',
TypertRemoteEventSource: 'typert.md',
TypertFace: 'invariants.md',
TypertPackageFilter: 'invariants.md',
TypertPackageRecord: 'invariants.md',
@@ -538,7 +604,9 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
'AsyncIterable',
'Context',
'Error',
'EntryTree',
'Exclude',
'Extract',
'Map',
'NonNullable',
'Omit',
+42 -11
View File
@@ -1,6 +1,6 @@
/** Generate model-visible Host/Client Service and Event inspect catalogs. */
import { mkdirSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { projectCordisCatalog } from '@deepseek-ai/dsh-typert-generator'
import type { CordisCatalogModel, ServiceMethodEntry } from '@deepseek-ai/dsh-typert-generator'
@@ -15,10 +15,11 @@ const CLIENT_SERVICES: Readonly<Record<string, readonly string[]>> = {
sessions: ['open', 'openSubagent', 'setSubagentCatalogOpen', 'refreshSubagents', 'search', 'fork', 'scope', 'binding'],
slots: ['register', 'inject'],
theme: ['getTheme', 'setTheme', 'register', 'overrideTokens'],
workspaces: [
'connectWorkspace', 'startSession', 'create', 'pickDirectory', 'listDirectory', 'createDirectory',
'openPath', 'rename', 'delete', 'insertSessionBefore', 'archiveSession',
uiWorkspace: [
'connectWorkspace', 'startSession', 'archiveSession', 'pickDirectory', 'listDirectory',
'createDirectory', 'openPath',
],
workspaces: ['create', 'rename', 'delete', 'insertSessionBefore', 'archiveSession'],
}
const CLIENT_EVENTS = new Set([
@@ -33,14 +34,34 @@ function methodName(method: ServiceMethodEntry): string | undefined {
}
function clientModel(model: CordisCatalogModel): CordisCatalogModel {
const services = Object.entries(CLIENT_SERVICES).map(([key, allowed]) => {
const matches = model.services.filter(service => service.key === key)
if (matches.length !== 1) {
throw new Error(`gen-cordis-inspect-catalog: expected one Client Service ${JSON.stringify(key)}, found ${matches.length}`)
}
const service = matches[0] as (typeof matches)[number]
const names = new Set(allowed)
const methods = service.methods.filter(method => names.has(methodName(method) ?? ''))
const found = new Set(methods.map(method => methodName(method)))
const missing = allowed.filter(name => !found.has(name))
if (missing.length > 0) {
throw new Error(
`gen-cordis-inspect-catalog: Client Service ${JSON.stringify(key)}`
+ ` is missing allowlisted method(s): ${missing.join(', ')}`,
)
}
return { ...service, methods }
})
const events = [...CLIENT_EVENTS].map((name) => {
const matches = model.events.filter(event => event.name === name)
if (matches.length !== 1) {
throw new Error(`gen-cordis-inspect-catalog: expected one Client Event ${JSON.stringify(name)}, found ${matches.length}`)
}
return matches[0] as (typeof matches)[number]
})
return {
services: model.services.flatMap((service) => {
const allowed = CLIENT_SERVICES[service.key]
if (allowed === undefined) return []
const names = new Set(allowed)
return [{ ...service, methods: service.methods.filter(method => names.has(methodName(method) ?? '')) }]
}),
events: model.events.filter(event => CLIENT_EVENTS.has(event.name)),
services,
events,
}
}
@@ -48,7 +69,17 @@ function main(): void {
const { projector, model } = projectCordisCatalog(root, CORDIS_CATALOG_POLICY, 'client')
const destination = resolve(root, CLIENT_OUT)
const source = projector.renderRuntimeApi(clientModel(model))
.replace('Generated by scripts/gen-cordis-api.ts', 'Generated by scripts/gen-cordis-inspect-catalog.ts')
.replaceAll('pnpm run gen-cordis-api', 'pnpm run gen-cordis-inspect-catalog')
.replaceAll('pnpm run verify-cordis-api', 'pnpm run verify-cordis-inspect-catalog')
.replaceAll('@deepseek-ai/dsh-tool-cordis/api-catalog', '@deepseek-ai/dsh-cordis-client-runner/client/api-catalog')
if (process.argv.includes('--check')) {
if (!existsSync(destination) || readFileSync(destination, 'utf8') !== source) {
throw new Error(`gen-cordis-inspect-catalog: ${CLIENT_OUT} is stale; run pnpm run gen-cordis-inspect-catalog`)
}
console.log(`gen-cordis-inspect-catalog: ${CLIENT_OUT} is up to date`)
return
}
mkdirSync(dirname(destination), { recursive: true })
writeFileSync(destination, source)
console.log(`gen-cordis-inspect-catalog: wrote ${CLIENT_OUT}`)
+44 -44
View File
@@ -80,6 +80,7 @@ const GROUP_ORDER = [
'tasks',
'workflow',
'web',
'webhook',
'spill',
'todo',
'plan',
@@ -115,6 +116,15 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['agent-loop', 'compaction-basic'],
note: 'Adapters register provider implementations; the loop and compaction call the provider-neutral stream service.',
},
{
key: 'deepseekLlmApiExtensions',
pkg: 'deepseek-llm-api-extensions',
title: 'Official DeepSeek request extensions',
mode: 'seam',
implementations: ['session-log-deepseek', 'plugin-package-inventory-deepseek'],
consumers: ['llm-deepseek'],
note: 'Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance.',
},
{
key: 'tokenMeter',
pkg: 'token-meter',
@@ -139,6 +149,21 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants', 'message-feedback'],
note: 'Owns append-only Session instances and emits the durable session event feed.',
},
{
key: 'sessionController',
pkg: 'api-session-controller',
title: 'Host Session Remote controller',
mode: 'core',
consumers: ['apiproxy'],
note: 'Owns Session commands, cold reads, durable-event following, live control state, and Agent activation policy; apiProxy reuses its inspection and Agent-resolution operations for Session-aware domains.',
},
{
key: 'workspaceController',
pkg: 'api-workspace-controller',
title: 'Host Workspace Remote controller',
mode: 'core',
note: 'Owns Workspace commands and reconnect-safe Workspace state delivery through the generated Remote namespace.',
},
{
key: 'invariants',
pkg: 'invariants',
@@ -180,6 +205,14 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['llm-deepseek', 'llm-pi-ai', 'apiproxy'],
note: 'Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer.',
},
{
key: 'subagentModelSelection',
pkg: 'tool-subagent',
title: 'Subagent model-selection preference',
mode: 'core',
consumers: ['tool-subagent'],
note: 'Owns the default-off settings namespace that Agent-scoped delegation tools sample when composing a new top-level Session.',
},
{
key: 'credentials',
pkg: 'credentials',
@@ -556,6 +589,14 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['tool-workflow', 'tool-ralph'],
note: 'One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents.',
},
{
key: 'webhookRuntime',
pkg: 'webhook',
title: 'Webhook rule runtime',
mode: 'core',
consumers: ['webhook-github'],
note: 'Provider adapters dispatch authenticated deliveries; trusted plugins register independent process-local rules, and the runtime turns non-null results into ordinary Workspace-backed Sessions without delivery or completion state.',
},
{
key: 'lsp',
pkg: 'lsp',
@@ -730,47 +771,15 @@ const APP_EXAMPLES = [
title: 'DSH Base Composition',
label: 'packages/bundle/base/cordis.patch.yml',
config: 'packages/bundle/base/cordis.patch.yml',
summary: 'The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app, dsh-headless) and the user\'s profile layer patch over it.',
},
{
id: 'headless',
rel: 'examples/headless-agent/composition.md',
title: 'Headless Agent Snapshot Composition',
label: 'examples/headless-agent',
config: 'examples/headless-agent/cordis.yml',
summary: 'The headless snapshot composition combines the real DeepSeek adapter and coding capabilities with one explicitly configured persisted top-level agent; its JSONL driver is test-only.',
},
{
id: 'acp',
rel: 'examples/acp-agent/composition.md',
title: 'ACP Automation App Composition',
label: 'examples/acp-agent',
config: 'examples/acp-agent/cordis.yml',
summary: 'The ACP demo exposes fresh baseline-prompt agent sessions to programmatic clients over JSON-RPC stdio, with no stdout logger, human UI, or pre-created agent.',
summary: 'The dsh-base bundle patch shared by the web, headless, sdk, and acp profiles; their mode bundles and user layers patch over it, while sdk-minimal owns a separate standalone tree.',
},
]
type AppExample = typeof APP_EXAMPLES[number]
function renderAppExpansion(lines: string[], appNode: string, pluginName: string): void {
const agentCore = nodeId('bundle', 'agent_core')
const jsonl = nodeId('bundle', 'jsonl')
lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`)
lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`)
if (pluginName === '@deepseek-ai/dsh-acp-demo') {
lines.push(` ${appNode} --> ${nodeId('entrypoint', 'acp')}["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]`)
}
lines.push(
` ${agentCore} --> ${nodeId('spine', 'llm')}["ctx.llm"]`,
` ${agentCore} --> ${nodeId('spine', 'sessions')}["ctx.sessions"]`,
` ${agentCore} --> ${nodeId('spine', 'tools')}["ctx.tools + tool-bash"]`,
` ${agentCore} --> ${nodeId('spine', 'loop')}["ctx.agents + ctx.agentLoop"]`,
)
}
function renderAppComposition(example: AppExample): string {
const plugins = parseExampleCordis(example.config)
const maintenance = 'hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source'
const maintenance = 'hybrid: the patch row list is parsed from its `cordis.yml`; app package expansion is curated from package source'
const lines = generatedHeader(example.title)
lines.push(
example.summary,
@@ -783,9 +792,6 @@ function renderAppComposition(example: AppExample): string {
const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
lines.push(` ${pluginNode}["${escLabel(plugin.id)}<br/>${escLabel(plugin.name)}"]`)
lines.push(` cfg --> ${pluginNode}`)
if (plugin.name === '@deepseek-ai/dsh-acp-demo') {
renderAppExpansion(lines, pluginNode, plugin.name)
}
}
lines.push(
'```',
@@ -1326,7 +1332,7 @@ function renderLifecycle(): string {
'',
'`dsh-compaction-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
'',
'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.',
'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages and `startsRequestSeries` unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.',
'',
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination API for queue/status, prompt interception, request construction, steering, continuation, and errors.',
'',
@@ -1416,9 +1422,6 @@ function renderIndex(docs: GraphDoc[]): string {
const labels: Record<string, string> = {
'docs/capability-seams.md': 'capability seams and core services',
'apps/cli/composition.md': 'dsh shared base composition',
'examples/headless-agent/composition.md': 'headless-agent app composition',
'examples/cordis-agent/composition.md': 'cordis-agent app composition',
'examples/acp-agent/composition.md': 'acp-agent app composition',
'docs/event-producer-consumer.md': 'event producer/consumer matrix',
'docs/agent-lifecycle.md': 'agent turn and step lifecycle',
'docs/tool-execution-pipeline.md': 'tool execution pipeline',
@@ -1426,9 +1429,6 @@ function renderIndex(docs: GraphDoc[]): string {
const modes: Record<string, string> = {
'docs/capability-seams.md': 'hybrid generated',
'apps/cli/composition.md': 'hybrid generated',
'examples/headless-agent/composition.md': 'hybrid generated',
'examples/cordis-agent/composition.md': 'hybrid generated',
'examples/acp-agent/composition.md': 'hybrid generated',
'docs/event-producer-consumer.md': 'hybrid generated',
'docs/agent-lifecycle.md': 'curated',
'docs/tool-execution-pipeline.md': 'curated',
+21 -21
View File
@@ -36,27 +36,27 @@ const EVENT_ENVELOPE_TYPE_NAMES = [
type EventEnvelopeTypeName = typeof EVENT_ENVELOPE_TYPE_NAMES[number]
/** Primary subsystems page for linked payload types. */
/** Documentation target, relative to `docs/`, for linked payload types. */
const LINK_MAP: Record<string, string> = {
CallId: 'core.md',
ContentBlock: 'core.md',
MessageSource: 'core.md',
ScheduleChange: 'schedule.md',
StreamChunk: 'llm-streaming.md',
TokenUsage: 'llm-streaming.md',
TodoItem: 'session.md',
TurnTrigger: 'session.md',
TurnEndReason: 'session.md',
SessionTitleEventData: 'session-title.md',
SessionTitleLlmRequestEventData: 'session-title.md',
SessionTitleModelProvenance: 'session-title.md',
SessionTitleProviderId: 'session-title.md',
SessionTitleSource: 'session-title.md',
TeamId: 'agent-team.md',
TeamMemberSnapshot: 'agent-team.md',
TeamMessageId: 'agent-team.md',
TeamMessageSnapshot: 'agent-team.md',
TeamTaskSnapshot: 'agent-team.md',
CallId: 'subsystems/core.md',
ContentBlock: 'subsystems/core.md',
MessageSource: 'subsystems/core.md',
ScheduleChange: 'subsystems/schedule.md',
StreamChunk: 'subsystems/llm-streaming.md',
TokenUsage: 'subsystems/llm-streaming.md',
TodoItem: 'subsystems/todo.md',
TurnTrigger: 'subsystems/session.md',
TurnEndReason: 'subsystems/session.md',
SessionTitleEventData: 'subsystems/session-title.md',
SessionTitleLlmRequestEventData: 'subsystems/session-title.md',
SessionTitleModelProvenance: 'subsystems/session-title.md',
SessionTitleProviderId: 'subsystems/session-title.md',
SessionTitleSource: 'subsystems/session-title.md',
TeamId: 'subsystems/agent-team.md',
TeamMemberSnapshot: 'subsystems/agent-team.md',
TeamMessageId: 'subsystems/agent-team.md',
TeamMessageSnapshot: 'subsystems/agent-team.md',
TeamTaskSnapshot: 'subsystems/agent-team.md',
}
/** One log event, extracted from a `SessionEventMap` declaration. */
@@ -341,7 +341,7 @@ function typeLinks(payload: string): string {
if (new RegExp(`\\b${name}\\b`).test(payload)) seen.add(name)
}
if (seen.size === 0) return ''
const links = [...seen].sort().map(n => `[${n}](subsystems/${LINK_MAP[n]})`)
const links = [...seen].sort().map(n => `[${n}](${LINK_MAP[n]})`)
return `Types: ${links.join(' · ')}`
}
-1
View File
@@ -339,7 +339,6 @@ describe('manifestPatterns', () => {
'tools/*/package.json',
'native/landlock-run/package.json',
'native/landlock-run/packages/*/package.json',
'examples/*/package.json',
])
})
})
+2 -6
View File
@@ -24,8 +24,8 @@ const ALL_KINDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'p
/**
* Workspace areas that never reach a user: repository tooling and gates (the
* root manifest), test infrastructure, the documentation site, the runnable
* demo leaves, and the native launcher's build workspace. A runtime
* root manifest), test infrastructure, the documentation site, and the native
* launcher's build workspace. A runtime
* declaration by anything outside these areas is a disclosure-relevant
* runtime dependency because any plugin package can be mounted from a user's
* `cordis.yml`.
@@ -35,7 +35,6 @@ const DEV_ONLY_AREAS = [
'packages/test-support/',
'packages/test-support/client-runtime/',
'website/',
'examples/',
'native/',
] as const
@@ -136,9 +135,6 @@ export function manifestPatterns(rootMembers: readonly string[]): string[] {
return [
'package.json',
...rootMembers.map(member => `${member}/package.json`),
// The demo leaves join the workspace through `examples/package.json`, so
// their own manifests are members of nothing and no glob above reaches them.
'examples/*/package.json',
]
}
+13 -8
View File
@@ -9,6 +9,7 @@
import { globSync, readFileSync, writeFileSync } from 'node:fs'
import { basename, resolve } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -104,7 +105,7 @@ const OUT = 'docs/tool-catalog.md'
function registerCatalogSubagentProvider(ctx: Context, name: string): void {
const provider: SubagentProvider = {
name,
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true },
capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true },
inheritsParentContext: false,
start: () => Promise.reject(new Error('tool-catalog provider cannot start a child')),
// Declared so consumers configured for continuable background mode mount.
@@ -455,17 +456,21 @@ const TOOL_PACKAGES: ToolPackage[] = [
{
pkg: '@deepseek-ai/dsh-tool-subagent',
dir: 'tool-subagent',
source: 'packages/subagent/tool-subagent/src/index.ts',
requires: ['ctx.tools', 'ctx.subagents', 'ctx.systemPrompt'],
source: {
list_subagent_models: 'packages/subagent/tool-subagent/src/list-models.ts',
subagent: 'packages/subagent/tool-subagent/src/index.ts',
},
requires: ['ctx.tools', 'ctx.subagents', 'ctx.systemPrompt', 'ctx.llm for model discovery and selected-route validation'],
writes: ['tool/call', 'tool/result', 'child session events through the chosen provider'],
shippedNames: ['subagent', 'subagent_fork'],
async mount(ctx) {
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LlmRuntime)
registerCatalogSubagentProvider(ctx, 'mock')
await ctx.plugin(ToolSubagent, { provider: 'mock' })
await ctx.plugin(ToolSubagent, { provider: 'mock', enableModelSelection: true })
},
note:
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance\'s description, `run_in_background` parameter, and system-prompt policy follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable` and defaults omitted calls to background with automatic settlement delivery, while `subagent_fork` stays `one-shot` and defaults them to foreground — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.',
'The registered delegation name is the load-time `toolName` config (default `subagent`); the schema above shows static model selection enabled for reference. Model selection defaults off. Web presets sample the default-off Models preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Explicit compositions may instead use static `enableModelSelection`. Each instance independently controls model selection, discovery ownership, and background behavior through `enableModelSelection`, `modelSelectionSettings`, `backgroundMode`, and `enableRunInBackground`.',
},
{
pkg: '@deepseek-ai/dsh-tool-subagent-control',
@@ -651,9 +656,9 @@ export function assertManifestComplete(packages: ToolPackage[] = TOOL_PACKAGES,
* A tool package that boots without registering anything is a broken boot, not
* an empty catalog section. The usual cause is an `inject` the entry's `mount`
* does not satisfy: cordis leaves the plugin PENDING, every step here still
* succeeds, and the generator writes a catalog missing that package's tools —
* with the freshness gate green on it, because the omission is now what the
* generator produces. {@link assertManifestComplete} cannot see this: the
* succeeds, and the generator writes a catalog missing that package's tools.
* The freshness gate stays green because regeneration reproduces the omission.
* {@link assertManifestComplete} cannot see this: the
* package IS listed, it just contributed nothing.
* @param entry - the manifest entry that was booted.
* @param harvested - how many schemas its boot registered.
+1 -1
View File
@@ -56,7 +56,7 @@ describe('Oxlint executable contract', () => {
// A test under packages/client states its face in the filename, so the
// probe carries the Client suffix to reach the Client aggregate.
['client package test', 'packages/client/ui-trajectory/tests', 'tsconfig.client.json', '.client.ts'],
['example', 'examples/headless-agent/tests', 'tsconfig.host.json'],
['CLI profile test', 'apps/cli/tests/profiles/headless/tests', 'tsconfig.host.json'],
['website', 'website', 'tsconfig.host.json'],
] as const
const source = `export function probePromise(): Promise<void> {
+51
View File
@@ -0,0 +1,51 @@
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 { collectPackageGraph } from './package-graph.ts'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function fixture(packages: Readonly<Record<string, readonly string[]>>): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-graph-'))
roots.push(root)
for (const [name, dependencies] of Object.entries(packages)) {
const directory = join(root, 'packages', 'client', name)
mkdirSync(directory, { recursive: true })
writeFileSync(join(directory, 'package.json'), `${JSON.stringify({
name: `@deepseek-ai/dsh-${name}`,
peerDependencies: Object.fromEntries(dependencies.map(dependency => [
`@deepseek-ai/dsh-${dependency}`,
'workspace:^',
])),
}, null, 2)}\n`)
}
return root
}
describe('collectPackageGraph', () => {
it('orders packages after their dependencies', () => {
const root = fixture({ application: ['feature'], feature: ['foundation'], foundation: [] })
expect(collectPackageGraph(root, ['client'], 'fixture').map(pkg => pkg.short))
.toEqual(['foundation', 'feature', 'application'])
})
it('keeps a dependency cycle together and before its consumers', () => {
const root = fixture({ consumer: ['left'], left: ['right'], right: ['left'], foundation: [] })
expect(collectPackageGraph(root, ['client'], 'fixture').map(pkg => pkg.short))
.toEqual(['foundation', 'left', 'right', 'consumer'])
})
it('rejects a missing in-repo peer', () => {
const root = fixture({ consumer: ['missing'] })
expect(() => collectPackageGraph(root, ['client'], 'fixture'))
.toThrow('fixture: @deepseek-ai/dsh-consumer references missing in-repo peer @deepseek-ai/dsh-missing')
})
})
+80 -5
View File
@@ -25,11 +25,12 @@ export interface PackageGraphNode {
}
/**
* Read every harness package manifest and return dependency-safe graph nodes.
* Read every harness package manifest and return dependency-first graph nodes.
* @param root - absolute repository root.
* @param groupOrder - caller-specific tiebreak order for packages in the same dependency layer.
* @param gate - command name used in structural error messages.
* @returns package nodes ordered after all of their in-repo dependencies.
* @returns package nodes ordered after their in-repo dependencies, except for
* stable back edges inside a dependency cycle.
*/
export function collectPackageGraph(root: string, groupOrder: readonly string[], gate: string): PackageGraphNode[] {
const packages: PackageGraphNode[] = []
@@ -57,14 +58,28 @@ export function collectPackageGraph(root: string, groupOrder: readonly string[],
}
function topoSort(packages: PackageGraphNode[], groupOrder: readonly string[], gate: string): PackageGraphNode[] {
const remaining = new Map(packages.map(pkg => [pkg.short, pkg]))
const byName = new Map(packages.map(pkg => [pkg.short, pkg]))
for (const pkg of packages) {
for (const dependency of pkg.deps) {
if (!byName.has(dependency)) {
throw new Error(`${gate}: ${pkg.name} references missing in-repo peer ${SCOPE}${dependency}`)
}
}
}
const remaining = new Map(byName)
const placed = new Set<string>()
const out: PackageGraphNode[] = []
while (remaining.size > 0) {
const ready = [...remaining.values()]
let ready = [...remaining.values()]
.filter(pkg => pkg.deps.every(dep => placed.has(dep)))
.sort((a, b) => comparePackages(a, b, groupOrder))
if (ready.length === 0) throw new Error(`${gate}: dependency cycle among ${[...remaining.keys()].join(', ')}`)
if (ready.length === 0) {
const cycle = sinkCycles(remaining)
.map(component => component.sort((a, b) => comparePackages(a, b, groupOrder)))
.sort((a, b) => comparePackages(a[0], b[0], groupOrder))[0]
if (cycle === undefined) throw new Error(`${gate}: could not order package dependency graph`)
ready = cycle
}
for (const pkg of ready) {
out.push(pkg)
placed.add(pkg.short)
@@ -74,6 +89,66 @@ function topoSort(packages: PackageGraphNode[], groupOrder: readonly string[], g
return out
}
type PackageGraphComponent = [PackageGraphNode, ...PackageGraphNode[]]
function sinkCycles(remaining: ReadonlyMap<string, PackageGraphNode>): PackageGraphComponent[] {
let nextIndex = 0
const indices = new Map<string, number>()
const lowLinks = new Map<string, number>()
const stack: PackageGraphNode[] = []
const stacked = new Set<string>()
const components: PackageGraphComponent[] = []
const visit = (pkg: PackageGraphNode): void => {
const index = nextIndex
nextIndex += 1
indices.set(pkg.short, index)
lowLinks.set(pkg.short, index)
stack.push(pkg)
stacked.add(pkg.short)
for (const dependency of pkg.deps) {
const target = remaining.get(dependency)
if (target === undefined) continue
if (!indices.has(target.short)) {
visit(target)
lowLinks.set(pkg.short, Math.min(requiredValue(lowLinks, pkg.short), requiredValue(lowLinks, target.short)))
} else if (stacked.has(target.short)) {
lowLinks.set(pkg.short, Math.min(requiredValue(lowLinks, pkg.short), requiredValue(indices, target.short)))
}
}
if (lowLinks.get(pkg.short) !== indices.get(pkg.short)) return
const first = stack.pop()
if (first === undefined) throw new Error('package graph traversal lost its active component')
stacked.delete(first.short)
const component: PackageGraphComponent = [first]
let member = first
while (member !== pkg) {
const next = stack.pop()
if (next === undefined) throw new Error('package graph traversal lost its active component')
stacked.delete(next.short)
component.push(next)
member = next
}
components.push(component)
}
for (const pkg of remaining.values()) {
if (!indices.has(pkg.short)) visit(pkg)
}
return components.filter((component) => {
const names = new Set(component.map(pkg => pkg.short))
const first = component[0]
const cyclic = component.length > 1 || first.deps.includes(first.short)
return cyclic && component.every(pkg => pkg.deps.every(dep => !remaining.has(dep) || names.has(dep)))
})
}
function requiredValue<K, V>(values: ReadonlyMap<K, V>, key: K): V {
const value = values.get(key)
if (value === undefined) throw new Error('package graph traversal lost an indexed node')
return value
}
function comparePackages(a: PackageGraphNode, b: PackageGraphNode, groupOrder: readonly string[]): number {
const groupA = groupOrder.indexOf(a.group)
const groupB = groupOrder.indexOf(b.group)
+1 -1
View File
@@ -377,7 +377,7 @@ describe('docsPages locale routes', () => {
const translated = rootPages.filter(page => page.contentLocale === 'zh-CN')
const fallbacks = rootPages.filter(page => page.contentLocale === 'en-US')
expect(translated).toHaveLength(43)
expect(translated).toHaveLength(46)
expect(translated.every(page => page.source.endsWith('.zh.md'))).toBe(true)
expect(fallbacks).toEqual([])
})
+5 -2
View File
@@ -29,6 +29,7 @@ function write(path: string, content: string): void {
function buildFixture(environment: Record<string, string>): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-release-build-'))
roots.push(root)
write(join(root, 'package.json'), `${JSON.stringify({ version: environment.DSH_CLIENT_VERSION ?? '0.0.1' })}\n`)
write(join(root, 'apps/web/dist/index.html'), '<main></main>')
write(join(root, 'packages/client/example/lib/client.js'), 'module.exports = {}\n')
writeClientBuildRecord(root, environment)
@@ -106,11 +107,13 @@ describe('release families', () => {
vi.stubEnv('DSH_CLIENT_COMMIT_HASH', officialEnvironment.DSH_CLIENT_COMMIT_HASH)
const official = buildFixture(officialEnvironment)
const defaultBuild = buildFixture({})
const missing = join(defaultBuild, 'missing')
write(join(missing, 'package.json'), `${JSON.stringify({ version: officialEnvironment.DSH_CLIENT_VERSION })}\n`)
expect(() => { dsh.verifyBuildArtifacts(official) }).not.toThrow()
expect(() => { dsh.verifyBuildArtifacts(defaultBuild) }).toThrow(/DSH_CLIENT_TITLE/)
expect(() => { dsh.verifyBuildArtifacts(join(defaultBuild, 'missing')) }).toThrow(/record.*missing/)
expect(() => { vendor.verifyBuildArtifacts(join(defaultBuild, 'missing')) }).not.toThrow()
expect(() => { dsh.verifyBuildArtifacts(missing) }).toThrow(/record.*missing/)
expect(() => { vendor.verifyBuildArtifacts(missing) }).not.toThrow()
write(join(official, 'packages/client/example/lib/client.js'), 'module.exports = { changed: true }\n')
expect(() => { dsh.verifyBuildArtifacts(official) }).toThrow(/artifacts differ/)
+7 -44
View File
@@ -77,17 +77,16 @@ interface GenericSkip {
}
const GENERIC_SKIPS: readonly GenericSkip[] = [
// `vendorPackages` lists vendor/ directory names, joined with 'vendor' below it.
{ file: 'packages/examples/acp-demo/tests/built-bin.e2e.ts', upstream: ['cordis', 'cosmokit', 'schemastery'] },
// `Symbol.for('schemastery')` and the `vendor:` metadata field are upstream identifiers.
{ file: 'vendor/schemastery/src/index.ts', upstream: ['schemastery'] },
// Asserts the vendored-manifest table, which gains an upstream-name column.
{ file: 'scripts/gen-third-party-notices.spec.ts', upstream: RENAMES.map(rename => rename.upstream) },
// `cordis` is also an agent-preset id — the directory name under
// apps/cli/config/agent-presets/ — so in these files the bare name is
// packages/preset/agent-presets/presets/ — so in these files the bare name is
// product data, not a package reference. Renaming it changed which preset
// the creator flow stages and which id the roster reports.
{ file: 'packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx', upstream: ['cordis'] },
{ file: 'packages/preset/agent-presets/tests/shipped-root.spec.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/src/client/index.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/apply.client.spec.ts', upstream: ['cordis'] },
{ file: 'packages/client/ui-agent-preset/tests/locales.client.spec.ts', upstream: ['cordis'] },
@@ -98,7 +97,7 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [
// The preset's own composition: its header comment and its system prompt name
// the preset a model mounts, so the scoped name would send the model after an
// id no roster reports.
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', upstream: ['cordis'] },
{ file: 'packages/preset/agent-presets/presets/cordis/agent.cordis.yml', upstream: ['cordis'] },
// The preset-roster loop names the `cordis` preset id, not a package.
{ file: 'apps/cli/tests/windows-shell.spec.ts', upstream: ['cordis'] },
// GROUP_ORDER holds `packages/<group>/` directory names, not package names.
@@ -159,9 +158,8 @@ const POSTCONDITIONS: readonly PostCondition[] = [
// The preset ids in this table are product data, not package names.
{ file: 'packages/client/ui-agent-preset/tests/locales.client.spec.ts', text: '[\'cordis\', \'presetCordisName\'', count: 1 },
// The preset id the shipped composition documents to its own model.
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'The `cordis` agent preset', count: 1 },
{ file: 'apps/cli/config/agent-presets/cordis/agent.cordis.yml', text: 'corrupting the `cordis` preset', count: 1 },
{ file: 'packages/examples/acp-demo/tests/built-bin.e2e.ts', text: '\'cordis\', \'loader\', \'include\', \'timer\', \'hmr\', \'logger-console\',', count: 1 },
{ file: 'packages/preset/agent-presets/presets/cordis/agent.cordis.yml', text: 'The `cordis` agent preset', count: 1 },
{ file: 'packages/preset/agent-presets/presets/cordis/agent.cordis.yml', text: 'corrupting the `cordis` preset', count: 1 },
]
/**
@@ -196,23 +194,6 @@ const EXACT_EDITS: readonly ExactEdit[] = [
errors.push(\`\${label}: @deepseek-ai/cordis peer (\${peer}) and dev (\${dev}) ranges must match\`)`,
expect: 1,
},
{
// The rescoped name is already covered by the `@deepseek-ai/.+` pattern beside it.
id: 'knip-logger-console',
file: 'knip.json',
find: ` "ignoreDependencies": [
"@cordisjs/plugin-logger-console",
"@deepseek-ai/.+"
]
},
"packages/util/home": {`,
replace: ` "ignoreDependencies": [
"@deepseek-ai/.+"
]
},
"packages/util/home": {`,
expect: 1,
},
{
id: 'knip-bundle-base',
file: 'knip.json',
@@ -348,7 +329,7 @@ const VENDORED_LIBRARY = /^@deepseek-ai\\/(cosmokit|schemastery)(\\/|$)/
id: 'vendoring-cookbook-name-invariant-zh',
file: 'docs/cookbook/adding-a-vendored-package.zh.md',
find: '保留上游的 `name`/`version`/`exports`/`type`',
replace: '改写 `name` 的 scope([映射](../rescope.md)),保留上游的 `version`/`exports`/`type`',
replace: '改写 `name` 的 scope([映射](../rescope.zh.md)),保留上游的 `version`/`exports`/`type`',
expect: 1,
},
{
@@ -433,24 +414,6 @@ const VENDORED_LIBRARY = /^@deepseek-ai\\/(cosmokit|schemastery)(\\/|$)/
replace: 'parseVendoredRows(\'| `cordis/` | `@deepseek-ai/cordis` | cordis | 4.0.0 | https://example.com | `abc123` |\\n\')',
expect: 1,
},
{
// The framework peer is no longer a registry name, so the rehearsal must install this
// repository's vendored copies; cosmokit comes along as cordis's own dependency.
id: 'packed-install-vendored-peer',
file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',
find: ` 'packages/runtime-diagnostics/invariants',
]`,
replace: ` 'packages/runtime-diagnostics/invariants',
// The framework and the vendored packages the closure declares outright:
// rescoped into @deepseek-ai, so the consumer installs this repository's
// copies. Schemastery is a hard dependency of three members above, not a
// peer, so npm resolves it while installing them.
'vendor/cordis',
'vendor/cosmokit',
'vendor/schemastery',
]`,
expect: 1,
},
{
id: 'packed-install-registry-spec',
file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',
@@ -552,7 +515,7 @@ function rewriteLine(line: string, file: string, all: readonly Pattern[]): strin
* Markdown splits in two. Every fence is code a reader copies or a
* configuration they mount, so every fence follows the rename regardless of its
* info string. Prose follows it only under `docs/`, where a sentence quoting
* `` `cordis` `` teaches a name this repository no longer resolves; elsewhere
* `` `cordis` `` teaches an unresolved package name; elsewhere
* prose is a record of what was true when it was written, and the same spelling
* can mean something else entirely — the Python SDK's `cordis` option, or the
* unvendored `@cordisjs/plugin-http`.
+34 -6
View File
@@ -84,13 +84,19 @@ describe('gate graph validation', () => {
expect(ids).toContain('public-repository-links')
})
it('keeps package-group subsystem ownership in the documentation gate', () => {
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
expect(ids).toContain('subsystem-pages')
})
it('keeps the hygiene aggregate aligned with the package script checks', () => {
const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
expect(ids).toEqual([
'rescope-vendor', 'knip', 'publint', 'constraints', 'dsh-package-licenses',
'package-invariants', 'built-package-invariants', 'node-next-types',
'optional-dependency-imports', 'client-packages', 'cordis-config',
'rescope-vendor', 'knip', 'publint', 'constraints', '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',
])
expect(defaultConcurrency('hygiene', ids.length, 8)).toEqual({
@@ -104,7 +110,7 @@ describe('gate graph validation', () => {
expect(ids.slice(0, 10)).toEqual([
'doc-typecheck', 'docs-site-build', 'doc-graphs', 'markdown-links', 'type-equivalence',
'cordis-catalog', 'mermaid', 'scoped-events', 'translation-pairing', 'markdown-wrap',
'cordis-catalog', 'cordis-inspect-catalog', 'mermaid', 'scoped-events', 'translation-pairing',
])
})
@@ -136,7 +142,25 @@ describe('gate graph validation', () => {
},
)
it('keeps native Windows coverage blocking while retaining the observational inventory', () => {
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
'keeps hard-coded Client UI copy enforcement in %s',
(mode) => {
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
expect(ids).toContain('client-ui-i18n')
},
)
it.each(['ci-primary', 'ci-static', 'check-all', 'hygiene'] as const)(
'keeps application entrypoint enforcement in %s',
(mode) => {
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
expect(ids).toContain('application-entrypoints')
},
)
it('keeps native Windows coverage blocking and behind the complete build', () => {
const complete = withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))
const observational = withPnpmEntrypoint(() => gatesForMode('ci-windows-observational'))
.filter(gate => gate.id !== 'build' && gate.id !== 'docs-site-build')
@@ -144,6 +168,7 @@ describe('gate graph validation', () => {
expect(byId.get('coverage')?.allowFailure).not.toBe(true)
expect(byId.get('coverage-exempt-heavy')?.allowFailure).not.toBe(true)
expect(byId.get('coverage')?.needs).toContain('build')
expect(byId.get('coverage-exempt-heavy')?.needs).toContain('build')
expect(observational).not.toHaveLength(0)
for (const gate of observational) {
@@ -363,7 +388,7 @@ describe('Node 24 lane ownership', () => {
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
expect(defaultConcurrency('ci-consumers', subject.length, 4)).toEqual({
workers: 10,
workers: 11,
source: 'ci-consumers gate count',
})
expect(subject.map(item => item.id)).toEqual([
@@ -373,6 +398,7 @@ describe('Node 24 lane ownership', () => {
'built-package-invariants',
'lint-and-duplication',
'snapshot',
'expected-output',
'web-snapshot',
'doc-typecheck',
'node-next-types',
@@ -389,6 +415,7 @@ describe('Node 24 lane ownership', () => {
expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['built-package-invariants'])
for (const id of [
'snapshot',
'expected-output',
'web-snapshot',
'doc-typecheck',
'node-next-types',
@@ -397,6 +424,7 @@ describe('Node 24 lane ownership', () => {
expect(subject.find(item => item.id === id)?.needs).toEqual(['built-package-invariants'])
}
expect(subject.find(item => item.id === 'snapshot')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
expect(subject.find(item => item.id === 'expected-output')?.env).toEqual({ DSH_EXAMPLE_MODE: 'lib' })
expect(subject.find(item => item.id === 'doc-typecheck')?.env).toEqual({
DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1',
})
+24 -8
View File
@@ -241,6 +241,7 @@ export function gatesForMode(selected: Mode): Gate[] {
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
pnpmScript('duplication', 'duplication'),
snapshotGate(),
expectedOutputGate(),
pnpmScript('build', 'build'),
pnpmScript('build:web', 'build:web'),
...hygieneLeafGates({ artifactNeeds: ['build'] }),
@@ -266,6 +267,7 @@ export function gatesForMode(selected: Mode): Gate[] {
function ciSharedStaticGates(): Gate[] {
return [
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('application-entrypoints', 'verify-application-entrypoints', { label: 'application entrypoints' }),
pnpmScript('constraints', 'constraints'),
pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
@@ -274,6 +276,7 @@ function ciSharedStaticGates(): Gate[] {
label: 'optional dependency imports',
}),
pnpmScript('client-packages', 'verify-client-packages', { label: 'client packages' }),
pnpmScript('client-ui-i18n', 'verify-client-ui-i18n', { label: 'client UI i18n' }),
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
]
}
@@ -425,6 +428,7 @@ function ciConsumerGates(): Gate[] {
needs: validatedBuild,
}),
snapshotGate(validatedBuild),
expectedOutputGate(validatedBuild),
webSnapshotGate(validatedBuild),
pnpmScript('doc-typecheck', 'doc-typecheck:contracts-ready', {
needs: validatedBuild,
@@ -469,9 +473,10 @@ function ciWindowsBlockingGates(): Gate[] {
}
function ciWindowsCompleteGates(): Gate[] {
const coverage = coverageGates().map(gate => gate.id === 'coverage-exempt-heavy'
? { ...gate, needs: [...new Set(['build', ...(gate.needs ?? [])])] }
: gate)
const coverage = coverageGates().map(gate => ({
...gate,
needs: [...new Set(['build', ...(gate.needs ?? [])])],
}))
const coverageAfter = coverage.map(gate => gate.id)
const observational = ciWindowsObservationalGates()
// The required production site replaces the observational MPA build; both
@@ -583,9 +588,8 @@ function coverageGates(): Gate[] {
]
}
// Example and package snapshots boot their bins in `lib` mode (built artifacts under plain Node,
// plugins via real exports); script snapshots execute their real source entry path.
// Callers wait either on `build` or on a validation gate that transitively owns that build.
// Recorded-session adapters boot process scenarios in `lib` mode. Callers wait
// either on `build` or on a validation gate that transitively owns that build.
function snapshotGate(needs: string[] = ['build']): Gate {
return pnpmScript('snapshot', 'test:snapshot', {
env: { DSH_EXAMPLE_MODE: 'lib' },
@@ -593,6 +597,15 @@ function snapshotGate(needs: string[] = ['build']): Gate {
})
}
// Owner-local process expectations consume built package exports without entering
// the recorded-session corpus or the credentialed provider lane.
function expectedOutputGate(needs: string[] = ['build']): Gate {
return pnpmScript('expected-output', 'test:expected', {
env: { DSH_EXAMPLE_MODE: 'lib' },
needs,
})
}
function builtPackageInvariantsGate(needs?: string[]): Gate {
return pnpmScript('built-package-invariants', 'verify-built-package-invariants', {
label: 'built package invariants',
@@ -624,6 +637,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
pnpmScript('knip', 'knip'),
pnpmScript('publint', 'publint', artifactOptions),
pnpmScript('constraints', 'constraints'),
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' }),
builtPackageInvariantsGate(options.artifactNeeds),
@@ -635,6 +649,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
label: 'optional dependency imports',
}),
pnpmScript('client-packages', 'verify-client-packages', { label: 'client packages' }),
pnpmScript('client-ui-i18n', 'verify-client-ui-i18n', { label: 'client UI i18n' }),
]
}
@@ -658,6 +673,7 @@ function docSyncLeafGates(options: {
pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }),
pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }),
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),
pnpmScript('cordis-inspect-catalog', 'verify-cordis-inspect-catalog', { label: 'Cordis inspect catalog' }),
pnpmScript('mermaid', 'verify-mermaid'),
pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }),
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
@@ -669,6 +685,7 @@ function docSyncLeafGates(options: {
pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }),
pnpmScript('public-repository-links', 'verify-public-repository-links', { label: 'public repository links' }),
pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }),
pnpmScript('subsystem-pages', 'verify-subsystem-pages', { label: 'subsystem pages' }),
pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }),
pnpmScript('config-source-ownership', 'verify-config-source-ownership', { label: 'config source ownership' }),
pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience' }),
@@ -691,9 +708,8 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
'run',
'--config',
'vitest.e2e.config.ts',
'examples/headless-agent/tests/keyless-smoke.e2e.ts',
'apps/cli/tests/profiles/headless/tests/keyless-smoke.e2e.ts',
'apps/cli/tests/built-bin.e2e.ts',
'packages/examples/acp-demo/tests/built-bin.e2e.ts',
'packages/host/directory-picker-native/tests/built-worker.e2e.ts',
'packages/sdk/server/tests/built-scope-carrier.e2e.ts',
'packages/subprocess/subprocess-local/tests/spawn-runner-built.e2e.ts',
+1 -1
View File
@@ -18,7 +18,7 @@ describe('Oxlint invocation', () => {
it('uses location-preserving diagnostics in CI', () => {
expect(resolveOxlintInvocation(['.'], { CI: 'true', DSH_OXLINT_THREADS: '4' })).toEqual({
args: ['.', '--format=unix', '--threads=4'],
args: ['.', '--format=default', '--threads=4'],
env: { CI: 'true', DSH_OXLINT_THREADS: '4', GOMAXPROCS: '4' },
})
})
+1 -1
View File
@@ -32,7 +32,7 @@ export interface OxlintInvocation {
*/
export function resolveOxlintInvocation(args: readonly string[], env: NodeJS.ProcessEnv): OxlintInvocation {
const resolvedArgs = [...args]
if (env.CI === 'true' && !hasOutputFormat(args)) resolvedArgs.push('--format=unix')
if (env.CI === 'true' && !hasOutputFormat(args)) resolvedArgs.push('--format=default')
const raw = env.DSH_OXLINT_THREADS
if (raw === undefined || raw === '') return { args: resolvedArgs, env: { ...env } }
const parsed = Number.parseInt(raw, 10)
@@ -1,17 +0,0 @@
/** Repository-wide canonical-layout check for committed session snapshots. */
import { resolve } from 'node:path'
import { expect, it } from 'vitest'
import { inspectSessionFixtureLayouts } from './session-fixture-layout.ts'
const root = resolve(import.meta.dirname, '..')
it('keeps every session-format JSONL fixture projected into canonical packed layout', () => {
const nonCanonical = inspectSessionFixtureLayouts(root)
.filter(fixture => fixture.source !== fixture.canonical)
.map(fixture => fixture.path)
expect(
nonCanonical,
'Run `pnpm run migrate:packed-session-fixtures` and commit the mechanical fixture rewrite.',
).toEqual([])
})
+29 -1
View File
@@ -1,9 +1,15 @@
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { type SessionEvent } from '@deepseek-ai/dsh-session'
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import { canonicalSessionFixture } from './session-fixture-layout.ts'
import {
canonicalSessionFixture,
inspectSessionFixtureLayouts,
isPhysicalSessionFixture,
} from './session-fixture-layout.ts'
const HEADER = ' {"type":"session","version":0,"id":"fixture","createdAt":1,"delegationDepth":0} '
const root = resolve(import.meta.dirname, '..')
function chunkRun(): SessionEvent[] {
return Array.from({ length: 4 }, (_, index) => ({
@@ -68,3 +74,25 @@ describe('canonicalSessionFixture', () => {
.toThrow(/broken\.jsonl: session snapshot line 2: malformed text-chunks storage row/)
})
})
describe('isPhysicalSessionFixture', () => {
it('excludes only persisted logs under the WebWorker example root', () => {
expect(isPhysicalSessionFixture(
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/main/session.jsonl',
)).toBe(true)
expect(isPhysicalSessionFixture(
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/README.jsonl',
)).toBe(false)
expect(isPhysicalSessionFixture('apps/web/tests/snapshots/example/session.jsonl')).toBe(false)
})
})
it('keeps every session-format JSONL fixture projected into canonical packed layout', () => {
const nonCanonical = inspectSessionFixtureLayouts(root)
.filter(fixture => fixture.source !== fixture.canonical)
.map(fixture => fixture.path)
expect(
nonCanonical,
'Run `pnpm run migrate:packed-session-fixtures` and commit the mechanical fixture rewrite.',
).toEqual([])
})
+15
View File
@@ -7,6 +7,10 @@ import { resolve } from 'node:path'
import { packChunkRuns, type SessionEvent } from '@deepseek-ai/dsh-session'
import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
/** Physical persistence artifacts validated by the WebWorker runtime fixture spec. */
const PHYSICAL_SESSION_FIXTURE_ROOT =
'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/'
/** One repository session fixture and its canonical projected representation. */
export interface SessionFixtureLayout {
/** Repository-relative path with `/` separators. */
@@ -17,6 +21,16 @@ export interface SessionFixtureLayout {
canonical: string
}
/**
* Whether a repository JSONL is a production-layout persistence artifact rather
* than an envelope-free replay snapshot owned by this script.
* @param path - Repository-relative path with `/` separators.
* @returns True only for Session logs under the WebWorker VFS example root.
*/
export function isPhysicalSessionFixture(path: string): boolean {
return path.startsWith(PHYSICAL_SESSION_FIXTURE_ROOT) && path.endsWith('/session.jsonl')
}
function isSessionHeader(value: unknown): boolean {
return value !== null && typeof value === 'object' && (value as { type?: unknown }).type === 'session'
}
@@ -109,6 +123,7 @@ function discoverJsonlFiles(root: string): string[] {
*/
export function inspectSessionFixtureLayouts(root: string): SessionFixtureLayout[] {
return discoverJsonlFiles(root).flatMap((path) => {
if (isPhysicalSessionFixture(path)) return []
const source = readFileSync(resolve(root, path), 'utf8')
const canonical = canonicalSessionFixture(source, path)
return canonical === undefined ? [] : [{ path, source, canonical }]
+180
View File
@@ -0,0 +1,180 @@
/** Repository-wide ownership and storage invariants for the recorded-session corpus. */
import { existsSync } from 'node:fs'
import { lstat, readFile, readdir, realpath } from 'node:fs/promises'
import { dirname, join, relative, resolve } from 'node:path'
import { expect, it } from 'vitest'
import {
captureExpectedWorkspaceSnapshot,
EMPTY_WORKSPACE_MARKER,
parseSnapshotManifest,
redactSessionSnapshotIds,
scrubSystemPrompts,
scrubToolSchemas,
sessionFixtureNames,
type SnapshotManifest,
} from '@deepseek-ai/dsh-session-snapshot'
const repoRoot = resolve(import.meta.dirname, '..')
const corpusRoot = join(repoRoot, 'snapshots')
const profiles = ['acp', 'sdk', 'session', 'web'] as const
const snapshotAdapters = [
'apps/web/tests/message-feedback-protocol.snapshot.ts',
'apps/web/tests/minimal-preset.snapshot.ts',
'snapshots/acp/acp.snapshot.ts',
'snapshots/sdk/sdk.snapshot.ts',
'snapshots/session/headless.snapshot.ts',
] as const
interface Scenario {
readonly key: string
readonly profile: string
readonly name: string
readonly dir: string
readonly manifest: SnapshotManifest & {
composition: string
recording: 'live' | 'authored'
header: NonNullable<SnapshotManifest['header']>
}
}
async function scenarios(): Promise<Scenario[]> {
const result: Scenario[] = []
for (const profile of profiles) {
const root = join(corpusRoot, profile)
for (const entry of await readdir(root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const dir = join(root, entry.name)
const path = join(dir, 'snapshot.yml')
expect(existsSync(path), `${profile}/${entry.name}/snapshot.yml`).toBe(true)
const manifest = parseSnapshotManifest(await readFile(path, 'utf8'), path)
expect(manifest.scenario, `${profile}/${entry.name}: scenario`).toBe(entry.name)
expect(manifest.profile, `${profile}/${entry.name}: profile`).toBe(profile === 'session' ? 'headless' : profile)
expect(manifest.composition, `${profile}/${entry.name}: composition`).toBeTypeOf('string')
expect(manifest.recording, `${profile}/${entry.name}: recording`).toMatch(/^(live|authored)$/)
expect(manifest.header, `${profile}/${entry.name}: header`).toBeDefined()
result.push({
key: `${profile}/${entry.name}`,
profile,
name: entry.name,
dir,
manifest: {
...manifest,
composition: manifest.composition as string,
recording: manifest.recording as 'live' | 'authored',
header: manifest.header as NonNullable<SnapshotManifest['header']>,
},
})
}
}
return result
}
function referencedScenario(owner: Scenario, source: string): string {
return source.includes('/') ? source : `${owner.profile}/${source}`
}
async function snapshotNamedTests(): Promise<string[]> {
const files: string[] = []
const visit = async (directory: string, relativeDir: string): Promise<void> => {
for (const entry of await readdir(directory, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (['dist', 'lib', 'node_modules'].includes(entry.name)) continue
await visit(join(directory, entry.name), join(relativeDir, entry.name))
} else if (entry.isFile() && /\.snapshot\.tsx?$/u.test(entry.name)) {
files.push(join(relativeDir, entry.name).split(/[/\\]/u).join('/'))
}
}
}
for (const root of ['apps', 'native', 'packages', 'python', 'scripts', 'snapshots', 'website']) {
await visit(join(repoRoot, root), root)
}
return files.sort()
}
it('reserves the snapshot test suffix for recorded-session adapters', async () => {
expect(await snapshotNamedTests()).toEqual([...snapshotAdapters])
})
it('keeps every recorded session owned, pinned, redacted, and header-scrubbed', async () => {
const all = await scenarios()
const byKey = new Map(all.map(scenario => [scenario.key, scenario]))
const pinByClass = new Map<string, Scenario>()
for (const scenario of all) {
if (scenario.manifest.header.pin !== true) continue
const key = `${scenario.manifest.composition}/${scenario.manifest.header.class}`
expect(pinByClass.has(key), `${key}: duplicate header pin`).toBe(false)
pinByClass.set(key, scenario)
}
for (const scenario of all) {
const { manifest, dir, key } = scenario
const classKey = `${manifest.composition}/${manifest.header.class}`
expect(pinByClass.has(classKey), `${key}: missing composition/header pin ${classKey}`).toBe(true)
const localSession = join(dir, 'session.jsonl')
if (manifest.session === undefined) {
expect(existsSync(localSession), `${key}: owner session.jsonl`).toBe(true)
} else {
expect(existsSync(localSession), `${key}: borrower must not own session.jsonl`).toBe(false)
const target = resolve(dir, manifest.session.source)
expect(existsSync(target), `${key}: session source`).toBe(true)
const targetDir = await realpath(dirname(target))
const sourceKey = relative(corpusRoot, targetDir).split(/[/\\]/).join('/')
expect(byKey.has(sourceKey), `${key}: session source must name a corpus owner`).toBe(true)
expect(byKey.get(sourceKey)?.manifest.session, `${key}: session source cannot chain through a borrower`).toBeUndefined()
}
expect(existsSync(join(dir, 'replay.override.json')), `${key}: replay override presence`)
.toBe(manifest.replay?.override === true)
expect(existsSync(join(dir, 'workspace.expected')), `${key}: final workspace presence`)
.toBe(manifest.workspace?.final === true)
if (manifest.workspace?.final === true) {
const expectedRoot = join(dir, 'workspace.expected')
const expectedWorkspace = await captureExpectedWorkspaceSnapshot(expectedRoot)
expect(existsSync(join(expectedRoot, EMPTY_WORKSPACE_MARKER)), `${key}: empty workspace marker`)
.toBe(expectedWorkspace.length === 0)
}
expect(existsSync(join(dir, 'input.json')), `${key}: executable input metadata is ACP-only`)
.toBe(scenario.profile === 'acp')
if (scenario.profile !== 'acp') {
expect(existsSync(join(dir, 'stdout.expected.jsonl')), `${key}: ACP transcript outside ACP`).toBe(false)
}
if (manifest.header.pin === true) {
const promptSource = byKey.get(referencedScenario(scenario, manifest.header.systemPromptSource ?? scenario.name))
const schemaSource = byKey.get(referencedScenario(scenario, manifest.header.toolSchemasSource ?? scenario.name))
expect(promptSource, `${key}: system-prompt source`).toBeDefined()
expect(schemaSource, `${key}: tool-schema source`).toBeDefined()
expect(existsSync(join((promptSource as Scenario).dir, 'system-prompt.expected.md')), `${key}: system-prompt sidecar`).toBe(true)
expect(existsSync(join((schemaSource as Scenario).dir, 'tool-schemas.expected.json')), `${key}: tool-schema sidecar`).toBe(true)
for (const [field, source] of [
['system-prompt.expected.md', promptSource],
['tool-schemas.expected.json', schemaSource],
] as const) {
const local = join(dir, field)
if (!existsSync(local) || !(await lstat(local)).isSymbolicLink()) continue
expect(await realpath(local), `${key}: ${field} symlink follows its manifest source`)
.toBe(await realpath(join((source as Scenario).dir, field)))
}
}
if (manifest.session !== undefined) continue
const names = sessionFixtureNames(await readdir(dir))
const fixtures = await Promise.all(names.map(name => readFile(join(dir, name), 'utf8')))
expect(redactSessionSnapshotIds(fixtures), `${key}: typed identity fixed point`).toEqual(fixtures)
for (const [index, fixture] of fixtures.entries()) {
expect(scrubSystemPrompts(fixture), `${key}/${names[index]}: system prompt must be a sidecar`).toBe(fixture)
expect(scrubToolSchemas(fixture), `${key}/${names[index]}: tool schemas must be a sidecar`).toBe(fixture)
}
for (const index of manifest.header.childSystemPrompts ?? []) {
expect(names[index], `${key}: child prompt index ${index}`).toBeDefined()
expect(existsSync(join(dir, `system-prompt.${index}.expected.md`)), `${key}: child prompt sidecar ${index}`).toBe(true)
}
for (const index of manifest.header.childToolSchemas ?? []) {
expect(names[index], `${key}: child schema index ${index}`).toBeDefined()
expect(existsSync(join(dir, `tool-schemas.${index}.expected.json`)), `${key}: child schema sidecar ${index}`).toBe(true)
}
}
})
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,19 +1,23 @@
{"type":"session","version":0,"id":"{{child-1}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","origin":"subagent","delegationDepth":1}
{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}}
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","data":{"turn":1}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Check direct child"}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","throughSeq":12}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -1,19 +1,23 @@
{"type":"session","version":0,"id":"{{child-2}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","origin":"subagent","delegationDepth":1}
{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}}
{"type":"approval/policy","data":{"policy":"never","source":"delegation"}}
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","data":{"turn":1}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"spawn"}}
{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn"}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","throughSeq":12}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -1,85 +1,96 @@
{"type":"session","version":0,"id":"{{parent}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
{"type":"approval/policy","data":{"policy":"never"}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","data":{"turn":1}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[7],"source":{"kind":"fallback"}}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":11}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-define","name":"cordis_define","argumentsDelta":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"step/start","data":{"turn":1,"step":2}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":22}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-run","name":"cordis_run","argumentsDelta":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[24,25,26,27,28],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[24],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[30],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":2}}
{"type":"step/start","data":{"turn":1,"step":3}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"change"}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":34}}
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[36,37,38,39,40],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}
{"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}}
{"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}}
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[42],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":3}}
{"type":"step/start","data":{"turn":1,"step":4}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":47}}
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[41,42,43,44,45],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[47],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":4}}
{"type":"step/start","data":{"turn":1,"step":5}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":58}}
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[51,52,53,54,55],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[60,61,62,63,64],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}
{"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}}
{"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}}
{"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}}
{"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}}
{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[57],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[66],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":5}}
{"type":"step/start","data":{"turn":1,"step":6}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":73}}
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-undefine","name":"cordis_undefine","argumentsDelta":"{\"pluginId\": \"snap-1\"}"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[65,66,67,68,69],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[75,76,77,78,79],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}
{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[71],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[81],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":6}}
{"type":"step/start","data":{"turn":1,"step":7}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change"}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":85}}
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[76,77,78,79,80],"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[87,88,89,90,91],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":7}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -24,7 +24,7 @@
"type": "function",
"function": {
"name": "str_replace_editor",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"parameters": {
"type": "object",
"properties": {
@@ -43,27 +43,62 @@
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
},
"file_text": {
"type": "string",
"description": "Required parameter of `create` command, with the content of the file to be created."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"insert_line": {
"type": "integer",
"description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`."
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"new_str": {
"type": "string",
"description": "Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
},
"old_str": {
"type": "string",
"description": "Required parameter of `str_replace` command containing the string in `path` to replace."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"view_range": {
"type": "array",
"description": "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.",
"items": {
"type": "integer"
}
"oneOf": [
{
"type": "array",
"items": {
"type": "integer"
}
},
{
"type": "null"
}
],
"description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
}
},
"required": [
@@ -81,7 +116,7 @@
},
{
"role": "user",
"text": "Exercise the packaged minimal agent's persistent Bash and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
"text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
}
]
},
@@ -110,7 +145,7 @@
"type": "function",
"function": {
"name": "str_replace_editor",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"parameters": {
"type": "object",
"properties": {
@@ -129,27 +164,62 @@
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
},
"file_text": {
"type": "string",
"description": "Required parameter of `create` command, with the content of the file to be created."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"insert_line": {
"type": "integer",
"description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`."
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"new_str": {
"type": "string",
"description": "Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
},
"old_str": {
"type": "string",
"description": "Required parameter of `str_replace` command containing the string in `path` to replace."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"view_range": {
"type": "array",
"description": "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.",
"items": {
"type": "integer"
}
"oneOf": [
{
"type": "array",
"items": {
"type": "integer"
}
},
{
"type": "null"
}
],
"description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
}
},
"required": [
@@ -167,7 +237,7 @@
},
{
"role": "user",
"text": "Exercise the packaged minimal agent's persistent Bash and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
"text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
},
{
"role": "assistant",
@@ -210,7 +280,7 @@
"type": "function",
"function": {
"name": "str_replace_editor",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"parameters": {
"type": "object",
"properties": {
@@ -229,27 +299,62 @@
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
},
"file_text": {
"type": "string",
"description": "Required parameter of `create` command, with the content of the file to be created."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"insert_line": {
"type": "integer",
"description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`."
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"new_str": {
"type": "string",
"description": "Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
},
"old_str": {
"type": "string",
"description": "Required parameter of `str_replace` command containing the string in `path` to replace."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"view_range": {
"type": "array",
"description": "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.",
"items": {
"type": "integer"
}
"oneOf": [
{
"type": "array",
"items": {
"type": "integer"
}
},
{
"type": "null"
}
],
"description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
}
},
"required": [
@@ -267,7 +372,7 @@
},
{
"role": "user",
"text": "Exercise the packaged minimal agent's persistent Bash and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
"text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
},
{
"role": "assistant",
@@ -324,7 +429,7 @@
"type": "function",
"function": {
"name": "str_replace_editor",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"parameters": {
"type": "object",
"properties": {
@@ -343,27 +448,62 @@
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
},
"file_text": {
"type": "string",
"description": "Required parameter of `create` command, with the content of the file to be created."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"insert_line": {
"type": "integer",
"description": "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`."
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"new_str": {
"type": "string",
"description": "Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
},
"old_str": {
"type": "string",
"description": "Required parameter of `str_replace` command containing the string in `path` to replace."
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"view_range": {
"type": "array",
"description": "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.",
"items": {
"type": "integer"
}
"oneOf": [
{
"type": "array",
"items": {
"type": "integer"
}
},
{
"type": "null"
}
],
"description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
}
},
"required": [
@@ -381,7 +521,7 @@
},
{
"role": "user",
"text": "Exercise the packaged minimal agent's persistent Bash and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
"text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}/created.txt"
},
{
"role": "assistant",
@@ -0,0 +1,570 @@
[
{
"tools": [
{
"type": "function",
"function": {
"name": "pwsh",
"description": "Run commands in a PowerShell shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* State is persistent across command calls and discussions with the user.\n* Use native Windows paths (C:\\...) and $env:NAME variables; this is PowerShell, not bash.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The PowerShell command to run. Relative path is preferred in the command."
}
},
"required": [
"command"
]
}
}
},
{
"type": "function",
"function": {
"name": "str_replace_editor",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
"enum": [
"view",
"create",
"str_replace",
"insert"
]
},
"path": {
"type": "string",
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
},
"file_text": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"insert_line": {
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"new_str": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
},
"old_str": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"view_range": {
"oneOf": [
{
"type": "array",
"items": {
"type": "integer"
}
},
{
"type": "null"
}
],
"description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
}
},
"required": [
"command",
"path"
]
}
}
}
],
"messages": [
{
"role": "system",
"text": "You are a helpful software engineer assistant."
},
{
"role": "user",
"text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt"
}
]
},
{
"tools": [
{
"type": "function",
"function": {
"name": "pwsh",
"description": "Run commands in a PowerShell shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* State is persistent across command calls and discussions with the user.\n* Use native Windows paths (C:\\...) and $env:NAME variables; this is PowerShell, not bash.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The PowerShell command to run. Relative path is preferred in the command."
}
},
"required": [
"command"
]
}
}
},
{
"type": "function",
"function": {
"name": "str_replace_editor",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
"enum": [
"view",
"create",
"str_replace",
"insert"
]
},
"path": {
"type": "string",
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
},
"file_text": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"insert_line": {
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"new_str": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
},
"old_str": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"view_range": {
"oneOf": [
{
"type": "array",
"items": {
"type": "integer"
}
},
{
"type": "null"
}
],
"description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
}
},
"required": [
"command",
"path"
]
}
}
}
],
"messages": [
{
"role": "system",
"text": "You are a helpful software engineer assistant."
},
{
"role": "user",
"text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt"
},
{
"role": "assistant",
"toolCalls": [
{
"id": "minimal-bash-1",
"name": "pwsh"
}
]
},
{
"role": "tool",
"toolCallId": "minimal-bash-1",
"text": "{{tool-result}}"
}
]
},
{
"tools": [
{
"type": "function",
"function": {
"name": "pwsh",
"description": "Run commands in a PowerShell shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* State is persistent across command calls and discussions with the user.\n* Use native Windows paths (C:\\...) and $env:NAME variables; this is PowerShell, not bash.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The PowerShell command to run. Relative path is preferred in the command."
}
},
"required": [
"command"
]
}
}
},
{
"type": "function",
"function": {
"name": "str_replace_editor",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
"enum": [
"view",
"create",
"str_replace",
"insert"
]
},
"path": {
"type": "string",
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
},
"file_text": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"insert_line": {
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"new_str": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
},
"old_str": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"view_range": {
"oneOf": [
{
"type": "array",
"items": {
"type": "integer"
}
},
{
"type": "null"
}
],
"description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
}
},
"required": [
"command",
"path"
]
}
}
}
],
"messages": [
{
"role": "system",
"text": "You are a helpful software engineer assistant."
},
{
"role": "user",
"text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt"
},
{
"role": "assistant",
"toolCalls": [
{
"id": "minimal-bash-1",
"name": "pwsh"
}
]
},
{
"role": "tool",
"toolCallId": "minimal-bash-1",
"text": "{{tool-result}}"
},
{
"role": "assistant",
"toolCalls": [
{
"id": "minimal-bash-2",
"name": "pwsh"
}
]
},
{
"role": "tool",
"toolCallId": "minimal-bash-2",
"text": "{{tool-result}}"
}
]
},
{
"tools": [
{
"type": "function",
"function": {
"name": "pwsh",
"description": "Run commands in a PowerShell shell\n* When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.\n* You don't have access to the internet via this tool.\n* State is persistent across command calls and discussions with the user.\n* Use native Windows paths (C:\\...) and $env:NAME variables; this is PowerShell, not bash.\n* Please avoid commands that may produce a very large amount of output.\n* Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The PowerShell command to run. Relative path is preferred in the command."
}
},
"required": [
"command"
]
}
}
},
{
"type": "function",
"function": {
"name": "str_replace_editor",
"description": "Custom editing tool for viewing, creating and editing files\n* State is persistent across command calls and discussions with the user\n* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep\n* The `create` command cannot be used if the specified `path` already exists as a file\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit `str_replace.new_str` rather than setting it to null when deleting a match\n\nNotes for using the `str_replace` command:\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!\n* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
"enum": [
"view",
"create",
"str_replace",
"insert"
]
},
"path": {
"type": "string",
"description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
},
"file_text": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"insert_line": {
"oneOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"description": "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"new_str": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
},
"old_str": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
},
"view_range": {
"oneOf": [
{
"type": "array",
"items": {
"type": "integer"
}
},
{
"type": "null"
}
],
"description": "Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
}
},
"required": [
"command",
"path"
]
}
}
}
],
"messages": [
{
"role": "system",
"text": "You are a helpful software engineer assistant."
},
{
"role": "user",
"text": "Exercise the packaged minimal agent's persistent shell and string-replacement editor.\nEditor path: {{cwd}}\\created.txt"
},
{
"role": "assistant",
"toolCalls": [
{
"id": "minimal-bash-1",
"name": "pwsh"
}
]
},
{
"role": "tool",
"toolCallId": "minimal-bash-1",
"text": "{{tool-result}}"
},
{
"role": "assistant",
"toolCalls": [
{
"id": "minimal-bash-2",
"name": "pwsh"
}
]
},
{
"role": "tool",
"toolCallId": "minimal-bash-2",
"text": "{{tool-result}}"
},
{
"role": "assistant",
"toolCalls": [
{
"id": "minimal-editor",
"name": "str_replace_editor"
}
]
},
{
"role": "tool",
"toolCallId": "minimal-editor",
"text": "{{tool-result}}"
}
]
}
]
@@ -0,0 +1,66 @@
[
{
"model": "smoke-model",
"messages": [
{
"role": "system",
"content": "{{system}}"
},
{
"role": "user",
"content": "Complete the first isolated Python SDK process turn."
},
{
"role": "user",
"content": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."
}
],
"toolNames": [
"cordis_define",
"cordis_inspect_list",
"cordis_inspect_query",
"cordis_inspect_self",
"cordis_run",
"cordis_stop",
"cordis_undefine",
"job_kill",
"job_list",
"job_output",
"run_code",
"subagent",
"workflow"
]
},
{
"model": "smoke-model",
"messages": [
{
"role": "system",
"content": "{{system}}"
},
{
"role": "user",
"content": "Complete the second isolated Python SDK process turn."
},
{
"role": "user",
"content": "Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."
}
],
"toolNames": [
"cordis_define",
"cordis_inspect_list",
"cordis_inspect_query",
"cordis_inspect_self",
"cordis_run",
"cordis_stop",
"cordis_undefine",
"job_kill",
"job_list",
"job_output",
"run_code",
"subagent",
"workflow"
]
}
]
@@ -0,0 +1,96 @@
[
{
"session_id": "{{session-1}}",
"final_response": "PROCESS_ONE_OK",
"finish_reason": "completed",
"eventTypes": [
"agent/inbox/spliced",
"turn/start",
"agent/inbox/spliced",
"step/start",
"user/message",
"user/message",
"session/title",
"request/header",
"request/context",
"session-log-deepseek/delivery-accepted",
"assistant/chunk",
"assistant/chunk",
"assistant/chunk",
"assistant/chunk",
"assistant/chunk",
"assistant/message",
"step/end",
"turn/end"
],
"notificationMethods": [
"session.event",
"session.status",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.status"
]
},
{
"session_id": "{{session-2}}",
"final_response": "PROCESS_TWO_OK",
"finish_reason": "completed",
"eventTypes": [
"agent/inbox/spliced",
"turn/start",
"agent/inbox/spliced",
"step/start",
"user/message",
"user/message",
"session/title",
"request/header",
"request/context",
"session-log-deepseek/delivery-accepted",
"assistant/chunk",
"assistant/chunk",
"assistant/chunk",
"assistant/chunk",
"assistant/chunk",
"assistant/message",
"step/end",
"turn/end"
],
"notificationMethods": [
"session.event",
"session.status",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.event",
"session.status"
]
}
]
@@ -0,0 +1,22 @@
{"type":"session","version":0,"id":"{{session-1}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
{"type":"approval/policy","data":{"policy":"never"}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Complete the first isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","data":{"turn":1}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Complete the first isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Complete the first isolated Python","messageSeqs":[7],"source":{"kind":"fallback"}}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-1}}","throughSeq":11}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"PROCESS_ONE_OK"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PROCESS_ONE_OK"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_ONE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -0,0 +1,22 @@
{"type":"session","version":0,"id":"{{session-2}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
{"type":"approval/policy","data":{"policy":"never"}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Complete the second isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}}
{"type":"turn/start","data":{"turn":1}}
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"user/message","data":{"content":[{"type":"text","text":"Complete the second isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
{"type":"session/title","data":{"title":"Complete the second isolated Python","messageSeqs":[7],"source":{"kind":"fallback"}}}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-2}}","throughSeq":11}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"PROCESS_TWO_OK"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PROCESS_TWO_OK"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"PROCESS_TWO_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}},"sourceEventSeqs":[13,14,15,16,17],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
@@ -8,43 +8,27 @@
},
{
"role": "user",
"content": "# DeepSeek Harness\n\nEnglish | [中文](README.zh.md)\n\nDeepSeek Harness (`dsh`) is an open-source agent harness developed by [DeepSeek AI](https://deepseek.com).\n\nIt uses an architecture where **everything is a plugin**, and is powered by [Cordis](https://github.com/cordiverse/cordis), whose design is described in [_A Programming Paradigm for Spatiotemporal Composability_](https://github.com/cordiverse/paper).\n\nDocumentation: [https://deepseek-harness.github.io/deepseek-harness/](https://deepseek-harness.github.io/deepseek-harness/)\n\n## Developer preview\n\nDeepSeek Harness is currently in _developer preview_ and is iterating rapidly. **THERE WILL BE COMPATIBILITY-BREAKING CHANGES.**\n\n## Run\n\n### Run from `npm`\n\nInstall `Node.js`, then run:\n\n```sh\nnpx @deepseek-ai/dsh web\n```\n\nThe command starts the Web UI at `http://127.0.0.1:3080` by default and opens it in the default browser for a local launch. An SSH launch only prints the host URL because the SSH client or editor owns the local forwarded address. Pass `--no-open` to run the server without opening a browser. See [Web UI guide](docs/user/guide/index.md).\n\n### Run from source\n\nTo run from a repository checkout:\n\n```sh\ngit clone https://github.com/deepseek-ai/deepseek-harness.git\ncd deepseek-harness\npnpm install\npnpm run build\npnpm dsh web\n```\n\n`pnpm run build` prepares the repository artifacts. `pnpm dsh web` uses those built artifacts without rebuilding.\n\n## Community and support\n\n- Feel free to submit feedback or bug reports through [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions).\n- Add the [`dsh-plugin`](https://github.com/topics/dsh-plugin) topic to your plugin repository for discoverability.\n- Join <a href=\"https://discord.gg/Ycq5dCaS4\">DeepSeek Harness Discord community</a>.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## Development\n\nStart with the [development guide](docs/development.md) and [architecture documentation](docs/architecture.md).\n\nFor agents, follow [AGENTS.md](AGENTS.md).\n\n## License\n\n[MIT](LICENSE)\n\nThird-party dependencies and their licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).\n"
"content": "# Acme Agent\n\nEnglish | [中文](product.zh.md)\n\nAcme Agent is an open-source agent harness that automates repository chores.\n\nThis paragraph contains neutral placeholder text for the example.\n\n## Install\n\nInstall Node.js 24, then run:\n\n```sh\nnpx acme-agent setup\n```\n\nThe command prints the setup URL, which is `http://127.0.0.1:3080` by default.\n\n## Community\n\n- Report bugs in the issue tracker.\n- Add the `acme-agent` topic to your plugin repository.\n"
},
{
"role": "assistant",
"content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是由 [DeepSeek AI](https://deepseek.com) 开发的开源 agent harness(智能体框架)。\n\n它采用**一切皆插件**的架构,并由 [Cordis](https://github.com/cordiverse/cordis) 驱动,其设计参见论文 [_A Programming Paradigm for Spatiotemporal Composability_](https://github.com/cordiverse/paper)。\n\n文档:[https://deepseek-harness.github.io/deepseek-harness/](https://deepseek-harness.github.io/deepseek-harness/)\n\n## 开发者预览\n\nDeepSeek Harness 目前处于 _开发者预览_ 阶段,正在快速迭代。**未来将出现破坏兼容性的变更。**\n\n<a id=\"run\"></a>\n\n## 运行\n\n### 通过 `npm` 运行\n\n安装 `Node.js`,然后运行:\n\n```sh\nnpx @deepseek-ai/dsh web\n```\n\n该命令默认会在 `http://127.0.0.1:3080` 启动 Web UI,本机启动时还会用默认浏览器打开页面。通过 SSH 启动时只打印宿主机 URL,因为本地转发地址由 SSH 客户端或编辑器持有。传入 `--no-open` 可仅运行服务器而不打开浏览器。详见 [Web UI 指南](docs/user/guide/index.zh.md)。\n\n<a id=\"run-from-source\"></a>\n\n### 从源码运行\n\n如需从仓库源码运行:\n\n```sh\ngit clone https://github.com/deepseek-ai/deepseek-harness.git\ncd deepseek-harness\npnpm install\npnpm run build\npnpm dsh web\n```\n\n`pnpm run build` 会准备仓库产物。`pnpm dsh web` 会直接使用这些已构建产物,不会重新构建。\n\n## 社区与支持\n\n- 欢迎通过 [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions) 提交反馈或 bug 报告。\n- 为你的插件仓库添加 [`dsh-plugin`](https://github.com/topics/dsh-plugin) 话题,便于被发现。\n- 欢迎加入 DeepSeek Harness 企微群:扫码添加企微小助手并填写入群问卷,完成后小助手会邀请你入群。\n\n<table>\n <thead>\n <tr>\n <th align=\"center\">企微小助手</th>\n <th align=\"center\">入群问卷</th>\n <th align=\"center\">微信公众号</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <td align=\"center\"><img src=\"https://cdn.deepseek.com/harness/readme/community-wecom-assistant.png\" alt=\"DeepSeek Harness 企微小助手二维码\" width=\"180\" height=\"180\"></td>\n <td align=\"center\"><a href=\"https://trtgsjkv6r.feishu.cn/share/base/form/shrcnIt5twSVdLGD52KJBckGCgg\"><img src=\"https://cdn.deepseek.com/harness/readme/community-wecom-survey.png\" alt=\"DeepSeek Harness 入群问卷二维码\" width=\"180\" height=\"180\"></a></td>\n <td align=\"center\"><img src=\"https://cdn.deepseek.com/harness/readme/community-wechat-official-account.png\" alt=\"DeepSeek Harness 团队微信公众号二维码\" width=\"180\" height=\"180\"></td>\n </tr>\n </tbody>\n</table>\n\n## 参与贡献\n\n参见 [CONTRIBUTING.md](CONTRIBUTING.zh.md)。\n\n## 开发\n\n请先阅读[开发指南](docs/development.zh.md)与[架构文档](docs/architecture.zh.md)。\n\n面向 agent:请遵循 [AGENTS.md](AGENTS.md)。\n\n## 许可证\n\n[MIT](LICENSE)\n\n第三方依赖及其许可证见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。\n"
"content": "# Acme Agent\n\n[English](product.md) | 中文\n\nAcme Agent 是一款开源 agent harness(智能体框架),用于自动化仓库日常事务。\n\n本段包含供示例使用的中性占位文本。\n\n## 安装\n\n安装 Node.js 24,然后运行:\n\n```sh\nnpx acme-agent setup\n```\n\n该命令会打印设置地址,默认地址为 `http://127.0.0.1:3080`。\n\n## 社区\n\n- 在 Issue 跟踪器中报告 bug。\n- 为你的插件仓库添加 `acme-agent` 主题。\n"
},
{
"role": "user",
"content": "# Development guide\n\nEnglish | [中文](development.zh.md)\n\nThe setup tutorial takes a new contributor from prerequisites to a checked checkout. The contributor reference that follows covers repository layout, daily workflow, and CI organization. Design rationale and implementation details belong to the linked Agent Notes and scripts.\n\n## Setup tutorial\n\n### Prerequisites\n\n- Node.js supports 22.19+ and 24+. CI covers 22.19, 24, and 26; see the [Node engine floor Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md).\n- Corepack-enabled pnpm. The repo pins `pnpm@11.7.0` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack.\n- Git 2.26 or newer; hook setup enables Git's worktree-specific configuration extension.\n- Optional: a DeepSeek API key for the Web, headless, and ACP automation demos and real-API e2e tests.\n\n### First-time setup\n\nInstall dependencies from the repo root:\n\n```sh\npnpm install\n```\n\nThe install also configures worktree-local Lefthook hooks and the `dsh-translation-pairing` Git merge driver through `scripts/install-lefthook.mjs`. The [worktree-local hooks Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md) owns the hook-path safety contract; the [automatic pairing merges Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) owns the merge driver.\n\nIf either integration is missing because dependencies were restored from cache or `postinstall` was skipped, install them manually:\n\n```sh\nnode scripts/install-lefthook.mjs\n```\n\nIf the wrapper rejects existing Git configuration or reports a stale lock, follow its diagnostic and the linked Agent Note rather than editing worktree metadata speculatively. After moving a checkout, rerun the wrapper to regenerate the owned path.\n\nRun typecheck once after a fresh clone:\n\n```sh\npnpm run typecheck\n```\n\nSetup is complete when `pnpm run typecheck` exits successfully.\n\n## Contributor reference\n\n### TypeScript project layout\n\nThe repository uses isolated Host and Client aggregates. An ordinary package is registered in exactly one aggregate: Host packages in `tsconfig.host.json` and Client packages in `tsconfig.client.json`.\n\n| File | Role | Forms a program? |\n|---|---|---|\n| `tsconfig.json` | Solution root: `extends` base, `files: []`, and references to the two aggregates. It is the tsserver discovery entry and the entry for explicitly running the complete Project Reference graph; through the inherited `paths`, it is also the resolution config for tsx running `examples/` and `scripts/`. | No |\n| `tsconfig.host.json` | Host aggregate: Host packages, examples, tests, scripts, website, and the exceptional Host project of `api/remotes`. | Yes |\n| `tsconfig.client.json` | Client aggregate: `packages/client/*` packages and their tests, `apps/web`, and the exceptional Client project of `api/remotes`. | Yes |\n| `tsconfig.base.json` | Shared compilerOptions and the source `paths` map. Also the resolution facade the vitest configs point vite-tsconfig-paths at: it has no `include`, so its `paths` apply to every importer. | No |\n| `tsconfig.base.client.json` | Browser compiler settings (`jsx`, DOM libs, `types: []`) extended by the Client aggregate and every `packages/client/*` package. | No |\n\nHost and Client stay two aggregate programs because both sides declaration-merge the cordis `Context` interface under the same keys with different services; one program seeing both merges reports a collision. The collision exists only inside a `ts.Program` — module resolution never triggers it — which is why the solution may reference both aggregates and one paths facade may span both sides. Three disciplines follow:\n\n- `tsconfig.base.json` never gains `include` or `files`: they would leak into every extending package project and narrow the facade's match-all scope.\n- A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges.\n- A new package is registered in exactly one aggregate. Having both a Node loader entry and a browser entry is not a reason to split a package; an ordinary Client plugin produces both runtime artifacts during the Client build phase.\n\n`api/remotes` is the repository's only package with split Host and Client tsconfigs. Its Host entry must participate in the Host Typert graph, while its Client entry imports `/remote` declarations that Host tsdown must generate first. The package-root `tsconfig.json` is therefore only a solution, and the two aggregates and direct consumers reference `tsconfig.host.json` or `tsconfig.client.json` respectively. The workspace `constraints` gate walks the reachable Project Reference graph and checks each referencing project's own compiler face:Line truncated
"content": "# Pairing rules\n\nEnglish | [中文](rules.zh.md)\n\nThese rules govern the Chinese counterpart of every documentation pair.\n\n## Priority levels\n\n| Level | Meaning |\n|---|---|\n| MUST | The pairing gate rejects a non-conforming pair. |\n| SHOULD | Deviate only with a stated reason. |\n\n- Preserve every proposition of the source.\n- Keep code spans verbatim.\n"
},
{
"role": "assistant",
"content": "# 开发指南\n\n[English](development.md) | 中文\n\n搭建教程引导新贡献者从准备前置条件开始,直到检出目录通过检查。后面的贡献者参考介绍仓库布局、日常工作流和 CI 组织方式。设计依据与实现细节属于链接的 Agent Note 和脚本。\n\n<a id=\"setup-tutorial\"></a>\n\n## 搭建教程\n\n### 前置条件\n\n- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.zh.md)。\n- 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `[email protected]`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。\n- Git 2.26 或更高版本;钩子设置会启用 Git 的 worktree 专属配置扩展。\n- 可选:一个 DeepSeek API key,用于 Web、headless 和 ACP(Agent Client Protocol)自动化 agent(智能体)演示以及真实 API 的 e2e 测试。\n\n### 首次搭建\n\n在仓库根目录安装依赖:\n\n```sh\npnpm install\n```\n\n安装过程还会通过 `scripts/install-lefthook.mjs` 配置 worktree 本地的 Lefthook 钩子和 `dsh-translation-pairing` Git 合并驱动。[worktree 本地钩子 Agent Note](../.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.zh.md) 负责钩子路径的安全约定;[自动配对合并 Agent Note](../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.zh.md) 负责合并驱动。\n\n如果依赖是从缓存恢复或 `postinstall` 被跳过而导致任一集成缺失,请手动安装:\n\n```sh\nnode scripts/install-lefthook.mjs\n```\n\n如果包装脚本拒绝现有 Git 配置或报告陈旧锁,请遵循其诊断和所链接的 Agent Note,不要凭猜测编辑 worktree 元数据。移动检出目录后,请重新运行包装脚本以重新生成自有路径。\n\n新克隆后请先运行一次类型检查:\n\n```sh\npnpm run typecheck\n```\n\n`pnpm run typecheck` 成功退出即表示搭建完成。\n\n## 贡献者参考\n\n<a id=\"typescript-project-layout\"></a>\n\n### TypeScript 项目布局\n\n仓库使用相互隔离的 Host 与 Client aggregate。普通包只登记进其中一个 aggregate;Host 包进入 `tsconfig.host.json`,Client 包进入 `tsconfig.client.json`。\n\n| 文件 | 角色 | 是否构成 program? |\n|---|---|---|\n| `tsconfig.json` | solution 根:`extends` base、`files: []`、引用两个 aggregate。它是 tsserver 发现入口,也是显式执行整张 Project Reference 图时的入口;经继承的 `paths` 充当 tsx 运行 `examples/` 与 `scripts/` 时的解析配置。 | 否 |\n| `tsconfig.host.json` | Host aggregate:Host 包、示例、测试、脚本和 website,以及 `api/remotes` 的 Host 特例 project。 | 是 |\n| `tsconfig.client.json` | Client aggregate:`packages/client/*` 包及其测试、`apps/web`,以及 `api/remotes` 的 Client 特例 project。 | 是 |\n| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 |\n| `tsconfig.base.client.json` | 浏览器编译设置(`jsx`、DOM lib、`types: []`),由 Client aggregate 和每个 `packages/client/*` 包 extends。 | 否 |\n\nHost 与 Client 保持两个 aggregate program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个 aggregate,一个 paths 门面也可以横跨两侧。由此推出三条纪律:\n\n- `tsconfig.base.json` 永不添加 `include` 或 `files`:它们会泄漏进每个 extends 它的包项目,并收窄门面的全匹配范围。\n- 构造全仓 `ts.Program` 的脚本显式以 `tsconfig.host.json` 或 `tsconfig.client.json` 为种子——根 solution 永不作为种子,因为把两个 aggregate 展平进一个 program 会撞上 `Context` 合并冲突。\n- 新包只登记进一个 aggregate。包同时具有 Node loader 入口和 browser 入口并不构成拆分理由;普通 Client 插件的两份运行时产物都在 Client 构建阶段生成。\n\n`api/remotes` 是唯一拆分 Host/Client tsconfig 的仓库特例。它的 Host 入口必须进入 Host Typert 图,而 Client 入口导入 Host tsdown 才会生成的 `/remote` 声明,因此本包根 `tsconfig.json` 只作为 solution,两个 aggregate 和直接消费方分别引用 `tsconfig.host.json` 或 `tsconfig.client.json`。workspace `constraints` 门禁遍历可达的 Project Reference 图,并按各引用 project 自身的 compiler face 检查:只有单一配置的目标可由任一 face 引用,拆分配置的目标则必须引用匹配的 leaf,不得引用 solution 根或另一侧Line truncated
"content": "# 配对规则\n\n[English](rules.md) | 中文\n\n这些规则约束每个文档配对的中文对侧。\n\n## 优先级\n\n| 级别 | 含义 |\n|---|---|\n| MUST | 配对门禁会拒绝不符合要求的配对。 |\n| SHOULD | 仅在说明理由后偏离。 |\n\n- 保留源文的每个命题。\n- 代码片段原样保留。\n"
},
{
"role": "user",
"content": "# Bilingual documentation\n\nEnglish | [中文](README.zh.md)\n\nThis repo's documentation is read by people and agents both inside and outside the company, so every document in scope is maintained in English and Simplified Chinese. This page defines the pairing contract, checks, scope, and exclusions; [translation-rules.md](translation-rules.md) defines how to translate; [terminology.md](terminology.md) is the terminology source of truth. Routine agent work follows the lightweight path in [docs/AGENTS.md](../AGENTS.md); the extended [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) workflow is available only through explicit user invocation.\n\n## The pairing contract\n\n- **Both languages carry equal authority.** A document may be authored and reviewed in either language first — a Chinese-first Agent Note is as legitimate as an English-first one — and the counterpart is translated from it. Neither file outranks the other; what binds them is that they must say the same thing.\n- **A pair is three sibling files.** The English `foo.md`, the Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`, all in the same directory. No locale directories, no separate translation repo, no interleaved bilingual files. Pairs merge whole: a PR never lands one language without the other two files.\n- **The consistency record.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last time the two were confirmed to say the same thing:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n Blob hashes, not commit hashes, so the record is computable for files edited in the same PR (`git hash-object foo.md`) and consistency is a pure content comparison. `--write` stores those snapshots in the local Git object database before recording them, including uncommitted working-tree contents, and pins every distinct stored blob under a content-addressed `refs/dsh/translation-pairing/snapshots/` ref so garbage collection cannot invalidate a recorded recovery pointer. The recorded hashes therefore recover the exact last-confirmed text of either side, so an out-of-sync pair is updated by patching the counterpart minimally against the edited side's diff — never by re-translating whole files. Routine work makes that patch directly; when the user explicitly invokes the extended workflow, `pnpm run gen-translation-brief <pair>` can instead assemble the update at the narrowest safely aligned granularity and `--apply` can splice a code-fence-only change after structural validation ([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.md)). After bringing the pair back in line, `pnpm run verify-translation-pairing --write <pair>` re-records both hashes; that yaml diff is the reviewable act of confirming consistency, which is why `--write` requires naming the pairs you confirmed (`--write --all` is the explicit corpus-wide form).\n\n When two branches contain valid confirmations of the same pair, the installed `dsh-translation-pairing` Git merge driver composes a new record only if Git's default text merge succeeds for both recorded owner-blob triplets and the merged pair retains its required switchers and structural signature. The Chinese file must retain its English backlink; an authored English source must retain its Chinese link, while a listed generated English source is exempt. Any structure the driver cannot verify remains an ordinary conflict; `pnpm run resolve-translation-pairing-conflicts` applies the same fail-closed operation to a merge that has already stopped, stages every safe pairing record, and exits unsuccessfully when other pairing conflicts remain. The [automatic pairing merges Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.md) owns the mechanism and alternatives.\n- **Language switcher.** The Chinese file always links back immediately after its H1 heading with `[English](foo.md) | 中文`. An authored English file reciprocates there with `English | [中文](foo.zh.md)`; a listed generated English source omits that line so it remains byte-identical to generator output. A README published outside GitHub, such as PyPI project metadata, may use the canonical `https://github.com/deepseek-ai/deepseek-harness/blob/master/<repository-path>` URL to the same counterpart so the switcher still resolves there.\n- **Structure mirrors the counterpart.** Heading depths and order, list kinds, ordered-list starts, list item counts, table row and column counts, semantic link targets with exact query/fragment suffixes, and verbatim code blocks match one to one across the pair. When a relative document link targets the active bilingual corpus, the English side uses its `.md` path and the Chinese side uses its `.zh.md` path. A missing counterpart in that corpus is a pair-cLine truncated
"content": "# Agent Note: Consistent examples\n\nStatus: implemented\n\nEnglish | [中文](agent-note.zh.md)\n\n## Problem\n\nSimilar examples used different headings.\n\n## Decision\n\nUse the same headings for similar examples.\n\n## Consequences\n\nExamples are easier to compare.\n"
},
{
"role": "assistant",
"content": "# 双语文档\n\n[English](README.md) | 中文\n\n本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此范围内的每篇文档都以英文和简体中文维护。本页定义配对约定、检查、范围与排除规则;[translation-rules.md](translation-rules.zh.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。agent 的日常工作遵循 [docs/AGENTS.md](../AGENTS.md) 中的轻量路径;扩展版 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 工作流仅在用户显式调用时可用。\n\n<a id=\"the-pairing-contract\"></a>\n\n## 配对约定\n\n- **两种语言同权。** 一篇文档可以先用任一语言撰写和评审(先写中文的 Agent Note 与先写英文的一样正当),另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。\n- **一对文档是三个同目录文件。** 英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对必须整体合并:PR(Pull Request)永远不会只带一种语言而缺其余两个文件。\n- **一致性记录。**`foo.i18n.yaml` 保存两侧文件在上一次被确认「说同样的话」时各自的完整 Git blob hash:\n\n ```yaml\n foo.md: 3f786850e387550fdab836ed7e6dc881de23001b\n foo.zh.md: 89e6c98d92887913cadf06b2adb97f26cde4849b\n ```\n\n 用 blob hash 而不是 commit hash,这样同一个 PR 里改动的文件也能算出记录(`git hash-object foo.md`),一致性是纯内容比较。`--write` 会先把这些快照存入本地 Git 对象库再写下记录,未提交的 worktree 内容也不例外;它还会在内容寻址的 `refs/dsh/translation-pairing/snapshots/` ref 下固定每个不同的已存 blob,使垃圾回收无法让已记录的恢复指针失效。因此记录的 hash 能还原任一侧上次确认时的确切文本,所以失去同步的配对是「按被改一侧的 diff 最小化地修补另一侧」,从不整篇重译。日常工作会直接完成这份修补;用户显式调用扩展工作流时,可改由 `pnpm run gen-translation-brief <pair>` 以能安全对齐的最窄粒度汇集这次更新,并由 `--apply` 在结构校验后拼接仅涉及围栏代码块的改动([briefed-updates Agent Note](../../.agents/notes/implemented/process/2026-07-26-briefed-minimal-translation-updates.zh.md))。两侧对齐后,`pnpm run verify-translation-pairing --write <pair>` 重新记录两个 hash;那份 YAML diff 就是「确认一致」这个动作本身,可以被评审,也正因如此,`--write` 要求点名你确认过的配对(`--write --all` 是显式的全语料形式)。\n\n 当两个分支都包含同一配对的有效确认时,已安装的 `dsh-translation-pairing` Git 合并驱动只会在 Git 默认文本合并能分别干净合并记录所指向的英文三方 blob 与中文三方 blob,且合并后的配对仍保留必需的语言切换行和结构签名时,组合出一份新记录。中文文件必须保留指向英文的反向链接;普通撰写的英文源必须保留指向中文的链接,而清单内的生成英文源不作此要求。任何合并驱动无法验证的结构都保留为普通冲突;`pnpm run resolve-translation-pairing-conflicts` 会对已经停止的合并执行同一套遇错即保留冲突的操作,暂存每份可安全生成的配对记录,并在还有其他配对冲突时以非零状态退出。[自动配对合并 Agent Note](../../.agents/notes/implemented/process/2026-08-08-automatic-translation-pairing-merges.zh.md) 负责记录该机制与备选方案。\n- **语言切换行。** 中文文件一律在 H1 标题后立即以 `[English](foo.md) | 中文` 链回英文。普通撰写的英文文件在同一位置以 `English | [中文](foo.zh.md)` 互链;清单内的生成英文源省略此行,以便与生成器输出逐字节一致。发布到 GitHub 以外位置的 README(例如 PyPI 项目元数据)可以改用指向同一对侧文件的规范 `https://github.com/deepseek-ai/deepseek-harness/blob/master/<repository-path>` URL,使切换行在该位置仍可访问。\n- **结构与另一侧一一对应。** 标题深度与顺序、列表类型、有序列表起始编号、列表项数量、表格行列数、保留原样 query/fragment 后缀的语义链接目标,以及逐字节一致的代码块在配对两侧一一对应。相对文档链接的目标属于活跃双语语料时,英文侧使用其 `.md` 路径,中文侧使用其 `.zh.md` 路径。该范围内缺少对侧属于配对完整性错误,不得回退;范围外的目标保留原路径。完整保持规则见 [translation-rules.md](translation-rules.zh.md)。既有 Markdown 门禁对 `.zh.md` 文件原样生效(`verify-md-wrap`、`verify-md-links`)。\n\n## Line truncated
},
{
"role": "user",
"content": "# Translation rules\n\nEnglish | [中文](translation-rules.zh.md)\n\nHow to translate between the two sides of a documentation pair in this repo. Both languages carry equal authority ([README.md](README.md)): a change is authored in either language, and that side is the source for that update — these rules govern producing or updating the counterpart. They bind humans and agents equally. Routine agent work translates the changed content directly in one terminology-guided pass; the extended [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) workflow runs only when the user explicitly invokes it. Rule levels follow RFC 2119 usage: **MUST** / **MUST NOT** are gate- or review-blocking; **SHOULD** needs a stated reason to deviate; **MAY** is discretionary.\n\n## Faithfulness\n\n- The counterpart *MUST* say what the authored side says — no added behavior, prerequisites, warnings, version claims, or examples, and no dropped ones. If the pair disagrees on substance, neither language wins by default: fix the side that is wrong, then bring the other along in the same change.\n- The counterpart *SHOULD* read as natural technical writing in its own language, not word-by-word gloss. Translate meaning, restructure sentences where the target grammar wants it, and keep the author's register — terse stays terse.\n- Do not translate the untranslatable: if a sentence resists natural rendering because it leans on an idiom of the source language, translate the idea, not the idiom.\n\n## Voice\n\n- The register is calibrated by [style-samples.md](style-samples.md) — human-approved gold pairs, one per document genre. The counterpart MUST match the target-language side of the nearest sample; where its voice and a prose voice rule disagree, the sample wins. Chinese targets use institutional technical Chinese; English targets use concise professional developer prose.\n- Write as a native technical author restating the content, not as a translator transposing sentences, while preserving every source clause: nothing added, nothing dropped — fluency never justifies losing a clause.\n- Give sentences an explicit actor when the target language would otherwise obscure it; for Chinese, replace vague passives or abstract subjects with the actual actor (系统、门禁、评审人).\n- Prefer established target-language engineering idiom over calques (误报/漏检 for false positive/negative, 执行红线 for enforcement frontier); localize metaphors instead of transplanting them, and unpack noun chains where the target language requires it.\n- Split long paragraphs by semantic unit — one idea per paragraph. Paragraph boundaries MAY differ from the source; the structural signature does not count paragraphs.\n- When translating into Chinese, category nouns use Chinese with a first-mention English annotation (实操手册(cookbook)); when translating into English, use the conventional English category name. Literal directory or file references stay code-formatted English.\n\n## Structure preservation\n\nThe pairing gate checks heading depths, fenced code blocks, table row and column counts, list kinds, ordered-list starts, list item counts, link locale, and semantic targets. Preserve the rest of the frame manually; the paired files MUST match one to one in:\n\n- heading hierarchy (same levels, same order — heading TEXT is translated),\n- list shape and numbering,\n- tables (same columns, same row order; header cells translated per terminology),\n- fenced code blocks — **byte-identical, including comments**; the pairing signature compares their info strings and contents, and ` ```ts ` blocks compile under `doc-typecheck`,\n- inline code spans (commands, flags, config keys, file paths, event names, API names, version numbers) — verbatim, never translated or reformatted,\n- links and anchors: every relative document link MUST keep the same semantic target and exact query/fragment suffix. When the target belongs to the active bilingual corpus, the English side uses its `.md` path and the Chinese side uses its `.zh.md` path; a missing counterpart in that corpus is an error, while targets outside it keep the original path. External URLs, images, and pure in-page fragments stay unchanged. The language switcher remains the explicit cross-locale exception, and a README rendered outside GitHub MAY use the canonical public repository URL to its exact counterpart as documented in [README.md](README.md). Link TEXT is translated.\n\nThe repo's Markdown conventions apply to `.zh.md` files unchanged: one physical line per paragraph (`verify-md-wrap`), resolving relative links (`verify-md-links`), exactly one trailing newline.\n\n## Terminology\n\n- [terminology.md](terminology.md) is the source of truth in both directions. Before translating, load it; every listed term MUST follow its row and its \"不要译作\" prohibitions. A Chinese target uses the \"中文\" column and its Line truncated
},
{
"role": "assistant",
"content": "# 翻译规则\n\n[English](translation-rules.md) | 中文\n\n本文规定:如何在本仓库文档配对的中英文两种语言之间进行翻译。两种语言同权(见 [README.md](README.zh.md)):每次变更可以用任一语言撰写,被编辑的一侧即为本次更新的源;本文的规则约束如何产出或更新对侧文件。这些规则对人类和 agent(智能体)同等生效。日常工作中,agent 会在术语指导下直接一次完成有改动内容的翻译;扩展版 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md) 工作流仅在用户显式调用时运行。规则级别沿用 RFC 2119 的用法:**必须(MUST)** / **禁止(MUST NOT)** 会卡门禁或评审;**应当(SHOULD)** 偏离时要说明理由;**可以(MAY)** 自行裁量。\n\n## 忠实性\n\n- 对侧文件*必须*传达与撰写侧相同的内容:不添加行为、前置条件、警告、版本声明或示例,也不漏掉任何一项。如果两侧在实质内容上不一致,没有哪种语言默认获胜;请修正错误的一侧,并在同一个变更里同步更新另一侧。\n- 对侧文件读起来*应当*是其语言自然的技术文字,而非逐词对照的译文。请根据语义翻译,在目标语言语法需要时重组句子,并保持原作者的语域(比如:简练的保持简练)。\n- 不要翻译不可译的内容:如果一句话依赖源语言的习语、无法自然转换,请翻译它的意思,而非习语本身。\n\n## 行文\n\n- 语体以 [style-samples.md](style-samples.md) 为校准锚点。人工定稿的金标样例按文体各一组,译文必须参照文体最接近的样例,采用其中目标语言一侧的语体;如果样例与本文的行文规则冲突,以样例为准。译成中文时,采用规范的技术制度文;译成英文时,采用简洁、专业的开发者文档语体。\n- 以母语技术作者的身份重述内容,而不是以译者身份逐句转写,同时保留原文的每个语义成分:不添加、不遗漏——流畅永远不是丢掉语义成分的理由。\n- 如果直译会让执行主体含糊,请明确写出实际执行者;译成中文时,应由「系统、门禁、评审人」等实际执行者作主语,避免含糊的被动句或抽象主语。\n- 优先采用目标语言中通行的工程表达,避免生硬直译(false positive/negative→误报/漏检、enforcement frontier→执行红线);隐喻应自然改写,名词链则按目标语言的习惯拆开。\n- 长段按语义单元拆分,一段一件事。段落边界可以与原文不同;结构签名不比对段落数。\n- 翻译为中文时,类别名词使用中文并在首现括注英文(实操手册(cookbook));翻译为英文时,使用通行的英文类别名。指目录或文件本身时保留代码体英文。\n\n## 结构保持\n\n配对门禁会检查标题深度、围栏代码块、表格行列数、列表类型、有序列表起始编号、列表项数量、链接 locale 与语义目标;门禁未覆盖的结构仍需人工核对。两个配对文件必须在以下方面一一对应:\n\n- 标题层级(相同级别、相同顺序;标题的**文字**要翻译);\n- 列表形态与编号;\n- 表格(相同的列、相同的行序;表头单元格按术语表翻译);\n- 围栏代码块:**逐字节一致,包括注释**。配对签名比对信息字符串与内容,` ```ts ` 块还要通过 `doc-typecheck` 编译;\n- 行内代码(命令、flag、配置键、文件路径、事件名、API 名、版本号):原样保留,从不翻译或重排;\n- 链接与锚点:每个相对文档链接必须保持相同的语义目标和完全相同的 query/fragment 后缀。目标属于活跃双语语料时,英文侧使用其 `.md` 路径,中文侧使用其 `.zh.md` 路径;该范围内缺少对侧属于错误,范围外的目标保留原路径。外部 URL、图片与纯页内 fragment 保持不变。语言切换行仍是显式跨 locale 例外;在 GitHub 以外位置渲染的 README 可以按 [README.md](README.zh.md) 的规定,使用指向确切对侧文件的规范公开仓库 URL。链接**文字**翻译。\n\n本仓库的 Markdown 约定对 `.zh.md` 文件原样生效:一个段落一个物理行(`verify-md-wrap`)、相对链接必须可解析(`verify-md-links`)、文件末尾恰好一个换行。\n\n## 术语\n\n- [terminology.md](terminology.md) 是双向的术语真源。翻译前请先加载它;表内术语必须遵守对应行与「不要译作」禁项。译成中文时,采用「中文」列,并按「首次出现」列括注;译成英文时,采用「English」列,不加中文括注。\n- 译成中文时,术语表未收录的技术术语只有在主流中文 OSS 文档或厂商资料中已有通行译法时才可以翻译(K8s/Vue/MDN 中文文档、微软简中风格Line truncated
},
{
"role": "user",
"content": "# Agent Note: Bilingual documentation via paired sibling files and a pairing gate\n\nStatus: implemented\n\nEnglish | [中文](2026-07-02-bilingual-docs-and-pairing-gate.zh.md)\n\n## Problem\n\nThis repo's documentation corpus is read by people and agents inside and outside the company, in both English and Chinese. Maintaining a second language by hand, with no mechanism, is how translations rot: one side moves on, the other silently lies, and no gate notices. The repo's standing answer to invariants of this kind is to encode them as a mechanical check (see [quality gates](2026-06-11-quality-gates.md) and [doc-sync enforcement](../../archived/process/2026-06-11-doc-sync-enforcement.md)), so the bilingual policy ships with one.\n\n## Decision\n\n- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../../docs/i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md).\n- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write <pair>`, which requires naming the confirmed pairs — bulk re-record is an explicit `--write --all`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.\n- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: every discovered, non-excluded source has a complete pair; every existing pair is complete (all three files) and consistent (both hashes match, the Chinese side and every authored English source carry their switchers while listed generated English sources are exempt, structural signatures identical); and excluded generated, instruction, or bilingual-by-construction files stay unpaired. Relative document links whose targets belong to that active corpus use the target sibling matching the source locale, while the structure signature normalizes `.md` and `.zh.md` siblings to one semantic target and retains the exact query/fragment suffix; the [localized bilingual links decision](2026-08-18-localized-bilingual-links.md) owns that refinement. [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) contains only explicit exclusions, so no requirement can bypass discovery and receive a weaker check. Source-oriented code gates consume a `.zh.md` fence sequence as a derivative only when its unsuffixed sibling has the same tracked fences in the same order with byte-identical bodies; an incomplete, reordered, reclassified, or changed sequence stays independent, so the owning code gate or pairing gate reports the mismatch.\n- **One corpus-wide requirement.** Every document in scope requires a complete pair from creation; the policy has no per-file rollout state, date cutoff, or README-specific class. README discovery covers every case-insensitive README basename outside vendored, dependency, and ignored build-output trees, including future top-level directories. A site-published pair uses `pairedPages()` so the root locale projects `.zh.md` and `/en/` projects `.md`; creating a counterpart alone does not publish it.\n- **Pairing records are metadata, not Cordis Loader configuration.** Cordis configuration discovery accepts actual `.cordis.yml` and `.cordis.yaml` files while excluding `*.i18n.yaml`, even when the document name contains `cordis`. This preserves validation of executable Loader entries without parsing translation hashes as configuration.\n- **Translation is agent work with human review.** Routine changes use the direct one-pass path owned by the [lightweight-translation decision](2026-08-08-lightweight-routine-documentation-translation.md). The [extended translation skill](../../../skills/dsh-translate-docs/SKILL.md) retains delegated translation and the other heavier mechanisms for explicit user invocation; both paths defer to the documentation contracts as their sources of truth.\n\n## Verification\n\nThe verification contract covers each boundary independently. `verify-translation-pairing` pinsLine truncated
},
{
"role": "assistant",
"content": "# Agent Note: 通过配对兄弟文件与配对门禁实现双语文档\n\nStatus: implemented\n\n[English](2026-07-02-bilingual-docs-and-pairing-gate.md) | 中文\n\n## 问题\n\n本仓库的文档语料会被公司内外的人和 agent(智能体)以中英两种语言阅读。在没有机制的情况下纯靠手工维护第二语言,正是译文腐烂的根源:一侧持续演进,另一侧默默失实,而没有门禁能够发现。对于这类不变式,本仓库一贯的做法是将其编码为机械检查(见[质量门禁](2026-06-11-quality-gates.zh.md)与 [doc-sync(文档同步门禁)强制](../../archived/process/2026-06-11-doc-sync-enforcement.md)),因此双语政策随附一道门禁一起交付。\n\n## 决策\n\n- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../../docs/i18n/README.zh.md);翻译规则见 [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.zh.md);术语真源见 [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md)。\n- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 Git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write <pair>`,要求点名所确认的配对;批量重新记录是显式的 `--write --all`)会产生一份可评审的 YAML diff:确认一致在 PR 中是一个显式、可见的动作。\n- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:每个已发现且未排除的源文档都有完整配对;每个现有配对都完整(三个文件齐全)且一致(两侧的 hash 均与记录匹配、中文侧和所有人工撰写的英文源都带语言切换行而清单内的生成英文源除外、结构签名一致);被排除的生成文档、指令文档或本身即双语的文档不得配对。目标属于该活跃语料的相对文档链接使用与源文件 locale 相同的目标兄弟文件;结构签名则把 `.md` 与 `.zh.md` 兄弟文件规范化为同一个语义目标,并保留完全相同的 query/fragment 后缀;该细化规则由[双语文档链接本地化决策](2026-08-18-localized-bilingual-links.zh.md)负责。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 只包含显式排除项,因此任何要求都无法绕过发现流程而接受较弱的检查。只有当 `.zh.md` 围栏序列与其无后缀兄弟文件拥有顺序相同、正文按字节一致的同一组受跟踪围栏时,面向源码的代码门禁才会将其作为派生内容消费;不完整、顺序变更、重分类或已改动的序列仍会独立受检,因此由其所属的代码门禁或配对门禁报告不匹配。\n- **全语料统一要求。** 范围内的每篇文档从创建起就必须有完整配对;政策没有逐文件推进状态、日期分界或 README 专用类别。README 发现会覆盖 vendor 源码、依赖目录与被忽略的构建产物目录之外所有文件名不区分大小写匹配 README 的文件,包括今后新增的顶层目录。发布到文档站的配对使用 `pairedPages()`,由根 locale 投影 `.zh.md`,由 `/en/` 投影 `.md`;仅创建对侧文件并不会发布它。\n- **配对记录是元数据,而不是 Cordis Loader 配置。** Cordis 配置发现会接受实际的 `.cordis.yml` 和 `.cordis.yaml` 文件,同时排除 `*.i18n.yaml`,即使文档名中包含 `cordis` 也不例外。这样既能继续校验可执行的 Loader 配置项,又不会把翻译 hash 当作配置来解析。\n- **翻译是 agent 的工作,由人评审。** 常规改动采用由[轻量翻译决策](2026-08-08-lightweight-routine-documentation-translation.zh.md)确立的直接单遍路径。[扩展翻译 skill(技能)](../../../skills/dsh-translate-docs/SKILL.md)保留委派翻译和其他较重机制,供用户显式调用;两条路径均以文档契约为真源。\n\n## 验证\n\n验证约定分别覆盖每个边界。`verify-translation-pairing` 固定配对完整性、hash、语言切换行和结构;[`project-doc-site.spec.ts`](../../../../scripts/project-doc-site.spec.ts) 固定已发布配对按 locale Line truncated
"content": "# Agent Note: 一致的示例\n\nStatus: implemented\n\n[English](agent-note.md) | 中文\n\n## 问题\n\n相似示例使用了不同的标题。\n\n## 决策\n\n相似示例使用相同的标题。\n\n## 后果\n\n示例更易于比较。\n"
},
{
"role": "user",
+1 -1
View File
@@ -120,7 +120,7 @@ describe('global test invariant host', () => {
it('mounts the owning package companion while leaving non-package roots service-only', () => {
expect(testInvariantCompanionPaths('/repo/packages/core/tools/tests/tools.spec.ts'))
.toEqual(['../packages/core/tools/src/invariant.ts'])
expect(testInvariantCompanionPaths('/repo/examples/echo-agent/tests/echo.spec.ts')).toEqual([])
expect(testInvariantCompanionPaths('/repo/apps/cli/tests/profiles/headless/example.spec.ts')).toEqual([])
expect(testInvariantCompanionPaths('/repo/scripts/test-invariants.spec.ts'))
.toEqual(Object.keys(testInvariantCompanions).sort())
})
+5 -2
View File
@@ -285,6 +285,9 @@ describe('translation scope discovery', () => {
'BRAND_GUIDELINES.md',
'BRAND_GUIDELINES.zh.md',
'BRAND_GUIDELINES.i18n.yaml',
'SAFETY.md',
'SAFETY.zh.md',
'SAFETY.i18n.yaml',
'apps/cli/README.md',
'future/subtree/readme.md',
'packages/example/README.zh.md',
@@ -300,14 +303,14 @@ describe('translation scope discovery', () => {
'packages/example/guide.md',
'packages/example/CONTRIBUTING.md',
'packages/example/BRAND_GUIDELINES.md',
'examples/tutorial.md',
'other/tutorial.md',
'website/reference.md',
'packages/example/README.txt',
'vendor/example/README.md',
'packages/example/node_modules/dependency/README.md',
'packages/example/lib/README.md',
'coverage/report/README.md',
'python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-macos-arm64/README.md',
'python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-macos-arm64/README.md',
'python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/README.md',
])('excludes non-source or non-README path %s', (file) => {
expect(isTranslationScopeFile(file)).toBe(false)
+4 -6
View File
@@ -130,8 +130,7 @@ export interface TranslationPairingManifest {
}
const README_ARTIFACT = /(?:^|\/)readme(?:\.md|\.zh\.md|\.i18n\.yaml)$/i
const ROOT_CONTRIBUTING_ARTIFACT = /^contributing(?:\.md|\.zh\.md|\.i18n\.yaml)$/i
const ROOT_BRAND_GUIDELINES_ARTIFACT = /^brand_guidelines(?:\.md|\.zh\.md|\.i18n\.yaml)$/i
const ROOT_PAIRED_DOCUMENT_ARTIFACT = /^(?:brand_guidelines|contributing|safety)(?:\.md|\.zh\.md|\.i18n\.yaml)$/i
const NON_SOURCE_DIRECTORIES = new Set([
'node_modules',
'lib',
@@ -166,7 +165,7 @@ export const TRANSLATION_SCOPE_GLOB_EXCLUDES = [
'**/.pytest_cache/**',
'apps/web/dist/**',
'.artifacts/**',
'python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-*/**',
'python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*/**',
'python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/**',
'vendor/**',
]
@@ -178,7 +177,7 @@ function isTranslationSourceExcluded(file: string): boolean {
|| segment.startsWith('.doc-typecheck-')
|| segment.startsWith('.node-next-types-'))
|| file.startsWith('apps/web/dist/')
|| file.startsWith('python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-')
|| file.startsWith('python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-')
|| file.startsWith('python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/')
}
@@ -186,8 +185,7 @@ function isTranslationSourceExcluded(file: string): boolean {
export function isTranslationScopeFile(file: string): boolean {
return !file.startsWith('.agents/notes/archived/')
&& !isTranslationSourceExcluded(file) && (README_ARTIFACT.test(file)
|| ROOT_CONTRIBUTING_ARTIFACT.test(file)
|| ROOT_BRAND_GUIDELINES_ARTIFACT.test(file)
|| ROOT_PAIRED_DOCUMENT_ARTIFACT.test(file)
|| file.startsWith('.agents/notes/')
|| file.startsWith('docs/')
|| file.startsWith('python/'))
+44 -8
View File
@@ -36,6 +36,11 @@
"symbol": "ContextFormed",
"source": "packages/llm/llm/src/message.ts"
},
{
"doc": "docs/subsystems/llm-streaming.md",
"symbol": "ImageAttachmentAccess",
"source": "packages/llm/llm/src/content.ts"
},
{
"doc": "docs/subsystems/llm-streaming.md",
"symbol": "FinishReasonMap",
@@ -129,7 +134,13 @@
{
"doc": "docs/subsystems/core.md",
"symbol": "Agent",
"source": "packages/core/agent/src/runtime-types.ts"
"source": "packages/core/agent/src/types.ts",
"augmentations": [
{
"source": "packages/core/agent/src/runtime-types.ts",
"module": "./types.ts"
}
]
},
{
"doc": "docs/subsystems/core.md",
@@ -391,6 +402,16 @@
"symbol": "TokenUsage",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/subsystems/llm-streaming.md",
"symbol": "LlmImageRequestPrice",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/subsystems/llm-streaming.md",
"symbol": "LlmImageRequestPricing",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/subsystems/llm-streaming.md",
"symbol": "ContentBlockMap",
@@ -449,9 +470,9 @@
"source": "packages/core/session/src/types.ts"
},
{
"doc": "docs/subsystems/session.md",
"doc": "docs/subsystems/todo.md",
"symbol": "TodoItem",
"source": "packages/core/session/src/types.ts"
"source": "packages/todo/tool-todo/src/types.ts"
},
{
"doc": "docs/subsystems/session.md",
@@ -870,11 +891,6 @@
"symbol": "AskUserQuestionAnswer",
"source": "packages/interaction/user-questions/src/types.ts"
},
{
"doc": "docs/subsystems/user-questions.md",
"symbol": "UserQuestionProvider",
"source": "packages/interaction/user-questions/src/index.ts"
},
{
"doc": "docs/subsystems/user-questions.md",
"symbol": "UserQuestionError",
@@ -1755,11 +1771,26 @@
"symbol": "WebBootEntry",
"source": "packages/client/modules/src/client/manifest.ts"
},
{
"doc": "docs/subsystems/client-modules.md",
"symbol": "WebBootBatchPhase",
"source": "packages/client/modules/src/client/manifest.ts"
},
{
"doc": "docs/subsystems/client-modules.md",
"symbol": "WebBootBatch",
"source": "packages/client/modules/src/client/manifest.ts"
},
{
"doc": "docs/subsystems/client-modules.md",
"symbol": "WebBootGraph",
"source": "packages/client/modules/src/client/manifest.ts"
},
{
"doc": "docs/subsystems/client-modules.md",
"symbol": "ClientArtifactBaseline",
"source": "packages/client/modules/src/index.ts"
},
{
"doc": "docs/subsystems/session-telemetry.md",
"symbol": "SessionTelemetrySharingStatus",
@@ -1974,6 +2005,11 @@
"doc": "docs/subsystems/feedback.md",
"symbol": "MessageFeedbackDeleteResult",
"source": "packages/feedback/message-feedback/src/types.ts"
},
{
"doc": "packages/client/ui-conversation/README.md",
"symbol": "ComposerChainProps",
"source": "packages/client/ui-conversation/src/client/contract/slots.ts"
}
]
}
@@ -0,0 +1,106 @@
/** Application-entrypoint classification and dsh-launch enforcement. */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { applicationEntrypointViolations } from './verify-application-entrypoints.ts'
const cleanups: string[] = []
afterEach(() => {
for (const path of cleanups.splice(0)) rmSync(path, { recursive: true, force: true })
})
function fixture(): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-application-entrypoints-'))
cleanups.push(root)
return root
}
function write(root: string, path: string, content: string): void {
const target = resolve(root, path)
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, content)
}
describe('application entrypoints', () => {
it('accepts the repository launcher inventory', () => {
expect(applicationEntrypointViolations(resolve(import.meta.dirname, '..'))).toEqual([])
})
it('rejects a package-level application bin', () => {
const root = fixture()
write(root, 'packages/example/app/package.json', JSON.stringify({ bin: { app: 'lib/bin.js' } }))
expect(applicationEntrypointViolations(root)).toEqual([
'packages/example/app/package.json: package bin bypasses the dsh launcher; applications use apps/cli profiles',
])
})
it('rejects an unclassified executable source', () => {
const root = fixture()
write(root, 'packages/example/app/src/bin.ts', '#!/usr/bin/env node\n')
expect(applicationEntrypointViolations(root)).toEqual([
'packages/example/app/src/bin.ts: executable source has no application/build/test classification',
])
})
it('rejects an executable at an application package root', () => {
const root = fixture()
write(root, 'apps/example/rogue.mjs', '#!/usr/bin/env node\n')
expect(applicationEntrypointViolations(root)).toEqual([
'apps/example/rogue.mjs: executable source has no application/build/test classification',
])
})
it('rejects an executable at the repository root', () => {
const root = fixture()
write(root, 'rogue.mjs', '#!/usr/bin/env node\n')
expect(applicationEntrypointViolations(root)).toEqual([
'rogue.mjs: executable source has no application/build/test classification',
])
})
it('rejects an unclassified executable in an app workspace', () => {
const root = fixture()
write(root, 'apps/rogue/src/bin.ts', '#!/usr/bin/env node\n')
expect(applicationEntrypointViolations(root)).toEqual([
'apps/rogue/src/bin.ts: executable source has no application/build/test classification',
])
})
it('rejects a private Python application carrier outside dsh', () => {
const root = fixture()
write(root, 'packages/sdk/rogue-python-runtime/package.json', JSON.stringify({ private: true }))
write(root, 'packages/sdk/rogue-python-runtime/src/bin.ts', '#!/usr/bin/env node\n')
expect(applicationEntrypointViolations(root)).toEqual([
'packages/sdk/rogue-python-runtime/src/bin.ts: executable source has no application/build/test classification',
])
})
it('rejects a classified demo wrapper that launches a package entry', () => {
const root = fixture()
write(root, 'package.json', JSON.stringify({ scripts: { 'demo:code-mode': 'node scripts/demo-code-mode.mjs' } }))
write(root, 'scripts/demo-code-mode.mjs', "spawn('node', ['packages/example/app/src/bin.ts'])\n")
expect(applicationEntrypointViolations(root)).toEqual([
'scripts/demo-code-mode.mjs: application demo wrapper must launch apps/cli/src/bin.ts',
'scripts/demo-code-mode.mjs: application demo wrapper must not launch a package entry directly',
])
})
it('rejects a new root demo until its launch role is classified', () => {
const root = fixture()
write(root, 'package.json', JSON.stringify({ scripts: { 'demo:new-app': 'dsh --profile new-app' } }))
expect(applicationEntrypointViolations(root)).toEqual([
'package.json scripts.demo:new-app: demo launcher has no explicit dsh or in-process classification',
])
})
})
+193
View File
@@ -0,0 +1,193 @@
/**
* Enforce dsh profiles as the only supported Node application launcher.
* Vendor CLIs, build tools, and test tools are explicit classifications
* rather than implicit holes.
*/
import { existsSync, globSync, readFileSync } from 'node:fs'
import { resolve, sep } from 'node:path'
import { pathToFileURL } from 'node:url'
type ManifestBin = string | Record<string, string>
interface PackageManifest {
readonly bin?: unknown
}
interface RootManifest {
readonly scripts?: Record<string, unknown>
}
interface DemoPolicy {
readonly kind: 'dsh-direct' | 'dsh-wrapper'
readonly wrapper?: string
}
/** Public product launcher plus the private build-only WebWorker packer. */
const MANIFEST_BIN_ALLOWLIST = new Map<string, ManifestBin>([
['apps/cli/package.json', { dsh: 'lib/bin.js' }],
['packages/experimental/webworker-packer/package.json', { 'dsh-pack-vfs-image': './bin.js' }],
])
/** Every executable in a Node application workspace has one explicit role. */
const EXECUTABLE_SOURCE_ALLOWLIST = new Map<string, string>([
['apps/cli/src/bin.ts', 'supported dsh application launcher'],
['packages/context/time-context/tests/fixtures/driver.ts', 'test-only subprocess driver'],
['packages/experimental/webworker-packer/bin.js', 'private build-only wrapper'],
['packages/experimental/webworker-packer/src/bin.ts', 'private build-only implementation'],
['packages/sdk/client/tests/fake-runtime.ts', 'test-only SDK runtime peer'],
['packages/session/session-telemetry-otel/tests/fixtures/driver.ts', 'test-only subprocess driver'],
['packages/shell/tool-pwsh/tests/fixtures/loader/driver.ts', 'test-only subprocess driver'],
['packages/subagent/subagent-acp/tests/fixtures/loader/driver.ts', 'test-only subprocess driver'],
['packages/subagent/subagent-claude-code/tests/fixtures/loader/driver.ts', 'test-only subprocess driver'],
['packages/subagent/subagent-codex/tests/fixtures/loader/driver.ts', 'test-only subprocess driver'],
['packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/driver.ts', 'test-only subprocess driver'],
['packages/subprocess/subprocess-local/src/bin.ts', 'private subprocess runner implementation'],
['packages/test-support/loader-smoke/tests/fixtures/headless-driver.ts', 'test-only subprocess driver'],
['packages/test-support/llm-mock-server/src/bin.ts', 'test-only model server'],
])
/** Root demos are application wrappers and therefore must visibly select dsh. */
const ROOT_DEMO_POLICIES = new Map<string, DemoPolicy>([
['demo:code-mode', { kind: 'dsh-wrapper', wrapper: 'scripts/demo-code-mode.mjs' }],
])
const SOURCE_PATTERNS = [
'*.ts',
'*.js',
'*.mjs',
'*.cjs',
'apps/**/*.ts',
'apps/**/*.js',
'apps/**/*.mjs',
'apps/**/*.cjs',
'packages/**/*.ts',
'packages/**/*.js',
'packages/**/*.mjs',
'packages/**/*.cjs',
]
const SOURCE_EXCLUDES = [
'**/node_modules/**',
'**/lib/**',
'**/dist/**',
'**/coverage/**',
]
/** Convert a host path from glob output to the repository's slash form. */
function repositoryPath(path: string): string {
return path.split(sep).join('/')
}
/** Stable comparison for string and object npm `bin` declarations. */
function normalizedBin(value: unknown): string | undefined {
if (typeof value === 'string') return JSON.stringify(value)
if (!isRecord(value)) return undefined
const entries = Object.entries(value)
if (!entries.every(([, target]) => typeof target === 'string')) return undefined
return JSON.stringify(Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right))))
}
function manifestBinViolations(root: string): string[] {
const failures: string[] = []
const manifests = globSync(['apps/*/package.json', 'packages/*/*/package.json'], { cwd: root }).sort()
for (const rawPath of manifests) {
const path = repositoryPath(rawPath)
const manifest = JSON.parse(readFileSync(resolve(root, path), 'utf8')) as PackageManifest
if (manifest.bin === undefined) continue
const expected = MANIFEST_BIN_ALLOWLIST.get(path)
if (expected === undefined) {
failures.push(`${path}: package bin bypasses the dsh launcher; applications use apps/cli profiles`)
continue
}
if (normalizedBin(manifest.bin) !== normalizedBin(expected)) {
failures.push(`${path}: classified bin must remain ${JSON.stringify(expected)}, got ${JSON.stringify(manifest.bin)}`)
}
}
return failures
}
function executableSourceViolations(root: string): string[] {
const failures: string[] = []
for (const rawPath of globSync(SOURCE_PATTERNS, { cwd: root, exclude: SOURCE_EXCLUDES }).sort()) {
const path = repositoryPath(rawPath)
const source = readFileSync(resolve(root, path), 'utf8')
if (!source.startsWith('#!')) continue
if (!EXECUTABLE_SOURCE_ALLOWLIST.has(path)) {
failures.push(`${path}: executable source has no application/build/test classification`)
}
}
return failures
}
function referencesDshCli(source: string): boolean {
return source.includes('apps/cli/src/bin.ts')
}
function referencesPackageEntry(source: string): boolean {
return /packages\/[^/\s'"`]+\/[^/\s'"`]+\/(?:src|lib)\/[^\s'"`]+/.test(source)
}
function rootDemoViolations(root: string): string[] {
const manifestPath = resolve(root, 'package.json')
if (!existsSync(manifestPath)) return []
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as RootManifest
const failures: string[] = []
for (const [name, commandValue] of Object.entries(manifest.scripts ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
if (!name.startsWith('demo:')) continue
const command = typeof commandValue === 'string' ? commandValue : ''
const policy = ROOT_DEMO_POLICIES.get(name)
if (policy === undefined) {
failures.push(`package.json scripts.${name}: demo launcher has no explicit dsh or in-process classification`)
continue
}
if (policy.kind === 'dsh-direct') {
if (!referencesDshCli(command)) failures.push(`package.json scripts.${name}: application demo must launch apps/cli/src/bin.ts`)
if (referencesPackageEntry(command)) failures.push(`package.json scripts.${name}: application demo must not launch a package entry directly`)
continue
}
const wrapper = policy.wrapper
if (wrapper === undefined || !command.includes(wrapper)) {
failures.push(`package.json scripts.${name}: classified wrapper must be ${String(wrapper)}`)
continue
}
const wrapperPath = resolve(root, wrapper)
if (!existsSync(wrapperPath)) {
failures.push(`${wrapper}: classified demo wrapper is missing`)
continue
}
const source = readFileSync(wrapperPath, 'utf8')
if (!referencesDshCli(source)) failures.push(`${wrapper}: application demo wrapper must launch apps/cli/src/bin.ts`)
if (referencesPackageEntry(source)) failures.push(`${wrapper}: application demo wrapper must not launch a package entry directly`)
}
return failures
}
/**
* Find unsupported application entrypoints below a repository root.
* @param root - repository or test-fixture root.
* @returns deterministic path-qualified violations.
*/
export function applicationEntrypointViolations(root: string): string[] {
return [
...manifestBinViolations(root),
...executableSourceViolations(root),
...rootDemoViolations(root),
]
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
const root = resolve(import.meta.dirname, '..')
const failures = applicationEntrypointViolations(root)
if (failures.length > 0) {
console.error('verify-application-entrypoints: unsupported launcher(s):')
for (const failure of failures) console.error(` ${failure}`)
process.exitCode = 1
} else {
console.log('verify-application-entrypoints: dsh is the only supported Node application launcher.')
}
}
+91 -25
View File
@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import {
collectClientPackageViolations,
collectRuntimeSourcePackageUses,
collectRuntimeSourceSpecifiers,
collectSourcePackageUses,
fixClientPackageManifests,
readClientDeclarations,
@@ -32,6 +33,8 @@ function declaration(
dynamic: true,
external: [],
inject: [],
runtimeSourceUses: {},
runtimeSourceSpecifiers: {},
...fields,
}
}
@@ -75,7 +78,7 @@ describe('source package uses', () => {
const uses = collectSourcePackageUses('feature.tsx', [
"import type { A } from '@deepseek-ai/dsh-a/subpath'",
"declare module '@deepseek-ai/dsh-client-ui-slots' {}",
"const load = () => import('@deepseek-ai/dsh-b')",
"const load = () => import('@deepseek-ai/dsh-b/remote')",
'export const view = <div />',
"export type { Local } from './local.ts'",
].join('\n'))
@@ -95,12 +98,20 @@ describe('source package uses', () => {
'@deepseek-ai/dsh-b',
'react',
])
expect([...collectRuntimeSourceSpecifiers('feature.tsx', [
"import type { A } from '@deepseek-ai/dsh-a/subpath'",
"const load = () => import('@deepseek-ai/dsh-b/remote')",
'export const view = <div />',
].join('\n'))].sort()).toEqual([
'@deepseek-ai/dsh-b/remote',
'react',
])
})
})
describe('package modes', () => {
it('accepts one dynamic package and one statically linked package', () => {
const dynamic = pkg('runtime')
const dynamic = pkg('feature')
const shell = pkg('ui-slots', { dynamic: false, staticLinked: true })
expect(collectClientPackageViolations(facts([dynamic, shell]))).toEqual([])
})
@@ -116,11 +127,11 @@ describe('package modes', () => {
it('requires seeded workspace packages to use staticLinked and preloads to name dynamic rows', () => {
const slots = declaration('ui-slots', { dynamic: false })
const runtime = declaration('runtime', { dynamic: false })
const bootstrap = declaration('bootstrap', { dynamic: false })
const found = collectClientPackageViolations(facts([], {
declarations: [slots, runtime],
declarations: [slots, bootstrap],
platformModules: [slots.name],
preloadedExternals: [runtime.name + '/client'],
preloadedExternals: [bootstrap.name + '/client'],
}))
expect(found).toHaveLength(2)
expect(found.join('\n')).toContain('does not use the staticLinked preset')
@@ -128,14 +139,14 @@ describe('package modes', () => {
})
it('requires every preloaded external to have a parser preload row', () => {
const runtime = declaration('runtime')
const bootstrap = declaration('bootstrap')
expect(collectClientPackageViolations(facts([], {
declarations: [runtime],
preloadedExternals: [runtime.name + '/client'],
declarations: [bootstrap],
preloadedExternals: [bootstrap.name + '/client'],
parserPreloadIds: [],
}))).toEqual([
'packages/client/web/src/platform.ts: parser-preloaded external '
+ '"@deepseek-ai/dsh-client-runtime/client" has no matching PARSER_PRELOAD_IDS row in '
+ '"@deepseek-ai/dsh-client-bootstrap/client" has no matching PARSER_PRELOAD_IDS row in '
+ 'packages/client/modules/src/index.ts',
])
})
@@ -144,12 +155,12 @@ 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 runtime = pkg('runtime', {
const conversation = pkg('conversation', {
inject: ['@deepseek-ai/dsh-client-feature'],
sourceUses: {
'@deepseek-ai/dsh-agent': ['packages/client/runtime/src/index.ts'],
'@deepseek-ai/dsh-client-ui-slots': ['packages/client/runtime/src/client/slots.ts'],
react: ['packages/client/runtime/src/client/view.tsx'],
'@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: {
@@ -165,7 +176,7 @@ describe('dependency sections', () => {
react: '^18.2.0',
},
})
expect(collectClientPackageViolations(facts([slots, runtime], {
expect(collectClientPackageViolations(facts([slots, conversation], {
platformModules: ['react', slots.name],
}))).toEqual([])
})
@@ -245,10 +256,61 @@ describe('dependency sections', () => {
})
describe('module requests', () => {
it('accepts a dynamic row supplier and its client subpath', () => {
const ui = declaration('ui', { external: ['@deepseek-ai/dsh-client-slots/client'] })
it('rejects runtime requests from one client feature package to another dynamic row', () => {
const ui = declaration('ui', {
external: ['@deepseek-ai/dsh-client-slots/client'],
runtimeSourceUses: {
'@deepseek-ai/dsh-client-slots': ['packages/client/ui/src/client/index.ts'],
},
})
const slots = declaration('slots')
expect(collectClientPackageViolations(facts([], { declarations: [ui, slots] }))).toEqual([])
expect(collectClientPackageViolations(facts([], { declarations: [ui, slots] }))).toEqual([
ui.manifest + ': client feature package requests runtime external '
+ '"@deepseek-ai/dsh-client-slots/client"; import shared types only or call an injected Cordis service',
])
})
it('rejects stale externals and accepts a runtime import outside client feature packages', () => {
const gateway = {
...declaration('@deepseek-ai/dsh-api-gateway'), manifest: 'packages/api/gateway/package.json',
}
const stale = { ...declaration('@deepseek-ai/dsh-api-stale', {
external: ['@deepseek-ai/dsh-api-gateway/client'],
}), manifest: 'packages/api/stale/package.json' }
const live = { ...declaration('@deepseek-ai/dsh-api-live', {
external: ['@deepseek-ai/dsh-api-gateway/client'],
runtimeSourceUses: {
'@deepseek-ai/dsh-api-gateway': ['packages/api/live/src/client/index.ts'],
},
runtimeSourceSpecifiers: {
'@deepseek-ai/dsh-api-gateway/client': ['packages/api/live/src/client/index.ts'],
},
}), manifest: 'packages/api/live/package.json' }
expect(collectClientPackageViolations(facts([], {
declarations: [gateway, stale, live],
}))).toEqual([
stale.manifest + ': dsh.client.external "@deepseek-ai/dsh-api-gateway/client"'
+ ' has no runtime import or re-export in production source; remove the stale declaration',
])
})
it('requires the exact external subpath to be imported at runtime', () => {
const gateway = {
...declaration('@deepseek-ai/dsh-api-gateway'), manifest: 'packages/api/gateway/package.json',
}
const subject = { ...declaration('@deepseek-ai/dsh-api-session-controller', {
external: ['@deepseek-ai/dsh-api-gateway/client'],
runtimeSourceUses: {
'@deepseek-ai/dsh-api-gateway': ['packages/api/session-controller/src/client/index.ts'],
},
runtimeSourceSpecifiers: {
'@deepseek-ai/dsh-api-gateway/remote': ['packages/api/session-controller/src/client/index.ts'],
},
}), manifest: 'packages/api/session-controller/package.json' }
expect(collectClientPackageViolations(facts([], { declarations: [gateway, subject] }))).toEqual([
subject.manifest + ': dsh.client.external "@deepseek-ai/dsh-api-gateway/client"'
+ ' has no runtime import or re-export in production source; remove the stale declaration',
])
})
it('rejects an explicit baseline request', () => {
@@ -275,14 +337,18 @@ describe('module requests', () => {
})
it('rejects synchronous module-request cycles but ignores inject cycles', () => {
const a = declaration('a', {
external: ['@deepseek-ai/dsh-client-b'],
inject: ['@deepseek-ai/dsh-client-b'],
})
const b = declaration('b', {
external: ['@deepseek-ai/dsh-client-a'],
inject: ['@deepseek-ai/dsh-client-a'],
})
const a = { ...declaration('@deepseek-ai/dsh-api-a', {
external: ['@deepseek-ai/dsh-api-b'],
inject: ['@deepseek-ai/dsh-api-b'],
runtimeSourceUses: { '@deepseek-ai/dsh-api-b': ['packages/api/a/src/client.ts'] },
runtimeSourceSpecifiers: { '@deepseek-ai/dsh-api-b': ['packages/api/a/src/client.ts'] },
}), manifest: 'packages/api/a/package.json' }
const b = { ...declaration('@deepseek-ai/dsh-api-b', {
external: ['@deepseek-ai/dsh-api-a'],
inject: ['@deepseek-ai/dsh-api-a'],
runtimeSourceUses: { '@deepseek-ai/dsh-api-a': ['packages/client/b/src/client.ts'] },
runtimeSourceSpecifiers: { '@deepseek-ai/dsh-api-a': ['packages/client/b/src/client.ts'] },
}), manifest: 'packages/api/b/package.json' }
const found = collectClientPackageViolations(facts([], { declarations: [a, b] }))
expect(found).toHaveLength(1)
expect(found[0]).toContain('synchronous dsh.client.external cycle')
+83 -12
View File
@@ -26,6 +26,9 @@ export interface ClientDeclaration {
readonly manifest: string
readonly dynamic: boolean
readonly external: readonly string[]
readonly runtimeSourceUses: Readonly<Record<string, readonly string[]>>
/** Exact runtime specifiers used to validate `dsh.client.external` declarations. */
readonly runtimeSourceSpecifiers: Readonly<Record<string, readonly string[]>>
/** Informational package dependencies declared by the row. */
readonly inject: readonly string[]
}
@@ -34,7 +37,6 @@ export interface ClientDeclaration {
export interface ClientPackage extends ClientDeclaration {
readonly staticLinked: boolean
readonly sourceUses: Readonly<Record<string, readonly string[]>>
readonly runtimeSourceUses: Readonly<Record<string, readonly string[]>>
readonly dependencies: Readonly<Record<string, string>>
readonly peerDependencies: Readonly<Record<string, string>>
readonly devDependencies: Readonly<Record<string, string>>
@@ -65,7 +67,7 @@ export interface ClientDeclarations {
*/
export function collectSourcePackageUses(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFilePackageUses(sourceFile, false)
return collectSourceFileUses(sourceFile, false, 'package')
}
/**
@@ -76,7 +78,18 @@ export function collectSourcePackageUses(path: string, source: string): Set<stri
*/
export function collectRuntimeSourcePackageUses(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFilePackageUses(sourceFile, true)
return collectSourceFileUses(sourceFile, true, 'package')
}
/**
* Collect exact bare 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 Exact specifiers retained by runtime imports, exports, requires, or JSX.
*/
export function collectRuntimeSourceSpecifiers(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return collectSourceFileUses(sourceFile, true, 'specifier')
}
function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
@@ -98,12 +111,16 @@ function exportCarriesRuntimeValue(node: ts.ExportDeclaration): boolean {
return clause.elements.length === 0 || clause.elements.some(element => !element.isTypeOnly)
}
function collectSourceFilePackageUses(sourceFile: ts.SourceFile, runtimeOnly: boolean): Set<string> {
function collectSourceFileUses(
sourceFile: ts.SourceFile,
runtimeOnly: boolean,
key: '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
uses.add(packageNameOf(specifier.text))
uses.add(key === 'package' ? packageNameOf(specifier.text) : specifier.text)
}
const visit = (node: ts.Node): void => {
if (ts.isImportDeclaration(node)) {
@@ -564,6 +581,20 @@ function collectModuleViolations(facts: ClientPackageFacts): string[] {
if (supplier === pkg.name) {
violations.push(pkg.manifest + ': dsh.client.external names its own row ' + JSON.stringify(specifier))
} else if (supplier !== undefined) {
if (pkg.manifest.startsWith('packages/client/')) {
violations.push(
pkg.manifest + ': client feature package requests runtime external ' + JSON.stringify(specifier)
+ '; import shared types only or call an injected Cordis service',
)
continue
}
if (pkg.runtimeSourceSpecifiers[specifier] === undefined) {
violations.push(
pkg.manifest + ': dsh.client.external ' + JSON.stringify(specifier)
+ ' has no runtime import or re-export in production source; remove the stale declaration',
)
continue
}
edges.push({ from: pkg.name, to: supplier, specifier })
} else {
const owner = stripClientSuffix(specifier)
@@ -654,11 +685,17 @@ function readDeclaration(
const dsh = isRecord(manifest.dsh) ? manifest.dsh : undefined
const rawClient = dsh?.client
if (rawClient === undefined) {
return { name: manifest.name, manifest: manifestPath, dynamic: false, external: [], inject: [] }
return {
name: manifest.name, manifest: manifestPath, dynamic: false, external: [], inject: [],
runtimeSourceUses: {}, runtimeSourceSpecifiers: {},
}
}
if (!isRecord(rawClient)) {
malformed.push(manifestPath + ': ' + manifest.name + ' dsh.client must be an object')
return { name: manifest.name, manifest: manifestPath, dynamic: false, external: [], inject: [] }
return {
name: manifest.name, manifest: manifestPath, dynamic: false, external: [], inject: [],
runtimeSourceUses: {}, runtimeSourceSpecifiers: {},
}
}
return {
name: manifest.name,
@@ -666,6 +703,8 @@ function readDeclaration(
dynamic: true,
external: stringArray(rawClient.external, manifest.name, manifestPath, 'external', malformed),
inject: stringArray(rawClient.inject, manifest.name, manifestPath, 'inject', malformed),
runtimeSourceUses: {},
runtimeSourceSpecifiers: {},
}
}
@@ -747,10 +786,42 @@ function readStringLiteralArray(root: string, sourcePath: string, name: string):
}
async function readFacts(root: string): Promise<ClientPackageFacts> {
const { declarations, malformed } = readClientDeclarations(root)
const byManifest = new Map(declarations.map(entry => [entry.manifest, entry]))
const { declarations: bareDeclarations, malformed } = readClientDeclarations(root)
const staticLinkedPackages = await readStaticLinkedRoster(root)
const project = new TypeScriptProject(root, 'client')
const sourceFiles = project.sourceFiles()
const declarations = bareDeclarations.map((declaration): ClientDeclaration => {
const runtimeSourceUses = new Map<string, Set<string>>()
const runtimeSourceSpecifiers = new Map<string, Set<string>>()
const sourcePrefix = dirname(declaration.manifest) + '/src/'
for (const sourceFile of sourceFiles) {
if (sourceFile.isDeclarationFile) continue
const file = project.relativePath(sourceFile)
if (!file.startsWith(sourcePrefix)) continue
for (const name of collectSourceFileUses(sourceFile, true, 'package')) {
const locations = runtimeSourceUses.get(name) ?? new Set<string>()
locations.add(file)
runtimeSourceUses.set(name, locations)
}
for (const specifier of collectSourceFileUses(sourceFile, true, 'specifier')) {
const locations = runtimeSourceSpecifiers.get(specifier) ?? new Set<string>()
locations.add(file)
runtimeSourceSpecifiers.set(specifier, locations)
}
}
return {
...declaration,
runtimeSourceUses: Object.fromEntries(
[...runtimeSourceUses].sort(([left], [right]) => left.localeCompare(right))
.map(([name, locations]) => [name, [...locations].sort()]),
),
runtimeSourceSpecifiers: Object.fromEntries(
[...runtimeSourceSpecifiers].sort(([left], [right]) => left.localeCompare(right))
.map(([specifier, locations]) => [specifier, [...locations].sort()]),
),
}
})
const byManifest = new Map(declarations.map(entry => [entry.manifest, entry]))
const packages: ClientPackage[] = []
for (const manifestPath of globSync(CLIENT_MANIFEST_GLOB, { cwd: root }).map(normalizePath).sort()) {
@@ -762,16 +833,16 @@ async function readFacts(root: string): Promise<ClientPackageFacts> {
const runtimeSourceUses = new Map<string, Set<string>>()
const packageDirectory = dirname(manifestPath)
const sourcePrefix = packageDirectory + '/src/'
for (const sourceFile of project.sourceFiles()) {
for (const sourceFile of sourceFiles) {
if (sourceFile.isDeclarationFile) continue
const file = project.relativePath(sourceFile)
if (!file.startsWith(sourcePrefix)) continue
for (const name of collectSourceFilePackageUses(sourceFile, false)) {
for (const name of collectSourceFileUses(sourceFile, false, 'package')) {
const locations = sourceUses.get(name) ?? new Set<string>()
locations.add(file)
sourceUses.set(name, locations)
}
for (const name of collectSourceFilePackageUses(sourceFile, true)) {
for (const name of collectSourceFileUses(sourceFile, true, 'package')) {
const locations = runtimeSourceUses.get(name) ?? new Set<string>()
locations.add(file)
runtimeSourceUses.set(name, locations)
+65
View File
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest'
import { clientSourceRoot, findUiI18nViolations } from './verify-client-ui-i18n.ts'
function messages(source: string): string[] {
return findUiI18nViolations('packages/client/ui-example/src/client/View.tsx', source)
.map(violation => violation.text)
}
describe('Client UI i18n source check', () => {
it('rejects direct JSX copy and copy-bearing attributes', () => {
expect(messages(`
const View = ({ ready }: { ready: boolean }) => <section aria-label="Overview">
<span>Hard-coded text</span>
<input placeholder={ready ? 'Search now' : ` + "`Wait ${'${ready}'}`" + `} />
<div runningSummary="Still working" />
</section>
`)).toEqual(['Overview', 'Hard-coded text', 'Search now', 'Wait', 'Still working'])
})
it('rejects copy kept in label data and copy helper returns', () => {
expect(messages(`
const TABS = [{ id: 'summary', label: 'Summary' }]
function statusLabel(status: string): string {
if (status === 'done') return 'Complete'
return 'Still running'
}
function duration(): string { return 'Not recorded' }
function mode(): string { return 'compact' }
function displayFailureMessage(): string { return 'API key is invalid' }
const emptySummary = 'Nothing to show'
function Dialog({ closeLabel = 'Close dialog' }: { closeLabel?: string }) { return closeLabel }
`)).toEqual([
'Summary', 'Complete', 'Still running', 'Not recorded', 'API key is invalid',
'Nothing to show', 'Close dialog',
])
})
it('normalizes native separators before deriving a Client source root', () => {
expect(clientSourceRoot('packages/extensions/sample/src/client/View.tsx'))
.toBe('packages/extensions/sample/src/client')
expect(clientSourceRoot('packages\\extensions\\sample\\src\\client\\View.tsx'))
.toBe('packages/extensions/sample/src/client')
expect(clientSourceRoot('packages/extensions/sample/src/server/index.ts')).toBeUndefined()
})
it('accepts translated copy, dynamic values, structural attributes, and language tokens', () => {
expect(messages(`
const View = ({ t, value }: { t: (key: string) => string; value: string }) => (
<section className="root" role="region" aria-label={t('overview')}>
<span>{t('status.complete')}</span>
<code>null</code>
{value === 'pending' && <output>{value}</output>}
<output>{value}</output>
</section>
)
`)).toEqual([])
})
it('does not inspect locale dictionary owners', () => {
expect(findUiI18nViolations(
'packages/client/ui-example/src/client/locales.ts',
'export const en = { title: "Hard-coded by design" }',
)).toEqual([])
})
})
+342
View File
@@ -0,0 +1,342 @@
/**
* Reject product UI copy embedded directly in Client source.
*
* Locale dictionaries are the only source files allowed to own translated
* text. Presentation code receives copy through its typed `t` seat or through
* an already-localized prop. This check covers JSX text and copy-bearing
* attributes, plus the common data/helper forms that feed them.
*/
import { globSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import ts from 'typescript'
const root = resolve(import.meta.dirname, '..')
const MINIMUM_CLIENT_UI_SOURCES = 450
const COPY_ATTRIBUTES = new Set([
'alt',
'aria-description',
'aria-label',
'aria-valuetext',
'cancelLabel',
'closeLabel',
'confirmLabel',
'copyLabel',
'description',
'emptyLabel',
'label',
'placeholder',
'title',
'truncatedLabel',
])
const COPY_ATTRIBUTE_SUFFIX = /(?:Aria|Copy|Description|Heading|Label|Message|Placeholder|Summary|Text|Title|Tooltip)$/
const COPY_NAME = /(?:^|_)(?:aria|copy|description|empty|heading|label|message|placeholder|summary|text|title|tooltip)(?:s|_.*)?$/i
const COPY_SUFFIX = /(?:aria|copy|description|empty|heading|label|labels|message|placeholder|summary|text|title|tooltip|tabs)$/i
const IMMUTABLE_LANGUAGE_TOKENS = new Set([
'Function',
'K',
'M',
'MB',
'Symbol',
'false',
'function()',
'n',
'null',
'true',
'undefined',
])
const LOCALE_KEY = /^[a-z][a-zA-Z0-9]*(?:[._-][a-zA-Z0-9]+)+$/
/** One hard-coded product-copy occurrence. */
export interface UiI18nViolation {
/** One-based source column. */
column: number
/** Repository-relative source path. */
file: string
/** One-based source line. */
line: number
/** Why this literal is treated as product copy. */
reason: string
/** Compact literal text for the diagnostic. */
text: string
}
function localeOwner(file: string): boolean {
const normalized = file.replaceAll('\\', '/')
const base = normalized.slice(normalized.lastIndexOf('/') + 1)
return base === 'locale.ts'
|| base === 'locales.ts'
|| normalized.includes('/locales/')
}
function containsProductText(text: string): boolean {
const normalized = text.replace(/\s+/g, ' ').trim()
return normalized !== ''
&& !IMMUTABLE_LANGUAGE_TOKENS.has(normalized)
&& !LOCALE_KEY.test(normalized)
&& /\p{L}/u.test(normalized)
}
function propertyName(node: ts.PropertyName | ts.BindingName): string | undefined {
return ts.isIdentifier(node) || ts.isStringLiteral(node) ? node.text : undefined
}
function copyAttribute(name: string): boolean {
return !name.endsWith('Key')
&& (COPY_ATTRIBUTES.has(name) || COPY_ATTRIBUTE_SUFFIX.test(name))
}
function compactText(text: string): string {
const normalized = text.replace(/\s+/g, ' ').trim()
return normalized.length <= 80 ? normalized : `${normalized.slice(0, 77)}...`
}
function looksLikeNaturalText(text: string): boolean {
const normalized = text.replace(/\s+/g, ' ').trim()
return /\s|[\u3400-\u9fff]/u.test(normalized) || /^[A-Z]/.test(normalized)
}
/**
* Find hard-coded product copy in one Client source file.
* @param file - repository-relative path used in diagnostics.
* @param sourceText - TypeScript or TSX source.
* @returns violations in source order.
*/
export function findUiI18nViolations(file: string, sourceText: string): UiI18nViolation[] {
if (localeOwner(file)) return []
const source = ts.createSourceFile(
file,
sourceText,
ts.ScriptTarget.Latest,
true,
file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
)
const violations = new Map<number, UiI18nViolation>()
const report = (
node: ts.Node,
text: string,
reason: string,
naturalOnly = false,
): void => {
if (
!containsProductText(text)
|| (naturalOnly && !looksLikeNaturalText(text))
|| violations.has(node.getStart(source))
) return
const position = source.getLineAndCharacterOfPosition(node.getStart(source))
violations.set(node.getStart(source), {
column: position.character + 1,
file,
line: position.line + 1,
reason,
text: compactText(text),
})
}
const collectExpression = (
node: ts.Expression,
reason: string,
naturalOnly = false,
): void => {
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
report(node, node.text, reason, naturalOnly)
return
}
if (ts.isTemplateExpression(node)) {
report(
node,
[node.head.text, ...node.templateSpans.map(span => span.literal.text)].join(''),
reason,
naturalOnly,
)
return
}
if (ts.isCallExpression(node)) {
// A call result is dynamic; copy-bearing arguments are visited through their own syntax.
return
}
if (
ts.isParenthesizedExpression(node)
|| ts.isAsExpression(node)
|| ts.isSatisfiesExpression(node)
|| ts.isNonNullExpression(node)
) {
collectExpression(node.expression, reason, naturalOnly)
return
}
if (ts.isConditionalExpression(node)) {
collectExpression(node.whenTrue, reason, naturalOnly)
collectExpression(node.whenFalse, reason, naturalOnly)
return
}
if (ts.isBinaryExpression(node)) {
if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
collectExpression(node.right, reason, naturalOnly)
} else if (
node.operatorToken.kind === ts.SyntaxKind.PlusToken
|| node.operatorToken.kind === ts.SyntaxKind.BarBarToken
|| node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken
) {
collectExpression(node.left, reason, naturalOnly)
collectExpression(node.right, reason, naturalOnly)
}
return
}
if (ts.isArrayLiteralExpression(node)) {
for (const element of node.elements) {
if (ts.isExpression(element)) collectExpression(element, reason, naturalOnly)
}
return
}
if (ts.isObjectLiteralExpression(node)) {
for (const property of node.properties) {
if (ts.isPropertyAssignment(property)) {
const name = propertyName(property.name)
const propertyOwnsCopy = name !== undefined
&& (COPY_NAME.test(name) || COPY_SUFFIX.test(name))
collectExpression(property.initializer, reason, naturalOnly || !propertyOwnsCopy)
}
}
}
}
const enclosingFunctionName = (node: ts.Node): string | undefined => {
let current = node.parent
while (!ts.isSourceFile(current)) {
if (ts.isFunctionDeclaration(current) || ts.isMethodDeclaration(current)) {
return current.name === undefined ? undefined : propertyName(current.name)
}
if (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
const parent = current.parent
return ts.isVariableDeclaration(parent) ? propertyName(parent.name) : undefined
}
current = current.parent
}
return undefined
}
const hasExplicitStringReturn = (node: ts.Node): boolean => {
let current = node.parent
while (!ts.isSourceFile(current)) {
if (
ts.isFunctionDeclaration(current)
|| ts.isMethodDeclaration(current)
|| ts.isArrowFunction(current)
|| ts.isFunctionExpression(current)
) return current.type?.kind === ts.SyntaxKind.StringKeyword
current = current.parent
}
return false
}
const visit = (node: ts.Node): void => {
if (ts.isJsxText(node)) report(node, node.text, 'JSX text')
if (ts.isJsxAttribute(node)) {
const name = node.name.getText(source)
if (copyAttribute(name) && node.initializer !== undefined) {
if (ts.isStringLiteral(node.initializer)) report(node.initializer, node.initializer.text, `${name} attribute`)
else if (ts.isJsxExpression(node.initializer) && node.initializer.expression !== undefined) {
collectExpression(node.initializer.expression, `${name} attribute`)
}
}
}
if (
ts.isJsxExpression(node)
&& node.expression !== undefined
&& (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))
) collectExpression(node.expression, 'JSX child')
if (file.endsWith('.tsx') && ts.isPropertyAssignment(node)) {
const name = propertyName(node.name)
if (name !== undefined && (COPY_NAME.test(name) || COPY_SUFFIX.test(name))) {
collectExpression(node.initializer, `${name} property`)
}
}
if (ts.isVariableDeclaration(node) && node.initializer !== undefined) {
const name = propertyName(node.name)
if (name !== undefined && (COPY_NAME.test(name) || COPY_SUFFIX.test(name))) {
collectExpression(node.initializer, `${name} value`)
}
}
if (ts.isBindingElement(node) && node.initializer !== undefined) {
const name = propertyName(node.name)
if (name !== undefined && (COPY_NAME.test(name) || COPY_SUFFIX.test(name))) {
collectExpression(node.initializer, `${name} default value`)
}
}
if (ts.isReturnStatement(node) && node.expression !== undefined) {
const name = enclosingFunctionName(node)
if (name !== undefined && (COPY_NAME.test(name) || COPY_SUFFIX.test(name))) {
collectExpression(node.expression, `${name} return value`)
} else if (file.endsWith('.tsx') && hasExplicitStringReturn(node)) {
collectExpression(node.expression, 'string return value', true)
}
}
ts.forEachChild(node, visit)
}
visit(source)
return [...violations.values()].sort((left, right) => left.line - right.line || left.column - right.column)
}
/**
* Resolve the normalized Client source root containing one TSX component.
* @param file - Glob result using native or POSIX separators.
* @returns Repository-relative `src/client` root, or undefined outside that tree.
*/
export function clientSourceRoot(file: string): string | undefined {
const normalized = file.replaceAll('\\', '/')
const marker = '/src/client/'
const index = normalized.indexOf(marker)
return index < 0 ? undefined : normalized.slice(0, index + marker.length - 1)
}
function sourceFiles(): string[] {
const clientComponentRoots = new Set(
globSync('packages/*/*/src/client/**/*.tsx', { cwd: root })
.map(clientSourceRoot)
.filter((clientRoot): clientRoot is string => clientRoot !== undefined),
)
return [...new Set([
...globSync('packages/client/*/src/**/*.tsx', { cwd: root }),
...globSync('packages/client/ui-*/src/**/*.{ts,tsx}', { cwd: root }),
...[...clientComponentRoots].flatMap(clientRoot =>
globSync(`${clientRoot}/**/*.{ts,tsx}`, { cwd: root })),
...globSync('apps/web/src/**/*.{ts,tsx}', { cwd: root }),
])]
.map(file => file.replaceAll('\\', '/'))
.filter(file => !file.endsWith('.d.ts'))
.sort()
}
function main(): void {
const files = sourceFiles()
if (files.length < MINIMUM_CLIENT_UI_SOURCES) {
throw new Error(
`verify-client-ui-i18n: discovery narrowed to ${files.length} source file(s); expected at least ${MINIMUM_CLIENT_UI_SOURCES}.`,
)
}
const violations = files.flatMap(file =>
findUiI18nViolations(file, readFileSync(resolve(root, file), 'utf8')))
if (violations.length > 0) {
console.error(`verify-client-ui-i18n: ${violations.length} hard-coded UI string(s):`)
for (const violation of violations) {
console.error(
` ${violation.file}:${violation.line}:${violation.column} ${violation.reason}: ${JSON.stringify(violation.text)}`,
)
}
process.exitCode = 1
return
}
console.log(`verify-client-ui-i18n: ${files.length} Client UI source file(s) use locale-owned copy.`)
}
if (import.meta.filename === resolve(process.argv[1] ?? '')) main()
+1 -3
View File
@@ -11,9 +11,7 @@ const ROOT = resolve(import.meta.dirname, '..')
/** Shipped Cordis configuration these rules apply to. */
const SHIPPED_CONFIG_GLOBS = [
'apps/*/config/*.yml',
'examples/*/*.cordis.yml',
'examples/*/cordis.yml',
'apps/*/config/**/*.yml',
// Bundle identity comes from the package manifest, not the domain directory.
'packages/*/*/cordis.patch.yml',
// The Python runtime ships its own default composition inside the wheel.
+67
View File
@@ -12,6 +12,8 @@ import {
bundleManifestPaths,
bundlePluginDependencyErrors,
metadataExpressionErrors,
packageTestFixtureDependencyErrors,
packageTestPluginDependencyErrors,
} from './verify-cordis-config.ts'
describe('verify-cordis-config metadata expressions', () => {
@@ -86,3 +88,68 @@ describe('workspace Bundle discovery and product dependency closures', () => {
])
})
})
describe('package-owned Loader test dependency closures', () => {
it('requires package test configs to declare each named plugin they load', () => {
const manifestPath = 'packages/example/owner/package.json'
const file = 'packages/example/owner/tests/fixtures/cordis.yml'
const manifest = {
name: '@deepseek-ai/dsh-owner',
dependencies: {},
devDependencies: {
'@deepseek-ai/dsh-declared': 'workspace:^',
},
}
expect(packageTestPluginDependencyErrors(manifestPath, manifest, [
{ file, name: '@deepseek-ai/dsh-owner' },
{ file, name: '@deepseek-ai/dsh-declared' },
{ file, name: '@deepseek-ai/dsh-missing' },
])).toEqual([
`${file}: @deepseek-ai/dsh-missing must be declared in ${manifestPath} dependencies or devDependencies`,
])
})
it('requires executable package test fixtures to declare their bare imports', () => {
const fixture = mkdtempSync(join(tmpdir(), 'dsh-package-test-entrypoint-'))
try {
const packageDir = join(fixture, 'packages/example/owner')
const driverDir = join(packageDir, 'tests/fixtures/loader')
mkdirSync(driverDir, { recursive: true })
writeFileSync(join(packageDir, 'package.json'), JSON.stringify({
name: '@deepseek-ai/dsh-owner',
devDependencies: {
'@deepseek-ai/dsh-declared': 'workspace:^',
},
}))
writeFileSync(join(driverDir, 'driver.ts'), [
"import '@deepseek-ai/dsh-owner'",
"import '@deepseek-ai/dsh-declared'",
"import '@deepseek-ai/dsh-missing'",
].join('\n'))
writeFileSync(join(driverDir, 'cordis.yml'), '[]\n')
writeFileSync(join(driverDir, 'fixture.mjs'), "import '@deepseek-ai/dsh-declared'\n")
const unrelatedDir = join(packageDir, 'tests/fixtures/unrelated')
mkdirSync(unrelatedDir, { recursive: true })
writeFileSync(join(unrelatedDir, 'driver.ts'), "import '@deepseek-ai/dsh-unrelated'\n")
expect(packageTestFixtureDependencyErrors(fixture)).toEqual([
'packages/example/owner/tests/fixtures/loader/driver.ts: '
+ '@deepseek-ai/dsh-missing must be declared in '
+ 'packages/example/owner/package.json dependencies or devDependencies',
])
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
it('fails loud when package-owned Loader fixtures disappear from the scan', () => {
const fixture = mkdtempSync(join(tmpdir(), 'dsh-empty-package-test-entrypoint-'))
try {
expect(packageTestFixtureDependencyErrors(fixture)).toEqual([
'package test fixture dependency scan found no package-owned Loader configs',
])
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
})
+115 -66
View File
@@ -5,9 +5,9 @@
* activate, against that plugin context) and the entry `disabled` field (at
* every mount decision, against the loader context). Every other entry
* metadata field stays static, so an expression there remains truthy data and
* silently changes composition. Example configs and the dsh Web composition
* resolve named plugins from their owning workspace manifests. Local example
* packages must also be in the root TypeScript project graph.
* silently changes composition. Shipped and test-only dsh overlays resolve
* named plugins from the CLI application's owning manifest; package-owned
* Loader fixtures resolve from their package manifest.
*/
import { globSync, readFileSync } from 'node:fs'
@@ -20,6 +20,7 @@ import { isCordisGroupEntry, isJsExpr, loadCordisYaml } from './cordis-yaml.ts'
export interface PackageManifest {
name?: string
dependencies?: Record<string, string>
devDependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
dsh?: { bundle?: { patch?: string } }
}
@@ -30,12 +31,10 @@ export interface PluginReference {
}
const root = resolve(import.meta.dirname, '..')
// These example files are overlays consumed by the built dsh app, so their bare
// specifiers resolve from apps/cli rather than the examples workspace.
// These overlays are consumed by the built dsh app, so their bare specifiers
// resolve from apps/cli.
const appOverlayFiles = new Set([
'examples/web-cordis/cordis.yml',
'examples/web-schedule/cordis.yml',
...globSync('examples/mcp-memory/*.cordis.yml', { cwd: root }),
...globSync('apps/cli/config/examples/**/*.yml', { cwd: root }),
])
const metadataFields = ['id', 'name', 'group', 'inject', 'intercept', 'isolate'] as const
@@ -72,8 +71,9 @@ if (import.meta.main) {
}
}
errors.push(...validateExampleResolution())
errors.push(...validateAppResolution())
errors.push(...validatePackageTestResolution())
errors.push(...packageTestFixtureDependencyErrors())
errors.push(...validateSourcePlaneResolution())
errors.push(...validatePresetPlaneSeparation())
errors.push(...validateClientHalvesDeclared())
@@ -147,7 +147,7 @@ function validatePresetPlaneSeparation(): string[] {
}
// The overlay's own inserts are host-plane too; its disables take them back out.
const active = new Set([...hostRows, ...rowIds(overlayFile)].filter(id => !disabled.has(id)))
for (const file of globSync('apps/cli/config/agent-presets/*/agent.cordis.yml', { cwd: root })) {
for (const file of globSync('packages/preset/agent-presets/presets/*/agent.cordis.yml', { cwd: root })) {
for (const id of rowIds(file)) {
if (!active.has(id)) continue
problems.push(
@@ -223,37 +223,14 @@ function recordPlugin(entry: Record<string, unknown>, file: string): void {
if (typeof entry.name === 'string') pluginReferences.push({ file, name: entry.name })
}
function validateExampleResolution(): string[] {
const violations: string[] = []
const exampleManifest = readManifest('examples/package.json')
const dependencies = exampleManifest.dependencies ?? {}
const localPackages = localPackageDirectories()
const rootReferences = rootProjectReferences()
const exampleReferences = pluginReferences.filter(reference => reference.file.startsWith('examples/') && !appOverlayFiles.has(reference.file))
violations.push(...missingPluginDependencies(exampleReferences, dependencies, 'examples/package.json'))
const requiredPackages = new Set(exampleReferences.map(reference => packageNameFromSpecifier(reference.name)))
const localExamplePackages = new Set([
...Object.keys(dependencies),
...[...requiredPackages].filter(packageName => packageName !== undefined),
])
for (const packageName of localExamplePackages) {
const packageDirectory = localPackages.get(packageName)
if (packageDirectory === undefined || rootReferences.has(packageDirectory)) continue
const repoPath = relative(root, packageDirectory).replaceAll('\\', '/')
violations.push(`tsconfig.json: missing project reference for ${packageName} (${repoPath})`)
}
return violations
}
function validateAppResolution(): string[] {
const violations: string[] = []
const bundleManifests = bundleManifestPaths()
// App overlays (and any config left under apps/cli/config) resolve from the
// dsh app's own dependency surface — the profile module fallback mirrors it.
const appManifest = readManifest('apps/cli/package.json')
const appDependencies = {
...readManifest('apps/cli/package.json').dependencies,
...appManifest.dependencies,
// The fallback also links every in-box bundle's own dependencies
// (healProfilesModuleFallback). Optional Profile bundles stay outside the
// app installation until that Profile installs them.
@@ -263,7 +240,17 @@ function validateAppResolution(): string[] {
const shipped = new Set(globSync('*.cordis.yml', { cwd: resolve(root, 'apps/cli/config') })
.map(file => `apps/cli/config/${file}`))
const appReferences = pluginReferences.filter(reference => shipped.has(reference.file) || appOverlayFiles.has(reference.file))
violations.push(...missingPluginDependencies(appReferences, appDependencies, 'apps/cli/package.json or a bundle manifest'))
violations.push(...missingPluginDependencies(
appReferences,
appDependencies,
'apps/cli/package.json dependencies or a bundle manifest',
))
const appTestReferences = pluginReferences.filter(reference => reference.file.startsWith('apps/cli/tests/'))
violations.push(...missingPluginDependencies(
appTestReferences,
{ ...appManifest.dependencies, ...appManifest.devDependencies },
'apps/cli/package.json dependencies or devDependencies',
))
// Each bundle's patch rows must resolve from that bundle's own dependencies:
// per-layer resolution anchors on the bundle package directory.
for (const manifestPath of bundleManifests) {
@@ -278,6 +265,95 @@ function validateAppResolution(): string[] {
return violations
}
/**
* Package-owned Loader fixtures resolve named plugins from their package's
* dependency surface, not from a repository-level test umbrella.
* @returns one violation per configured package absent from the owner manifest.
*/
function validatePackageTestResolution(): string[] {
const referencesByManifest = new Map<string, PluginReference[]>()
for (const reference of pluginReferences) {
const manifestPath = packageTestManifestPath(reference.file)
if (manifestPath === undefined) continue
const references = referencesByManifest.get(manifestPath) ?? []
references.push(reference)
referencesByManifest.set(manifestPath, references)
}
return [...referencesByManifest].flatMap(([manifestPath, references]) =>
packageTestPluginDependencyErrors(manifestPath, readManifest(manifestPath), references))
}
/**
* Validate the named plugins one package-owned Loader fixture resolves.
* Self-references use Node package self-resolution; every other package must
* be an ordinary production or test dependency of the owner.
* @param manifestPath Repository-relative owner manifest path.
* @param manifest Parsed owner manifest.
* @param references Named plugin references from owner-local test configs.
* @returns Missing dependency diagnostics.
*/
export function packageTestPluginDependencyErrors(
manifestPath: string,
manifest: PackageManifest,
references: readonly PluginReference[],
): string[] {
return missingPluginDependencies(
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
{ ...manifest.dependencies, ...manifest.devDependencies },
`${manifestPath} dependencies or devDependencies`,
)
}
/**
* Validate imports made by fixture modules adjacent to package-owned Loader
* configs. These files execute as plain Node/tsx children, so a stale root
* `node_modules` link must not hide an undeclared dependency.
* @param repoRoot Repository root to scan.
* @returns Missing dependency diagnostics.
*/
export function packageTestFixtureDependencyErrors(repoRoot: string = root): string[] {
const fixtureDirectories = new Set(cordisConfigFiles(repoRoot)
.filter(file => packageTestManifestPath(file) !== undefined)
.map(file => dirname(file).replaceAll('\\', '/')))
if (fixtureDirectories.size === 0) {
return ['package test fixture dependency scan found no package-owned Loader configs']
}
const referencesByManifest = new Map<string, PluginReference[]>()
let fixtureModuleCount = 0
for (const fixtureDirectory of fixtureDirectories) {
const files = globSync([
`${fixtureDirectory}/**/*.ts`,
`${fixtureDirectory}/**/*.mjs`,
], { cwd: repoRoot })
fixtureModuleCount += files.length
for (const file of files) {
const manifestPath = packageTestManifestPath(file)
if (manifestPath === undefined) continue
const references = referencesByManifest.get(manifestPath) ?? []
const source = readFileSync(resolve(repoRoot, file), 'utf8')
for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
references.push({ file: file.replaceAll('\\', '/'), name: imported.fileName })
}
referencesByManifest.set(manifestPath, references)
}
}
if (fixtureModuleCount === 0) {
return ['package test fixture dependency scan found no fixture modules beside Loader configs']
}
return [...referencesByManifest].flatMap(([manifestPath, references]) =>
packageTestPluginDependencyErrors(
manifestPath,
readManifest(manifestPath, repoRoot),
references,
))
}
/** Owner manifest for a package-local test path. */
function packageTestManifestPath(file: string): string | undefined {
const match = /^(packages\/[^/]+\/[^/]+)\/tests(?:\/|$)/.exec(file.replaceAll('\\', '/'))
return match?.[1] === undefined ? undefined : `${match[1]}/package.json`
}
/**
* Discover workspace Bundle packages from their manifest declaration.
* @param repoRoot Repository root to scan.
@@ -306,7 +382,7 @@ export function bundlePluginDependencyErrors(
// A Bundle may mount its own package (for example, its provider or runtime row).
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
manifest.dependencies ?? {},
manifestPath,
`${manifestPath} dependencies`,
)
}
@@ -364,7 +440,7 @@ function validateSourcePlaneResolution(): string[] {
function missingPluginDependencies(
references: readonly PluginReference[],
dependencies: Readonly<Record<string, string>>,
manifestPath: string,
dependencyOwner: string,
): string[] {
const requiredPackages = new Map<string, Set<string>>()
const require = (packageName: string, file: string): void => {
@@ -382,7 +458,7 @@ function missingPluginDependencies(
}
return [...requiredPackages].flatMap(([packageName, locations]) => packageName in dependencies
? []
: `${[...locations].join(', ')}: ${packageName} must be declared in ${manifestPath} dependencies`)
: `${[...locations].join(', ')}: ${packageName} must be declared in ${dependencyOwner}`)
}
function readManifest(path: string, repoRoot: string = root): PackageManifest {
@@ -399,33 +475,6 @@ function localPackageDirectories(): Map<string, string> {
return packages
}
function rootProjectReferences(): Set<string> {
// The root solution references the host and client aggregates (the two
// sides merge cordis Context under the same keys, so one program cannot see
// both — but this BFS only collects reference paths, it never forms a
// program). Seed the solution and follow nested aggregate references to
// collect the covered leaf project set.
const collected = new Set<string>()
const queue = [resolve(root, 'tsconfig.json')]
const seen = new Set<string>()
for (let file = queue.pop(); file !== undefined; file = queue.pop()) {
if (seen.has(file)) continue
seen.add(file)
const config = ts.readConfigFile(file, path => ts.sys.readFile(path))
if (config.error !== undefined) {
throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'))
}
const references = (config.config as { references?: Array<{ path?: unknown }> }).references ?? []
for (const reference of references) {
if (typeof reference.path !== 'string') continue
const target = resolve(dirname(file), reference.path)
if (target.endsWith('.json')) queue.push(target)
else collected.add(target)
}
}
return collected
}
function packageNameFromSpecifier(specifier: string): string | undefined {
if (specifier.startsWith('.') || specifier.startsWith('/') || /^[a-z][a-z+.-]*:/i.test(specifier)) return undefined
const segments = specifier.split('/')
+1 -1
View File
@@ -12,7 +12,7 @@ import { findReferenceViolations, uniqueRepoFiles, type ReferenceViolation as Vi
const root = resolve(import.meta.dirname, '..')
/** Repo-authored TypeScript that may cite docs in comments. */
const PATTERNS = ['packages/**/*.ts', 'examples/**/*.ts']
const PATTERNS = ['packages/**/*.ts']
/** Paths excluded from the scan: built output and vendored upstream source. */
const isExcluded = (p: string): boolean =>
-1
View File
@@ -23,7 +23,6 @@ const PATTERNS = [
'docs/**/*.md',
'packages/*/*.md',
'packages/*/*/*.md',
'examples/**/*.md',
'AGENTS.md',
'packages/AGENTS.md',
'.agents/skills/**/*.md',
+2 -1
View File
@@ -22,10 +22,11 @@ const PATTERNS = [
'docs/**/*.md',
'packages/*/*.md',
'packages/*/*/*.md',
'examples/**/system-prompt.expected.md',
'snapshots/**/system-prompt.expected.md',
'packages/**/system-prompt.expected.md',
'AGENTS.md',
'packages/AGENTS.md',
'snapshots/AGENTS.md',
]
/** A located hard-wrap: a prose paragraph spanning more than one source line. */
-1
View File
@@ -22,7 +22,6 @@ const PATTERNS = [
'docs/**/*.md',
'packages/*/*.md',
'packages/*/*/*.md',
'examples/**/*.md',
'AGENTS.md',
'packages/AGENTS.md',
'.agents/skills/**/*.md',
-1
View File
@@ -26,7 +26,6 @@ const PATTERNS = [
'AGENTS.md',
'packages/AGENTS.md',
'packages/**/*.ts',
'examples/**/*.ts',
]
/** Paths excluded from the scan: built output and vendored upstream source. */
@@ -34,6 +34,7 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
'packages/util/brand': 'The package is a type-only primitive erased at compile time.',
'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.',
}
/**
@@ -55,6 +56,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'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/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.' },
'packages/typert/registry': { kind: 'none', reason: 'Runtime type registry; consumers (cordis_inspect, wire faces, gates) own any model-visible projection of registry contents.' },
'packages/typert/loader': { kind: 'none', reason: 'Loader integration only registers generated artifacts; consumers own any model-visible projection.' },
@@ -67,14 +69,17 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-slots': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-attachment': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-primitives': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/store': { kind: 'none', reason: 'Browser-side state primitives; register nothing model-facing.' },
'packages/client/ui-renderer': { kind: 'none', reason: 'Browser-side render assembly; registers nothing model-facing.' },
'packages/client/ui-session': { kind: 'none', reason: 'Browser-side Session adapter; registers nothing model-facing.' },
'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/api/remotes': { kind: 'none', reason: 'The Remote BFF selects business methods and identity policy; selected services own any model-visible effect.' },
'packages/client/runtime': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/api/remotes': { kind: 'none', reason: 'The Remote BFF selects business methods and forwarded events; selected services own any model-visible effect.' },
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-brand-official': { kind: 'none', reason: 'Browser-side presentation occupants; registers nothing model-facing.' },
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-approval': { kind: 'none', reason: 'Browser-side approval presentation; registers nothing model-facing.' },
'packages/client/ui-chat': { kind: 'none', reason: 'Browser-side Chat presentation; registers nothing model-facing.' },
'packages/client/ui-message-feedback': { kind: 'none', reason: 'Browser-side controls over the message-feedback sidecar; ratings and notes never enter the Session log, model context, or telemetry.' },
'packages/client/ui-tool': { kind: 'none', reason: 'Browser-side Tool presentation layer; renders logged calls without changing model context.' },
'packages/client/ui-jobs': { kind: 'none', reason: 'Browser-side read-only projection of ctx.jobs records; dsh-tool-jobs owns the model-facing behavior.' },
@@ -112,6 +117,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/host/directory-picker-browse': { kind: 'none', reason: 'The GUI-host picking backend registers nothing model-facing.' },
'packages/host/directory-picker-native': { kind: 'none', reason: 'The GUI-host picking backend registers nothing model-facing.' },
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers nothing model-facing.' },
'packages/webhook/webhook-github': { kind: 'indirect', reason: 'The adapter delegates model-visible text to matching rules and dsh-webhook.' },
'packages/host/frontend-static': { kind: 'none', reason: 'The SPA dist server answers browser asset requests and registers nothing model-facing.' },
'packages/host/plugin-inventory': { kind: 'none', reason: 'Host-side read-only Loader projection; registers nothing model-facing.' },
'packages/bundle/base': { kind: 'indirect', reason: 'The bundle is a patch-list carrier; each inserted row\'s package owns its model-facing behavior.' },
@@ -146,21 +152,21 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/skill/skill-filesystem': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
'packages/spill/spill': { kind: 'indirect', reason: 'The storage seam delegates model rendering to spill consumers.' },
'packages/spill/spill-local': { kind: 'indirect', reason: 'The storage backend delegates model rendering to spill consumers.' },
'packages/test-support/acp-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' },
'packages/test-support/session-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' },
'packages/test-support/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model requests.' },
'packages/runtime-diagnostics/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' },
'packages/test-support/loader-smoke': { kind: 'none', reason: 'The test harness submits an ordinary user task but delegates prompt and tool composition to the loaded tree.' },
'packages/test-support/llm-mock-server': { kind: 'none', reason: 'The test server substitutes provider wire behavior without invoking a real model.' },
'packages/test-support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
'packages/api/gateway': { kind: 'none', reason: 'Remote dispatch infrastructure; invoked business methods own any model-visible effect.' },
'packages/api/session-controller': { kind: 'none', reason: 'Session API and transport owner; invoked Agent commands own any model-visible effect.' },
'packages/api/workspace-controller': { kind: 'none', reason: 'Workspace API and state projection owner; it registers no prompt, tool, or session event.' },
'packages/typert/protocol': { kind: 'none', reason: 'Compiler-independent Remote protocol declarations; registers nothing model-facing.' },
'packages/typert/generator': { kind: 'none', reason: 'The build-time generator runs outside any agent runtime and touches no model request.' },
'packages/jobs/jobs': { kind: 'indirect', reason: 'Producer and controller plugins own all model rendering over the job registry.' },
'packages/jobs/jobs-local': { kind: 'indirect', reason: 'The registry backend delegates model rendering to producer plugins and dsh-tool-jobs.' },
'packages/examples/acp-demo': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-spine-demo and dsh-acp.' },
'packages/boot/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' },
'packages/boot/cmdline': { kind: 'none', reason: 'Resolves the process command line before any session exists; configured rows own every model-visible consequence.' },
'packages/examples/jsonrpc-demo': { kind: 'indirect', reason: 'Only the externally configured plugin tree contributes model context.' },
'packages/interaction/permission-presets': { kind: 'indirect', reason: 'The service writes mechanism events rendered by dsh-user-approval and dsh-tool-bash.' },
'packages/interaction/user-questions': { kind: 'indirect', reason: 'Model-facing consumers render provider answers and seam errors.' },
'packages/util/timeout': { kind: 'indirect', reason: 'Only timeout consumers render timeout outcomes.' },
+15 -10
View File
@@ -21,6 +21,7 @@ const platforms = {
'linux-x64': { tag: 'manylinux_2_28_x86_64', executable: 'runtime-linux-x64' },
'linux-arm64': { tag: 'manylinux_2_28_aarch64', executable: 'runtime-linux-arm64' },
'macos-arm64': { tag: 'macosx_14_0_arm64', executable: 'runtime-macos-arm64' },
'win-x64': { tag: 'win_amd64', executable: 'runtime-win-x64.exe' },
}
function workspace(root: string, name: string, manifest: Record<string, unknown>): void {
@@ -35,11 +36,11 @@ afterEach(() => {
})
describe('verifyRuntimeClosure', () => {
it('requires only plugins active for a Linux or macOS target', async () => {
it('requires only plugins active for each published target', async () => {
const root = fixture({
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/shared': 'workspace:^' } },
'python/sdk-runtime/platforms.json': platforms,
'apps/cli/config/agent-presets/standard/agent.cordis.yml': `
'packages/preset/agent-presets/presets/standard/agent.cordis.yml': `
- id: tools
name: cordis:group
group: true
@@ -52,6 +53,9 @@ describe('verifyRuntimeClosure', () => {
- id: macos
name: '@scope/macos'
disabled: !!js process.platform !== 'darwin'
- id: windows
name: '@scope/windows'
disabled: !!js process.platform !== 'win32'
`,
})
@@ -61,6 +65,7 @@ describe('verifyRuntimeClosure', () => {
expect(result.failures).toEqual([
'standard preset -> @scope/linux (linux-arm64, linux-x64)',
'standard preset -> @scope/macos (macos-arm64)',
'standard preset -> @scope/windows (win-x64)',
])
})
@@ -68,7 +73,7 @@ describe('verifyRuntimeClosure', () => {
const root = fixture({
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: {} },
'python/sdk-runtime/platforms.json': platforms,
'apps/cli/config/agent-presets/standard/agent.cordis.yml': `
'packages/preset/agent-presets/presets/standard/agent.cordis.yml': `
- id: conditional
name: '@scope/conditional'
disabled: !!js process.env.DSH_DISABLE_CONDITIONAL === '1'
@@ -78,7 +83,7 @@ describe('verifyRuntimeClosure', () => {
const result = await verifyRuntimeClosure(root)
expect(result.failures).toEqual([
'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64)',
'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64, win-x64)',
])
})
@@ -86,7 +91,7 @@ describe('verifyRuntimeClosure', () => {
const root = fixture({
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/plugin': 'workspace:^' } },
'python/sdk-runtime/platforms.json': platforms,
'apps/cli/config/agent-presets/standard/agent.cordis.yml': `
'packages/preset/agent-presets/presets/standard/agent.cordis.yml': `
- id: plugin
name: '@scope/plugin'
config:
@@ -103,7 +108,7 @@ describe('verifyRuntimeClosure', () => {
const root = fixture({
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/plugin': '1.2.3' } },
'python/sdk-runtime/platforms.json': platforms,
'apps/cli/config/agent-presets/standard/agent.cordis.yml': `
'packages/preset/agent-presets/presets/standard/agent.cordis.yml': `
- id: plugin
name: '@scope/plugin'
`,
@@ -112,7 +117,7 @@ describe('verifyRuntimeClosure', () => {
const result = await verifyRuntimeClosure(root)
expect(result.failures).toEqual([
'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64)',
'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64, win-x64)',
])
})
@@ -126,7 +131,7 @@ describe('verifyRuntimeClosure', () => {
expect(result.presetCount).toBe(0)
expect(result.failures).toEqual([
'no agent presets matched apps/cli/config/agent-presets/*/agent.cordis.yml',
'no agent presets matched packages/preset/agent-presets/presets/*/agent.cordis.yml',
])
})
@@ -134,7 +139,7 @@ describe('verifyRuntimeClosure', () => {
const root = fixture({
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: {} },
'python/sdk-runtime/platforms.json': {},
'apps/cli/config/agent-presets/standard/agent.cordis.yml': '[]\n',
'packages/preset/agent-presets/presets/standard/agent.cordis.yml': '[]\n',
})
const result = await verifyRuntimeClosure(root)
@@ -148,7 +153,7 @@ describe('verifyRuntimeClosure', () => {
const root = fixture({
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/root': 'workspace:^' } },
'python/sdk-runtime/platforms.json': platforms,
'apps/cli/config/agent-presets/minimal/agent.cordis.yml': '[]\n',
'packages/preset/agent-presets/presets/minimal/agent.cordis.yml': '[]\n',
})
workspace(root, '@scope/root', {
peerDependencies: { '@scope/required': 'workspace:^', '@scope/optional': 'workspace:^' },
+2 -1
View File
@@ -30,7 +30,7 @@ interface RuntimePlatform {
type RuntimePlatformManifest = Record<string, RuntimePlatform>
const AGENT_PRESET_GLOB = 'apps/cli/config/agent-presets/*/agent.cordis.yml'
const AGENT_PRESET_GLOB = 'packages/preset/agent-presets/presets/*/agent.cordis.yml'
export interface RuntimeClosureResult {
failures: string[]
@@ -181,6 +181,7 @@ function disabledOnPlatform(value: unknown, processPlatform: string): boolean {
function processPlatformForTarget(target: string): string {
if (target.startsWith('linux-')) return 'linux'
if (target.startsWith('macos-')) return 'darwin'
if (target.startsWith('win-')) return 'win32'
throw new Error(`verify-runtime-closure: unsupported runtime target ${JSON.stringify(target)}`)
}
+123
View File
@@ -0,0 +1,123 @@
/** Regression coverage for package-group subsystem-page ownership. */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { describe, expect, it, onTestFinished } from 'vitest'
import { auditSubsystemPages } from './verify-subsystem-pages.ts'
function fixture(): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-subsystem-pages-'))
onTestFinished(() => {
rmSync(root, { recursive: true, force: true })
})
return root
}
function write(root: string, path: string, source: string): void {
const absolute = join(root, path)
mkdirSync(dirname(absolute), { recursive: true })
writeFileSync(absolute, source)
}
describe('package-group subsystem pages', () => {
it('accepts a direct page link and a justified no-page group', () => {
const root = fixture()
write(root, 'packages/alpha/README.md', '[types](../../docs/subsystems/alpha.md#contract)\n')
write(root, 'packages/alpha/alpha/package.json', '{}\n')
write(root, 'docs/subsystems/alpha.md', '# Alpha\n')
write(root, 'packages/adapter/README.md', '# Adapter\n')
expect(auditSubsystemPages(root, { adapter: 'Adapter over an existing subsystem.' })).toEqual({
groups: 2,
linked: 1,
exempt: 1,
violations: [],
})
})
it('rejects a new group whose README never declares subsystem ownership', () => {
const root = fixture()
write(root, 'packages/schedule/README.md', '# Schedule\n')
write(root, 'packages/schedule/tool-schedule/package.json', '{}\n')
expect(auditSubsystemPages(root, {}).violations).toEqual([
'packages/schedule/README.md: no reader-visible direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry',
])
})
it('does not treat the subsystem index or a Chinese counterpart as an owning page', () => {
const root = fixture()
write(
root,
'packages/wrong/README.md',
'[index](../../docs/subsystems/README.md) [Chinese](../../docs/subsystems/wrong.zh.md)\n',
)
write(root, 'docs/subsystems/README.md', '# Subsystems\n')
write(root, 'docs/subsystems/wrong.zh.md', '# Wrong\n')
expect(auditSubsystemPages(root, {}).violations).toEqual([
'packages/wrong/README.md: no reader-visible direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry',
])
})
it('does not count links hidden in code, comments, or image syntax', () => {
const root = fixture()
write(
root,
'packages/hidden/README.md',
[
'`[inline](../../docs/subsystems/hidden.md)`',
'```md',
'[fenced](../../docs/subsystems/hidden.md)',
'```',
'<!-- [comment](../../docs/subsystems/hidden.md) -->',
'![image](../../docs/subsystems/hidden.md)',
'',
].join('\n'),
)
write(root, 'docs/subsystems/hidden.md', '# Hidden\n')
expect(auditSubsystemPages(root, {}).violations).toEqual([
'packages/hidden/README.md: no reader-visible direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry',
])
})
it('rejects a link that escapes the subsystem directory', () => {
const root = fixture()
write(root, 'packages/escape/README.md', '[escape](../../docs/subsystems/../architecture.md)\n')
write(root, 'docs/architecture.md', '# Architecture\n')
expect(auditSubsystemPages(root, {}).violations).toEqual([
'packages/escape/README.md: no reader-visible direct docs/subsystems/*.md link; add the owning page and link, or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry',
])
})
it('rejects missing group READMEs and missing linked pages', () => {
const root = fixture()
write(root, 'packages/no-readme/pkg/package.json', '{}\n')
write(root, 'packages/broken/README.md', '[missing](../../docs/subsystems/missing.md)\n')
expect(auditSubsystemPages(root, {}).violations).toEqual([
'packages/broken/README.md: linked subsystem page does not exist: docs/subsystems/missing.md',
'packages/no-readme/README.md: package group has no group README declaring subsystem ownership',
])
})
it('rejects blank, orphaned, and stale exemptions', () => {
const root = fixture()
write(root, 'packages/linked/README.md', '[types](../../docs/subsystems/linked.md)\n')
write(root, 'docs/subsystems/linked.md', '# Linked\n')
write(root, 'packages/blank/README.md', '# Blank\n')
expect(auditSubsystemPages(root, {
blank: ' ',
linked: 'No page.',
orphan: 'Removed group.',
}).violations).toEqual([
'exemption blank: missing justification for omitting a subsystem page',
'exemption orphan: no matching package group; remove the stale entry',
'packages/linked/README.md: links a subsystem page but remains exempt; remove the stale exemption',
])
})
})
+145
View File
@@ -0,0 +1,145 @@
/**
* Doc-sync gate for package-group subsystem references. Every package group
* either links at least one existing `docs/subsystems/` page from its English
* group README or carries an explicit, justified exemption below.
*/
import { existsSync, globSync, readFileSync } from 'node:fs'
import { resolve, sep } from 'node:path'
import { parseMarkdown, visitMarkdown } from './markdown.ts'
const root = resolve(import.meta.dirname, '..')
/**
* Package groups that do not own a standalone subsystem reference. Reasons
* are reviewable policy: a new group cannot silently inherit an exemption.
*/
export const GROUPS_WITHOUT_SUBSYSTEM_PAGE: Readonly<Record<string, string>> = {
acp: 'Protocol transport entry point; the server package README owns its interoperability contract.',
api: 'Remote transport and BFF assembly; Typert and the package READMEs own the underlying contracts.',
boot: 'Shared application-bin boot library rather than a runtime subsystem.',
bundle: 'Composition patch carriers whose mounted packages own all runtime contracts.',
e2b: 'Provider implementations of the filesystem and subprocess subsystems, not a new capability contract.',
examples: 'Non-product demonstration compositions whose mounted packages own all runtime contracts.',
experimental: 'Empty staging group; promoted packages move to their product-role group before release.',
feedback: 'One command producer and inline log-event payload; its package README and persistence catalog own the complete contract.',
hooks: 'External hook-protocol bridges over existing interception points, not a new Harness service.',
identity: 'Shared anonymous correlation values rather than authenticated account or authorization behavior.',
mcp: 'Integration adapter that contributes external tools through the existing tool registry.',
sdk: 'Out-of-process protocol and client packages whose package READMEs own the SDK contracts.',
util: 'Low-level primitives whose business semantics remain with their consuming subsystems.',
}
/** Result of auditing package-group subsystem documentation. */
export interface SubsystemPageAudit {
/** Package groups discovered from group READMEs or child package manifests. */
readonly groups: number
/** Groups carrying at least one direct subsystem-page link. */
readonly linked: number
/** Groups covered by an explicit no-page policy. */
readonly exempt: number
/** Actionable contract violations. */
readonly violations: readonly string[]
}
/** Normalize one filesystem glob result to repository slash form. */
function normalize(path: string): string {
return path.split(sep).join('/')
}
/** Extract the package-group segment from a repository-relative path. */
function groupOf(path: string): string {
const group = path.split('/')[1]
if (group === undefined || group.length === 0) throw new Error(`invalid package path: ${path}`)
return group
}
/** Return canonical subsystem-page targets linked by one group README. */
function subsystemLinks(source: string): string[] {
const links = new Set<string>()
visitMarkdown(parseMarkdown(source), (node) => {
if (node.type !== 'link') return
const match = /^\.\.\/\.\.\/docs\/subsystems\/([^/#?]+\.md)(?:#[^?#]*)?$/.exec(node.url)
const page = match?.[1]
if (page !== undefined && page !== 'README.md' && !page.endsWith('.zh.md')) links.add(`docs/subsystems/${page}`)
})
return [...links].sort()
}
/**
* Audit package-group subsystem ownership for one repository tree.
* @param scanRoot - repository root containing `packages/` and `docs/`.
* @param exemptions - groups intentionally carrying no subsystem-page link.
* @returns counts plus every actionable violation.
*/
export function auditSubsystemPages(
scanRoot: string = root,
exemptions: Readonly<Record<string, string>> = GROUPS_WITHOUT_SUBSYSTEM_PAGE,
): SubsystemPageAudit {
const readmes = globSync('packages/*/README.md', { cwd: scanRoot }).map(normalize).sort()
const manifests = globSync('packages/*/*/package.json', { cwd: scanRoot }).map(normalize).sort()
const groups = new Set([...readmes, ...manifests].map(groupOf))
const violations: string[] = []
let linked = 0
let exempt = 0
for (const [group, reason] of Object.entries(exemptions)) {
if (!groups.has(group)) {
violations.push(`exemption ${group}: no matching package group; remove the stale entry`)
}
if (reason.trim().length === 0) {
violations.push(`exemption ${group}: missing justification for omitting a subsystem page`)
}
}
for (const group of [...groups].sort()) {
const readme = `packages/${group}/README.md`
const readmePath = resolve(scanRoot, readme)
if (!existsSync(readmePath)) {
violations.push(`${readme}: package group has no group README declaring subsystem ownership`)
continue
}
const links = subsystemLinks(readFileSync(readmePath, 'utf8'))
const isExempt = Object.hasOwn(exemptions, group)
if (links.length === 0) {
if (isExempt) {
exempt += 1
} else {
violations.push(
`${readme}: no reader-visible direct docs/subsystems/*.md link; add the owning page and link,`
+ ' or add a justified GROUPS_WITHOUT_SUBSYSTEM_PAGE entry',
)
}
continue
}
linked += 1
if (isExempt) {
violations.push(`${readme}: links a subsystem page but remains exempt; remove the stale exemption`)
}
for (const page of links) {
if (!existsSync(resolve(scanRoot, page))) {
violations.push(`${readme}: linked subsystem page does not exist: ${page}`)
}
}
}
return { groups: groups.size, linked, exempt, violations }
}
/** Run the repository audit as a standalone doc-sync gate. */
function main(): void {
const audit = auditSubsystemPages()
if (audit.violations.length > 0) {
console.error('verify-subsystem-pages: package-group documentation violations found:')
for (const violation of audit.violations) console.error(` ${violation}`)
process.exit(1)
}
console.log(
`verify-subsystem-pages: ${String(audit.groups)} group(s) checked`
+ ` (${String(audit.linked)} linked, ${String(audit.exempt)} explicitly exempt), all conform.`,
)
}
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) main()
+7 -6
View File
@@ -24,14 +24,15 @@ try {
if (mode !== undefined && mode !== '--snapshot') throw new Error(`unsupported argument ${JSON.stringify(mode)}`)
const document = read('docs/i18n/translation-prompt.md')
const terminology = read('docs/i18n/terminology.md')
// Synthetic reviewed examples, not live documents: editing a paired document must not
// churn the prompt snapshot. Each pair mirrors the other side's structure and uses
// terminology-table forms.
const examplePaths = [
['README.md', 'README.zh.md'],
['docs/development.md', 'docs/development.zh.md'],
['docs/i18n/README.md', 'docs/i18n/README.zh.md'],
['docs/i18n/translation-rules.md', 'docs/i18n/translation-rules.zh.md'],
['scripts/fixtures/translation-prompt/examples/product.md', 'scripts/fixtures/translation-prompt/examples/product.zh.md'],
['scripts/fixtures/translation-prompt/examples/rules.md', 'scripts/fixtures/translation-prompt/examples/rules.zh.md'],
[
'.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md',
'.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md',
'scripts/fixtures/translation-prompt/examples/agent-note.md',
'scripts/fixtures/translation-prompt/examples/agent-note.zh.md',
],
] as const
const examples: TranslationExample[] = examplePaths.map(([english, chinese]) => ({
+73 -3
View File
@@ -28,6 +28,13 @@ interface ManifestEntry {
symbol: string
/** Source file (repo-relative) that exports the symbol. */
source: string
/** Explicit module augmentations whose members complete an interface. */
augmentations?: Array<{
/** Source file (repo-relative) containing the augmentation. */
source: string
/** String-literal module specifier containing the merged interface. */
module: string
}>
/** Complete declaration (default), or a body-stripped public class API. */
projection?: 'public-api'
}
@@ -133,6 +140,67 @@ function sourceDeclaration(sourceRel: string, symbol: string): string | null {
return null
}
interface InterfacePart {
text: string
sourceFile: ts.SourceFile
declaration: ts.InterfaceDeclaration
}
/** Find one top-level interface declaration in a source file. */
function sourceInterface(sourceRel: string, symbol: string): InterfacePart | null {
const abs = resolve(root, sourceRel)
const text = readFileSync(abs, 'utf8')
const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, /* setParentNodes */ true)
const declaration = sourceFile.statements.find((statement): statement is ts.InterfaceDeclaration =>
ts.isInterfaceDeclaration(statement) && statement.name.text === symbol,
)
return declaration === undefined ? null : { text, sourceFile, declaration }
}
/** Find one interface declaration inside an explicit string-literal module augmentation. */
function augmentedInterface(sourceRel: string, moduleName: string, symbol: string): InterfacePart | null {
const abs = resolve(root, sourceRel)
const text = readFileSync(abs, 'utf8')
const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, /* setParentNodes */ true)
for (const statement of sourceFile.statements) {
if (!ts.isModuleDeclaration(statement) || !ts.isStringLiteral(statement.name)
|| statement.name.text !== moduleName || !statement.body || !ts.isModuleBlock(statement.body)) continue
const declaration = statement.body.statements.find((member): member is ts.InterfaceDeclaration =>
ts.isInterfaceDeclaration(member) && member.name.text === symbol,
)
if (declaration !== undefined) return { text, sourceFile, declaration }
}
return null
}
/** Render one interface plus explicitly named module augmentations as its merged declaration. */
function mergedInterfaceDeclaration(entry: ManifestEntry): string | null {
const base = sourceInterface(entry.source, entry.symbol)
if (base === null) return null
const additions: InterfacePart[] = []
for (const augmentation of entry.augmentations ?? []) {
const part = augmentedInterface(augmentation.source, augmentation.module, entry.symbol)
if (part === null) return null
additions.push(part)
}
const parts = [base, ...additions]
const docs = parts.map(part => sourceJSDoc(part.text, part.declaration)).filter(Boolean)
const typeParameters = base.declaration.typeParameters
?.map(parameter => parameter.getText(base.sourceFile)).join(', ')
const heritage = parts.flatMap(part =>
part.declaration.heritageClauses?.map(clause => clause.getText(part.sourceFile)) ?? [],
).join(' ')
const header = `interface ${entry.symbol}${typeParameters ? `<${typeParameters}>` : ''}${heritage ? ` ${heritage}` : ''} {`
const members = parts.flatMap(part => part.declaration.members.map((member) => {
const jsDoc = sourceJSDoc(part.text, member)
const declaration = part.text.slice(member.getStart(part.sourceFile), member.getEnd())
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
}))
const declaration = [header, ...members.map(member => member.split('\n').map(line => ` ${line}`).join('\n')), '}'].join('\n')
return docs.length === 0 ? declaration : `${docs.join('\n')}\n${declaration}`
}
/** Leading source JSDoc attached to one declaration or member. */
function sourceJSDoc(text: string, node: ts.Node): string {
return ts.getJSDocCommentsAndTags(node)
@@ -270,9 +338,11 @@ let verified = 0
for (const e of entries) {
const b = blockByKey.get(keyOf(e))
if (!b) continue // already reported as an orphan entry
const decl = e.projection === 'public-api'
? sourcePublicApi(e.source, e.symbol)
: sourceDeclaration(e.source, e.symbol)
const decl = e.augmentations !== undefined
? mergedInterfaceDeclaration(e)
: e.projection === 'public-api'
? sourcePublicApi(e.source, e.symbol)
: sourceDeclaration(e.source, e.symbol)
if (decl === null) {
errors.push(`symbol ${e.symbol} not found in ${e.source} (manifest entry for ${e.doc})`)
continue
+1 -1
View File
@@ -243,7 +243,7 @@ grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-ga
# Host face before compiling and bundling the Client face.
# Both statuses are captured so one failure cannot hide the other's result.
build_gate() {
wine_node "$scratch/logs/host-tsc.log" "$tsc_js" -b tsconfig.host.json --pretty false || return $?
wine_node "$scratch/logs/host-tsc.log" --max-old-space-size=4096 "$tsc_js" -b tsconfig.host.json --pretty false || return $?
wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $?
wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $?
wine_node "$scratch/logs/client-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE client