mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(dx): scopeHost, agent-aware ACP presentation, and the scoped-dispatch drift gate
scopeHost(ctx, services) is the sanctioned way to mint scopes in tests: it names absent services loudly instead of the cryptic cordis without-inject dead end, and catches the silent-no-op host (cordis resolves a dependency-pending fiber's await without running the inject callback). The ACP ToolPresenter resolves presentations through the session agent's view (tools.get(name, agent)) so a scoped/shadowed tool renders with the same definition that executed. verify-scoped-dispatch (doc-sync + pre-push) pins the dev-invariants carrier table against the declaration JSDoc set: an event enforced but undocumented, documented but unenforced, or a registry-subject notification leaking into the table fails the build. subagent/start|end docs gain their scoped-dispatch sentence (a real gap the gate caught on first run).
This commit is contained in:
@@ -262,6 +262,7 @@ function docSyncLeafGates(): Gate[] {
|
||||
pnpmScript('config-catalog', 'verify-config-catalog', { label: 'config catalog' }),
|
||||
pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }),
|
||||
pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }),
|
||||
pnpmScript('scoped-dispatch', 'verify-scoped-dispatch', { label: 'scoped dispatch' }),
|
||||
pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }),
|
||||
pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }),
|
||||
pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }),
|
||||
|
||||
78
scripts/verify-scoped-dispatch.ts
Normal file
78
scripts/verify-scoped-dispatch.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Scoped-dispatch drift gate: the set of scope-filtered events is declared in
|
||||
* TWO places that must never diverge — the dev-invariants runtime table (the
|
||||
* `scopedSubject` map in `packages/support/invariants/src/index.ts`, which
|
||||
* enforces carriers at dispatch time) and the event declarations' JSDoc (the
|
||||
* "Scope-filtered dispatch" sentence rendered into the events catalog, which
|
||||
* tells plugin authors what a scoped listener will and won't hear). An event
|
||||
* added to one side without the other either silently escapes runtime
|
||||
* enforcement or documents filtering that never happens; this gate fails the
|
||||
* build instead.
|
||||
*
|
||||
* Sources of truth: the invariant table is parsed from the invariants source;
|
||||
* the documented set is parsed from every `declare module 'cordis'` Events
|
||||
* JSDoc in packages/*\/*\/src carrying the marker sentence. Registry-subject
|
||||
* notifications (`tools/change`, `system-prompt/change`, `subagent/provider-*`)
|
||||
* are deliberately unfiltered and must appear in NEITHER set.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** The marker sentence every scope-filtered event's JSDoc carries. */
|
||||
const MARKER = 'Scope-filtered dispatch'
|
||||
|
||||
/** Events that are deliberately UNFILTERED registry-subject notifications. */
|
||||
const REGISTRY_SUBJECT = new Set(['tools/change', 'system-prompt/change', 'subagent/provider-added', 'subagent/provider-removed'])
|
||||
|
||||
function invariantTable(): Set<string> {
|
||||
const source = readFileSync(resolve(root, 'packages/support/invariants/src/index.ts'), 'utf8')
|
||||
const start = source.indexOf('const scopedSubject')
|
||||
if (start < 0) throw new Error('verify-scoped-dispatch: cannot find the scopedSubject table in dsh-invariants')
|
||||
const block = source.slice(start, source.indexOf('}', start))
|
||||
return new Set([...block.matchAll(/'([a-z-]+\/[a-z-]+)':/g)].flatMap(match => match[1] === undefined ? [] : [match[1]]))
|
||||
}
|
||||
|
||||
function documentedSet(): Set<string> {
|
||||
const documented = new Set<string>()
|
||||
for (const rel of globSync('packages/*/*/src/**/*.ts', { cwd: root })) {
|
||||
const source = readFileSync(resolve(root, rel), 'utf8')
|
||||
if (!source.includes(MARKER)) continue
|
||||
// Each event declaration: a JSDoc block followed by the quoted event name.
|
||||
// Tolerate `//` comment lines between the JSDoc and the declaration
|
||||
// (e.g. an inline TODO under the doc block).
|
||||
for (const match of source.matchAll(/\/\*\*([\s\S]*?)\*\/\s*\n(?:\s*\/\/[^\n]*\n)*\s*'([a-z-]+\/[a-z-]+)'\(/g)) {
|
||||
const [, doc, event] = match
|
||||
if (doc === undefined || event === undefined) continue
|
||||
if (doc.includes(MARKER)) documented.add(event)
|
||||
}
|
||||
}
|
||||
return documented
|
||||
}
|
||||
|
||||
const table = invariantTable()
|
||||
const documented = documentedSet()
|
||||
|
||||
const problems: string[] = []
|
||||
for (const event of table) {
|
||||
if (!documented.has(event)) {
|
||||
problems.push(`"${event}" is enforced by the dev-invariants carrier table but its declaration JSDoc carries no "${MARKER}" sentence — document the filtering plugin authors will observe.`)
|
||||
}
|
||||
if (REGISTRY_SUBJECT.has(event)) {
|
||||
problems.push(`"${event}" is a registry-subject notification (deliberately unfiltered) but appears in the dev-invariants carrier table.`)
|
||||
}
|
||||
}
|
||||
for (const event of documented) {
|
||||
if (!table.has(event)) {
|
||||
problems.push(`"${event}" documents scope-filtered dispatch but is missing from the dev-invariants carrier table (packages/support/invariants) — a bare dispatch of it would silently revert to global delivery.`)
|
||||
}
|
||||
}
|
||||
|
||||
if (problems.length > 0) {
|
||||
console.error(`verify-scoped-dispatch: ${problems.length} drift(s) between the invariant table and the documented scoped-event set:`)
|
||||
for (const problem of problems) console.error(` - ${problem}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`verify-scoped-dispatch: ${table.size} scope-filtered event(s) consistent between the invariant table and the declaration docs.`)
|
||||
Reference in New Issue
Block a user