mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge latest master into Codex subagent provider
This commit is contained in:
BIN
scripts/attribute-chunk-bytes.mjs
Normal file
BIN
scripts/attribute-chunk-bytes.mjs
Normal file
Binary file not shown.
@@ -7,6 +7,7 @@
|
||||
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||
import { join, relative, resolve } from 'node:path'
|
||||
import { isForbiddenPublicationFile } from './publication-payload.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
// vendor/* is single-level; packages/<group>/<pkg> nests one level deeper
|
||||
@@ -14,6 +15,7 @@ const root = resolve(import.meta.dirname, '..')
|
||||
const workspaceGlobs = [
|
||||
{ dir: 'vendor', depth: 1 },
|
||||
{ dir: 'packages', depth: 2 },
|
||||
{ dir: 'apps', depth: 1 },
|
||||
] as const
|
||||
const vendoredPackages = new Set([
|
||||
'cordis',
|
||||
@@ -28,6 +30,10 @@ const vendoredPackages = new Set([
|
||||
])
|
||||
|
||||
const localArtifactDirs = new Set(['node_modules'])
|
||||
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
|
||||
'@deepseek-ai/dsh': ['lib/*.js', 'config'],
|
||||
'@deepseek-ai/dsh-frontend': ['dist'],
|
||||
}
|
||||
|
||||
/** The subset of package.json fields this constraint check cares about. */
|
||||
interface PackageManifest {
|
||||
@@ -96,7 +102,13 @@ function workspaceManifests(): WorkspaceManifest[] {
|
||||
}
|
||||
|
||||
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// Profile bundles publish their dsh.bundle.patch layer beside the lib.
|
||||
'@deepseek-ai/dsh-base': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
|
||||
'@deepseek-ai/dsh-helper': ['lib/assets'],
|
||||
'@deepseek-ai/dsh-pty-local': ['scripts/ensure-spawn-helper.mjs'],
|
||||
'@deepseek-ai/dsh-scripts': [
|
||||
'lib/dev/tsdown-config.js',
|
||||
'lib/local-plugin-loader-hooks.js',
|
||||
@@ -133,8 +145,6 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
||||
// declarations.
|
||||
...usesEmittedTreeDefaults(manifest) ? ['lib/types/**/*.js'] : [],
|
||||
'lib/types/**/*.d.ts',
|
||||
'lib/types/**/*.d.ts.map',
|
||||
'src',
|
||||
]
|
||||
}
|
||||
|
||||
@@ -164,7 +174,24 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
return errors
|
||||
}
|
||||
|
||||
if (manifest.name?.startsWith('@deepseek-ai/dsh-') && manifest.name !== '@deepseek-ai/dsh-root') {
|
||||
if (manifest.name?.startsWith('@deepseek-ai/')) {
|
||||
for (const file of manifest.files ?? []) {
|
||||
if (isForbiddenPublicationFile(file)) {
|
||||
errors.push(`${label}: package.json files must not publish ${JSON.stringify(file)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dir.startsWith('apps/') && manifest.name?.startsWith('@deepseek-ai/')) {
|
||||
const expectedFiles = appPackageFiles[manifest.name]
|
||||
if (expectedFiles === undefined) {
|
||||
errors.push(`${label}: app package has no publication files policy`)
|
||||
} else if (!sameStringList(manifest.files, expectedFiles)) {
|
||||
errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (dir.startsWith('packages/') && manifest.name?.startsWith('@deepseek-ai/dsh-')) {
|
||||
const peer = manifest.peerDependencies?.cordis
|
||||
const dev = manifest.devDependencies?.cordis
|
||||
|
||||
|
||||
108
scripts/coverage-uncovered-locations.cjs
Normal file
108
scripts/coverage-uncovered-locations.cjs
Normal file
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Istanbul coverage reporter printing one clickable `path:line:col` record per
|
||||
* uncovered statement, branch path, and function. Vitest's per-file threshold
|
||||
* failures name only the file; this reporter supplies the exact locations,
|
||||
* printed just above those ERROR lines (reports run before threshold checks).
|
||||
* Files at 100% print nothing, so a green run stays silent.
|
||||
*
|
||||
* CommonJS by requirement: istanbul-reports loads custom reporters with a bare
|
||||
* require() outside the tsx/ESM pipeline (istanbul-reports index.js create()),
|
||||
* so this file can be neither TypeScript nor ESM. Wired into vitest.config.ts
|
||||
* by absolute path — require() would resolve a relative specifier against
|
||||
* istanbul-reports' own directory.
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
const { ReportBase } = require('istanbul-lib-report');
|
||||
|
||||
/**
|
||||
* Editor-convention `line:column` of an istanbul location start (istanbul
|
||||
* columns are 0-based; editors and terminal link handlers expect 1-based).
|
||||
*/
|
||||
function pos(loc) {
|
||||
return `${loc.start.line}:${loc.start.column + 1}`;
|
||||
}
|
||||
|
||||
/** Whether a location carries a usable 1-based start line. */
|
||||
function usable(loc) {
|
||||
return Boolean(loc && loc.start && Number.isFinite(loc.start.line) && loc.start.line >= 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* ` (to line:col)` suffix when the range end adds information beyond the
|
||||
* start. v8-remapped whole-line statements carry end.column = Infinity; those
|
||||
* degrade to a line-only suffix, or to nothing on a single line.
|
||||
*/
|
||||
function endSuffix(loc) {
|
||||
const end = loc.end;
|
||||
if (!end || !Number.isFinite(end.line) || end.line < 1) return '';
|
||||
if (!Number.isFinite(end.column)) {
|
||||
return end.line === loc.start.line ? '' : ` (to ${end.line})`;
|
||||
}
|
||||
if (end.line === loc.start.line && end.column === loc.start.column) return '';
|
||||
return ` (to ${end.line}:${end.column + 1})`;
|
||||
}
|
||||
|
||||
class UncoveredLocationsReport extends ReportBase {
|
||||
constructor(opts = {}) {
|
||||
super(opts);
|
||||
// Vitest passes the resolved config root alongside reporter options.
|
||||
this.projectRoot = opts.projectRoot || process.cwd();
|
||||
this.records = [];
|
||||
}
|
||||
|
||||
onStart() {
|
||||
this.records = [];
|
||||
}
|
||||
|
||||
onDetail(node) {
|
||||
const fc = node.getFileCoverage();
|
||||
const rel = path.relative(this.projectRoot, fc.path).split(path.sep).join('/');
|
||||
const items = [];
|
||||
const add = (loc, text) => items.push({ line: loc.start.line, column: loc.start.column, text });
|
||||
|
||||
for (const id of Object.keys(fc.statementMap)) {
|
||||
if (fc.s[id] !== 0) continue;
|
||||
const loc = fc.statementMap[id];
|
||||
if (!usable(loc)) continue;
|
||||
add(loc, `${rel}:${pos(loc)} uncovered statement${endSuffix(loc)}`);
|
||||
}
|
||||
|
||||
for (const id of Object.keys(fc.fnMap)) {
|
||||
if (fc.f[id] !== 0) continue;
|
||||
const fn = fc.fnMap[id];
|
||||
const loc = usable(fn.decl) ? fn.decl : fn.loc;
|
||||
if (!usable(loc)) continue;
|
||||
const name = fn.name ? ` ${fn.name}` : '';
|
||||
add(loc, `${rel}:${pos(loc)} uncovered function${name}`);
|
||||
}
|
||||
|
||||
for (const id of Object.keys(fc.branchMap)) {
|
||||
const counts = fc.b[id];
|
||||
const branch = fc.branchMap[id];
|
||||
for (let i = 0; i < counts.length; i += 1) {
|
||||
if (counts[i] !== 0) continue;
|
||||
// Implicit arms (e.g. a missing else) may carry an empty location;
|
||||
// fall back to the branch's own span so the record stays clickable.
|
||||
const loc = usable(branch.locations && branch.locations[i]) ? branch.locations[i] : branch.loc;
|
||||
if (!usable(loc)) continue;
|
||||
add(loc, `${rel}:${pos(loc)} uncovered branch (${branch.type}, path ${i + 1}/${counts.length})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0) return;
|
||||
items.sort((a, b) => a.line - b.line || a.column - b.column);
|
||||
for (const item of items) this.records.push(item.text);
|
||||
}
|
||||
|
||||
onEnd() {
|
||||
if (this.records.length === 0) return;
|
||||
console.log(`\nUncovered locations (per-file 100% gate): ${this.records.length}`);
|
||||
for (const record of this.records) console.log(record);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UncoveredLocationsReport;
|
||||
@@ -6,7 +6,7 @@ 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', '--config', 'examples/web-cordis/cordis.yml']],
|
||||
['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']],
|
||||
])
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"AGENTS.md": 1775,
|
||||
"docs/AGENTS.md": 1150,
|
||||
"docs/AGENTS.md": 1320,
|
||||
"docs/architecture.md": 2160,
|
||||
"docs/cordis-primer.md": 600,
|
||||
"docs/defensive-patterns.md": 550,
|
||||
|
||||
@@ -28,12 +28,11 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ContinuationDecision: 'core.md',
|
||||
ContinuationStop: 'core.md',
|
||||
GenerateOptions: 'core.md',
|
||||
InboxItem: 'core.md',
|
||||
InboxPlacement: 'core.md',
|
||||
MessageId: 'core.md',
|
||||
HookContext: 'core.md',
|
||||
SettleReason: 'core.md',
|
||||
AdapterRegistrationHandle: 'core.md',
|
||||
DirectoryRegistrationHandle: 'core.md',
|
||||
LlmCallConfig: 'core.md',
|
||||
LlmModelContext: 'core.md',
|
||||
LlmModelReasoningInfo: 'core.md',
|
||||
@@ -42,13 +41,16 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
LlmModelInfo: 'core.md',
|
||||
LlmProviderInfo: 'core.md',
|
||||
LlmConfigurableProvider: 'core.md',
|
||||
LlmModelDiscoveryRequest: 'core.md',
|
||||
LlmDiscoveredModel: 'core.md',
|
||||
ResolvedRetryPolicy: 'llm-streaming.md',
|
||||
Message: 'core.md',
|
||||
MessageSource: 'core.md',
|
||||
UserMessage: 'session.md',
|
||||
PromptDecision: 'core.md',
|
||||
RequestError: 'core.md',
|
||||
PreStepDecision: 'core.md',
|
||||
PreStepContext: 'core.md',
|
||||
RequestErrorAction: 'core.md',
|
||||
RequestFailureContext: 'core.md',
|
||||
PreparedReferencedMessage: 'session-reference.md',
|
||||
SessionReferenceCandidate: 'session-reference.md',
|
||||
SessionReferenceInput: 'session-reference.md',
|
||||
@@ -103,8 +105,11 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
StreamChunk: 'llm-streaming.md',
|
||||
SkillProviderControl: 'skills.md',
|
||||
CreateSessionOptions: 'persistence.md',
|
||||
PrepareSessionOptions: 'persistence.md',
|
||||
SessionHeader: 'persistence.md',
|
||||
SessionInspection: 'persistence.md',
|
||||
SessionLocation: 'persistence.md',
|
||||
SessionPreparation: 'persistence.md',
|
||||
SessionPersistenceSnapshot: 'persistence.md',
|
||||
ConfinedArgv: 'sandbox.md',
|
||||
SandboxExecutionPolicy: 'sandbox.md',
|
||||
|
||||
@@ -598,7 +598,8 @@ function parseExampleCordis(rel: string): ExamplePlugin[] {
|
||||
if (current?.name) plugins.push({ id: current.id, name: current.name })
|
||||
}
|
||||
for (const line of text.split('\n')) {
|
||||
const id = /^-\s+id:\s+(.+?)\s*$/.exec(line)
|
||||
// Top-level rows (`- id:`) and bundle-patch insert rows (` - id:`).
|
||||
const id = /^\s*-\s+id:\s+(.+?)\s*$/.exec(line)
|
||||
if (id?.[1] !== undefined) {
|
||||
flush()
|
||||
current = { id: stripYamlScalar(id[1]) }
|
||||
@@ -620,9 +621,9 @@ const APP_EXAMPLES = [
|
||||
id: 'dsh_base',
|
||||
rel: 'apps/cli/composition.md',
|
||||
title: 'DSH Base Composition',
|
||||
label: 'apps/cli/config/base.cordis.yml',
|
||||
config: 'apps/cli/config/base.cordis.yml',
|
||||
summary: 'The raw CLI applies one required caller-selected patch list over this shared base; Web and headless apply their own shipped overlays.',
|
||||
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',
|
||||
@@ -1153,20 +1154,22 @@ function renderLifecycle(): string {
|
||||
' participant Session',
|
||||
' participant SDK as UI or SDK listener',
|
||||
' User->>Agent: followup(content)',
|
||||
` Agent-->>SDK: ${mermaidCode('agent/inbox/enqueue')}`,
|
||||
` Agent-->>SDK: ${mermaidCode('agent/inbox/spliced')}`,
|
||||
` Agent-->>SDK: ${mermaidCode('agent/inbox/inserted')} { message }`,
|
||||
' Agent->>Driver: queued work wakes driver',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/status')} running`,
|
||||
' Note over Agent,Driver: next-step acceptance window opens',
|
||||
` Driver->>Hooks: ${mermaidCode('agent/prompt-submit')} waterfall`,
|
||||
' Hooks-->>Driver: authoritative allow, block, or add context',
|
||||
' alt prompt blocked or admission failed',
|
||||
' Driver-->>Driver: append context-only batch or keep steering boundary pending',
|
||||
' else prompt allowed',
|
||||
' Note over Agent,Driver: claim pending next-step input plus one queued prompt',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/inbox/spliced')} pure deletion`,
|
||||
` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`,
|
||||
' Hooks-->>Driver: authoritative reject or enter(messages)',
|
||||
' alt proposed step rejected or pre-step failed',
|
||||
' Driver-->>Driver: claimed batch stays removed, no turn opens',
|
||||
' else enter proposed step',
|
||||
` Driver->>Session: ${mermaidCode('turn/start')}`,
|
||||
` Driver->>Session: ${mermaidCode('user/message')}`,
|
||||
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
|
||||
` Driver-->>Driver: ${mermaidCode('agent/step')} serial checkpoint`,
|
||||
` Driver->>Session: ${mermaidCode('step/start')}`,
|
||||
` Driver->>Session: ${mermaidCode('user/message')} per entered message`,
|
||||
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
|
||||
` Driver->>LLM: ${mermaidCode('agent/request')} waterfall, then ${mermaidCode('llm/stream')} waterfall`,
|
||||
' LLM-->>Driver: StreamChunk*',
|
||||
` Driver->>Session: ${mermaidCode('assistant/chunk')}*`,
|
||||
@@ -1189,11 +1192,17 @@ function renderLifecycle(): string {
|
||||
` Driver->>Session: ${mermaidCode('tool/result')}`,
|
||||
' end',
|
||||
' end',
|
||||
' Driver->>Session: post-tool context and steering (no prompt-submit)',
|
||||
` Driver->>Session: ${mermaidCode('step/end')}`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`,
|
||||
' opt natural stop and next-step inbox empty',
|
||||
` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`,
|
||||
' end',
|
||||
' opt next-step input is pending',
|
||||
' Driver-->>Driver: claim pending next-step input',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`,
|
||||
' Hooks-->>Driver: authoritative reject or enter(messages)',
|
||||
' end',
|
||||
' end',
|
||||
' Note over Agent,Driver: next-step acceptance window closes',
|
||||
` Driver->>Session: ${mermaidCode('turn/end')}`,
|
||||
' end',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/status')} idle`,
|
||||
@@ -1201,9 +1210,9 @@ function renderLifecycle(): string {
|
||||
'',
|
||||
'The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.',
|
||||
'',
|
||||
'`dsh-compact-basic` uses `agent/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.',
|
||||
'`dsh-compact-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/prompt-submit` allow is authoritative; listeners wrapping `next()` preserve downstream content and additional contexts unless replacement is intentional. Steering bypasses that waterfall and joins at its durable checkpoint.',
|
||||
'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 boundary claims their next-step batch.',
|
||||
'',
|
||||
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.',
|
||||
'',
|
||||
|
||||
@@ -562,7 +562,7 @@ ${vendored.map(row => `| \`${row.npmName}\` | [${row.upstream.replace('https://'
|
||||
|
||||
## Runtime npm dependencies
|
||||
|
||||
External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI/TUI, the Web UI, and the Python SDK runtime load by default.
|
||||
External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default.
|
||||
|
||||
${renderNpmTable(runtimeDeps)}
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
dir: 'tool-goal',
|
||||
source: 'packages/goal/tool-goal/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.agents', 'ctx.goals', 'ctx.systemPrompt', 'a calling Agent in an authorized open turn'],
|
||||
writes: ['tool/call', 'user/message goal snapshot for mutations', 'tool/result'],
|
||||
writes: ['tool/call', 'goal/change for mutations', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(GoalService)
|
||||
@@ -392,7 +392,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
await ctx.plugin(ToolSubagent, { provider: 'mock' })
|
||||
},
|
||||
note:
|
||||
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `apps/cli/config/base.cordis.yml` and `examples/acp-agent/cordis.yml`.',
|
||||
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-subagent-control',
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
import lefthookPackage from 'lefthook/package.json' with { type: 'json' }
|
||||
|
||||
const MINIMUM_GIT = [2, 26, 0]
|
||||
const HOOKS_DIRECTORY = 'dsh-hooks'
|
||||
@@ -596,6 +597,7 @@ function refuseScopedHooksPath(entry) {
|
||||
|
||||
async function main() {
|
||||
if (process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true') return
|
||||
if (typeof lefthookPackage.bin?.lefthook !== 'string') return
|
||||
const probe = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' })
|
||||
if (probe.status !== 0) return
|
||||
const root = stripGitLineTerminator(probe.stdout)
|
||||
|
||||
@@ -19,6 +19,9 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const installer = fileURLToPath(new URL('./install-lefthook.mjs', import.meta.url))
|
||||
const fixtures: string[] = []
|
||||
// Multi-worktree cases spawn several Git and Node subprocesses; coverage concurrency can
|
||||
// legitimately exceed Vitest's default deadline without changing the installer behavior.
|
||||
const MULTI_PROCESS_TEST_TIMEOUT_MS = 20_000
|
||||
|
||||
interface Fixture {
|
||||
container: string
|
||||
@@ -260,7 +263,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
git(fixture, fixture.main, ['worktree', 'remove', '--force', fixture.linked])
|
||||
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBeforeRemoval)
|
||||
expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy hook\n')
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it('replaces the owned hook path Git copies into a newly added worktree', async () => {
|
||||
const fixture = createFixture()
|
||||
@@ -284,7 +287,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
'# config=late-linked-worktree-config',
|
||||
)
|
||||
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBefore)
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it('serializes concurrent installs and keeps repeated output stable', async () => {
|
||||
const fixture = createFixture()
|
||||
@@ -305,7 +308,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
expect(readFileSync(mainHookPath, 'utf8')).toBe(initialHook)
|
||||
expect(existsSync(join(commonDirectory(fixture), 'dsh-lefthook-install.lock'))).toBe(false)
|
||||
expect(existsSync(join(hooksPath(fixture, fixture.main), '.fake-lefthook-running'))).toBe(false)
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it('waits for a concurrent installer to finish publishing its lock record', async () => {
|
||||
const fixture = createFixture()
|
||||
@@ -343,7 +346,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
expect(readFileSync(join(movedHooks, '.dsh-lefthook-owned'), 'utf8')).toContain(
|
||||
JSON.stringify(movedHooks),
|
||||
)
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it.skipIf(process.platform === 'win32')('refuses a multiply linked ownership marker before relocation rewrites it', async () => {
|
||||
const fixture = createFixture()
|
||||
@@ -384,7 +387,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
expect(result.stderr).toContain('non-regular or multiply linked hook entry')
|
||||
expect(readFileSync(externalHook, 'utf8')).toBe(externalContent)
|
||||
}
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it('restores the marker-backed stale hook path when relocation reinstall fails', async () => {
|
||||
const fixture = createFixture()
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
# DSH_MASTER master clone directory (default: $DSH_SOURCE/master)
|
||||
# DSH_CURRENT stable symlink to the active worktree (default: $DSH_SOURCE/current)
|
||||
# DSH_BIN_DIR directory the `dsh` symlink lands in (default: ~/.local/bin)
|
||||
# DSH_HOME Harness home holding the personal config (default: ~/.dsh)
|
||||
# DSH_HOME Harness home holding profiles and user patches (default: ~/.dsh)
|
||||
# FIXME(install-ts): Move the post-checkout workflow into a tested TypeScript
|
||||
# entrypoint; keep this POSIX shell file as the curl/source bootstrap.
|
||||
set -eu
|
||||
|
||||
@@ -47,7 +47,7 @@ function fixture(options: {
|
||||
default: './lib/invariant.js',
|
||||
},
|
||||
},
|
||||
files: ['lib/index.js', 'lib/invariant.js', 'src'],
|
||||
files: ['lib/index.js', 'lib/invariant.js'],
|
||||
peerDependencies: options.invariantDependency === false ? {} : {
|
||||
'@deepseek-ai/dsh-invariants': '^0.0.1',
|
||||
},
|
||||
|
||||
54
scripts/publication-payload.spec.ts
Normal file
54
scripts/publication-payload.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isForbiddenPublicationFile, validateTarballPayload } from './publication-payload.ts'
|
||||
|
||||
function validateFixtureTarball(files: readonly string[]): () => void {
|
||||
return () => {
|
||||
validateTarballPayload(files, 'fixture.tgz')
|
||||
}
|
||||
}
|
||||
|
||||
describe('publication payload policy', () => {
|
||||
it.each([
|
||||
'lib/index.js',
|
||||
'lib/types/index.d.ts',
|
||||
'lib/styles/base.css',
|
||||
])('accepts %s', (file) => {
|
||||
expect(isForbiddenPublicationFile(file)).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src',
|
||||
'./src',
|
||||
'src/',
|
||||
'src/index.ts',
|
||||
'./src/index.ts',
|
||||
String.raw`src\index.ts`,
|
||||
'lib/types/index.d.ts.map',
|
||||
'./lib/types/index.d.ts.map',
|
||||
])('rejects static manifest path %s', (file) => {
|
||||
expect(isForbiddenPublicationFile(file)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects source members in packed tarballs', () => {
|
||||
expect(validateFixtureTarball([
|
||||
'package/package.json',
|
||||
'package/src/index.ts',
|
||||
])).toThrow('fixture.tgz publishes source file package/src/index.ts')
|
||||
})
|
||||
|
||||
it('rejects declaration maps in packed tarballs', () => {
|
||||
expect(validateFixtureTarball([
|
||||
'package/package.json',
|
||||
'package/lib/types/index.d.ts.map',
|
||||
])).toThrow('fixture.tgz publishes declaration map package/lib/types/index.d.ts.map')
|
||||
})
|
||||
|
||||
it('accepts a clean packed tarball', () => {
|
||||
expect(validateFixtureTarball([
|
||||
'package/package.json',
|
||||
'package/lib/index.js',
|
||||
'package/lib/types/index.d.ts',
|
||||
'package/lib/styles/base.css',
|
||||
])).not.toThrow()
|
||||
})
|
||||
})
|
||||
27
scripts/publication-payload.ts
Normal file
27
scripts/publication-payload.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/** Publication payload policy shared by static manifests and packed tarballs. */
|
||||
|
||||
/** Normalize a package manifest path or npm tarball member to its payload-relative path. */
|
||||
function payloadPath(file: string): string {
|
||||
const normalized = file.replaceAll('\\', '/').replace(/^\.\/+/, '').replace(/\/+$/, '')
|
||||
return normalized.startsWith('package/') ? normalized.slice('package/'.length) : normalized
|
||||
}
|
||||
|
||||
/** Whether a package payload path exposes source or declaration-map intermediates. */
|
||||
export function isForbiddenPublicationFile(file: string): boolean {
|
||||
const normalized = payloadPath(file)
|
||||
return normalized === 'src'
|
||||
|| normalized.startsWith('src/')
|
||||
|| normalized.endsWith('.d.ts.map')
|
||||
}
|
||||
|
||||
/** Reject source and declaration-map members in a packed npm tarball. */
|
||||
export function validateTarballPayload(files: readonly string[], context: string): void {
|
||||
for (const file of files) {
|
||||
if (!isForbiddenPublicationFile(file)) continue
|
||||
const normalized = payloadPath(file)
|
||||
if (normalized === 'src' || normalized.startsWith('src/')) {
|
||||
throw new Error(`${context} publishes source file ${file}`)
|
||||
}
|
||||
throw new Error(`${context} publishes declaration map ${file}`)
|
||||
}
|
||||
}
|
||||
1084
scripts/publish-npm-baseline.ts
Normal file
1084
scripts/publish-npm-baseline.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
/** Unit tests for the prompt-v4 renderer and three-section response parser. */
|
||||
/** Unit tests for the prompt-v7 content and unchanged three-section protocol. */
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
@@ -15,6 +15,20 @@ const root = resolve(import.meta.dirname, '..')
|
||||
const document = readFileSync(join(root, 'docs/i18n/translation-prompt.md'), 'utf8')
|
||||
const terminology = '| English | 中文 |\n|---|---|\n| agent | agent |'
|
||||
|
||||
const retainedExamples = [
|
||||
['### Colloquial verb → Professional verb', 'The repo pins pnpm@11.7.0 in package.json', '该仓库在 package.json 中固定使用 pnpm@11.7.0'],
|
||||
['### Run-on sentence → Natural phrasing with pause', 'Read docs/architecture.md before changing anything under packages/.', '在修改 packages/ 目录下的任何内容之前,请先阅读 docs/architecture.md。'],
|
||||
['### Stiff passive voice → Active and natural', 'a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.', '门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。'],
|
||||
['### Invented word → Natural expression', 'A sidecar record of both blob hashes makes consistency checkable', '伴随记录保存两侧 blob hash,使一致性可检查'],
|
||||
['### Em-dash → Colon/period', 'FIXME — an issue that should block a new release.', 'FIXME:应当阻塞新版本发布的问题。'],
|
||||
['### Overly literal → Meaningful rendering', 'awkward phrasing is easier to hear without the source anchoring you', '不对照原文时,更容易察觉别扭的表达'],
|
||||
['### Terminology — do not translate what should be kept in English', 'typed service seams, and explicit extension points', '类型化的服务 seam 与显式扩展点'],
|
||||
['### Slang/jargon → Professional phrasing', 'The committed agent workflow lives in .agents/skills/dsh-translate-docs', '仓库内置的 agent 工作流见 .agents/skills/dsh-translate-docs'],
|
||||
['### "For humans" — translate the intent, not the word', 'For humans, start with the development guide', '面向开发者:请先阅读开发指南'],
|
||||
['### Code block comments — NEVER translate', '# full-screen TUI coding agent (needs DEEPSEEK_API_KEY)', 'keep exactly as-is, byte-for-byte'],
|
||||
['### Language switcher — flip direction', 'English | [中文](README.zh.md)', '[English](README.md) | 中文'],
|
||||
]
|
||||
|
||||
describe('translation prompt rendering', () => {
|
||||
it('renders both directions with every placeholder resolved', () => {
|
||||
const en = renderTranslationPrompt(document, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })
|
||||
@@ -22,15 +36,31 @@ describe('translation prompt rendering', () => {
|
||||
expect(en).toContain(terminology)
|
||||
expect(en).not.toContain('{{')
|
||||
expect(en).toContain('plain source stays plain (必须)')
|
||||
expect(en).toContain('When the target language is English, use the "English" column without a Chinese gloss')
|
||||
expect(en).toContain('for a Chinese target, use an established Chinese rendering')
|
||||
expect(en).toContain('for an English target, use the established English technical term')
|
||||
expect(en).toContain('does an English target use established English terminology')
|
||||
expect(en).toContain('For an English target, use the established English technical term')
|
||||
expect(en).toContain('does a Chinese target use an established Chinese rendering')
|
||||
expect(en).toContain('does an English target use the established English technical term')
|
||||
expect(en).toContain('The parser removes exactly one framing escape')
|
||||
const zh = renderTranslationPrompt(document, { sourceLanguage: 'Chinese', sourceFilename: 'guide.zh.md', terminology })
|
||||
expect(zh).toContain('from Chinese to English')
|
||||
})
|
||||
|
||||
it('retains every v4 embedded example', () => {
|
||||
for (const example of retainedExamples) {
|
||||
for (const fragment of example) expect(document).toContain(fragment)
|
||||
}
|
||||
})
|
||||
|
||||
it('states the selected v7 safeguards', () => {
|
||||
const rendered = renderTranslationPrompt(document, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })
|
||||
expect(rendered).toContain('## Priority')
|
||||
expect(rendered).toContain('### Faithfulness')
|
||||
expect(rendered).toContain('do not invent a filename or switcher')
|
||||
expect(rendered).toContain('Markdown emphasis markers do not create a word boundary')
|
||||
expect(rendered).toContain('Never invent responsibility merely to avoid a passive construction')
|
||||
expect(rendered).toContain('Never vary a terminology-table form, defined concept, or contract verb merely for stylistic variety')
|
||||
expect(rendered).toContain('Return exactly three raw XML sections')
|
||||
})
|
||||
|
||||
it('rejects a template with unknown or missing placeholders', () => {
|
||||
const alien = document.replaceAll('{{terminology}}', '{{terms_prompt}}')
|
||||
expect(() => renderTranslationPrompt(alien, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })).toThrow(/unsupported placeholder/)
|
||||
|
||||
@@ -26,6 +26,21 @@
|
||||
"symbol": "MessageSourceMap",
|
||||
"source": "packages/llm/llm/src/message.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "ContextForm",
|
||||
"source": "packages/llm/llm/src/message.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "ContextSnapshotSection",
|
||||
"source": "packages/llm/llm/src/message.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "ContextFormed",
|
||||
"source": "packages/llm/llm/src/message.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "FinishReasonMap",
|
||||
@@ -46,6 +61,16 @@
|
||||
"symbol": "LlmModelInfo",
|
||||
"source": "packages/llm/llm/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmModelDiscoveryRequest",
|
||||
"source": "packages/llm/llm/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmDiscoveredModel",
|
||||
"source": "packages/llm/llm/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmModelContext",
|
||||
@@ -98,33 +123,8 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SendTarget",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxPlacement",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxItem",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxAction",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxActionResult",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SendOptions",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"symbol": "InboxTarget",
|
||||
"source": "packages/core/agent/src/inbox.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
@@ -134,7 +134,7 @@
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "AgentCancelCause",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
@@ -143,7 +143,7 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "PromptDecision",
|
||||
"symbol": "PreStepDecision",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
@@ -151,11 +151,6 @@
|
||||
"symbol": "RequestErrorAction",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "RequestError",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SessionStartSource",
|
||||
@@ -370,7 +365,7 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/session.md",
|
||||
"symbol": "TurnTriggerMap",
|
||||
"symbol": "TurnEndCancelCause",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
@@ -424,6 +419,32 @@
|
||||
"symbol": "CreateSessionOptions",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "RestoredSessionOptions",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "PrepareSessionOptions",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "SessionPreparationOptions",
|
||||
"source": "packages/core/session/src/preparation.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "SessionPreparation",
|
||||
"source": "packages/core/session/src/preparation.ts",
|
||||
"projection": "public-api"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "SessionInspection",
|
||||
"source": "packages/session-persistence/session-persistence/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "SessionLocation",
|
||||
@@ -889,6 +910,11 @@
|
||||
"symbol": "SandboxPolicyRequest",
|
||||
"source": "packages/sandbox/sandbox-policy/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/sandbox.md",
|
||||
"symbol": "RunnerFailureRule",
|
||||
"source": "packages/sandbox/sandbox/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/sandbox.md",
|
||||
"symbol": "ConfinedArgv",
|
||||
|
||||
@@ -149,11 +149,33 @@ function validateExampleResolution(): string[] {
|
||||
}
|
||||
|
||||
function validateAppResolution(): string[] {
|
||||
const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
|
||||
const violations: string[] = []
|
||||
// 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 appDependencies = {
|
||||
...readManifest('apps/cli/package.json').dependencies,
|
||||
// The fallback also links every bundle's own dependencies (healProfilesModuleFallback).
|
||||
...Object.fromEntries(globSync('packages/bundle/*/package.json', { cwd: root })
|
||||
.flatMap(file => Object.entries(readManifest(file).dependencies ?? {}))),
|
||||
}
|
||||
const shipped = new Set(globSync('*.cordis.yml', { cwd: resolve(root, 'apps/cli/config') })
|
||||
.map(file => `apps/cli/config/${file}`))
|
||||
const references = pluginReferences.filter(reference => shipped.has(reference.file) || appOverlayFiles.has(reference.file))
|
||||
return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
|
||||
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'))
|
||||
// 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 globSync('packages/bundle/*/package.json', { cwd: root })) {
|
||||
const bundleDir = manifestPath.replace(/\/package\.json$/, '')
|
||||
const manifest = readManifest(manifestPath)
|
||||
const references = pluginReferences.filter(reference => reference.file.startsWith(`${bundleDir}/`))
|
||||
violations.push(...missingPluginDependencies(
|
||||
// A bundle may mount its own package (the web-app runtime row).
|
||||
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
|
||||
manifest.dependencies ?? {},
|
||||
manifestPath,
|
||||
))
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -86,6 +86,9 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/host/directory-picker-browse': { kind: 'none', reason: 'The GUI-host picking backend registers no model surface.' },
|
||||
'packages/host/directory-picker-native': { kind: 'none', reason: 'The GUI-host picking backend registers no model surface.' },
|
||||
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers no model surface.' },
|
||||
'packages/host/frontend-static': { kind: 'none', reason: 'The SPA dist server answers browser asset requests and registers no model surface.' },
|
||||
'packages/bundle/base': { kind: 'indirect', reason: 'The bundle is a patch-list carrier; each inserted row\'s package owns its model surface.' },
|
||||
'packages/bundle/headless': { kind: 'none', reason: 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base/web bundles.' },
|
||||
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
|
||||
'packages/llm/token-meter': { kind: 'indirect', reason: 'The measurement service leaves model-visible changes to its consumers.' },
|
||||
'packages/lsp/lsp': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-lsp.' },
|
||||
|
||||
Reference in New Issue
Block a user