Merge remote-tracking branch 'origin/master' into worktree/plan-review-layout
This commit is contained in:
+10
-22
@@ -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) })
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"AGENTS.md": 1755,
|
||||
"AGENTS.md": 1765,
|
||||
"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": 905
|
||||
}
|
||||
@@ -186,6 +186,11 @@ 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',
|
||||
SettingsUpdateSource: 'settings.md',
|
||||
AskUserQuestionAnswer: 'user-interaction.md',
|
||||
AskUserQuestionRequest: 'user-interaction.md',
|
||||
UserInteractionProvider: 'user-interaction.md',
|
||||
@@ -216,6 +221,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',
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
})
|
||||
+136
-27
@@ -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,15 @@ 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: [],
|
||||
note: 'Plugins register namespace schemas and resolve layered values; providers store the raw document. No production consumer is migrated yet.',
|
||||
},
|
||||
{
|
||||
key: 'telemetry',
|
||||
pkg: 'session-telemetry',
|
||||
@@ -605,11 +618,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 +632,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 +696,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 +724,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 +744,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 +839,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 +942,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 +989,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 +1059,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 {
|
||||
|
||||
@@ -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',
|
||||
@@ -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',
|
||||
|
||||
@@ -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(19)
|
||||
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',
|
||||
|
||||
@@ -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' },
|
||||
|
||||
+32
-19
@@ -195,7 +195,7 @@ 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(),
|
||||
@@ -295,16 +295,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 +331,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 +387,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'] }),
|
||||
@@ -410,7 +420,7 @@ function coverageGate(): Gate {
|
||||
|
||||
// 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 +468,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 +477,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 +516,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',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1338,6 +1338,36 @@
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -29,6 +29,9 @@ 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
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
@@ -78,6 +81,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 +112,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,7 +132,9 @@ 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')
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,8 @@ 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/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.' },
|
||||
|
||||
Reference in New Issue
Block a user