Merge branch 'master' of https://github.com/deepseek-harness/deepseek-harness into xtr/react-loop-simplification

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml
#	docs/architecture.i18n.yaml
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/core-data-structures/core.md
#	docs/core-data-structures/core.zh.md
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	docs/core-data-structures/llm-streaming.md
#	docs/core-data-structures/llm-streaming.zh.md
#	docs/core-data-structures/session.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-loop/README.i18n.yaml
#	packages/core/agent-loop/src/agent.ts
#	packages/core/agent/README.i18n.yaml
#	packages/core/session/README.i18n.yaml
#	packages/core/session/src/types.ts
#	packages/llm/llm/README.i18n.yaml
#	packages/llm/llm/README.md
#	packages/llm/llm/README.zh.md
#	packages/llm/llm/src/index.ts
#	packages/llm/llm/tests/service.spec.ts
#	packages/sdk/sdk-client/README.i18n.yaml
#	packages/sdk/sdk-protocol/README.i18n.yaml
#	packages/sdk/sdk-protocol/README.md
#	packages/sdk/sdk-protocol/README.zh.md
#	packages/subagent/subagent-dsh-sdk/README.i18n.yaml
#	packages/ui/jsonrpc/README.i18n.yaml
#	packages/ui/jsonrpc/README.md
#	packages/ui/jsonrpc/README.zh.md
#	packages/ui/tui/src/index.ts
#	python/sdk/README.i18n.yaml
#	scripts/gen-cordis-catalog.ts
This commit is contained in:
_Kerman
2026-07-31 10:16:14 +08:00
1106 changed files with 33236 additions and 7168 deletions

View File

@@ -0,0 +1,55 @@
/**
* Mechanical guard for the coverage-exempt roster: each entry's positional
* filter and exclude glob must select the same non-empty file set out of the
* repository's spec inventory, so a renamed suite cannot silently fall out of
* the uninstrumented gate while its exclude goes stale.
*/
import { globSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { coverageExemptHeavySuites } from './coverage-exempt.ts'
const root = resolve(import.meta.dirname, '..')
/** The spec inventory mirrored from vitest.config.ts testIncludes. */
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('\\', '/')))
function excludeMatches(exclude: string): string[] {
return globSync(exclude, { cwd: root })
.map(path => path.replaceAll('\\', '/'))
.filter(path => allSpecs.has(path))
.sort()
}
function filterMatches(filter: string): string[] {
return [...allSpecs].filter(spec => spec.startsWith(filter)).sort()
}
describe('coverage-exempt roster', () => {
it.each(coverageExemptHeavySuites.map(suite => [suite.filter, suite] as const))(
'filter and exclude select the same non-empty spec set for %s',
(_filter, suite) => {
const fromExclude = excludeMatches(suite.exclude)
const fromFilter = filterMatches(suite.filter)
expect(fromExclude.length).toBeGreaterThan(0)
expect(fromFilter).toEqual(fromExclude)
},
)
it('entries never overlap, so no suite is double-run or double-excluded', () => {
const seen = new Map<string, string>()
for (const suite of coverageExemptHeavySuites) {
for (const spec of excludeMatches(suite.exclude)) {
expect(seen.get(spec), `${spec} matched by ${seen.get(spec) ?? ''} and ${suite.exclude}`).toBeUndefined()
seen.set(spec, suite.exclude)
}
}
})
})

View File

@@ -0,0 +1,41 @@
/**
* Heavy suites the coverage aggregate runs uninstrumented in a parallel gate.
* Membership contract: a suite qualifies only when every coverage-measured
* file it executes in-process (`coverage.include` spans package src trees;
* typert generator src is threshold-excluded in vitest.config.ts) is already
* fully covered by other suites, so removing it from the instrumented run
* changes no threshold outcome. The aggregate still runs every listed suite
* plain beside the instrumented gate, so correctness signal is unchanged —
* only the v8 instrumentation tax on compiler- and subprocess-heavy fixtures
* is dropped.
*/
/** One coverage-exempt suite: a Vitest CLI filter and its exclude glob. */
export interface CoverageExemptSuite {
/** Positional file filter selecting the suite in the uninstrumented gate. */
readonly filter: string
/** Exclude glob removing the suite from the instrumented gate. */
readonly exclude: string
}
/**
* Set to `1` by the instrumented coverage gate; vitest.config.ts then drops
* the exempt suites from every project. CLI `--exclude` cannot express this:
* it does not reach per-project include resolution.
*/
export const COVERAGE_EXEMPT_ENV = 'DSH_COVERAGE_EXEMPT_HEAVY'
/** Coverage-exempt heavy suites; keep filter and exclude selecting the same files. */
export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [
// Whole-workspace compiler analysis per case — the lane's longest tail.
// Generator src is threshold-excluded; tools-catalog's registry and
// tool-cordis imports are fully covered by those packages' own tests.
{
filter: 'packages/typert/generator/tests/',
exclude: 'packages/typert/generator/tests/**',
},
// Real child-process fixtures over scripts/ sources, which coverage never measures.
{ filter: 'scripts/install-lefthook.spec.ts', exclude: 'scripts/install-lefthook.spec.ts' },
{ filter: 'scripts/oxlint-contract.spec.ts', exclude: 'scripts/oxlint-contract.spec.ts' },
{ filter: 'scripts/change-scope.spec.ts', exclude: 'scripts/change-scope.spec.ts' },
]

View File

@@ -1,28 +1,16 @@
/**
* Boot the TUI or ACP Code Mode overlay, defaulting to TUI. Each overlay
* includes its base example, selects Code Mode, and adds the worker runtime.
* All require a DeepSeek API key; unsupported arguments fail with usage.
*/
/** Boot the ACP Code Mode overlay. Requires a DeepSeek API key. */
import { spawn } from 'node:child_process'
// Each UI's node invocation matches its base demo script plus the overlay config.
const UIS = new Map([
['tui', [
'--import',
'tsx/esm',
'apps/cli/src/bin.ts',
'--config',
'examples/tui-agent/code-mode.cordis.yml',
]],
['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', 'examples/acp-agent/code-mode.cordis.yml']],
])
const ui = process.argv[2] ?? 'tui'
const args = UIS.get(ui)
if (!args || process.argv.length > 3) {
console.error('usage: pnpm run demo:code-mode [tui|acp]')
if (process.argv.length > 2) {
console.error('usage: pnpm run demo:code-mode')
process.exit(2)
}
const child = spawn(process.execPath, args, { stdio: 'inherit' })
const child = spawn(process.execPath, [
'--import',
'tsx',
'packages/examples/acp-demo/src/bin.ts',
'--config',
'examples/acp-agent/code-mode.cordis.yml',
], { stdio: 'inherit' })
child.on('exit', (code, signal) => { process.exit(signal !== null ? 1 : code ?? 1) })

View File

@@ -1,21 +1,19 @@
/**
* Boot the self-referential Cordis tools under TUI, Web, or ACP, defaulting
* to TUI. This is a repository demo wrapper, not a product CLI feature.
* 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([
['tui', ['--import', 'tsx', 'apps/cli/src/bin.ts', '--config', 'examples/cordis-agent/cordis.yml']],
// `dsh web` does not accept alternate configs yet. The TUI config escape
// hatch still boots this browser-only tree; the config owns port 3081.
['web', ['--import', 'tsx', 'apps/cli/src/bin.ts', '--config', 'examples/web-cordis/cordis.yml']],
// 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']],
['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', 'examples/acp-agent/cordis-tools.cordis.yml']],
])
const surface = process.argv[2] ?? 'tui'
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 [tui|web|acp]')
console.error('usage: pnpm run demo:cordis [web|acp]')
process.exit(2)
}

View File

@@ -1,5 +1,5 @@
{
"AGENTS.md": 1755,
"AGENTS.md": 1775,
"docs/AGENTS.md": 1150,
"docs/architecture.md": 1920,
"docs/cordis-primer.md": 600,
@@ -7,5 +7,5 @@
"docs/testing.md": 1100,
"examples/AGENTS.md": 310,
"packages/AGENTS.md": 675,
"packages/README.md": 900
"packages/README.md": 920
}

View File

@@ -32,6 +32,8 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
InboxPlacement: 'core.md',
MessageId: 'core.md',
HookContext: 'core.md',
SettleReason: 'core.md',
AdapterRegistrationHandle: 'core.md',
LlmCallConfig: 'core.md',
LlmModelContext: 'core.md',
LlmModelReasoningInfo: 'core.md',
@@ -39,6 +41,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
LlmFailure: 'llm-streaming.md',
LlmModelInfo: 'core.md',
LlmProviderInfo: 'core.md',
LlmConfigurableProvider: 'core.md',
ResolvedRetryPolicy: 'llm-streaming.md',
Message: 'core.md',
MessageSource: 'core.md',
@@ -185,6 +188,16 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
ToolRegistry: 'tools.md',
ToolRestriction: 'tools.md',
ToolSchema: 'tools.md',
SettingsNamespace: 'settings.md',
SettingsRegisterOptions: 'settings.md',
SettingsScope: 'settings.md',
SettingsDescriptor: 'settings.md',
SettingsPathOp: 'settings.md',
SettingsDescribeOptions: 'settings.md',
SettingsUpdateSource: 'settings.md',
CredentialRef: 'credentials.md',
CredentialInfo: 'credentials.md',
ResolvedCredential: 'credentials.md',
AskUserQuestionAnswer: 'user-interaction.md',
AskUserQuestionRequest: 'user-interaction.md',
UserInteractionProvider: 'user-interaction.md',
@@ -215,6 +228,7 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
/** Project types deliberately documented outside the core-data catalog. */
export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
AgentFactory: 'agent creation seam is owned by packages/core/agent/README.md',
z: 'schemastery schema constructor is owned by vendor/schemastery (vendored upstream)',
BeginCommandRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
InsertReferenceRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
ConsumeTokenRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',

View File

@@ -0,0 +1,98 @@
/**
* Tests for the event-relation collector's demand-driven call-site indexing:
* the single-file fast path and the global fallback must recover the same
* helper-parameter event names, including shapes that defeat the locality
* proof (alias escapes and global script files).
*/
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterAll, describe, expect, it } from 'vitest'
import { collectPackageSources, EventRelationCollector } from './gen-doc-graphs.ts'
import { TypeScriptProject } from './ts-project.ts'
const FIXTURE: Record<string, string> = {
'tsconfig.host.json': JSON.stringify({
compilerOptions: {
target: 'es2022',
module: 'esnext',
moduleResolution: 'bundler',
allowImportingTsExtensions: true,
noEmit: true,
skipLibCheck: true,
types: [],
},
include: ['vendor/**/*.ts', 'packages/**/*.ts'],
}),
'vendor/cordis/src/context.ts': 'export class Context { private brand!: void }\n',
'vendor/cordis/src/events.ts': [
'export class EventsService {',
' dispatch(type: string, args: unknown[]): unknown[] { return [type, args] }',
'}',
'',
].join('\n'),
'packages/core/agent/src/dispatch.ts':
'export interface AgentEventDispatch { emit(...args: unknown[]): void }\n',
// fireLocal: every same-file reference is a direct callee, so the locality
// proof holds and only this file is indexed. fireAliased: the exported
// const is a value-position reference, so the proof fails and the global
// fallback must find the cross-file call in pkgb.
'packages/fix/pkga/src/index.ts': [
"import { EventsService } from '../../../../vendor/cordis/src/events.ts'",
'declare const events: EventsService',
"function fireLocal(args: [string]): void { void events.dispatch('emit', args) }",
"fireLocal(['pkga/local-event'])",
"function fireAliased(args: [string]): void { void events.dispatch('emit', args) }",
'export const aliased = fireAliased',
'',
].join('\n'),
'packages/fix/pkgb/src/index.ts': [
"import { aliased } from '../../pkga/src/index.ts'",
"aliased(['pkgb/aliased-event'])",
'',
].join('\n'),
// Global script files (no import/export): scriptFire is program-visible, so
// the cross-file call in caller.ts leaves no same-file reference. Only the
// module-ness premise check routes this helper to the global index; without
// it the proof would pass and the event would silently drop.
'packages/fix/pkgc/src/globals.ts':
"declare var gEvents: import('../../../../vendor/cordis/src/events.ts').EventsService\n",
'packages/fix/pkgc/src/helper.ts':
"function scriptFire(args: [string]): void { void gEvents.dispatch('emit', args) }\n",
'packages/fix/pkgc/src/caller.ts': "scriptFire(['pkgc/script-event'])\n",
}
const root = mkdtempSync(join(tmpdir(), 'gen-doc-graphs-'))
for (const [rel, content] of Object.entries(FIXTURE)) {
mkdirSync(dirname(join(root, rel)), { recursive: true })
writeFileSync(join(root, rel), content)
}
const project = new TypeScriptProject(root)
const sources = collectPackageSources(project)
afterAll(() => {
rmSync(root, { recursive: true, force: true })
})
function dispatchersOf(pkgs: readonly string[], event: string): string[] {
const subset = sources.filter(source => pkgs.includes(source.pkg))
const relations = new EventRelationCollector(project, subset).collect()
return [...(relations.get(event)?.dispatchers.keys() ?? [])]
}
describe('event relation call-site indexing', () => {
it('recovers a proven-local helper through the single-file fast path', () => {
expect(dispatchersOf(['pkga', 'pkgb'], 'pkga/local-event')).toEqual(['pkga'])
})
it('recovers an alias-escaped helper through the global fallback', () => {
expect(dispatchersOf(['pkga', 'pkgb'], 'pkgb/aliased-event')).toEqual(['pkga'])
})
it('rejects the locality proof for global script files', () => {
// pkgc alone: the script helper is the first demand, so a wrongly passing
// proof would index helper.ts only and lose the caller.ts call site.
expect(dispatchersOf(['pkgc'], 'pkgc/script-event')).toEqual(['pkgc'])
})
})

View File

@@ -48,9 +48,13 @@ interface EventRelation {
listeners: Set<string>
}
interface PackageSource {
/** One scanned package source file and its owning package short name. */
export interface PackageSource {
/** Repository-relative path. */
rel: string
/** Package short name from the `packages/<group>/<pkg>/src` path. */
pkg: string
/** The bound program source file. */
sourceFile: ts.SourceFile
}
@@ -148,6 +152,24 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'session-query', 'session-query-sqlite'],
note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
},
{
key: 'settings',
pkg: 'settings',
title: 'User-settings seam',
mode: 'seam',
implementations: ['settings-local'],
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: 'credentials',
pkg: 'credentials',
title: 'Credential seam',
mode: 'seam',
implementations: ['credentials-local'],
consumers: ['llm-deepseek', 'llm-pi-ai', 'apiproxy'],
note: 'Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the web gateway exposes value-free views and write-only storage.',
},
{
key: 'telemetry',
pkg: 'session-telemetry',
@@ -605,11 +627,11 @@ function stripYamlScalar(value: string): string {
const APP_EXAMPLES = [
{
id: 'tui',
rel: 'examples/tui-agent/composition.md',
rel: 'apps/cli/composition.md',
title: 'TUI Agent App Composition',
label: 'examples/tui-agent',
config: 'examples/tui-agent/cordis.yml',
summary: 'The TUI agent combines the real DeepSeek adapter, coding tools, compaction, subagents, and workflows with the full-screen terminal app package.',
label: 'apps/cli/config',
config: 'apps/cli/config/base.cordis.yml',
summary: 'The TUI surface combines the shared CLI base with its surface overlay and full-screen terminal package.',
},
{
id: 'headless',
@@ -619,14 +641,6 @@ const APP_EXAMPLES = [
config: 'examples/headless-agent/cordis.yml',
summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
},
{
id: 'cordis',
rel: 'examples/cordis-agent/composition.md',
title: 'Cordis Agent App Composition',
label: 'examples/cordis-agent',
config: 'examples/cordis-agent/cordis.yml',
summary: 'The self-referential demo puts @deepseek-ai/dsh-tool-cordis on the coding spine, letting the agent inspect its current-process runtime and mount or unmount in-memory temporary Plugins.',
},
{
id: 'acp',
rel: 'examples/acp-agent/composition.md',
@@ -691,13 +705,26 @@ function renderAppComposition(example: AppExample): string {
return lines.join('\n')
}
type CallSiteIndex = Map<ts.SignatureDeclaration | ts.JSDocSignature, ts.CallExpression[]>
/**
* The only method names visitSource classifies; receiver typing runs on these
* alone. Obligation: every method name matched by a branch inside visitSource
* must appear here — the prefilter drops non-members before any branch runs,
* so a branch for an unlisted name is silently dead.
*/
const EVENT_API_METHODS = new Set(['on', 'once', 'emit', 'parallel', 'serial', 'waterfall', 'dispatch'])
/** Collect event dispatch/listener relations from real cross-file receiver types. */
class EventRelationCollector {
export class EventRelationCollector {
private readonly relations = new Map<string, EventRelation>()
private readonly callSites = new Map<ts.SignatureDeclaration | ts.JSDocSignature, ts.CallExpression[]>()
private readonly fileCallSites = new Map<ts.SourceFile, CallSiteIndex>()
private readonly localCalleeProofs = new Map<ts.FunctionDeclaration, boolean>()
private globalCallSites: CallSiteIndex | null = null
private readonly contextType: ts.Type
private readonly agentDispatchType: ts.Type
private readonly eventsServiceType: ts.Type
private readonly packageSourceFiles: ReadonlySet<ts.SourceFile>
constructor(
private readonly project: TypeScriptProject,
@@ -706,7 +733,7 @@ class EventRelationCollector {
this.contextType = this.declaredType('vendor/cordis/src/context.ts', 'Context')
this.agentDispatchType = this.declaredType('packages/core/agent/src/dispatch.ts', 'AgentEventDispatch')
this.eventsServiceType = this.declaredType('vendor/cordis/src/events.ts', 'EventsService')
this.indexCallSites()
this.packageSourceFiles = new Set(sources.map(source => source.sourceFile))
}
/** Return all event relations discovered from the Program. */
@@ -726,20 +753,88 @@ class EventRelationCollector {
return this.project.checker.getDeclaredTypeOfSymbol(symbol)
}
/** Index resolved local function calls for narrow argument-flow recovery. */
private indexCallSites(): void {
/** Index resolved function calls in the given files for narrow argument-flow recovery. */
private buildCallSiteIndex(files: Iterable<ts.SourceFile>): CallSiteIndex {
const index: CallSiteIndex = new Map()
const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const declaration = this.project.checker.getResolvedSignature(node)?.declaration
if (declaration) {
const calls = this.callSites.get(declaration) ?? []
const calls = index.get(declaration) ?? []
calls.push(node)
this.callSites.set(declaration, calls)
index.set(declaration, calls)
}
}
ts.forEachChild(node, visit)
}
for (const source of this.sources) visit(source.sourceFile)
for (const file of files) visit(file)
return index
}
/**
* Return every indexed call resolving to one local helper declaration.
* Fast path: when every same-file reference to the non-exported helper is
* provably a direct callee, module scoping confines all of its calls to that
* file, so only that file is indexed. Any other reference shape may alias
* the function value outward, so the original full package-source index
* decides instead.
*/
private callSitesFor(owner: ts.FunctionDeclaration): ts.CallExpression[] {
if (!this.globalCallSites && !this.provenLocalCallee(owner)) {
this.globalCallSites = this.buildCallSiteIndex(this.packageSourceFiles)
}
if (this.globalCallSites) return this.globalCallSites.get(owner) ?? []
const file = owner.getSourceFile()
let index = this.fileCallSites.get(file)
if (!index) {
index = this.buildCallSiteIndex([file])
this.fileCallSites.set(file, index)
}
return index.get(owner) ?? []
}
/**
* Prove every same-file reference to one helper is a direct callee. The
* proof owns its premises: an exported helper or a helper in a global
* script file (no import/export means program-wide scope, callable from
* another file with no same-file reference at all) fails immediately.
* Alias escapes (re-export statements, default exports, value reads)
* resolve back to the owner symbol at a non-callee position and fail the
* proof, as does anything the scan cannot positively classify.
*/
private provenLocalCallee(owner: ts.FunctionDeclaration): boolean {
const cached = this.localCalleeProofs.get(owner)
if (cached !== undefined) return cached
if (hasExportModifier(owner) || !ts.isExternalModule(owner.getSourceFile())) {
this.localCalleeProofs.set(owner, false)
return false
}
const name = owner.name
const ownerSymbol = name && this.project.checker.getSymbolAtLocation(name)
let proven = !!ownerSymbol
const refersToOwner = (identifier: ts.Identifier): boolean => {
// Shorthand properties resolve to the property symbol; ask for the value side.
const local = ts.isShorthandPropertyAssignment(identifier.parent)
? this.project.checker.getShorthandAssignmentValueSymbol(identifier.parent)
: this.project.checker.getSymbolAtLocation(identifier)
if (!local) return false
const symbol = local.flags & ts.SymbolFlags.Alias
? this.project.checker.getAliasedSymbol(local)
: local
return symbol === ownerSymbol
}
const visit = (node: ts.Node): void => {
if (!proven) return
if (ts.isIdentifier(node) && node !== name && node.text === name?.text
&& !isDirectCallee(node) && refersToOwner(node)) {
proven = false
return
}
ts.forEachChild(node, visit)
}
visit(owner.getSourceFile())
this.localCalleeProofs.set(owner, proven)
return proven
}
/** Walk one package source file and classify event API calls by receiver type. */
@@ -753,7 +848,7 @@ class EventRelationCollector {
this.addDispatcher(name, source.pkg, 'emitAgentEvent')
}
}
} else if (ts.isPropertyAccessExpression(node.expression)) {
} else if (ts.isPropertyAccessExpression(node.expression) && EVENT_API_METHODS.has(node.expression.name.text)) {
const receiverKind = this.receiverKind(node.expression.expression)
const method = node.expression.name.text
if (receiverKind === 'events-service' && method === 'dispatch') {
@@ -856,7 +951,7 @@ class EventRelationCollector {
const index = owner.parameters.indexOf(parameter)
if (index < 0) return new Set()
const events = new Set<string>()
for (const call of this.callSites.get(owner) ?? []) {
for (const call of this.callSitesFor(owner)) {
const argument = call.arguments[index]
if (argument) addAll(events, this.eventNamesFromArgumentList(argument, new Set(seen)))
}
@@ -903,6 +998,21 @@ class EventRelationCollector {
}
}
/** Return whether an identifier is the callee of a call, seen through value-preserving wrappers. */
function isDirectCallee(identifier: ts.Identifier): boolean {
let current: ts.Node = identifier
while (
ts.isParenthesizedExpression(current.parent)
|| ts.isAsExpression(current.parent)
|| ts.isTypeAssertionExpression(current.parent)
|| ts.isNonNullExpression(current.parent)
|| ts.isSatisfiesExpression(current.parent)
) {
current = current.parent
}
return ts.isCallExpression(current.parent) && current.parent.expression === current
}
/** Peel syntax-only wrappers that do not change an expression's runtime value. */
function unwrapExpression(expression: ts.Expression): ts.Expression {
let current = expression
@@ -958,14 +1068,22 @@ function unionSets<T>(left: ReadonlySet<T>, right: ReadonlySet<T>): Set<T> {
return out
}
function collectEventRelations(): Map<string, EventRelation> {
const project = new TypeScriptProject(root)
const sources = project.sourceFiles().flatMap((sourceFile): PackageSource[] => {
/**
* Select the package source files of one project in deterministic order.
* @param project - the loaded repository TypeScript project.
* @returns `packages/<group>/<pkg>/src` files tagged with their package name.
*/
export function collectPackageSources(project: TypeScriptProject): PackageSource[] {
return project.sourceFiles().flatMap((sourceFile): PackageSource[] => {
const rel = project.relativePath(sourceFile)
const match = /^packages\/[^/]+\/([^/]+)\/src\/.+\.ts$/.exec(rel)
return match?.[1] ? [{ rel, pkg: match[1], sourceFile }] : []
}).sort((left, right) => left.rel.localeCompare(right.rel))
return new EventRelationCollector(project, sources).collect()
}
function collectEventRelations(): Map<string, EventRelation> {
const project = new TypeScriptProject(root)
return new EventRelationCollector(project, collectPackageSources(project)).collect()
}
function relationPackages(map: Map<string, Set<string>>, pkgsByShort: Map<string, Pkg>): string {

View File

@@ -217,7 +217,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
await ctx.plugin(ToolCordis)
},
note:
'Ships in examples/cordis-agent only (a deliberate opt-in — temporary Plugin code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins created by cordis_mount may register ADDITIONAL model-visible tools until unmounted or DSH restarts; a full changed request header logs those tool-set changes.',
'Not in any shipped tree (a deliberate opt-in — temporary Plugin code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins created by cordis_mount may register ADDITIONAL model-visible tools until unmounted or DSH restarts; a full changed request header logs those tool-set changes.',
},
{
pkg: '@deepseek-ai/dsh-tool-bash-persistent',
@@ -273,10 +273,10 @@ const TOOL_PACKAGES: ToolPackage[] = [
// never depends on the host PATH. `ctx.spillStore` is optional (read via
// ctx.get) and does not affect the schemas, so no spill backend is mounted.
await ctx.plugin(CatalogSearchBashExecutor)
await ctx.plugin(ToolFsSearch)
await ctx.plugin(ToolFsSearch, { sampleOverCapGlobResults: true })
},
note:
'glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.',
'glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.',
},
{
pkg: '@deepseek-ai/dsh-tool-pty',
@@ -377,7 +377,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 `examples/tui-agent/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 `apps/cli/config/base.cordis.yml` and `examples/acp-agent/cordis.yml`.',
},
{
pkg: '@deepseek-ai/dsh-tool-tasks',

View File

@@ -197,7 +197,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(18)
expect(translated).toHaveLength(20)
expect(translated.every(page => page.source.endsWith('.zh.md'))).toBe(true)
expect(fallbacks.map(page => page.source).sort()).toEqual([
'docs/core-data-structures/commands.md',

View File

@@ -135,31 +135,43 @@ describe('Oxlint gate', () => {
})
})
describe('Node 24 consumer graph', () => {
it('owns the eight-command pool and orders restored-artifact consumers', () => {
describe('Node 24 lane ownership', () => {
it('keeps the static lane source-only', () => {
const subject = withPnpmEntrypoint(() => gatesForMode('ci-static'))
expect(subject.map(item => item.id)).not.toContain('build')
expect(subject.map(item => item.id)).not.toContain('doc-typecheck')
})
it('owns the build and orders its artifact consumers', () => {
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
expect(defaultConcurrency('ci-consumers', subject.length, 4)).toEqual({
workers: 8,
workers: 10,
source: 'ci-consumers gate count',
})
expect(subject.map(item => item.id)).toEqual([
'lint-and-duplication',
'build',
'node-compat',
'publint',
'built-package-invariants',
'lint-and-duplication',
'snapshot',
'web-snapshot',
'publint',
'doc-typecheck',
'node-next-types',
'built-package-invariants',
'built-bin-smoke',
])
expect(subject.find(item => item.id === 'publint')?.needs).toBeUndefined()
expect(subject.find(item => item.id === 'publint')?.needs).toEqual(['build'])
expect(subject.find(item => item.id === 'built-package-invariants')?.needs).toEqual(['publint'])
expect(subject.find(item => item.id === 'lint-and-duplication')?.needs).toEqual(['built-package-invariants'])
for (const id of ['snapshot', 'web-snapshot', 'node-next-types', 'built-bin-smoke']) {
for (const id of ['snapshot', 'web-snapshot', 'doc-typecheck', 'node-next-types', 'built-bin-smoke']) {
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 === 'doc-typecheck')?.env).toEqual({
DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1',
})
expect(subject.find(item => item.id === 'web-snapshot')).toMatchObject({
displayCommand: 'DSH_SNAPSHOT=replay pnpm run test:web:built',
env: { DSH_SNAPSHOT: 'replay' },

View File

@@ -9,6 +9,7 @@ import { spawn } from 'node:child_process'
import { availableParallelism } from 'node:os'
import { resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './coverage-exempt.ts'
/** A named aggregate exposed by the gate runner. */
export type Mode =
@@ -195,14 +196,14 @@ export function gatesForMode(selected: Mode): Gate[] {
case 'ci-linux-primary':
return [...ciPrimaryGates(), webSnapshotGate(['built-package-invariants'])]
case 'ci-static':
return ciStaticGates()
return ciStaticGates({ ownsBuild: false })
case 'ci-lint':
return [
lintGate(),
pnpmScript('duplication', 'duplication'),
]
case 'ci-coverage':
return [coverageGate()]
return coverageGates()
case 'ci-snapshot':
return [pnpmScript('build', 'build'), snapshotGate()]
case 'ci-artifacts':
@@ -248,7 +249,7 @@ function ciPrimaryGates(): Gate[] {
pnpmScript('typecheck', 'typecheck'),
lintGate(),
pnpmScript('duplication', 'duplication'),
coverageGate(),
...coverageGates(),
...nodeCompatSmokeGates(),
snapshotGate(),
...docSyncLeafGates(),
@@ -295,16 +296,21 @@ function nodeCompatSmokeGates(): Gate[] {
]
}
function ciStaticGates(): Gate[] {
function ciStaticGates(options: { ownsBuild: boolean }): Gate[] {
return [
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('constraints', 'constraints'),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
pnpmScript('build', 'build'),
...options.ownsBuild ? [pnpmScript('build', 'build')] : [],
...docSyncLeafGates({
docTypecheckNeeds: ['build'],
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
includeDocTypecheck: options.ownsBuild,
...options.ownsBuild
? {
docTypecheckNeeds: ['build'],
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
}
: {},
docsBuildScript: 'docs:build:mpa',
}),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
@@ -326,23 +332,28 @@ function ciArtifactGates(): Gate[] {
}
function ciConsumerGates(): Gate[] {
const publicArtifacts = ['publint']
const restoredBuild = ['built-package-invariants']
const builtTree = ['build']
const validatedBuild = ['built-package-invariants']
return [
pnpmScript('build', 'build'),
pnpmScript('node-compat', 'check:node-compat', { label: 'Node compatibility' }),
pnpmScript('publint', 'publint', { needs: builtTree }),
builtPackageInvariantsGate(['publint']),
pnpmScript('lint-and-duplication', 'check:ci:lint', {
label: 'lint and duplication',
needs: restoredBuild,
needs: validatedBuild,
}),
snapshotGate(validatedBuild),
webSnapshotGate(validatedBuild),
pnpmScript('doc-typecheck', 'doc-typecheck', {
needs: validatedBuild,
env: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
}),
pnpmScript('node-compat', 'check:node-compat', { label: 'Node compatibility' }),
snapshotGate(restoredBuild),
webSnapshotGate(restoredBuild),
pnpmScript('publint', 'publint'),
pnpmScript('node-next-types', 'verify-node-next-types', {
label: 'node-next types',
needs: restoredBuild,
needs: validatedBuild,
}),
builtPackageInvariantsGate(publicArtifacts),
builtBinSmokeGate(restoredBuild),
builtBinSmokeGate(validatedBuild),
]
}
@@ -377,7 +388,7 @@ function ciWindowsCompleteGates(): Gate[] {
function ciWindowsObservationalGates(): Gate[] {
return [
...ciStaticGates(),
...ciStaticGates({ ownsBuild: true }),
// Linux owns required lint, coverage, and snapshots; Windows omits those duplicates.
pnpmScript('duplication', 'duplication'),
pnpmScript('publint', 'publint', { needs: ['build'] }),
@@ -397,20 +408,56 @@ function lintGate(): Gate {
: { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` })
}
function coverageGate(): Gate {
return pnpmExec('coverage', [
'vitest',
'run',
'--coverage',
...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'),
], {
label: 'test:coverage',
})
// The heavy suites run uninstrumented beside the thresholded gate: their
// compiler- and subprocess-bound fixtures pay a multiple of their runtime
// under v8 instrumentation while contributing nothing the thresholds need
// (membership contract in scripts/coverage-exempt.ts).
//
// DSH_COVERAGE_MAX_WORKERS is the lane's worker budget, so the two parallel
// gates split it instead of each claiming it whole (the failover pool's
// 8 x 6-instance bound assumes one lane never exceeds its value). The exempt
// gate's wall clock is dominated by its longest single file, so it takes the
// small share. A budget of 1 gives each gate 1 worker; lanes that need a
// strict total of one (the serial reference jobs) also set
// DSH_GATE_CONCURRENCY=1, which keeps the gates from overlapping at all.
function coverageWorkerArgs(): { instrumented: string[]; exempt: string[] } {
const [flag] = positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers')
if (flag === undefined) return { instrumented: [], exempt: [] }
const total = Number.parseInt(flag.split('=')[1] ?? '', 10)
const exempt = Math.max(1, Math.floor(total / 3))
const instrumented = Math.max(1, total - exempt)
return {
instrumented: [`--maxWorkers=${String(instrumented)}`],
exempt: [`--maxWorkers=${String(exempt)}`],
}
}
function coverageGates(): Gate[] {
const workers = coverageWorkerArgs()
return [
pnpmExec('coverage', [
'vitest',
'run',
'--coverage',
...workers.instrumented,
], {
label: 'test:coverage',
env: { [COVERAGE_EXEMPT_ENV]: '1' },
}),
pnpmExec('coverage-exempt-heavy', [
'vitest',
'run',
...coverageExemptHeavySuites.map(suite => suite.filter),
...workers.exempt,
], {
label: 'test:coverage-exempt-heavy',
}),
]
}
// Example and package snapshots boot their bins in `lib` mode (built artifacts under plain Node,
// plugins via real exports); repository-script snapshots execute their real source entry path.
// Build-owning modes wait on `build`; a restored-artifact mode passes its validation dependency.
// 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' },
@@ -458,6 +505,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
}
function docSyncLeafGates(options: {
includeDocTypecheck?: boolean
docTypecheckNeeds?: string[]
docTypecheckEnv?: Record<string, string | undefined>
docsBuildScript?: 'docs:build' | 'docs:build:mpa'
@@ -466,7 +514,9 @@ function docSyncLeafGates(options: {
if (options.docTypecheckNeeds !== undefined) docTypecheckOptions.needs = options.docTypecheckNeeds
if (options.docTypecheckEnv !== undefined) docTypecheckOptions.env = options.docTypecheckEnv
return [
pnpmScript('doc-typecheck', 'doc-typecheck', docTypecheckOptions),
...options.includeDocTypecheck === false
? []
: [pnpmScript('doc-typecheck', 'doc-typecheck', docTypecheckOptions)],
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),
pnpmScript('export-jsdoc', 'verify-export-jsdoc', { label: 'export jsdoc' }),
pnpmScript('tool-catalog', 'verify-tool-catalog', { label: 'tool catalog' }),
@@ -503,7 +553,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
'--config',
'vitest.e2e.config.ts',
'examples/headless-agent/tests/keyless-smoke.e2e.ts',
'examples/tui-agent/tests/tui-keyless-smoke.e2e.ts',
'apps/cli/tests/tui-keyless-smoke.e2e.ts',
'packages/examples/cli-demo/tests/built-bin.e2e.ts',
'packages/examples/acp-demo/tests/built-bin.e2e.ts',
'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts',

View File

@@ -510,7 +510,7 @@ def smoke_sdk_default(base_url: str) -> None:
root = Path(temporary).resolve()
sessions = root / "sessions"
with DeepSeekHarness(
provider="deepseek",
provider="deepseek-official",
model="smoke-model",
cwd=str(root),
session_root=str(sessions),
@@ -533,7 +533,7 @@ def smoke_sdk_custom(base_url: str, executable: Path) -> None:
cordis = root / "cordis.yml"
cordis.write_text(CUSTOM_CORDIS)
with DeepSeekHarness(
provider="deepseek",
provider="deepseek-official",
model="smoke-model",
cwd=str(root),
session_root=str(sessions),
@@ -598,7 +598,7 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool)
cordis = root / "cordis.yml"
cordis.write_text(CUSTOM_CORDIS)
with DeepSeekHarness(
provider="deepseek",
provider="deepseek-official",
model="smoke-model",
cwd=str(root),
session_root=str(sessions),
@@ -648,7 +648,7 @@ def smoke_direct(base_url: str, executable: Path) -> None:
}
peer = RuntimePeer([str(executable)], root, environment)
try:
peer.send({"jsonrpc": "2.0", "id": "initialize", "method": "initialize", "params": {"cwd": str(root), "provider": "deepseek", "model": "smoke-model"}})
peer.send({"jsonrpc": "2.0", "id": "initialize", "method": "initialize", "params": {"cwd": str(root), "provider": "deepseek-official", "model": "smoke-model"}})
peer.read_until(lambda message: message.get("id") == "initialize")
peer.send({
"jsonrpc": "2.0",

File diff suppressed because one or more lines are too long

View File

@@ -61,7 +61,8 @@ describe('global test invariant host', () => {
return () => {}
})
const fakeContext = { invariants: { register } } as unknown as Context
for (const [rawPath, companion] of Object.entries(testInvariantCompanions)) {
for (const [rawPath, load] of Object.entries(testInvariantCompanions)) {
const companion = await load()
const path = rawPath.replace(/^\.\.\//, '')
expect(companion.default, path).toBeUndefined()
const unwrapped = loader.unwrapExports(companion) as typeof companion

View File

@@ -12,8 +12,8 @@ import InvariantService from '@deepseek-ai/dsh-invariants'
declare global {
interface ImportMeta {
/** Eager Vite module-glob expansion used by the Vitest setup file. */
glob<TModule>(pattern: string, options: { eager: true }): Record<string, TModule>
/** Lazy Vite module-glob expansion used by the Vitest setup file. */
glob<TModule>(pattern: string): Record<string, () => Promise<TModule>>
}
}
@@ -25,9 +25,15 @@ export interface TestInvariantCompanion {
apply(ctx: Context): Promise<() => void>
}
/** Every package companion, discovered eagerly so coverage observes each registration. */
export const testInvariantCompanions: Readonly<Record<string, TestInvariantCompanion>> =
import.meta.glob<TestInvariantCompanion>('../packages/*/*/src/invariant.ts', { eager: true })
/**
* Every package companion as a lazy loader keyed by glob path. Ordinary tests
* load only their owner's module; the exhaustive topology test loads and
* executes all of them, so aggregated coverage still observes every
* registration while per-file setup stops importing 168 companions and their
* transitive package sources.
*/
export const testInvariantCompanions: Readonly<Record<string, () => Promise<TestInvariantCompanion>>> =
import.meta.glob<TestInvariantCompanion>('../packages/*/*/src/invariant.ts')
/** Manual-topology suites whose names cannot follow the focused invariant convention. */
const MANUAL_INVARIANT_TEST_EXCEPTIONS = [
@@ -36,7 +42,6 @@ const MANUAL_INVARIANT_TEST_EXCEPTIONS = [
] as const
interface InvariantHost {
readonly fibers: readonly PluginFiber[]
readonly byCallback: ReadonlyMap<unknown, PluginFiber>
readonly ready: Promise<void>
}
@@ -102,39 +107,40 @@ export function testInvariantCompanionPaths(testPath: string): string[] {
}
function startInvariantHost(root: Context): InvariantHost {
const fibers: PluginFiber[] = []
const byCallback = new Map<unknown, PluginFiber>()
const mount = (plugin: Plugin, config?: unknown): void => {
const mount = (plugin: Plugin, config?: unknown): PluginFiber => {
const fiber = originalPlugin.call(root.registry, plugin, config)
const callback = root.registry.resolve(plugin)
if (callback === undefined) throw new Error('test invariants: companion is not a valid Cordis plugin')
fibers.push(fiber)
byCallback.set(callback, fiber)
return fiber
}
mount(InvariantService, { enabled: true })
// The service mounts synchronously so the intercepted registration that
// started this host immediately finds its own fiber in byCallback.
// Companions load and mount inside the ready chain (after the service is
// active, so their startup is directly joinable); every joined root plugin
// awaits ready, so none starts ahead of its package checks. Tests plugging
// a companion directly must await an earlier root plugin first — the
// duplicate-mount failure otherwise is loud (owner name already reserved).
const serviceFiber = mount(InvariantService, { enabled: true })
const testPath = expect.getState().testPath ?? ''
const companionPaths = testInvariantCompanionPaths(testPath)
for (const path of companionPaths) {
const companion = testInvariantCompanions[path]
if (companion === undefined) {
throw new Error(`test invariants: selected companion vanished at ${path}`)
}
if (!companion.inject.includes('invariants')) {
throw new Error(`test invariants: ${path} must inject the invariant service`)
}
mount(companion)
}
const [serviceFiber, ...companionFibers] = fibers
if (serviceFiber === undefined) throw new Error('test invariants: service fiber was not mounted')
// A companion is initially PENDING on the invariant service, and Cordis
// Fiber.await() only joins work already in flight. Wait for the service to
// activate its dependants before joining their startup and failures.
const ready = serviceFiber.await()
.then(() => Promise.all(companionFibers.map(fiber => fiber.await())))
.then(() => undefined)
const host = { fibers, byCallback, ready }
const ready = serviceFiber.await().then(async () => {
const companionFibers = await Promise.all(companionPaths.map(async (path) => {
const load = testInvariantCompanions[path]
if (load === undefined) {
throw new Error(`test invariants: selected companion vanished at ${path}`)
}
const companion = await load()
if (!companion.inject.includes('invariants')) {
throw new Error(`test invariants: ${path} must inject the invariant service`)
}
return mount(companion)
}))
await Promise.all(companionFibers.map(fiber => fiber.await()))
})
const host = { byCallback, ready }
hosts.set(root, host)
return host
}

View File

@@ -31,6 +31,11 @@
"symbol": "FinishReasonMap",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "AdapterRegistrationHandle",
"source": "packages/llm/llm/src/index.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "LlmProviderInfo",
@@ -81,6 +86,11 @@
"symbol": "LlmCallConfig",
"source": "packages/llm/llm/src/call-config.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "LlmCallConfigAdapterDefaults",
"source": "packages/llm/llm/src/call-config.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "SessionEvent",
@@ -659,6 +669,11 @@
"symbol": "AskUserQuestionOption",
"source": "packages/ui/user-interaction/src/types.ts"
},
{
"doc": "docs/core-data-structures/user-interaction.md",
"symbol": "AskUserQuestionIntent",
"source": "packages/ui/user-interaction/src/types.ts"
},
{
"doc": "docs/core-data-structures/user-interaction.md",
"symbol": "AskUserQuestionItem",
@@ -1298,6 +1313,66 @@
"doc": "docs/core-data-structures/subprocess.md",
"symbol": "SubprocessCollectedOutputs",
"source": "packages/subprocess/subprocess/src/types.ts"
},
{
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsNamespace",
"source": "packages/settings/settings/src/index.ts"
},
{
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsRegisterOptions",
"source": "packages/settings/settings/src/index.ts"
},
{
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsApplies",
"source": "packages/settings/settings/src/index.ts"
},
{
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsScope",
"source": "packages/settings/settings/src/index.ts"
},
{
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsDescriptor",
"source": "packages/settings/settings/src/index.ts"
},
{
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsUpdateSource",
"source": "packages/settings/settings/src/index.ts"
},
{
"doc": "docs/core-data-structures/credentials.md",
"symbol": "CredentialRef",
"source": "packages/credentials/credentials/src/index.ts"
},
{
"doc": "docs/core-data-structures/credentials.md",
"symbol": "ResolvedCredential",
"source": "packages/credentials/credentials/src/index.ts"
},
{
"doc": "docs/core-data-structures/credentials.md",
"symbol": "CredentialInfo",
"source": "packages/credentials/credentials/src/index.ts"
},
{
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsDescribeOptions",
"source": "packages/settings/settings/src/index.ts"
},
{
"doc": "docs/core-data-structures/core.md",
"symbol": "LlmConfigurableProvider",
"source": "packages/llm/llm/src/types.ts"
},
{
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsPathOp",
"source": "packages/settings/settings/src/index.ts"
}
]
}

View File

@@ -29,7 +29,24 @@ 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.
const appOverlayFiles = new Set(['examples/web-cordis/cordis.yml'])
const metadataFields = ['id', 'name', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const
/** The adaptive directory-picker chooser package (mounts a backend row at boot). */
const CHOOSER_PACKAGE = '@deepseek-ai/dsh-host-directory-picker-auto'
/**
* The backends the chooser mounts by runtime string (mirror of its exported
* `BACKEND_PACKAGES`), invisible to yml-row scanning: a composition mounting
* the chooser must resolve both, or keyless Linux CI (which only ever
* resolves `browse`) hides a dropped `-native` dependency until a macOS boot.
*/
const CHOOSER_BACKEND_PACKAGES = [
'@deepseek-ai/dsh-host-directory-picker-native',
'@deepseek-ai/dsh-host-directory-picker-browse',
]
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
kind: 'scalar',
resolve: data => typeof data === 'string',
@@ -57,6 +74,7 @@ for (const file of files) {
errors.push(...validateExampleResolution())
errors.push(...validateAppResolution())
errors.push(...validateSourcePlaneResolution())
if (errors.length > 0) {
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
@@ -78,6 +96,11 @@ function validateEntry(value: unknown, file: string, path: string): void {
validateEntry(value.config[index], file, `${path}.config[${index}]`)
}
}
if (isUnknownArray(value.insert)) {
for (let index = 0; index < value.insert.length; index++) {
validateEntry(value.insert[index], file, `${path}.insert[${index}]`)
}
}
if (value.name !== '@cordisjs/plugin-include') return
const config = value.config
if (!isRecord(config) || !isUnknownArray(config.patches)) return
@@ -104,7 +127,7 @@ function validateExampleResolution(): string[] {
const dependencies = exampleManifest.dependencies ?? {}
const localPackages = localPackageDirectories()
const rootReferences = rootProjectReferences()
const exampleReferences = pluginReferences.filter(reference => reference.file.startsWith('examples/'))
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)))
@@ -124,22 +147,81 @@ function validateExampleResolution(): string[] {
function validateAppResolution(): string[] {
const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
const references = pluginReferences.filter(reference => reference.file === 'apps/cli/cordis.yml')
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')
}
/**
* Every configured specifier of a local workspace package must resolve through
* the tsconfig `paths` facade to a `.ts`/`.tsx` source file. The `dsh` source
* launch (tsx) and vitest resolve in the source plane; without a `paths` match
* they fall back to package `exports`, which reach built `lib/` — present on a
* built dev tree, absent on a clean one — so a missing mapping boots locally
* yet breaks every clean checkout. Anything but a `.ts`/`.tsx` hit (a `.d.ts`
* or `.js` under built `lib/`) is that artifact-plane fallback, not source.
*/
function validateSourcePlaneResolution(): string[] {
const violations: string[] = []
const localPackages = localPackageDirectories()
const config = ts.readConfigFile(resolve(root, 'tsconfig.base.json'), path => ts.sys.readFile(path))
if (config.error !== undefined) {
throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'))
}
const { options, errors: optionErrors } = ts.convertCompilerOptionsFromJson(
(config.config as { compilerOptions?: unknown }).compilerOptions,
root,
'tsconfig.base.json',
)
if (optionErrors.length > 0) {
throw new Error(optionErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, '\n')).join('\n'))
}
// convertCompilerOptionsFromJson leaves `pathsBasePath` unset, so relative
// `paths` targets resolve against the host's current directory; anchor it to
// the repository root to keep the gate cwd-independent.
const host: ts.ModuleResolutionHost = {
fileExists: path => ts.sys.fileExists(path),
readFile: path => ts.sys.readFile(path),
directoryExists: path => ts.sys.directoryExists(path),
getCurrentDirectory: () => root,
}
const sourceExtensions = new Set<string>([ts.Extension.Ts, ts.Extension.Tsx])
const containingFile = resolve(root, 'scripts/verify-cordis-config.ts')
const locationsBySpecifier = new Map<string, Set<string>>()
for (const reference of pluginReferences) {
const packageName = packageNameFromSpecifier(reference.name)
if (packageName === undefined || !localPackages.has(packageName)) continue
const locations = locationsBySpecifier.get(reference.name) ?? new Set<string>()
locations.add(reference.file)
locationsBySpecifier.set(reference.name, locations)
}
for (const [specifier, locations] of locationsBySpecifier) {
const resolved = ts.resolveModuleName(specifier, containingFile, options, host).resolvedModule
if (resolved !== undefined && sourceExtensions.has(resolved.extension)) continue
violations.push(`${[...locations].join(', ')}: ${specifier} does not resolve to workspace source through tsconfig.base.json paths (add a mapping so the tsx source launch does not depend on built lib/)`)
}
return violations
}
function missingPluginDependencies(
references: readonly PluginReference[],
dependencies: Readonly<Record<string, string>>,
manifestPath: string,
): string[] {
const requiredPackages = new Map<string, Set<string>>()
const require = (packageName: string, file: string): void => {
const locations = requiredPackages.get(packageName) ?? new Set<string>()
locations.add(file)
requiredPackages.set(packageName, locations)
}
for (const reference of references) {
const packageName = packageNameFromSpecifier(reference.name)
if (packageName === undefined) continue
const locations = requiredPackages.get(packageName) ?? new Set<string>()
locations.add(reference.file)
requiredPackages.set(packageName, locations)
require(packageName, reference.file)
if (packageName === CHOOSER_PACKAGE) {
for (const backend of CHOOSER_BACKEND_PACKAGES) require(backend, reference.file)
}
}
return [...requiredPackages].flatMap(([packageName, locations]) => packageName in dependencies
? []

View File

@@ -53,6 +53,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-slots': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-primitives': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/web-react': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/schema-form': { kind: 'none', reason: 'Browser-side form-rendering library; registers no model surface.' },
'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/runtime': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
@@ -80,6 +81,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/hooks/hook-protocol': { kind: 'indirect', reason: 'Only the hook bridge plugins render decoded hook output to a model.' },
'packages/host/apiproxy': { kind: 'none', reason: 'The wire contract and fetch carriers move already-composed messages and register no model surface.' },
'packages/host/directory-picker': { kind: 'none', reason: 'The GUI-host picking seam registers no model surface.' },
'packages/host/directory-picker-auto': { kind: 'none', reason: 'The GUI-host picking chooser only mounts a backend row; registers no model surface.' },
'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.' },
@@ -101,6 +103,11 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/session-projection/session-projection-cache': { kind: 'none', reason: 'The persisted cache accelerates host-side cold reads of projection state and registers no model surface.' },
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers no model surface.' },
'packages/session-query/session-query-sqlite': { kind: 'none', reason: 'The search backend returns hits only to callers and registers no model surface.' },
'packages/settings/settings': { kind: 'indirect', reason: 'The seam stores and resolves user settings; consumer plugins own any model surface a value feeds.' },
'packages/settings/settings-local': { kind: 'indirect', reason: 'The file provider stores and publishes namespace sections; consumers of ctx.settings own any model surface.' },
'packages/credentials/credentials': { kind: 'indirect', reason: 'The seam resolves credential references; the consuming adapter owns every model surface a value authorizes.' },
'packages/credentials/credentials-local': { kind: 'indirect', reason: 'The file/environment provider stores credential values; consumers of ctx.credentials own any model surface.' },
'packages/util/atomic-write': { kind: 'none', reason: 'Pure filesystem write primitive; registers no model surface.' },
'packages/telemetry/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers no model surface.' },
'packages/telemetry/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers no model surface.' },
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },