Merge commit '70396085b141370ce32de1be4e225b4384eaf46d' into HEAD
# Conflicts: # .agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml # .agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.i18n.yaml # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.i18n.yaml # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md # .agents/notes/implemented/simplification/2026-07-23-acp-automation-only-protocol.i18n.yaml # docs/config-catalog.i18n.yaml # docs/module-graph.i18n.yaml # docs/module-graph.md # docs/module-graph.zh.md # docs/tool-catalog.i18n.yaml # docs/tool-catalog.md # docs/tool-catalog.zh.md # examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json # examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json # examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json # packages/core/tools/README.i18n.yaml # packages/core/tools/README.zh.md # packages/core/tools/src/code-mode.ts # packages/host/apiproxy/tests/api-proxy-models.spec.ts # packages/host/plugin-inventory/tests/inventory.spec.ts # packages/mcp/mcp-client/tests/mcp-client.e2e.ts # packages/mcp/mcp-client/tests/mcp-client.spec.ts # packages/self-modification/tool-cordis/src/api-catalog.ts # packages/test-support/acp-snapshot/README.i18n.yaml # pnpm-lock.yaml
This commit is contained in:
@@ -17,7 +17,7 @@ const root = resolve(import.meta.dirname, '..')
|
||||
/** The closure manifest whose dependencies define the executable. */
|
||||
const DEPLOY_ROOT_PACKAGE = 'dsh-jsonrpc-agent-pkg'
|
||||
/** The closed-runtime app entry inside the deployed closure. */
|
||||
const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js'
|
||||
const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js'
|
||||
const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg'
|
||||
/** Default Node major; SEA mode requires at least Node 22. */
|
||||
const DEFAULT_NODE_RANGE = 'node24'
|
||||
|
||||
@@ -234,9 +234,9 @@ def verify_wheel(
|
||||
raise RuntimeError(
|
||||
f"{wheel} has distribution name {metadata.get('Name')}, expected {expected_distribution}"
|
||||
)
|
||||
if metadata.get("License-Expression") != "BSD-3-Clause":
|
||||
if metadata.get("License-Expression") != "MIT":
|
||||
raise RuntimeError(
|
||||
f"{wheel} has license expression {metadata.get('License-Expression')}, expected BSD-3-Clause"
|
||||
f"{wheel} has license expression {metadata.get('License-Expression')}, expected MIT"
|
||||
)
|
||||
expected_license_files = ["LICENSE"] if package == "sdk" else ["LICENSE", "THIRD_PARTY_NOTICES.md"]
|
||||
license_files = [Path(name).name for name in metadata.get_all("License-File") or []]
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||
import { join, relative, resolve } from 'node:path'
|
||||
import { hasTypeRTRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
|
||||
import { hasTypertRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
|
||||
import { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
// vendor/* is single-level; packages/<group>/<pkg> nests one level deeper
|
||||
// (the group dirs — core/llm/bash/… — are pure containers with no manifest).
|
||||
// (the group dirs — core/llm/shell/… — are pure containers with no manifest).
|
||||
const workspaceGlobs = [
|
||||
{ dir: 'vendor', depth: 1 },
|
||||
{ dir: 'packages', depth: 2 },
|
||||
@@ -55,7 +55,7 @@ const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
|
||||
'@deepseek-ai/dsh': ['lib/*.js', 'config'],
|
||||
// The Web build emits sourcemaps for browser debugging; publishing them is
|
||||
// what the payload policy forbids, so the bundle ships without them.
|
||||
'@deepseek-ai/dsh-frontend': ['dist', '!dist/**/*.map'],
|
||||
'@deepseek-ai/dsh-web-frontend': ['dist', '!dist/**/*.map'],
|
||||
}
|
||||
|
||||
/** The subset of package.json fields this constraint check cares about. */
|
||||
@@ -138,7 +138,7 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
|
||||
// The Python runtime uses a distinct closed-resolution bin; the public CLI
|
||||
// keeps config-owned bare-package resolution through lib/bin.js.
|
||||
'@deepseek-ai/dsh-jsonrpc-demo': ['lib/packaged-bin.js'],
|
||||
'@deepseek-ai/dsh-sdk-jsonrpc-demo': ['lib/packaged-bin.js'],
|
||||
// The argv-prefix runner entry ships beside the lib as its own bundle;
|
||||
// sandbox-local resolves it through the package's ./runner export. tsdown
|
||||
// also shares its generated FFI code through a hashed runtime chunk.
|
||||
@@ -185,7 +185,7 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
||||
...hasExportPair(manifest, './client/typert', './lib/typert.client.d.ts', './lib/typert.client.js')
|
||||
? ['lib/typert.client.js', 'lib/typert.client.d.ts']
|
||||
: [],
|
||||
...hasTypeRTRemoteNavigation(manifest)
|
||||
...hasTypertRemoteNavigation(manifest)
|
||||
? ['lib/typert.remote-client.js', 'lib/typert.remote-client.d.ts']
|
||||
: [],
|
||||
]
|
||||
@@ -231,8 +231,8 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
if (manifest.private === true) {
|
||||
errors.push(`${label}: published Landlock package must not set "private": true`)
|
||||
}
|
||||
if (manifest.publishConfig?.access !== 'restricted') {
|
||||
errors.push(`${label}: published Landlock package must set publishConfig.access to "restricted"`)
|
||||
if (manifest.publishConfig?.access !== 'public') {
|
||||
errors.push(`${label}: published Landlock package must set publishConfig.access to "public"`)
|
||||
}
|
||||
const expectedDirectory = dir
|
||||
if (manifest.repository?.type !== 'git'
|
||||
@@ -242,13 +242,20 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
}
|
||||
} else if (releaseMemberDirectory.test(dir)) {
|
||||
// Release members state that they are publishable: npm refuses a private
|
||||
// package, the scope is published privately, and the repository field is
|
||||
// how a consumer of a private package finds its source.
|
||||
// package, and the repository field is how a consumer finds the source of
|
||||
// the package it installed.
|
||||
//
|
||||
// Access is per release sequence, not per scope: the vendored framework and
|
||||
// the Landlock packages publish publicly because outside consumers install
|
||||
// them, while the dsh family stays restricted until its own sequence goes
|
||||
// public. A mixed scope is why no publish path passes `--access` — one flag
|
||||
// cannot serve both, so each packed manifest decides
|
||||
// ([rationale](../.agents/notes/implemented/process/2026-08-13-public-vendor-and-native-sequences.md)).
|
||||
if (manifest.private === true) {
|
||||
errors.push(`${label}: release member must not set "private": true`)
|
||||
}
|
||||
if (manifest.publishConfig?.access !== 'restricted') {
|
||||
errors.push(`${label}: release member must set publishConfig.access to "restricted"`)
|
||||
if (manifest.publishConfig?.access !== 'public') {
|
||||
errors.push(`${label}: release member must set publishConfig.access to "public"`)
|
||||
}
|
||||
if (manifest.repository?.type !== 'git'
|
||||
|| manifest.repository.url !== publishedRepositoryUrl
|
||||
|
||||
@@ -34,14 +34,20 @@ describe('CI workflow', () => {
|
||||
|| !isRecord(workflow.jobs['windows-native'])
|
||||
|| !isRecord(workflow.jobs['wine-apt-cache'])
|
||||
|| !isRecord(workflow.jobs['serial-windows'])
|
||||
|| !isRecord(workflow.jobs['node-24'])
|
||||
|| !isRecord(workflow.jobs['node-24-coverage'])
|
||||
|| !isRecord(workflow.jobs['node-24-consumers'])
|
||||
|| !isRecord(workflow.jobs['all-checks-passed'])) {
|
||||
throw new TypeError('CI workflow must define windows, windows-native, wine-apt-cache, serial-windows, and all-checks-passed jobs')
|
||||
throw new TypeError('CI workflow must define windows, windows-native, wine-apt-cache, serial-windows, node-24, node-24-coverage, node-24-consumers, and all-checks-passed jobs')
|
||||
}
|
||||
|
||||
const windows = workflow.jobs.windows
|
||||
const windowsNative = workflow.jobs['windows-native']
|
||||
const wineAptCache = workflow.jobs['wine-apt-cache']
|
||||
const serialWindows = workflow.jobs['serial-windows']
|
||||
const node24 = workflow.jobs['node-24']
|
||||
const node24Coverage = workflow.jobs['node-24-coverage']
|
||||
const node24Consumers = workflow.jobs['node-24-consumers']
|
||||
const aggregate = workflow.jobs['all-checks-passed']
|
||||
if (!Array.isArray(windows.steps) || !Array.isArray(aggregate.needs)) {
|
||||
throw new TypeError('Windows job must define steps and the aggregate must define needs')
|
||||
@@ -57,8 +63,10 @@ describe('CI workflow', () => {
|
||||
expect(commandSteps.some(step => step.run.includes('wine-windows-gates.sh'))).toBe(true)
|
||||
|
||||
// windows-native: non-blocking native job with failover, runs windows-complete.
|
||||
// Its pool is resolved by the Windows-specific switch.
|
||||
expect(typeof windowsNative['runs-on']).toBe('string')
|
||||
expect(windowsNative['runs-on']).toContain('DSH_CI_FAILOVER')
|
||||
expect(windowsNative['runs-on']).toContain('DSH_CI_FAILOVER_WINDOWS')
|
||||
expect(windowsNative['runs-on']).not.toContain('DSH_CI_FAILOVER_LINUX')
|
||||
expect(windowsNative['runs-on']).toContain('self-hosted')
|
||||
expect(windowsNative['runs-on']).toContain('dsh-win-ci')
|
||||
expect(windowsNative['runs-on']).toContain('dsh-windows-2025-16core')
|
||||
@@ -85,14 +93,94 @@ describe('CI workflow', () => {
|
||||
expect(aggregate.needs).toContain('windows')
|
||||
expect(aggregate.needs).not.toContain('windows-native')
|
||||
expect(aggregate.needs).not.toContain('serial-windows')
|
||||
|
||||
// Linux failover is a separate switch: the three required Linux workers
|
||||
// and the verdict job resolve their pool through DSH_CI_FAILOVER_LINUX,
|
||||
// never the Windows switch.
|
||||
for (const [jobName, job] of [['node-24', node24], ['node-24-coverage', node24Coverage], ['node-24-consumers', node24Consumers]] as const) {
|
||||
expect(typeof job['runs-on']).toBe('string')
|
||||
expect(job['runs-on'], `${jobName} runs-on must use the Linux failover switch`).toContain('DSH_CI_FAILOVER_LINUX')
|
||||
expect(job['runs-on'], `${jobName} runs-on must not use the Windows failover switch`).not.toContain('DSH_CI_FAILOVER_WINDOWS')
|
||||
expect(job['runs-on']).toContain('vm-backup')
|
||||
}
|
||||
expect(aggregate['runs-on']).toContain('DSH_CI_FAILOVER_LINUX')
|
||||
expect(aggregate['runs-on']).not.toContain('DSH_CI_FAILOVER_WINDOWS')
|
||||
expect(aggregate['runs-on']).toContain('vm-backup')
|
||||
})
|
||||
|
||||
it('exempts push from cancellation, so one master merge does not cancel the running drill', () => {
|
||||
const workflow = loadWorkflow('.github/workflows/ci.yml')
|
||||
if (!isRecord(workflow.jobs) || !isRecord(workflow.concurrency)) {
|
||||
throw new TypeError('CI workflow must define jobs and a workflow-level concurrency block')
|
||||
}
|
||||
|
||||
// Cancellation applies to the whole superseded RUN, so this has to be
|
||||
// decided at workflow level and gated on the event: a job-level group
|
||||
// cannot exempt its job from its run being cancelled. Only push is exempt —
|
||||
// a drill takes longer than the interval between master merges. The negated
|
||||
// form is load-bearing: `== 'pull_request'` would also stop cancelling
|
||||
// workflow_dispatch, and a re-dispatched runner benchmark holds up to 12
|
||||
// larger runners for 15 minutes in this same group on master. The
|
||||
// expression is evaluated against the NEWLY TRIGGERED run, so a dispatch on
|
||||
// master still cancels a mid-flight drill; the runbook records that bound.
|
||||
expect(workflow.concurrency['cancel-in-progress']).toBe("${{ github.event_name != 'push' }}")
|
||||
|
||||
// Neither drill may carry a job-level group: it would not exempt the job
|
||||
// from run-scoped cancellation.
|
||||
for (const name of ['serial-linux-selfhosted', 'serial-windows']) {
|
||||
const job = workflow.jobs[name]
|
||||
if (!isRecord(job)) throw new TypeError(`${name} must be defined`)
|
||||
expect(job.concurrency).toBeUndefined()
|
||||
// Both stay master-push-only; that is what makes the push carve-out safe.
|
||||
expect(job.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'")
|
||||
}
|
||||
|
||||
// What bounds the cost of exempting push: a master push may only carry the
|
||||
// cache seeder and the two drills. Any job reachable on push would start
|
||||
// accumulating uncancelled runs, so the set is pinned here.
|
||||
//
|
||||
// Classification is an exact allowlist of the conditions in use, not a
|
||||
// substring match: `github.event_name != 'pull_request'` mentions
|
||||
// `pull_request` yet IS push-reachable, so matching on the event name alone
|
||||
// would silently misclassify it as gated.
|
||||
const NOT_PUSH_REACHABLE = new Set([
|
||||
"github.event_name == 'pull_request'",
|
||||
"always() && github.event_name == 'pull_request'",
|
||||
"github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark'",
|
||||
"github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark'",
|
||||
])
|
||||
const pushReachable = Object.entries(workflow.jobs)
|
||||
.filter(([, job]) => {
|
||||
if (!isRecord(job)) return false
|
||||
if (job.if === undefined) return true // unconditional: runs on every event
|
||||
if (job.if === false) return false // `if: false` parses as a boolean
|
||||
if (typeof job.if !== 'string') return true // unrecognized shape: surface it
|
||||
return !NOT_PUSH_REACHABLE.has(job.if.trim())
|
||||
})
|
||||
.map(([name]) => name)
|
||||
.sort()
|
||||
expect(pushReachable).toEqual(['serial-linux-selfhosted', 'serial-windows', 'wine-apt-cache'])
|
||||
|
||||
// Why workflow_dispatch must keep cancelling: each benchmark fans out to a
|
||||
// dozen larger runners at once, in this same group on master. If it stopped
|
||||
// cancelling, a re-dispatch would queue ahead of a drill instead of
|
||||
// replacing the stale measurement.
|
||||
for (const name of ['larger-runner-benchmark', 'consolidated-runner-benchmark']) {
|
||||
const job = workflow.jobs[name]
|
||||
if (!isRecord(job) || !isRecord(job.strategy)) {
|
||||
throw new TypeError(`${name} must define a matrix strategy`)
|
||||
}
|
||||
expect(job.strategy['max-parallel']).toBe(12)
|
||||
expect(job['timeout-minutes']).toBe(15)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps supported LSP source under native Windows coverage', () => {
|
||||
const config = readFileSync(resolve(root, 'vitest.config.ts'), 'utf8')
|
||||
|
||||
expect(config).not.toContain('packages/lsp/lsp-local/src/connection.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-local/src/index.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-local/src/instance.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-stdio/src/connection.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-stdio/src/index.ts')
|
||||
expect(config).not.toContain('packages/lsp/lsp-stdio/src/instance.ts')
|
||||
})
|
||||
|
||||
it('requires one release-shaped Python runtime target on every pull request', () => {
|
||||
|
||||
@@ -9,10 +9,13 @@ import { describe, expect, it } from 'vitest'
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
|
||||
function clientCssDeclarations(): string[] {
|
||||
const clientRoot = resolve(root, 'packages/client')
|
||||
return readdirSync(clientRoot, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => resolve(clientRoot, entry.name, 'src/css-modules.d.ts'))
|
||||
const clientGroups = ['client', 'extensions']
|
||||
return clientGroups.flatMap((group) => {
|
||||
const clientRoot = resolve(root, 'packages', group)
|
||||
return readdirSync(clientRoot, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => resolve(clientRoot, entry.name, 'src/css-modules.d.ts'))
|
||||
})
|
||||
.filter(existsSync)
|
||||
.map(file => file.replaceAll(sep, '/'))
|
||||
.sort()
|
||||
|
||||
@@ -4,14 +4,14 @@ import { builtDeclarationPath } from './doc-typecheck-paths.ts'
|
||||
describe('builtDeclarationPath', () => {
|
||||
it('maps package source directories and exact entry files to built declarations', () => {
|
||||
expect(builtDeclarationPath('./packages/*/*/src')).toBe('./packages/*/*/lib/types')
|
||||
expect(builtDeclarationPath('./packages/support/invariants/src/index.ts'))
|
||||
.toBe('./packages/support/invariants/lib/types/index.d.ts')
|
||||
expect(builtDeclarationPath('./packages/runtime-diagnostics/invariants/src/index.ts'))
|
||||
.toBe('./packages/runtime-diagnostics/invariants/lib/types/index.d.ts')
|
||||
expect(builtDeclarationPath('./packages/core/session/src/invariant.ts'))
|
||||
.toBe('./packages/core/session/lib/types/invariant.d.ts')
|
||||
})
|
||||
|
||||
it('rejects aliases without a supported source target', () => {
|
||||
expect(() => builtDeclarationPath('./packages/support/invariants/source/index.ts'))
|
||||
expect(() => builtDeclarationPath('./packages/runtime-diagnostics/invariants/source/index.ts'))
|
||||
.toThrow('cannot map workspace source path')
|
||||
})
|
||||
})
|
||||
|
||||
214
scripts/gen-client-catalog.spec.ts
Normal file
214
scripts/gen-client-catalog.spec.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* The client slot catalog's judgement, proven on hand-built inputs: the
|
||||
* contract checks that must reject an unteachable slot, and the projection
|
||||
* facts a registrant depends on (who occupies a seat, what replacing it costs,
|
||||
* which owner has to be mounted). Run against the real workspace, the
|
||||
* generator's own `--check` covers freshness; these cases pin the rules that
|
||||
* make a stale or undocumented contract fail loudly instead of shipping.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { collectSlotEntries, oversizedSlotReports, resolveSlotEntries, validateSlotContracts } from './gen-client-catalog.ts'
|
||||
import type { SlotDeclaration, SlotRegistration, TypeDeclaration } from './slot-walk.ts'
|
||||
|
||||
/** A declaration with every field the catalog needs, overridable per case. */
|
||||
function declaration(over: Partial<SlotDeclaration> = {}): SlotDeclaration {
|
||||
return {
|
||||
key: 'demo.seat',
|
||||
kind: 'single',
|
||||
scope: 'root',
|
||||
jsDoc: '/** A seat. Registering here replaces the shipped entry. */',
|
||||
package: '@deepseek-ai/dsh-client-demo',
|
||||
source: 'packages/client/demo/src/client/contract/slots.ts:1',
|
||||
...over,
|
||||
}
|
||||
}
|
||||
|
||||
/** A registration into `demo.seat`, overridable per case. */
|
||||
function registration(over: Partial<SlotRegistration> = {}): SlotRegistration {
|
||||
return {
|
||||
key: 'demo.seat',
|
||||
package: '@deepseek-ai/dsh-client-demo',
|
||||
component: 'DemoSeat',
|
||||
children: [],
|
||||
source: 'packages/client/demo/src/client/index.ts:10',
|
||||
...over,
|
||||
}
|
||||
}
|
||||
|
||||
/** An exported owner-props declaration the catalog can resolve. */
|
||||
const OWNER_TYPES = new Map<string, TypeDeclaration>([
|
||||
['DemoOwnerProps', {
|
||||
name: 'DemoOwnerProps',
|
||||
text: '/** Owner share. */\nexport interface DemoOwnerProps {\n /** Column width. */\n width: number\n}',
|
||||
source: 'packages/client/demo/src/client/contract/slots.ts:20',
|
||||
}],
|
||||
])
|
||||
|
||||
describe('client slot contract validation', () => {
|
||||
it('accepts a documented slot whose owner props resolve', () => {
|
||||
expect(validateSlotContracts(
|
||||
[declaration({ ownerType: 'DemoOwnerProps' })],
|
||||
[registration()],
|
||||
OWNER_TYPES,
|
||||
)).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects a slot with no registrant-facing prose, naming the writing template', () => {
|
||||
const problems = validateSlotContracts([declaration({ jsDoc: '' })], [], new Map())
|
||||
expect(problems).toHaveLength(1)
|
||||
expect(problems[0]).toContain('has no JSDoc prose')
|
||||
expect(problems[0]).toContain('ui-settings')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['kind', { kind: 'whatever' }],
|
||||
['scope', { scope: 'whatever' }],
|
||||
])('rejects a slot whose %s is not one of the contract literals', (field, over) => {
|
||||
const problems = validateSlotContracts([declaration(over)], [], new Map())
|
||||
expect(problems).toHaveLength(1)
|
||||
expect(problems[0]).toContain(`no literal '${field}'`)
|
||||
})
|
||||
|
||||
it('rejects owner props no exported declaration provides', () => {
|
||||
const problems = validateSlotContracts([declaration({ ownerType: 'MissingProps' })], [], new Map())
|
||||
expect(problems).toHaveLength(1)
|
||||
expect(problems[0]).toContain('MissingProps')
|
||||
})
|
||||
|
||||
it('rejects the same key declared twice, because a merge would hide one contract', () => {
|
||||
const problems = validateSlotContracts(
|
||||
[declaration(), declaration({ source: 'packages/client/other/src/client/slots.ts:3' })],
|
||||
[],
|
||||
new Map(),
|
||||
)
|
||||
expect(problems).toHaveLength(1)
|
||||
expect(problems[0]).toContain('is also declared at')
|
||||
})
|
||||
|
||||
it('rejects a registration into an undeclared slot as a scan blind spot', () => {
|
||||
const problems = validateSlotContracts([declaration()], [registration({ key: 'ghost.seat' })], new Map())
|
||||
expect(problems).toHaveLength(1)
|
||||
expect(problems[0]).toContain('blind spot')
|
||||
})
|
||||
|
||||
it('rejects a children declaration for a slot no merge types', () => {
|
||||
const problems = validateSlotContracts([declaration()], [registration({ children: ['ghost.child'] })], new Map())
|
||||
expect(problems).toHaveLength(1)
|
||||
expect(problems[0]).toContain("child slot 'ghost.child'")
|
||||
})
|
||||
})
|
||||
|
||||
describe('client slot projection', () => {
|
||||
const kits = new Map<string, readonly string[]>([['root', ['useSessions: Hook']]])
|
||||
|
||||
it('warns that a single seat with a shipped occupant is replaced, not shared', () => {
|
||||
const [entry] = resolveSlotEntries([declaration()], [registration()], OWNER_TYPES, kits)
|
||||
expect(entry?.replaceRisk).toBe('shadows-shipped-ui')
|
||||
expect(entry?.occupants).toEqual(['client-demo DemoSeat'])
|
||||
})
|
||||
|
||||
it('treats a list seat as additive even when shipped entries exist', () => {
|
||||
const [entry] = resolveSlotEntries(
|
||||
[declaration({ kind: 'list' })],
|
||||
[registration({ id: 'shipped' })],
|
||||
OWNER_TYPES,
|
||||
kits,
|
||||
)
|
||||
expect(entry?.replaceRisk).toBe('none')
|
||||
expect(entry?.occupants).toEqual(["client-demo DemoSeat id 'shipped'"])
|
||||
expect(entry?.registerOptions.map(option => option.name)).toEqual(['id', 'order', 'label'])
|
||||
})
|
||||
|
||||
it('names the entry whose mount makes a child seat exist', () => {
|
||||
const parent = registration({ key: 'demo.parent', children: ['demo.seat'] })
|
||||
const entries = resolveSlotEntries(
|
||||
[declaration(), declaration({ key: 'demo.parent' })],
|
||||
[parent],
|
||||
OWNER_TYPES,
|
||||
kits,
|
||||
)
|
||||
expect(entries.find(entry => entry.key === 'demo.seat')?.declaredBy)
|
||||
.toContain("an entry in 'demo.parent' (client-demo)")
|
||||
expect(entries.find(entry => entry.key === 'demo.parent')?.declaredBy)
|
||||
.toContain('built in')
|
||||
})
|
||||
|
||||
it('reports an open keyed domain and the keys already taken', () => {
|
||||
const [entry] = resolveSlotEntries(
|
||||
[declaration({ kind: 'keyed' })],
|
||||
[registration({ entryKey: 'bash' }), registration({ entryKey: 'read' })],
|
||||
OWNER_TYPES,
|
||||
kits,
|
||||
)
|
||||
expect(entry?.keyDomain).toContain('open: any string')
|
||||
expect(entry?.keyDomain).toContain('already taken: bash, read')
|
||||
})
|
||||
|
||||
it('carries owner-props documentation into the entry, not just the type name', () => {
|
||||
const [entry] = resolveSlotEntries([declaration({ ownerType: 'DemoOwnerProps' })], [], OWNER_TYPES, kits)
|
||||
expect(entry?.ownerProps.join('\n')).toContain('Column width.')
|
||||
})
|
||||
|
||||
it('expands owner props one level and only names the shapes they reference', () => {
|
||||
// Transitive expansion once dragged the whole session model into four
|
||||
// seats; a registrant needs the fields, not the graph behind them.
|
||||
const types = new Map(OWNER_TYPES)
|
||||
types.set('Zone', {
|
||||
name: 'Zone',
|
||||
text: 'export interface Zone {\n session: BigSnapshot\n}',
|
||||
source: 'packages/client/demo/src/client/contract/slots.ts:30',
|
||||
})
|
||||
types.set('BigSnapshot', {
|
||||
name: 'BigSnapshot',
|
||||
text: 'export interface BigSnapshot {\n turns: number\n}',
|
||||
source: 'packages/client/demo/src/client/snapshot.ts:1',
|
||||
})
|
||||
const [entry] = resolveSlotEntries([declaration({ ownerType: 'Zone' })], [], types, kits)
|
||||
expect(entry?.ownerProps.join('\n')).toContain('export interface Zone')
|
||||
expect(entry?.ownerProps.join('\n')).not.toContain('export interface BigSnapshot')
|
||||
expect(entry?.ownerPropsReferences).toEqual(['BigSnapshot'])
|
||||
})
|
||||
|
||||
it('offers a runnable registration whose options match the cardinality', () => {
|
||||
const [entry] = resolveSlotEntries([declaration({ kind: 'list' })], [], OWNER_TYPES, kits)
|
||||
expect(entry?.example).toContain("ctx.slots.inject('demo.seat'")
|
||||
expect(entry?.example).toContain("id: 'my-entry'")
|
||||
})
|
||||
})
|
||||
|
||||
describe('the per-slot report budget', () => {
|
||||
it('rejects a slot whose report a model could not finish reading', () => {
|
||||
// Truncation already bounds one declaration, so the remaining runaway is
|
||||
// prose: a contract that grew into a manual costs exactly what narrowing to
|
||||
// one slot was supposed to save.
|
||||
const manual = ['/**', ...Array.from({ length: 150 }, (_, i) => ` * Paragraph ${String(i)} about this seat.`), ' */']
|
||||
const entries = resolveSlotEntries([declaration({ jsDoc: manual.join('\n') })], [], OWNER_TYPES, new Map())
|
||||
const problems = oversizedSlotReports(entries)
|
||||
expect(problems).toHaveLength(1)
|
||||
expect(problems[0]).toContain("slot 'demo.seat'")
|
||||
expect(problems[0]).toContain('tighten')
|
||||
})
|
||||
|
||||
it('passes a slot whose report stays within the budget', () => {
|
||||
const entries = resolveSlotEntries([declaration({ ownerType: 'DemoOwnerProps' })], [], OWNER_TYPES, new Map())
|
||||
expect(oversizedSlotReports(entries)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('the real workspace surface', () => {
|
||||
it('collects every declared slot with a teachable contract', { timeout: 30_000 }, () => {
|
||||
const entries = collectSlotEntries(process.cwd())
|
||||
expect(entries.length).toBeGreaterThan(30)
|
||||
for (const entry of entries) {
|
||||
expect(entry.summary, `${entry.key} has no summary`).not.toBe('')
|
||||
expect(['single', 'list', 'keyed', 'chain']).toContain(entry.kind)
|
||||
expect(['root', 'session', 'session-maybe']).toContain(entry.scope)
|
||||
}
|
||||
// The frame root is the canonical trap: occupied by the shipped app frame,
|
||||
// so a dynamic package registering there replaces the whole UI.
|
||||
const root = entries.find(entry => entry.key === 'root')
|
||||
expect(root?.replaceRisk).toBe('shadows-shipped-ui')
|
||||
expect(root?.occupants.join(' ')).toContain('AppFrame')
|
||||
})
|
||||
})
|
||||
558
scripts/gen-client-catalog.ts
Normal file
558
scripts/gen-client-catalog.ts
Normal file
@@ -0,0 +1,558 @@
|
||||
/**
|
||||
* Generate the model-facing client slot catalog consumed by `cordis_inspect
|
||||
* what:"client"`. A dynamic package's browser half can only contribute UI
|
||||
* through `ctx.slots.register`, and every fact it needs to do that safely —
|
||||
* which keys exist, what each register call must pass, what the component
|
||||
* receives, who already occupies the seat, and when the seat exists at all —
|
||||
* is decided at compile time by the shipped web bundle. This generator reads
|
||||
* those facts lexically (no type-checker program) and emits them as a data
|
||||
* module inside `tool-cordis`, so the host-side toolset teaches the browser
|
||||
* surface without importing a single client runtime module.
|
||||
*
|
||||
* `--check` verifies the committed artifact is fresh.
|
||||
*/
|
||||
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import {
|
||||
declaredTypes,
|
||||
indexExportedTypes,
|
||||
referencedTypeNames,
|
||||
scanSlotFiles,
|
||||
slotDeclarations,
|
||||
slotRegistrations,
|
||||
standardKitMembers,
|
||||
} from './slot-walk.ts'
|
||||
import type { ScannedFile, SlotDeclaration, SlotRegistration, TypeDeclaration } from './slot-walk.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'packages/extensions/cordis-client-runner/src/client/slot-catalog.ts'
|
||||
|
||||
/** Source globs: every workspace package's sources, `.tsx` included (a contract may live in one). */
|
||||
const SOURCE_GLOBS = ['packages/*/*/src/**/*.ts', 'packages/*/*/src/**/*.tsx']
|
||||
|
||||
/** Slot cardinalities the contract allows. */
|
||||
const KINDS = ['single', 'list', 'keyed', 'chain'] as const
|
||||
/** Slot data scopes the contract allows. */
|
||||
const SCOPES = ['root', 'session', 'session-maybe'] as const
|
||||
|
||||
/** Declarations longer than this render truncated; the full shape stays in source. */
|
||||
const MAX_DECL_CHARS = 1200
|
||||
|
||||
/**
|
||||
* Line budget for ONE slot's expanded report. The whole point of narrowing to a
|
||||
* single slot is to spend less context, so a report a model cannot finish
|
||||
* reading is a defect rather than a detail. Today's widest slot renders 60
|
||||
* lines, so this leaves room to document a slot properly while catching the two
|
||||
* ways a report runs away: an owner share that hands down a subsystem instead of
|
||||
* a share, and prose that grew into a manual.
|
||||
*/
|
||||
const MAX_ENTRY_LINES = 120
|
||||
|
||||
/** One register-call option as the catalog teaches it. */
|
||||
interface OptionDoc {
|
||||
readonly name: string
|
||||
readonly requirement: 'required' | 'optional'
|
||||
readonly type: string
|
||||
readonly doc: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Register options per cardinality, curated from `KindOptions` in
|
||||
* `packages/client/ui-slots/src/index.ts` — the authority for what a register
|
||||
* call may pass. Curated rather than projected because the authority is a
|
||||
* conditional type keyed on the slot's kind: it has no per-kind declaration a
|
||||
* lexical scan could read, and its own JSDoc addresses the compiler, not a
|
||||
* registrant. `verify-client-catalog` pins the authority's text so a change
|
||||
* there forces this table to be revisited.
|
||||
*/
|
||||
const REGISTER_OPTIONS: Readonly<Record<(typeof KINDS)[number], readonly OptionDoc[]>> = {
|
||||
single: [],
|
||||
list: [
|
||||
{ name: 'id', requirement: 'required', type: 'string', doc: 'Your cell key. Use an id of your own: a fresh id is added beside the shipped entries, while reusing a shipped id puts you in THAT cell and replaces it. Owners that filter by id address you by it.' },
|
||||
{ name: 'order', requirement: 'optional', type: 'number', doc: 'Position among the entries, ascending (default 0).' },
|
||||
{ name: 'label', requirement: 'optional', type: 'string | (() => string)', doc: 'Display text where the owner projects one (nav rows, tabs). A thunk is re-read on every projection, so localized text follows the active locale without re-registering.' },
|
||||
],
|
||||
keyed: [
|
||||
{ name: 'key', requirement: 'required', type: 'string', doc: 'Your cell key: the entry renders where the owner dispatches this exact key. Registering an already-occupied key replaces that occupant.' },
|
||||
],
|
||||
chain: [
|
||||
{ name: 'select', requirement: 'required', type: '(owner) => unknown | null', doc: 'Pure routing selector. Entries are tried in ascending order; the first non-null result wins and arrives as the component\'s `matched` prop. All-null falls through to the owner\'s fallback.' },
|
||||
],
|
||||
}
|
||||
|
||||
/** The one register option a dynamic package must NOT pass, and why. */
|
||||
const PRIORITY_NOTE = 'Do NOT pass `priority`: the browser-half facade assigns one automatically, and it is LOWER than every shipped entry — in a single or keyed cell that means your entry is the one that renders.'
|
||||
|
||||
/** Cross-cutting rules a registrant needs once, not per slot. */
|
||||
const CLIENT_NOTES: readonly string[] = [
|
||||
'Contribute UI only through `ctx.slots.register(options, Component)`; declare `inject: [\'slots\']` in your returned plugin (object form) or the seat is withheld.',
|
||||
'Wrap every registration in `ctx.slots.inject(key, () => ctx.slots.register(...))`. A slot exists only while the entry that declared it is mounted, and registering into an undeclared slot throws; `inject` runs your registration when the declaration is (or becomes) live and re-runs it if the owner remounts.',
|
||||
PRIORITY_NOTE,
|
||||
'You cannot `import` anything, so the design-system components are out of reach: build markup with `React.createElement` and ship CSS through `styles.insert(css)`. Use the theme CSS variables (`var(--dsw-alias-bg-layer-1)`, `var(--dsw-alias-label-primary)`, …) instead of literal colors, or your contribution breaks in the other color scheme.',
|
||||
'Every component receives the framework hook seats listed under `framework props` for its scope; a selector hook is called with a selector, e.g. `useSessions(state => state.current)`.',
|
||||
'This catalog is the COMPILE-TIME contract of the shipped web bundle, not a snapshot of one page: a key is registrable only where the owner that declares it is mounted. A failed registration surfaces in the browser-half load report — read it back with `cordis_inspect what:"temporary"`.',
|
||||
]
|
||||
|
||||
/** Standard-kit interface that applies to each scope, beyond the global one. */
|
||||
const SCOPE_KIT: Readonly<Record<(typeof SCOPES)[number], string | undefined>> = {
|
||||
'root': undefined,
|
||||
'session': 'SessionStandardProps',
|
||||
'session-maybe': 'SessionMaybeStandardProps',
|
||||
}
|
||||
|
||||
/** One resolved catalog entry, ready to render. */
|
||||
export interface SlotEntry {
|
||||
readonly key: string
|
||||
readonly kind: string
|
||||
readonly scope: string
|
||||
readonly summary: string
|
||||
readonly doc: string
|
||||
readonly registerOptions: readonly OptionDoc[]
|
||||
readonly ownerProps: readonly string[]
|
||||
readonly ownerPropsReferences: readonly string[]
|
||||
readonly standardProps: readonly string[]
|
||||
readonly keyDomain: string
|
||||
readonly hookContext: string
|
||||
readonly slotInject: string
|
||||
readonly declaredBy: string
|
||||
readonly occupants: readonly string[]
|
||||
readonly replaceRisk: string
|
||||
readonly example: string
|
||||
readonly source: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the workspace and resolve every catalog entry, failing loud on a
|
||||
* contract the catalog cannot teach.
|
||||
* @param scanRoot - repository root to scan.
|
||||
* @returns the entries, sorted by key.
|
||||
* @throws when any declared slot is unteachable or the scan contradicts itself.
|
||||
*/
|
||||
export function collectSlotEntries(scanRoot: string): SlotEntry[] {
|
||||
const files = scanSlotFiles(scanRoot, SOURCE_GLOBS)
|
||||
const declarations = files.flatMap(file => slotDeclarations(file))
|
||||
const registrations = files.flatMap(file => slotRegistrations(file))
|
||||
const types = indexExportedTypes(scanRoot, SOURCE_GLOBS)
|
||||
const problems = validateSlotContracts(declarations, registrations, types)
|
||||
if (problems.length > 0) {
|
||||
throw new Error(`gen-client-catalog: ${String(problems.length)} contract violation(s):\n${problems.map(problem => ` ${problem}`).join('\n')}`)
|
||||
}
|
||||
const entries = resolveSlotEntries(declarations, registrations, types, standardKits(files))
|
||||
const oversized = oversizedSlotReports(entries)
|
||||
if (oversized.length > 0) {
|
||||
throw new Error(`gen-client-catalog: ${String(oversized.length)} slot(s) exceed the per-slot report budget `
|
||||
+ `of ${String(MAX_ENTRY_LINES)} lines:\n${oversized.map(problem => ` ${problem}`).join('\n')}`)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
/**
|
||||
* Slots whose expanded report exceeds {@link MAX_ENTRY_LINES}. Separated from
|
||||
* the scan so the budget is provable on one hand-built entry.
|
||||
* @param entries - resolved catalog entries.
|
||||
* @returns one message per over-budget slot, empty when every report is readable.
|
||||
*/
|
||||
export function oversizedSlotReports(entries: readonly SlotEntry[]): string[] {
|
||||
return entries
|
||||
.filter(entry => entryLines(entry) > MAX_ENTRY_LINES)
|
||||
.map(entry => `slot '${entry.key}' (${entry.source}) reports ${String(entryLines(entry))} lines. `
|
||||
+ 'Narrow the owner share it passes down (a slot hands a registrant a share, not a subsystem) or tighten '
|
||||
+ 'its prose, so asking about one slot stays cheaper than asking about all of them.')
|
||||
}
|
||||
|
||||
/** Line count of one entry's variable-length content, the proxy for its rendered report. */
|
||||
function entryLines(entry: SlotEntry): number {
|
||||
const blocks = [entry.doc, entry.example, ...entry.ownerProps, ...entry.registerOptions.map(option => option.doc)]
|
||||
return blocks.reduce((total, block) => total + block.split('\n').length, 0)
|
||||
+ entry.standardProps.length + entry.ownerPropsReferences.length + entry.occupants.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail-closed contract checks: an unteachable slot must break the gate rather
|
||||
* than ship an entry a model cannot act on. Pure, so every rejection is
|
||||
* provable without scanning the workspace.
|
||||
* @param declarations - every declared slot.
|
||||
* @param registrations - every registration call site.
|
||||
* @param types - exported type index the owner-props reference resolves against.
|
||||
* @returns one message per violation, empty when the surface is teachable.
|
||||
*/
|
||||
export function validateSlotContracts(
|
||||
declarations: readonly SlotDeclaration[],
|
||||
registrations: readonly SlotRegistration[],
|
||||
types: ReadonlyMap<string, TypeDeclaration>,
|
||||
): string[] {
|
||||
const problems: string[] = []
|
||||
const byKey = new Map<string, SlotDeclaration>()
|
||||
for (const declaration of declarations) {
|
||||
const where = `slot '${declaration.key}' (${declaration.source})`
|
||||
const previous = byKey.get(declaration.key)
|
||||
if (previous !== undefined) {
|
||||
problems.push(`${where} is also declared at ${previous.source}; SlotMap merges duplicates silently, so the catalog cannot tell which documentation wins.`)
|
||||
continue
|
||||
}
|
||||
byKey.set(declaration.key, declaration)
|
||||
if (!(KINDS as readonly string[]).includes(declaration.kind)) {
|
||||
problems.push(`${where} has no literal 'kind'; the catalog derives the register options from it, so it must be one of ${KINDS.join('/')}.`)
|
||||
}
|
||||
if (!(SCOPES as readonly string[]).includes(declaration.scope)) {
|
||||
problems.push(`${where} has no literal 'scope'; the catalog derives the framework props from it, so it must be one of ${SCOPES.join('/')}.`)
|
||||
}
|
||||
if (docProse(declaration.jsDoc) === '') {
|
||||
problems.push(`${where} has no JSDoc prose. Write it from the REGISTRANT's side: what to pass, what the component receives, whom a registration replaces, and what absence looks like (packages/client/ui-settings/src/client/contract/slots.ts is the template).`)
|
||||
}
|
||||
if (declaration.ownerType !== undefined
|
||||
&& /^[A-Za-z_$][\w$]*$/.test(declaration.ownerType)
|
||||
&& !types.has(declaration.ownerType)) {
|
||||
problems.push(`${where} names owner props '${declaration.ownerType}' that no exported declaration provides; export the interface so the catalog can show what the component receives.`)
|
||||
}
|
||||
}
|
||||
for (const registration of registrations) {
|
||||
if (!byKey.has(registration.key)) {
|
||||
problems.push(`registration into '${registration.key}' (${registration.source}) targets a slot no SlotMap merge declares; either the scan has a blind spot or the registration is dead.`)
|
||||
}
|
||||
for (const child of registration.children) {
|
||||
if (!byKey.has(child)) {
|
||||
problems.push(`registration at ${registration.source} declares child slot '${child}' that no SlotMap merge types.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
/**
|
||||
* Project validated declarations into catalog entries: cardinality decides the
|
||||
* register options, scope decides the framework props, and the registration
|
||||
* call sites decide who already sits in the seat and which owner's mount makes
|
||||
* it exist. Pure, so the projection facts are provable without a workspace.
|
||||
* @param declarations - validated slot declarations.
|
||||
* @param registrations - every registration call site.
|
||||
* @param types - exported type index for owner-props expansion.
|
||||
* @param kits - framework prop seats per scope.
|
||||
* @returns the entries, sorted by key.
|
||||
*/
|
||||
export function resolveSlotEntries(
|
||||
declarations: readonly SlotDeclaration[],
|
||||
registrations: readonly SlotRegistration[],
|
||||
types: ReadonlyMap<string, TypeDeclaration>,
|
||||
kits: ReadonlyMap<string, readonly string[]>,
|
||||
): SlotEntry[] {
|
||||
const declaredBy = new Map<string, SlotRegistration>()
|
||||
for (const registration of registrations) {
|
||||
for (const child of registration.children) {
|
||||
if (!declaredBy.has(child)) declaredBy.set(child, registration)
|
||||
}
|
||||
}
|
||||
return declarations
|
||||
.map(declaration => entryOf(declaration, registrations, declaredBy.get(declaration.key), types, kits))
|
||||
.sort((left, right) => left.key.localeCompare(right.key))
|
||||
}
|
||||
|
||||
/** The framework prop seats per scope, read from the merged standard-kit interfaces. */
|
||||
function standardKits(files: readonly ScannedFile[]): ReadonlyMap<string, readonly string[]> {
|
||||
const global = standardKitMembers(files, 'GlobalStandardProps')
|
||||
const kits = new Map<string, readonly string[]>()
|
||||
for (const scope of SCOPES) {
|
||||
const extra = SCOPE_KIT[scope]
|
||||
kits.set(scope, [...global, ...extra === undefined ? [] : standardKitMembers(files, extra)])
|
||||
}
|
||||
return kits
|
||||
}
|
||||
|
||||
/** Resolve one declaration into its catalog entry. */
|
||||
function entryOf(
|
||||
declaration: SlotDeclaration,
|
||||
registrations: readonly SlotRegistration[],
|
||||
declaredBy: SlotRegistration | undefined,
|
||||
types: ReadonlyMap<string, TypeDeclaration>,
|
||||
kits: ReadonlyMap<string, readonly string[]>,
|
||||
): SlotEntry {
|
||||
const occupants = registrations.filter(registration => registration.key === declaration.key)
|
||||
const cellOccupied = occupants.some(occupant =>
|
||||
declaration.kind === 'single' || occupant.entryKey !== undefined)
|
||||
const doc = docProse(declaration.jsDoc)
|
||||
const owner = ownerShapes(declaration.ownerType, types)
|
||||
return {
|
||||
key: declaration.key,
|
||||
kind: declaration.kind,
|
||||
scope: declaration.scope,
|
||||
summary: firstSentence(doc),
|
||||
doc,
|
||||
registerOptions: REGISTER_OPTIONS[declaration.kind as (typeof KINDS)[number]],
|
||||
ownerProps: owner.declarations.map(type => truncate(type.text)),
|
||||
ownerPropsReferences: owner.references,
|
||||
standardProps: kits.get(declaration.scope) ?? [],
|
||||
keyDomain: keyDomainOf(declaration, occupants),
|
||||
hookContext: declaration.hookContext ?? '',
|
||||
slotInject: declaration.injectType ?? '',
|
||||
declaredBy: declaredBy === undefined
|
||||
? 'the runtime itself (built in; always present)'
|
||||
: `an entry in '${declaredBy.key}' (${shortPackage(declaredBy.package)}), so it exists while that entry is mounted`,
|
||||
occupants: occupants.map(occupant => [
|
||||
shortPackage(occupant.package),
|
||||
occupant.component,
|
||||
...occupant.id === undefined ? [] : [`id '${occupant.id}'`],
|
||||
...occupant.entryKey === undefined ? [] : [`key '${occupant.entryKey}'`],
|
||||
].join(' ')),
|
||||
replaceRisk: cellOccupied && (declaration.kind === 'single' || declaration.kind === 'keyed')
|
||||
? 'shadows-shipped-ui'
|
||||
: 'none',
|
||||
example: exampleOf(declaration),
|
||||
source: declaration.source,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The owner-props contract at ONE level: the owner declaration(s) themselves,
|
||||
* plus the names of the shapes their fields reference. Expanding transitively
|
||||
* pulled the whole session model into four seats (one report exceeded 2400
|
||||
* lines), which defeats the purpose of narrowing to a single slot — a registrant
|
||||
* needs the fields and their documented meaning, not the type graph behind them.
|
||||
*/
|
||||
function ownerShapes(
|
||||
ownerType: string | undefined,
|
||||
types: ReadonlyMap<string, TypeDeclaration>,
|
||||
): { declarations: TypeDeclaration[]; references: string[] } {
|
||||
if (ownerType === undefined) return { declarations: [], references: [] }
|
||||
const declarations = declaredTypes(referencedTypeNames([ownerType], types), types)
|
||||
const own = new Set(declarations.map(declaration => declaration.name))
|
||||
const references = referencedTypeNames(declarations.map(declaration => declaration.text), types)
|
||||
.filter(name => !own.has(name))
|
||||
return { declarations, references }
|
||||
}
|
||||
|
||||
/** How a keyed slot's key domain is constrained, '' for the other kinds. */
|
||||
function keyDomainOf(declaration: SlotDeclaration, occupants: readonly SlotRegistration[]): string {
|
||||
if (declaration.kind !== 'keyed') return ''
|
||||
const taken = [...new Set(occupants.flatMap(occupant => occupant.entryKey === undefined ? [] : [occupant.entryKey]))].sort()
|
||||
const shipped = taken.length === 0 ? 'none are taken yet' : `already taken: ${taken.join(', ')}`
|
||||
return declaration.keyProps === undefined
|
||||
? `open: any string the owner dispatches (no compile-time key set), ${shipped}`
|
||||
: `fixed by the owner's key table ${declaration.keyProps}, ${shipped}`
|
||||
}
|
||||
|
||||
/** A runnable minimal registration for one slot, per cardinality. */
|
||||
function exampleOf(declaration: SlotDeclaration): string {
|
||||
const options = [`name: '${declaration.key}'`, ...KIND_EXAMPLE[declaration.kind] ?? []].join(', ')
|
||||
return [
|
||||
'return {',
|
||||
" inject: ['slots'],",
|
||||
' apply(ctx) {',
|
||||
` ctx.slots.inject('${declaration.key}', () => ctx.slots.register(`,
|
||||
` { ${options} },`,
|
||||
" () => React.createElement('div', null, 'hello'),",
|
||||
' ))',
|
||||
' },',
|
||||
'}',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** Extra example options per cardinality. */
|
||||
const KIND_EXAMPLE: Readonly<Record<string, readonly string[]>> = {
|
||||
single: [],
|
||||
list: ["id: 'my-entry'", 'order: 100', "label: 'My entry'"],
|
||||
keyed: ["key: '<one key the owner dispatches>'"],
|
||||
chain: ['select: owner => null'],
|
||||
}
|
||||
|
||||
/** Drop the `@deepseek-ai/dsh-` prefix so rows stay readable. */
|
||||
function shortPackage(name: string): string {
|
||||
return name.replace('@deepseek-ai/dsh-', '')
|
||||
}
|
||||
|
||||
/** Truncate an over-long declaration, naming the truncation. */
|
||||
function truncate(text: string): string {
|
||||
return text.length > MAX_DECL_CHARS
|
||||
? `${text.slice(0, MAX_DECL_CHARS)} /* …truncated — full shape in source */`
|
||||
: text
|
||||
}
|
||||
|
||||
/** JSDoc prose: comment markers and block tags removed, paragraphs kept. */
|
||||
function docProse(jsDoc: string): string {
|
||||
const lines = jsDoc.replace(/^\/\*\*/, '').replace(/\*\/$/, '').split('\n')
|
||||
.map(line => line.replace(/^\s*\*?\s?/, '').replace(/\s+$/, ''))
|
||||
const kept: string[] = []
|
||||
for (const line of lines) {
|
||||
if (line.trimStart().startsWith('@')) break
|
||||
kept.push(line)
|
||||
}
|
||||
return kept.join('\n').replace(/\{@link\s+([^}]+)\}/g, '$1').replace(/\n{3,}/g, '\n\n').trim()
|
||||
}
|
||||
|
||||
/** First sentence of a prose block, for the compact listing. */
|
||||
function firstSentence(doc: string): string {
|
||||
const flat = doc.replace(/\s+/g, ' ').trim()
|
||||
const match = /^(.*?[.!?])(?:\s|$)/.exec(flat)
|
||||
return (match?.[1] ?? flat).trim()
|
||||
}
|
||||
|
||||
/** Render one value as a single-quoted TypeScript literal. */
|
||||
function quote(value: string): string {
|
||||
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'").replaceAll('\n', '\\n')}'`
|
||||
}
|
||||
|
||||
/** Render a readonly string-array literal. */
|
||||
function list(values: readonly string[], indent: string): string {
|
||||
if (values.length === 0) return '[]'
|
||||
return ['[', ...values.map(value => `${indent} ${quote(value)},`), `${indent}]`].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the generated data module.
|
||||
* @param entries - resolved catalog entries.
|
||||
* @returns the module source.
|
||||
*/
|
||||
export function renderClientCatalog(entries: readonly SlotEntry[]): string {
|
||||
const lines: string[] = [
|
||||
'/**',
|
||||
' * Generated by scripts/gen-client-catalog.ts — do not edit by hand; run',
|
||||
' * `pnpm run gen-client-catalog` to regenerate (freshness-gated by',
|
||||
' * `pnpm run verify-client-catalog` in doc-sync).',
|
||||
' *',
|
||||
' * The compile-time contract of the shipped web bundle\'s slot surface, as',
|
||||
' * `cordis_inspect what:"client"` serves it to the model: every SlotMap key a',
|
||||
' * browser half can register into, what that register call must pass, what the',
|
||||
' * component receives, who already occupies the seat, and which owner has to be',
|
||||
' * mounted for the seat to exist. Data only — this module is the one legitimate',
|
||||
' * meeting point of the two planes, so it carries strings, never client imports.',
|
||||
' *',
|
||||
' * @module @deepseek-ai/dsh-cordis-client-runner/client/slot-catalog',
|
||||
' */',
|
||||
'',
|
||||
'/* jscpd:ignore-start */',
|
||||
'/** One option a register call passes for a given slot cardinality. */',
|
||||
'export interface ClientSlotOption {',
|
||||
' /** Option name as written in the register options object. */',
|
||||
' name: string',
|
||||
' /** Whether the cardinality requires it. */',
|
||||
' requirement: string',
|
||||
' /** Accepted type, in source spelling. */',
|
||||
' type: string',
|
||||
' /** What it does, from the registrant\'s side. */',
|
||||
' doc: string',
|
||||
'}',
|
||||
'',
|
||||
'/** One browser-half slot a dynamic package can contribute UI into. */',
|
||||
'export interface ClientSlotEntry {',
|
||||
' /** SlotMap key passed as the register call\'s `name`. */',
|
||||
' key: string',
|
||||
' /** Cardinality: `single`, `list`, `keyed`, or `chain`. */',
|
||||
' kind: string',
|
||||
' /** Data scope: `root`, `session`, or `session-maybe`. */',
|
||||
' scope: string',
|
||||
' /** First sentence of the contract prose. */',
|
||||
' summary: string',
|
||||
' /** Full contract prose from the SlotMap declaration. */',
|
||||
' doc: string',
|
||||
' /** Options this cardinality accepts (beyond `name`). */',
|
||||
' registerOptions: readonly ClientSlotOption[]',
|
||||
' /** Declarations of the props the owner passes down, with their own documentation. */',
|
||||
' ownerProps: readonly string[]',
|
||||
' /** Names of the shapes those props reference; deliberately not expanded here. */',
|
||||
' ownerPropsReferences: readonly string[]',
|
||||
' /** Framework-supplied component props for this scope. */',
|
||||
' standardProps: readonly string[]',
|
||||
' /** For keyed slots: how the key set is constrained and which keys are taken. */',
|
||||
' keyDomain: string',
|
||||
' /** Opaque per-render-site context passed to slot-level hooks, when the slot declares one. */',
|
||||
' hookContext: string',
|
||||
' /** Slot-level inject face every entry receives, when the slot declares one. */',
|
||||
' slotInject: string',
|
||||
' /** Which mounted entry makes this slot exist. */',
|
||||
' declaredBy: string',
|
||||
' /** Entries the shipped composition already registered here. */',
|
||||
' occupants: readonly string[]',
|
||||
' /** `shadows-shipped-ui` when registering here replaces shipped UI; `none` when additive. */',
|
||||
' replaceRisk: string',
|
||||
' /** A minimal browser half that registers into this slot. */',
|
||||
' example: string',
|
||||
' /** Source pointer of the contract declaration. */',
|
||||
' source: string',
|
||||
'}',
|
||||
'',
|
||||
'/** Rules that apply to every browser-half contribution, in reading order. */',
|
||||
'export const CLIENT_NOTES: readonly string[] = [',
|
||||
...CLIENT_NOTES.map(note => ` ${quote(note)},`),
|
||||
']',
|
||||
'',
|
||||
'/** Every slot the shipped web bundle declares, sorted by key. */',
|
||||
// The entries below repeat by nature: seats of one cardinality share their
|
||||
// register options and framework props verbatim, and that sameness is the
|
||||
// contract a registrant reads, not a refactor waiting to happen. Clone
|
||||
// detection is told so here rather than through a config exception, which is
|
||||
// how this repository marks duplication that belongs to its subject.
|
||||
'// Seats of one cardinality repeat their register options and framework props',
|
||||
'// verbatim; that sameness IS the contract a registrant reads, so clone',
|
||||
'// detection is told to skip the data rather than the file.',
|
||||
'export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [',
|
||||
]
|
||||
for (const entry of entries) {
|
||||
lines.push(' {')
|
||||
lines.push(` key: ${quote(entry.key)},`)
|
||||
lines.push(` kind: ${quote(entry.kind)},`)
|
||||
lines.push(` scope: ${quote(entry.scope)},`)
|
||||
lines.push(` summary: ${quote(entry.summary)},`)
|
||||
lines.push(` doc: ${quote(entry.doc)},`)
|
||||
if (entry.registerOptions.length === 0) {
|
||||
lines.push(' registerOptions: [],')
|
||||
} else {
|
||||
lines.push(' registerOptions: [')
|
||||
for (const option of entry.registerOptions) {
|
||||
lines.push(' {')
|
||||
lines.push(` name: ${quote(option.name)},`)
|
||||
lines.push(` requirement: ${quote(option.requirement)},`)
|
||||
lines.push(` type: ${quote(option.type)},`)
|
||||
lines.push(` doc: ${quote(option.doc)},`)
|
||||
lines.push(' },')
|
||||
}
|
||||
lines.push(' ],')
|
||||
}
|
||||
lines.push(` ownerProps: ${list(entry.ownerProps, ' ')},`)
|
||||
lines.push(` ownerPropsReferences: ${list(entry.ownerPropsReferences, ' ')},`)
|
||||
lines.push(` standardProps: ${list(entry.standardProps, ' ')},`)
|
||||
lines.push(` keyDomain: ${quote(entry.keyDomain)},`)
|
||||
lines.push(` hookContext: ${quote(entry.hookContext)},`)
|
||||
lines.push(` slotInject: ${quote(entry.slotInject)},`)
|
||||
lines.push(` declaredBy: ${quote(entry.declaredBy)},`)
|
||||
lines.push(` occupants: ${list(entry.occupants, ' ')},`)
|
||||
lines.push(` replaceRisk: ${quote(entry.replaceRisk)},`)
|
||||
lines.push(` example: ${quote(entry.example)},`)
|
||||
lines.push(` source: ${quote(entry.source)},`)
|
||||
lines.push(' },')
|
||||
}
|
||||
lines.push(']', '/* jscpd:ignore-end */', '')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI entry: regenerate the catalog, or with `--check` fail when it is stale.
|
||||
* @returns nothing; writes the artifact or reports freshness through the process.
|
||||
*/
|
||||
export function main(): void {
|
||||
const content = renderClientCatalog(collectSlotEntries(root))
|
||||
const destination = resolve(root, OUT)
|
||||
if (process.argv.includes('--check')) {
|
||||
let committed: string | null = null
|
||||
try {
|
||||
committed = readFileSync(destination, 'utf8')
|
||||
} catch {
|
||||
// Only ENOENT (never generated) is expected here, and its remedy is the
|
||||
// same as a stale artifact's: regenerate.
|
||||
committed = null
|
||||
}
|
||||
if (committed === content) {
|
||||
console.log(`gen-client-catalog: ${OUT} is up to date.`)
|
||||
process.exit(0)
|
||||
}
|
||||
console.error(`gen-client-catalog: stale — ${OUT}. Run \`pnpm run gen-client-catalog\` and commit the result.`)
|
||||
process.exit(1)
|
||||
}
|
||||
mkdirSync(dirname(destination), { recursive: true })
|
||||
writeFileSync(destination, content)
|
||||
console.log(`gen-client-catalog: wrote ${OUT}.`)
|
||||
}
|
||||
|
||||
// Run only when invoked as a script, not when imported by a test.
|
||||
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
|
||||
main()
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { dirname, resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { LINK_MAP } from './gen-cordis-catalog.ts'
|
||||
import { parseJsDoc, pointer, rawJsDoc } from './jsdoc.ts'
|
||||
import { githubSlug } from './verify-md-links.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'docs/config-catalog.md'
|
||||
@@ -766,11 +767,6 @@ export function collectConfigCatalog(scanRoot: string = root): CatalogEntry[] {
|
||||
return entries.sort((a, b) => a.pkg.localeCompare(b.pkg))
|
||||
}
|
||||
|
||||
/** GitHub-style anchor slug for a `## \`pkg\`` heading. */
|
||||
function slug(heading: string): string {
|
||||
return heading.toLowerCase().replace(/[^a-z0-9 -]/g, '').replace(/ /g, '-')
|
||||
}
|
||||
|
||||
/** Render the `Requires:` service-key line, or '' when the plugin injects nothing. */
|
||||
function requiresLine(inject: string[]): string {
|
||||
return inject.length ? `Requires: ${inject.map(k => `\`${k}\``).join(' · ')}` : ''
|
||||
@@ -782,7 +778,7 @@ function requiresLine(inject: string[]): string {
|
||||
function refLink(ref: TypeRef, byName: Map<string, CatalogEntry>): string {
|
||||
const target = byName.get(ref.specifier)
|
||||
if (target?.kind === 'config' && ref.imported === target.configTypeName) {
|
||||
return `[\`${ref.alias}\`](#${slug(target.pkg)})`
|
||||
return `[\`${ref.alias}\`](#${githubSlug(target.pkg)})`
|
||||
}
|
||||
const page = LINK_MAP[ref.imported]
|
||||
if (page) return `[\`${ref.alias}\`](subsystems/${page})`
|
||||
@@ -792,7 +788,7 @@ function refLink(ref: TypeRef, byName: Map<string, CatalogEntry>): string {
|
||||
|
||||
/** Render one configurable plugin's section. */
|
||||
function renderConfigEntry(entry: CatalogEntry, byName: Map<string, CatalogEntry>): string[] {
|
||||
const out = [`## \`${entry.pkg}\``, '']
|
||||
const out = [`<a id="${githubSlug(entry.pkg)}"></a>`, '', `## \`${entry.pkg}\``, '']
|
||||
const requires = requiresLine(entry.inject)
|
||||
if (requires) out.push(requires, '')
|
||||
out.push('```' + FENCE, ...(entry.pastes ?? []).map(p => p.text).join('\n\n').split('\n'), '```', '')
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
* projection enforces event modes, JSDoc parameter/return completeness, and
|
||||
* signature type-link coverage; the inherited (vendor) tier renders to
|
||||
* `docs/cordis-api/inherited.md`. `--check` verifies every generated artifact.
|
||||
*
|
||||
* Generated regions embed `file:line` source pointers, so inserting lines ABOVE a
|
||||
* recorded symbol makes the committed output stale even though nothing about the
|
||||
* symbol changed. Regenerate after editing any file this projection records — the
|
||||
* failure otherwise surfaces as the "reproduces every committed catalog artifact
|
||||
* byte for byte" test failing, which reads like a snapshot regression rather than
|
||||
* a missing regeneration.
|
||||
*/
|
||||
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
@@ -32,7 +39,7 @@ import {
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const SUBSYSTEMS_DIR = 'docs/subsystems'
|
||||
const OUT_INHERITED = 'docs/cordis-api/inherited.md'
|
||||
const OUT_RUNTIME_API = 'packages/self-modification/tool-cordis/src/api-catalog.ts'
|
||||
const OUT_RUNTIME_API = 'packages/extensions/tool-cordis/src/api-catalog.ts'
|
||||
|
||||
export { REGION_BEGIN, REGION_END }
|
||||
|
||||
@@ -47,31 +54,35 @@ export const SERVICE_PAGE: Record<string, string> = {
|
||||
agentDefaultModel: 'core.md',
|
||||
agentPresets: 'core.md',
|
||||
agents: 'core.md',
|
||||
apiProxy: 'typert.md',
|
||||
approval: 'approval.md',
|
||||
attachments: 'attachment.md',
|
||||
bash: 'bash.md',
|
||||
bashEnv: 'bash.md',
|
||||
clientModuleHost: 'client-modules.md',
|
||||
shell: 'shell.md',
|
||||
shellEnv: 'shell.md',
|
||||
clientModules: 'client-modules.md',
|
||||
codeRuntime: 'code-runtime.md',
|
||||
commands: 'commands.md',
|
||||
compact: 'compaction.md',
|
||||
compaction: 'compaction.md',
|
||||
cordisInspect: 'extensions.md',
|
||||
credentials: 'credentials.md',
|
||||
directoryPicker: 'workspace.md',
|
||||
dynamicCordisRunner: 'extensions.md',
|
||||
e2b: 'subprocess.md',
|
||||
fs: 'filesystem.md',
|
||||
goals: 'goal.md',
|
||||
httpServer: 'http-server.md',
|
||||
webServer: 'web-server.md',
|
||||
invariants: 'invariants.md',
|
||||
llm: 'llm-streaming.md',
|
||||
lsp: 'lsp.md',
|
||||
messageFeedback: 'feedback.md',
|
||||
permission: 'permission.md',
|
||||
permissionPresets: 'permission-presets.md',
|
||||
planMode: 'plan.md',
|
||||
pty: 'pty.md',
|
||||
terminals: 'terminal.md',
|
||||
sandbox: 'sandbox.md',
|
||||
sandboxPolicy: 'sandbox.md',
|
||||
sessionPersistence: 'persistence.md',
|
||||
sessionQuery: 'session-query.md',
|
||||
sessionReferences: 'session-reference.md',
|
||||
sessionReferenceResolver: 'session-reference.md',
|
||||
sessionProjectionCache: 'session-projection.md',
|
||||
sessionProjections: 'session-projection.md',
|
||||
sessions: 'session.md',
|
||||
@@ -84,17 +95,17 @@ export const SERVICE_PAGE: Record<string, string> = {
|
||||
subagents: 'subagent.md',
|
||||
subprocess: 'subprocess.md',
|
||||
systemPrompt: 'system-prompt.md',
|
||||
tasks: 'tasks.md',
|
||||
telemetry: 'telemetry.md',
|
||||
jobs: 'jobs.md',
|
||||
sessionTelemetry: 'session-telemetry.md',
|
||||
tokenMeter: 'token-meter.md',
|
||||
toolResultPrune: 'compaction.md',
|
||||
toolResultPruner: 'compaction.md',
|
||||
tools: 'tools.md',
|
||||
typert: 'typert.md',
|
||||
typertGateway: 'typert.md',
|
||||
userInteraction: 'user-interaction.md',
|
||||
userQuestions: 'user-questions.md',
|
||||
web: 'web.md',
|
||||
workflows: 'workflow.md',
|
||||
workspace: 'workspace.md',
|
||||
workflowEngine: 'workflow.md',
|
||||
workspaceRegistry: 'workspace.md',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,10 +116,15 @@ export const SERVICE_PAGE: Record<string, string> = {
|
||||
* `index.ts` files with a same-named service class — so a new service can
|
||||
* never silently join this blind spot: it either enters {@link SERVICE_PAGE}
|
||||
* or names itself here. Client-face keys (the projection analyzes the host
|
||||
* face only) name the package README that owns their API.
|
||||
* TODO(cordis-catalog-interface-services): the interface-typed and
|
||||
* non-index-declared entries would all render once the projection resolves a
|
||||
* Context key through its declaring file's imports to the class declaration.
|
||||
* face only) name the package README that owns their surface.
|
||||
*
|
||||
* Two categories remain, and neither is a projection gap a scanning rule could
|
||||
* close. An OPTIONAL key (`key?: X`) is a value the launcher or boot code
|
||||
* installs before the tree mounts, which the analyzer skips by rule because no
|
||||
* plugin provides it and `inject` cannot reach it. A client-face key belongs to
|
||||
* the browser Context, which this host-face program never sees; the browser
|
||||
* surface has its own generated catalog (`scripts/gen-client-catalog.ts`, served
|
||||
* to a model as `cordis_runtime_inspect what:"client"`).
|
||||
*/
|
||||
export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
|
||||
agent: 'not a service: the DX accessor field on Agent.ctx (root accessor defaulting to undefined) — docs/subsystems/core.md owns the Agent handle',
|
||||
@@ -117,24 +133,23 @@ export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
|
||||
configuredAgentIdentities: 'not a service: launcher-provided boot-context value (ConfiguredAgentIdentities | undefined) — packages/core/agent-loop/README.md owns this launcher contract',
|
||||
launcherSessionQueryPath: 'not a service: launcher-provided boot-context value (string | undefined) — packages/session-query/session-query-sqlite/README.md owns this launcher contract',
|
||||
dshHomePath: 'not a service: boot-provided root accessor function (typeof dshHomePath | undefined) for Loader !!js config expressions — packages/boot/app-boot/README.md owns the boot contract',
|
||||
launcherEnvironment: 'not a service: launcher-provided root accessor value (EnvironmentSnapshot | undefined) — packages/util/environment/README.md owns this launcher contract',
|
||||
lsp: 'interface-typed (LspService); implementing class Lsp is not the declared type name — packages/lsp/lsp/README.md owns the API',
|
||||
apiProxy: 'interface-typed (ApiProxy) with the class in api-proxy.ts, not index.ts — packages/host/apiproxy/README.md owns the API',
|
||||
launchEnvironment: 'not a service: launcher-provided root accessor value (LaunchEnvironmentSnapshot | undefined) — packages/util/launch-environment/README.md owns this launcher contract',
|
||||
connection: 'interface-typed (HostConnectionHandle); implementing class HostConnectionService is declared in rpc-host.ts — packages/client/connection/README.md owns the API',
|
||||
appShell: 'client-side interface-typed browser service — packages/client/web/README.md owns the API',
|
||||
connection: 'client-side interface-typed browser service — packages/client/connection/README.md owns the API',
|
||||
settingsScope: 'client-side settings-namespace transport service — packages/client/ui-settings/README.md owns the API',
|
||||
chatFileMentions: 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-conversation/README.md owns the API',
|
||||
command: 'client-side interface-typed browser service — packages/client/ui-command/README.md owns the API',
|
||||
commandUi: 'client-side interface-typed browser service — packages/client/ui-commands/README.md owns the API',
|
||||
conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the API',
|
||||
conversationEvents: 'client-side interface-typed registry — packages/client/runtime/README.md owns the API',
|
||||
conversationViews: 'client-side interface-typed registry — packages/client/runtime/README.md owns the API',
|
||||
layout: 'client-side interface-typed browser service — packages/client/ui-layout/README.md owns the API',
|
||||
locale: 'client-side interface-typed browser service — packages/client/locale/README.md owns the API',
|
||||
models: 'client-side interface-typed browser service — packages/client/ui-model/README.md owns the API',
|
||||
modelDirectories: 'client-side interface-typed browser service — packages/client/ui-model-selection/README.md owns the API',
|
||||
modules: 'client-side interface-typed browser service — packages/client/modules/README.md owns the API',
|
||||
remote: 'client-side interface-typed gateway accessor (ClientRemote) — packages/api/gateway/README.md owns the API',
|
||||
sessionExport: 'client-side browser download controller — packages/session-query/session-export/README.md owns the API',
|
||||
slash: 'client-side interface-typed browser service — packages/client/ui-slash/README.md owns the API',
|
||||
sessionLogDownload: 'client-side browser download controller — packages/session-query/session-log-export/README.md owns the API',
|
||||
inputTriggers: 'client-side interface-typed browser service — packages/client/ui-input-trigger/README.md owns the API',
|
||||
timer: 'client-side dynamic-package timer service — packages/extensions/cordis-client-runner/README.md owns the API',
|
||||
slots: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the API',
|
||||
theme: 'client-side interface-typed browser service — packages/client/ui-theme/README.md owns the API',
|
||||
workspaces: 'client-side interface-typed browser service — packages/client/runtime/README.md owns the API',
|
||||
@@ -153,6 +168,7 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
|
||||
'agent-preset': 'core.md',
|
||||
'approval': 'approval.md',
|
||||
'commands': 'commands.md',
|
||||
'cordis': 'extensions.md',
|
||||
'credentials': 'credentials.md',
|
||||
'domain': 'storage.md',
|
||||
'fs': 'filesystem.md',
|
||||
@@ -163,7 +179,7 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
|
||||
'skills': 'skills.md',
|
||||
'subagent': 'subagent.md',
|
||||
'system-prompt': 'system-prompt.md',
|
||||
'telemetry': 'telemetry.md',
|
||||
'session-telemetry': 'session-telemetry.md',
|
||||
'tools': 'tools.md',
|
||||
'workflow': 'workflow.md',
|
||||
}
|
||||
@@ -179,13 +195,13 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
|
||||
* exemption cannot mask another declaration in that scope.
|
||||
*/
|
||||
export const EVENT_WALK_EXEMPTIONS: Record<string, string> = {
|
||||
'command/executed': 'client-face local command acknowledgment — packages/client/ui-command/README.md owns the API',
|
||||
'command/executed': 'client-face local command acknowledgment — packages/client/ui-commands/README.md owns the API',
|
||||
'connection/reset': 'client-face transport signal — packages/client/runtime/README.md owns the API',
|
||||
'locale/change': 'client-face locale switch signal — packages/client/locale/README.md owns the API',
|
||||
'slash/input-begin-command': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the API',
|
||||
'slash/input-consume-token': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the API',
|
||||
'slash/input-insert-reference': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the API',
|
||||
'slash/input-insert-text': 'client-face slash-input protocol — packages/client/ui-slash/README.md owns the API',
|
||||
'slash/input-begin-command': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
|
||||
'slash/input-consume-token': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
|
||||
'slash/input-insert-reference': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
|
||||
'slash/input-insert-text': 'client-face slash-input protocol — packages/client/ui-input-trigger/README.md owns the API',
|
||||
'slots/changed': 'client-face slot invalidation signal — packages/client/runtime/README.md owns the API',
|
||||
'theme/change': 'client-face theme switch signal — packages/client/ui-theme/README.md owns the API',
|
||||
}
|
||||
@@ -266,10 +282,10 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ImageAttachmentRef: 'attachment.md',
|
||||
SaveImageAttachment: 'attachment.md',
|
||||
StoredImageAttachment: 'attachment.md',
|
||||
BashExecRequest: 'bash.md',
|
||||
BashExecSpec: 'bash.md',
|
||||
BashProcess: 'bash.md',
|
||||
BashRunResult: 'bash.md',
|
||||
ShellExecRequest: 'shell.md',
|
||||
ShellExecSpec: 'shell.md',
|
||||
ShellProcess: 'shell.md',
|
||||
ShellRunResult: 'shell.md',
|
||||
DshEnvironment: 'subprocess.md',
|
||||
SubprocessHandle: 'subprocess.md',
|
||||
SubprocessOutcome: 'subprocess.md',
|
||||
@@ -290,7 +306,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
FsInfo: 'filesystem.md',
|
||||
FsObservation: 'filesystem.md',
|
||||
FsPathInfo: 'filesystem.md',
|
||||
FsPolicyExec: 'filesystem.md',
|
||||
FsObservationActor: 'filesystem.md',
|
||||
FsTarget: 'filesystem.md',
|
||||
FsVersion: 'filesystem.md',
|
||||
FsWriteIntent: 'filesystem.md',
|
||||
@@ -306,9 +322,13 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
CommandDescriptor: 'commands.md',
|
||||
CommandId: 'commands.md',
|
||||
CommandResult: 'commands.md',
|
||||
CommandSurface: 'commands.md',
|
||||
LspProvider: 'lsp.md',
|
||||
LspQueryRequest: 'lsp.md',
|
||||
LspQueryResult: 'lsp.md',
|
||||
LlmAdapter: 'llm-streaming.md',
|
||||
PreparedLlmCall: 'llm-streaming.md',
|
||||
LlmService: 'llm-streaming.md',
|
||||
LlmRuntime: 'llm-streaming.md',
|
||||
StreamChunk: 'llm-streaming.md',
|
||||
SkillProviderControl: 'skills.md',
|
||||
CreateSessionOptions: 'persistence.md',
|
||||
@@ -323,17 +343,17 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
SandboxExecutionPolicy: 'sandbox.md',
|
||||
SandboxMode: 'sandbox.md',
|
||||
SandboxPolicy: 'sandbox.md',
|
||||
PtyBackend: 'pty.md',
|
||||
PtyReadRequest: 'pty.md',
|
||||
PtyReadResult: 'pty.md',
|
||||
PtySendOperation: 'pty.md',
|
||||
PtySendRequest: 'pty.md',
|
||||
PtySessionId: 'pty.md',
|
||||
PtySessionSnapshot: 'pty.md',
|
||||
PtySignal: 'pty.md',
|
||||
PtySignalResult: 'pty.md',
|
||||
PtySpawnRequest: 'pty.md',
|
||||
PtySpawnResult: 'pty.md',
|
||||
TerminalBackend: 'terminal.md',
|
||||
TerminalReadRequest: 'terminal.md',
|
||||
TerminalReadResult: 'terminal.md',
|
||||
TerminalSendOperation: 'terminal.md',
|
||||
TerminalSendRequest: 'terminal.md',
|
||||
TerminalSessionId: 'terminal.md',
|
||||
TerminalSessionSnapshot: 'terminal.md',
|
||||
TerminalSignal: 'terminal.md',
|
||||
TerminalSignalResult: 'terminal.md',
|
||||
TerminalSpawnRequest: 'terminal.md',
|
||||
TerminalSpawnResult: 'terminal.md',
|
||||
SandboxPolicyRequest: 'sandbox.md',
|
||||
ScopeKey: 'scope.md',
|
||||
Scoped: 'scope.md',
|
||||
@@ -389,19 +409,19 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
SubagentReportMessageSource: 'subagent.md',
|
||||
SubagentReportOptions: 'subagent.md',
|
||||
SubagentRun: 'subagent.md',
|
||||
SubagentService: 'subagent.md',
|
||||
SubagentRuntime: 'subagent.md',
|
||||
SubagentStartRequest: 'subagent.md',
|
||||
AssembleContext: 'system-prompt.md',
|
||||
PromptContext: 'system-prompt.md',
|
||||
PromptSection: 'system-prompt.md',
|
||||
SystemPrompt: 'system-prompt.md',
|
||||
ToolProviderResult: 'system-prompt.md',
|
||||
TaskDoneListener: 'tasks.md',
|
||||
TaskId: 'tasks.md',
|
||||
TaskRead: 'tasks.md',
|
||||
TaskSnapshot: 'tasks.md',
|
||||
TaskStart: 'tasks.md',
|
||||
TasksChangedListener: 'tasks.md',
|
||||
JobDoneListener: 'jobs.md',
|
||||
JobId: 'jobs.md',
|
||||
JobRead: 'jobs.md',
|
||||
JobSnapshot: 'jobs.md',
|
||||
JobStart: 'jobs.md',
|
||||
JobsChangedListener: 'jobs.md',
|
||||
TokenMeasurement: 'token-meter.md',
|
||||
CodeDispatchLog: 'tools.md',
|
||||
PostToolDecision: 'tools.md',
|
||||
@@ -415,7 +435,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ToolExecutionToken: 'tools.md',
|
||||
ToolGuard: 'tools.md',
|
||||
ToolPresentationMode: 'tools.md',
|
||||
ToolRegistry: 'tools.md',
|
||||
ToolRuntime: 'tools.md',
|
||||
ToolRestriction: 'tools.md',
|
||||
ToolSchema: 'tools.md',
|
||||
SettingsNamespace: 'settings.md',
|
||||
@@ -428,9 +448,9 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
CredentialRef: 'credentials.md',
|
||||
CredentialInfo: 'credentials.md',
|
||||
ResolvedCredential: 'credentials.md',
|
||||
AskUserQuestionAnswer: 'user-interaction.md',
|
||||
AskUserQuestionRequest: 'user-interaction.md',
|
||||
UserInteractionProvider: 'user-interaction.md',
|
||||
AskUserQuestionAnswer: 'user-questions.md',
|
||||
AskUserQuestionRequest: 'user-questions.md',
|
||||
UserQuestionProvider: 'user-questions.md',
|
||||
WebFetchProvider: 'web.md',
|
||||
WebFetchRequest: 'web.md',
|
||||
WebFetchResult: 'web.md',
|
||||
@@ -438,10 +458,10 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
WebSearchRequest: 'web.md',
|
||||
WebSearchResult: 'web.md',
|
||||
WorkflowRun: 'workflow.md',
|
||||
PresetOption: 'permission.md',
|
||||
PresetSpec: 'permission.md',
|
||||
PresetOption: 'permission-presets.md',
|
||||
PresetSpec: 'permission-presets.md',
|
||||
InvariantInstaller: 'invariants.md',
|
||||
WebRoute: 'http-server.md',
|
||||
WebRoute: 'web-server.md',
|
||||
StorageBackend: 'storage.md',
|
||||
StorageForms: 'storage.md',
|
||||
Domain: 'storage.md',
|
||||
@@ -451,7 +471,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
Workspace: 'workspace.md',
|
||||
WorkspaceId: 'workspace.md',
|
||||
WebBootGraph: 'client-modules.md',
|
||||
TelemetryRecord: 'telemetry.md',
|
||||
SessionTelemetryRecord: 'session-telemetry.md',
|
||||
WorkflowRunInfo: 'workflow.md',
|
||||
WorkflowStartRequest: 'workflow.md',
|
||||
ProjectionDefinition: 'session-projection.md',
|
||||
@@ -486,32 +506,71 @@ export const FOUNDATION_TYPE_NAMES: ReadonlySet<string> = new Set([
|
||||
/** Project types deliberately documented outside the subsystems catalog. */
|
||||
export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
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',
|
||||
InsertTextRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
|
||||
BeginCommandRequest: 'event-local request contract is owned by packages/client/ui-input-trigger/src/types.ts',
|
||||
InsertReferenceRequest: 'event-local request contract is owned by packages/client/ui-input-trigger/src/types.ts',
|
||||
ConsumeTokenRequest: 'event-local request contract is owned by packages/client/ui-input-trigger/src/types.ts',
|
||||
InsertTextRequest: 'event-local request contract is owned by packages/client/ui-input-trigger/src/types.ts',
|
||||
AgentHandle: 'agent ownership handle is owned by packages/core/agent/README.md',
|
||||
AgentPreset: 'discovered preset record is owned by packages/preset/agent-presets/README.md',
|
||||
PresetMetadata: 'preset display text is owned by packages/preset/agent-presets/README.md',
|
||||
BashEnvContributor: 'service-local extension type is owned by packages/bash/tool-bash/src/index.ts',
|
||||
BashEnvVariableInfo: 'service-local metadata type is owned by packages/bash/tool-bash/src/index.ts',
|
||||
CompactAgentContext: 'compaction service input is owned by packages/compact/compact/src/index.ts',
|
||||
ManualCompactAgentContext: 'manual compaction service input is owned by packages/compact/compact/src/index.ts',
|
||||
BashEnvContributor: 'service-local extension type is owned by packages/shell/tool-bash/src/index.ts',
|
||||
BashEnvVariableInfo: 'service-local metadata type is owned by packages/shell/tool-bash/src/index.ts',
|
||||
CompactionAgentContext: 'compaction service input is owned by packages/compaction/compaction/src/index.ts',
|
||||
ManualCompactAgentContext: 'manual compaction service input is owned by packages/compaction/compaction/src/index.ts',
|
||||
ClientResponse: 'wire response message is owned by packages/host/apiproxy/src/api/rpc.ts',
|
||||
ApprovalRequestId: 'dynamic Plugin approval identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisErrorDetails: 'Cordis runtime error payload is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisInspectPlatform: 'Cordis inspect platform identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisInspectProviderManifest: 'Cordis inspect provider manifest is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisInspectProviderView: 'Cordis inspect provider view is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisInspectQueryRequest: 'Cordis inspect transport payload is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisInspectQueryResolution: 'Cordis inspect query result is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisInspectQueryResolved: 'Cordis inspect transport payload is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisInspectRequestId: 'Cordis inspect request identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisInspectResolveAck: 'Cordis inspect resolution acknowledgement is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisDynamicPackageId: 'dynamic Package identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisDynamicPluginId: 'dynamic Plugin identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisDynamicPluginRunId: 'dynamic Plugin run identity is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
CordisDynamicRunMode: 'dynamic Plugin activation mode is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisClientSource: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisDefineReceipt: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisDefineRequest: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisHostHalfResult: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisInventoryRow: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisInvokeResult: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisPackageInspection: 'dynamic Package source inspection is owned by packages/extensions/cordis-host-runner/src/registry.ts',
|
||||
DynamicCordisPluginInspection: 'dynamic Plugin inspection is owned by packages/extensions/cordis-host-runner/src/registry.ts',
|
||||
DynamicCordisRequestResolved: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisRetracted: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisRunRequest: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisPackage: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisReference: 'dynamic Plugin reference is owned by packages/extensions/cordis-host-runner/src/registry.ts',
|
||||
DynamicCordisRenderFailure: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisResolveAck: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisRunResolution: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisRunResponse: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisSnapshotRow: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisStopResponse: 'dynamic Plugin stop result is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
DynamicCordisUndefineReceipt: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
HostCordisInspectProviderRegistration: 'Host inspect provider registration is owned by packages/extensions/cordis-host-runner/src/inspect-registry.ts',
|
||||
DomainImpl: 'domain implementation contract is owned by packages/storage/storage-domain/README.md',
|
||||
CommandExecution: 'executor return contract is owned by packages/interaction/commands/src/index.ts',
|
||||
'z.core.JSONSchema.BaseSchema': 'zod projection output is owned by the zod v4 API',
|
||||
'z.core.ToJSONSchemaParams': 'zod projection parameters are owned by the zod v4 API',
|
||||
TypeRTDisposer: 'TypeRT lifecycle contract is owned by packages/typert/type-meta/README.md',
|
||||
TypertDisposer: 'Typert lifecycle contract is owned by packages/typert/protocol/README.md',
|
||||
InvokeRemoteRequest: 'gateway invocation contract is owned by packages/api/gateway/README.md',
|
||||
LocaleDict: 'service-local dictionary fields are owned by packages/client/i18n/src/index.ts',
|
||||
ThemeTokens: 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts',
|
||||
Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts',
|
||||
WebUpgradeRoute:
|
||||
'upgrade route registration contract is owned by packages/host/webserver/src/index.ts',
|
||||
InvariantRegistration: 'service-local lifecycle handle is owned by packages/support/invariants/README.md',
|
||||
KnobState: 'projection unit state fields are owned by packages/interaction/permission/README.md',
|
||||
PermissionSelect: 'permissions projection payload is owned by packages/interaction/permission/src/types.ts',
|
||||
InvariantRegistration: 'service-local lifecycle handle is owned by packages/runtime-diagnostics/invariants/README.md',
|
||||
JsonValue: 'JSON value union is owned by packages/core/session/src/json.ts',
|
||||
KnobState: 'projection unit state fields are owned by packages/interaction/permission-presets/README.md',
|
||||
PermissionSelect: 'permissions projection payload is owned by packages/interaction/permission-presets/src/types.ts',
|
||||
PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md',
|
||||
RequestRunId: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
|
||||
RpcReceipt: 'carrier-layer receipt is owned by packages/host/apiproxy/src/api/rpc.ts',
|
||||
Sandbox: 'external E2B SDK handle is owned by packages/e2b/e2b/README.md',
|
||||
SessionForkSource: 'service-local fork input is owned by packages/core/session/src/index.ts',
|
||||
SubagentRunEndInfo: 'event payload contract is owned by packages/subagent/subagent/src/types.ts',
|
||||
@@ -526,6 +585,40 @@ export const CORDIS_CATALOG_POLICY: CordisCatalogPolicy = {
|
||||
linkedTypePages: LINK_MAP,
|
||||
foundationTypeNames: FOUNDATION_TYPE_NAMES,
|
||||
typeLinkExemptions: TYPE_LINK_EXEMPTIONS,
|
||||
runtimeServiceExclusions: new Set(['cordisInspect', 'dynamicCordisRunner']),
|
||||
runtimeServices: [{
|
||||
key: 'timer',
|
||||
type: 'TimerService',
|
||||
abstract: false,
|
||||
doc: 'Disposable timer helpers mixed into Cordis contexts.',
|
||||
source: 'vendor/timer/src/index.ts:12',
|
||||
methods: [
|
||||
{
|
||||
signature: 'timeout(callback: () => void, delay: number): () => void',
|
||||
jsDoc: '/** Run a callback once and return its disposer. */',
|
||||
},
|
||||
{
|
||||
signature: 'timeout(delay: number): Promise<void>',
|
||||
jsDoc: '/** Resolve after a delay; disposal rejects the pending promise. */',
|
||||
},
|
||||
{
|
||||
signature: 'interval(callback: () => void, delay: number): () => void',
|
||||
jsDoc: '/** Run a callback repeatedly and return its disposer. */',
|
||||
},
|
||||
{
|
||||
signature: 'interval<R = any>(delay: number): AsyncIterableIterator<void, R, void>',
|
||||
jsDoc: '/** Return an async iterator of timer ticks. */',
|
||||
},
|
||||
{
|
||||
signature: 'throttle<F extends (...args: any[]) => void>(callback: F, delay: number, noTrailing?: boolean): F & { dispose: () => void }',
|
||||
jsDoc: '/** Return a throttled function whose timer is disposed with the current fiber. */',
|
||||
},
|
||||
{
|
||||
signature: 'debounce<F extends (...args: any[]) => void>(callback: F, delay: number): F & { dispose: () => void }',
|
||||
jsDoc: '/** Return a debounced function whose timer is disposed with the current fiber. */',
|
||||
},
|
||||
],
|
||||
}],
|
||||
inheritedEvents: [
|
||||
{ name: 'internal/plugin', summary: 'A plugin fiber was created.', source: 'vendor/cordis/src/events.ts:328' },
|
||||
{ name: 'internal/status', summary: 'A fiber changed lifecycle state.', source: 'vendor/cordis/src/events.ts:330' },
|
||||
@@ -551,7 +644,7 @@ export const CORDIS_CATALOG_POLICY: CordisCatalogPolicy = {
|
||||
{ name: 'ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin', summary: 'Low-level service-store access and binding.', source: 'vendor/cordis/src/reflect.ts:7' },
|
||||
{ name: 'ctx.extend / ctx.isolate / ctx.intercept', summary: 'Derive a child context (scoped services / isolation / interception).', source: 'vendor/cordis/src/context.ts:42' },
|
||||
{ name: 'ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger', summary: 'Ambient handles onto the running context graph.', source: 'vendor/cordis/src/context.ts:16' },
|
||||
{ name: 'ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)', summary: 'Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick).', source: 'vendor/timer/src/index.ts:4' },
|
||||
{ name: 'ctx.timer (+ interval / timeout / throttle / debounce)', summary: 'Disposable timer helpers. The `timer` key is provided at runtime; the four supported helpers are mixed onto ctx directly (declared via Pick).', source: 'vendor/timer/src/index.ts:4' },
|
||||
{ name: 'ctx.loader', summary: 'The config Loader that booted the app (present under the loader).', source: 'vendor/loader/src/index.ts:30' },
|
||||
{ name: 'ctx.hmr', summary: 'The hot-module-reload watcher (present under the hmr plugin).', source: 'vendor/hmr/src/index.ts:15' },
|
||||
],
|
||||
|
||||
57
scripts/gen-cordis-inspect-catalog.ts
Normal file
57
scripts/gen-cordis-inspect-catalog.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/** Generate model-visible Host/Client Service and Event inspect catalogs. */
|
||||
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { projectCordisCatalog } from '@deepseek-ai/dsh-typert-generator'
|
||||
import type { CordisCatalogModel, ServiceMethodEntry } from '@deepseek-ai/dsh-typert-generator'
|
||||
import { CORDIS_CATALOG_POLICY } from './gen-cordis-catalog.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const CLIENT_OUT = 'packages/extensions/cordis-client-runner/src/client/api-catalog.ts'
|
||||
|
||||
const CLIENT_SERVICES: Readonly<Record<string, readonly string[]>> = {
|
||||
layout: ['toggleSidebar', 'openDetails', 'closeDetails'],
|
||||
locale: ['getLocale', 'getSnapshot', 'subscribe', 'setLocale', 'register', 'bind'],
|
||||
sessions: ['open', 'openSubagent', 'setSubagentCatalogOpen', 'refreshSubagents', 'search', 'fork', 'scope', 'binding'],
|
||||
slots: ['register', 'inject'],
|
||||
theme: ['getTheme', 'setTheme', 'register', 'overrideTokens'],
|
||||
workspaces: [
|
||||
'connectWorkspace', 'startSession', 'create', 'pickDirectory', 'listDirectory', 'createDirectory',
|
||||
'openPath', 'rename', 'delete', 'insertSessionBefore', 'archiveSession',
|
||||
],
|
||||
}
|
||||
|
||||
const CLIENT_EVENTS = new Set([
|
||||
'connection/reset',
|
||||
'locale/change',
|
||||
'slots/changed',
|
||||
'theme/change',
|
||||
])
|
||||
|
||||
function methodName(method: ServiceMethodEntry): string | undefined {
|
||||
return /^(?:declare\s+)?(?:readonly\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)/.exec(method.signature)?.[1]
|
||||
}
|
||||
|
||||
function clientModel(model: CordisCatalogModel): CordisCatalogModel {
|
||||
return {
|
||||
services: model.services.flatMap((service) => {
|
||||
const allowed = CLIENT_SERVICES[service.key]
|
||||
if (allowed === undefined) return []
|
||||
const names = new Set(allowed)
|
||||
return [{ ...service, methods: service.methods.filter(method => names.has(methodName(method) ?? '')) }]
|
||||
}),
|
||||
events: model.events.filter(event => CLIENT_EVENTS.has(event.name)),
|
||||
}
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const { projector, model } = projectCordisCatalog(root, CORDIS_CATALOG_POLICY, 'client')
|
||||
const destination = resolve(root, CLIENT_OUT)
|
||||
const source = projector.renderRuntimeApi(clientModel(model))
|
||||
.replaceAll('@deepseek-ai/dsh-tool-cordis/api-catalog', '@deepseek-ai/dsh-cordis-client-runner/client/api-catalog')
|
||||
mkdirSync(dirname(destination), { recursive: true })
|
||||
writeFileSync(destination, source)
|
||||
console.log(`gen-cordis-inspect-catalog: wrote ${CLIENT_OUT}`)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -111,7 +111,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'LLM adapter registry',
|
||||
mode: 'seam',
|
||||
implementations: ['llm-deepseek', 'llm-pi-ai', 'llm-replay'],
|
||||
consumers: ['agent-loop', 'compact-basic'],
|
||||
consumers: ['agent-loop', 'compaction-basic'],
|
||||
note: 'Adapters register provider implementations; the loop and compaction call the provider-neutral stream service.',
|
||||
},
|
||||
{
|
||||
@@ -119,15 +119,15 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'token-meter',
|
||||
title: 'Replay token measurement',
|
||||
mode: 'core',
|
||||
consumers: ['compact-basic'],
|
||||
consumers: ['compaction-basic'],
|
||||
note: 'Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements.',
|
||||
},
|
||||
{
|
||||
key: 'toolResultPrune',
|
||||
pkg: 'compact-tool-result-prune',
|
||||
key: 'toolResultPruner',
|
||||
pkg: 'compaction-tool-result-pruner',
|
||||
title: 'Model-free tool-result pruning',
|
||||
mode: 'core',
|
||||
consumers: ['compact-basic'],
|
||||
consumers: ['compaction-basic'],
|
||||
note: 'Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction.',
|
||||
},
|
||||
{
|
||||
@@ -157,7 +157,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
{
|
||||
key: 'typertGateway',
|
||||
pkg: 'api-gateway',
|
||||
title: 'TypeRT Host invocation gateway',
|
||||
title: 'Typert Host invocation gateway',
|
||||
mode: 'core',
|
||||
note: 'Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier.',
|
||||
},
|
||||
@@ -167,7 +167,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'Durable session persistence seam',
|
||||
mode: 'seam',
|
||||
implementations: ['session-persistence-jsonl', 'session-persistence-sqlite'],
|
||||
consumers: ['agent-loop', 'tool-bash', 'hooks-claude', 'hooks-codex', 'session-query', 'session-query-sqlite', 'message-feedback'],
|
||||
consumers: ['agent-loop', 'tool-bash', 'hooks-claude-code', 'hooks-codex', 'session-query', 'session-query-sqlite', 'message-feedback'],
|
||||
note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
|
||||
},
|
||||
{
|
||||
@@ -175,7 +175,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'settings',
|
||||
title: 'User-settings seam',
|
||||
mode: 'seam',
|
||||
implementations: ['settings-local'],
|
||||
implementations: ['settings-file'],
|
||||
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.',
|
||||
},
|
||||
@@ -189,7 +189,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
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',
|
||||
key: 'sessionTelemetry',
|
||||
pkg: 'session-telemetry',
|
||||
title: 'Session telemetry seam',
|
||||
mode: 'seam',
|
||||
@@ -222,7 +222,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
note: 'Owns local per-assistant-message feedback, lifecycle and target validation, per-item compare-and-set, and the Host unary Remote contract without entering Session history or telemetry.',
|
||||
},
|
||||
{
|
||||
key: 'workspace',
|
||||
key: 'workspaceRegistry',
|
||||
pkg: 'workspace',
|
||||
title: 'Workspace entity registry',
|
||||
mode: 'core',
|
||||
@@ -239,7 +239,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
note: 'The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering.',
|
||||
},
|
||||
{
|
||||
key: 'sessionReferences',
|
||||
key: 'sessionReferenceResolver',
|
||||
pkg: 'session-reference',
|
||||
title: 'Cross-session snapshot preparation',
|
||||
mode: 'core',
|
||||
@@ -250,7 +250,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'session-title',
|
||||
title: 'Log-backed session titles',
|
||||
mode: 'seam',
|
||||
implementations: ['session-title-first-message-llm', 'session-title-all-messages-llm'],
|
||||
implementations: ['session-title-first-prompt-llm', 'session-title-all-prompts-llm'],
|
||||
note: 'Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration.',
|
||||
},
|
||||
{
|
||||
@@ -258,7 +258,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'system-prompt',
|
||||
title: 'System prompt assembly registry',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'tools', 'tool-fs', 'tool-pty', 'tool-web'],
|
||||
consumers: ['agent-loop', 'tools', 'tool-fs', 'tool-terminal', 'tool-web'],
|
||||
note: 'Collects prompt sections and model-facing tool schemas for each step.',
|
||||
},
|
||||
{
|
||||
@@ -266,12 +266,12 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'tools',
|
||||
title: 'Tool registry and guarded execution pipeline',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'tool-ask-user', 'tool-bash', 'tool-cordis', 'tool-fs', 'tool-pty', 'tool-skill', 'tool-subagent', 'tool-todo', 'tool-web'],
|
||||
consumers: ['agent-loop', 'tool-ask-user', 'tool-bash', 'tool-cordis', 'tool-fs', 'tool-terminal', 'tool-skill', 'tool-subagent', 'tool-todo', 'tool-web'],
|
||||
note: 'Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation.',
|
||||
},
|
||||
{
|
||||
key: 'userInteraction',
|
||||
pkg: 'user-interaction',
|
||||
key: 'userQuestions',
|
||||
pkg: 'user-questions',
|
||||
title: 'Human question/answer seam',
|
||||
mode: 'seam',
|
||||
consumers: ['tool-ask-user'],
|
||||
@@ -319,7 +319,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'skill',
|
||||
title: 'Skill provider registry',
|
||||
mode: 'seam',
|
||||
implementations: ['skill-badge', 'skill-local'],
|
||||
implementations: ['skill-badge', 'skill-filesystem'],
|
||||
consumers: ['tool-skill'],
|
||||
note: 'Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies.',
|
||||
},
|
||||
@@ -368,34 +368,34 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'Subprocess seam',
|
||||
mode: 'seam',
|
||||
implementations: ['subprocess-local', 'subprocess-e2b'],
|
||||
consumers: ['bash-local', 'bash-sandbox', 'pty-local', 'lsp-local', 'subagent-acp', 'subagent-codex', 'subagent-claude-code'],
|
||||
consumers: ['bash-local', 'bash-sandbox', 'terminal-bash', 'lsp-stdio', 'subagent-acp', 'subagent-codex', 'subagent-claude-code'],
|
||||
note: 'The bash executors, the PTY shell backend, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation.',
|
||||
},
|
||||
{
|
||||
key: 'bash',
|
||||
pkg: 'bash',
|
||||
key: 'shell',
|
||||
pkg: 'shell',
|
||||
title: 'Bash executor seam',
|
||||
mode: 'seam',
|
||||
implementations: ['bash-local', 'bash-sandbox', 'pwsh-local'],
|
||||
consumers: ['tool-bash', 'tool-pwsh', 'hooks-claude', 'hooks-codex'],
|
||||
consumers: ['tool-bash', 'tool-pwsh', 'hooks-claude-code', 'hooks-codex'],
|
||||
note: 'The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them.',
|
||||
},
|
||||
{
|
||||
key: 'bashEnv',
|
||||
pkg: 'bash-env',
|
||||
key: 'shellEnv',
|
||||
pkg: 'shell-env',
|
||||
title: 'Managed bash environment registry',
|
||||
mode: 'core',
|
||||
consumers: ['tool-bash', 'tool-pwsh'],
|
||||
note: 'Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace.',
|
||||
},
|
||||
{
|
||||
key: 'pty',
|
||||
pkg: 'pty',
|
||||
key: 'terminals',
|
||||
pkg: 'terminal',
|
||||
title: 'Persistent PTY session registry',
|
||||
mode: 'seam',
|
||||
implementations: ['pty-local'],
|
||||
consumers: ['tool-pty'],
|
||||
note: 'The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model tools.',
|
||||
implementations: ['terminal-bash'],
|
||||
consumers: ['tool-terminal'],
|
||||
note: 'The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-terminal exposes the owner-scoped model tools.',
|
||||
},
|
||||
{
|
||||
key: 'sandbox',
|
||||
@@ -403,7 +403,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'Process-sandbox seam',
|
||||
mode: 'seam',
|
||||
implementations: ['sandbox-local'],
|
||||
consumers: ['bash-sandbox', 'pty-local'],
|
||||
consumers: ['bash-sandbox', 'terminal-bash'],
|
||||
note: 'Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement.',
|
||||
},
|
||||
{
|
||||
@@ -412,7 +412,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'Sandbox policy home',
|
||||
mode: 'core',
|
||||
implementations: [],
|
||||
consumers: ['bash-sandbox', 'fs-sandbox', 'pty-local'],
|
||||
consumers: ['bash-sandbox', 'fs-sandbox', 'terminal-bash'],
|
||||
note: 'The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots.',
|
||||
},
|
||||
{
|
||||
@@ -425,8 +425,8 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
note: 'One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`.',
|
||||
},
|
||||
{
|
||||
key: 'permission',
|
||||
pkg: 'permission',
|
||||
key: 'permissionPresets',
|
||||
pkg: 'permission-presets',
|
||||
title: 'Permission presets',
|
||||
mode: 'core',
|
||||
implementations: [],
|
||||
@@ -448,16 +448,16 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
mode: 'seam',
|
||||
implementations: ['fs-local', 'fs-sandbox', 'fs-e2b'],
|
||||
consumers: ['tool-fs'],
|
||||
companions: ['fs-policy'],
|
||||
note: 'tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate.',
|
||||
companions: ['fs-observation-policy'],
|
||||
note: 'tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate.',
|
||||
},
|
||||
{
|
||||
key: 'compact',
|
||||
pkg: 'compact',
|
||||
key: 'compaction',
|
||||
pkg: 'compaction',
|
||||
title: 'Compaction seam',
|
||||
mode: 'seam',
|
||||
implementations: ['compact-basic'],
|
||||
consumers: ['compact-basic'],
|
||||
implementations: ['compaction-basic'],
|
||||
consumers: ['compaction-basic'],
|
||||
note: 'The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool.',
|
||||
},
|
||||
{
|
||||
@@ -465,25 +465,25 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'subagent',
|
||||
title: 'Subagent provider and continuation service',
|
||||
mode: 'seam',
|
||||
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-codex', 'subagent-claude-code', 'subagent-dsh-sdk'],
|
||||
implementations: ['subagent-spawn-in-process', 'subagent-fork-in-process', 'subagent-acp', 'subagent-codex', 'subagent-claude-code', 'subagent-dsh-sdk'],
|
||||
consumers: ['tool-subagent', 'tool-subagent-control', 'tool-ralph'],
|
||||
note: 'Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route.',
|
||||
},
|
||||
{
|
||||
key: 'tasks',
|
||||
pkg: 'tasks',
|
||||
title: 'Background task registry',
|
||||
key: 'jobs',
|
||||
pkg: 'jobs',
|
||||
title: 'Background job registry',
|
||||
mode: 'seam',
|
||||
implementations: ['tasks-local'],
|
||||
consumers: ['tool-bash', 'tool-pty', 'tool-subagent', 'tool-tasks'],
|
||||
note: 'Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing controller that reads, lists, and kills it; tasks-local is the process-local registry.',
|
||||
implementations: ['jobs-local'],
|
||||
consumers: ['tool-bash', 'tool-terminal', 'tool-subagent', 'tool-jobs'],
|
||||
note: 'Producers (background bash, PTY sends, and subagent delegations) register running work; tool-jobs is the model-facing controller that reads, lists, and kills it; jobs-local is the process-local registry.',
|
||||
},
|
||||
{
|
||||
key: 'web',
|
||||
pkg: 'web',
|
||||
title: 'Web access provider registry',
|
||||
mode: 'seam',
|
||||
implementations: ['web-search-exa', 'web-search-perplexity', 'web-search-deepseek', 'web-fetch-local'],
|
||||
implementations: ['web-search-exa', 'web-search-perplexity', 'web-search-deepseek', 'web-fetch-http'],
|
||||
consumers: ['tool-web'],
|
||||
note: 'Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names.',
|
||||
},
|
||||
@@ -506,7 +506,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
note: 'Discriminated interaction capability: the native backend opens one OS chooser on the host display, the browse backend serves listing/creation primitives for the in-app browser; dual-face backends fill ui-workspace directory-flow slots from their browser halves (no wire advertisement).',
|
||||
},
|
||||
{
|
||||
key: 'httpServer',
|
||||
key: 'webServer',
|
||||
pkg: 'webserver',
|
||||
title: 'HTTP route registration',
|
||||
mode: 'core',
|
||||
@@ -514,7 +514,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
note: 'Plain node:http carrier: named-route registry, index transform taps, and the static dist fallback; web-transport plugins register their own routes.',
|
||||
},
|
||||
{
|
||||
key: 'clientModuleHost',
|
||||
key: 'clientModules',
|
||||
pkg: 'modules',
|
||||
title: 'Client plugin graph host',
|
||||
mode: 'core',
|
||||
@@ -522,14 +522,47 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
note: 'Composes the __DSH_BOOT__ entry graph from an incremental dsh.client scan, serves plugin bundles, and notifies rebuilt/graph-changed subscribers.',
|
||||
},
|
||||
{
|
||||
key: 'workflows',
|
||||
key: 'workflowEngine',
|
||||
pkg: 'workflow',
|
||||
title: 'Workflow script engine',
|
||||
mode: 'seam',
|
||||
implementations: ['workflow-workerthread'],
|
||||
implementations: ['workflow-worker-thread'],
|
||||
consumers: ['tool-workflow', 'tool-ralph'],
|
||||
note: 'One engine per context, as in bash, with no named-provider registry; the general workflow and fixed Ralph consumers start runs whose agent() calls fan out through ctx.subagents.',
|
||||
},
|
||||
{
|
||||
key: 'lsp',
|
||||
pkg: 'lsp',
|
||||
title: 'Language-server navigation seam',
|
||||
mode: 'seam',
|
||||
implementations: ['lsp-local'],
|
||||
consumers: ['tool-lsp'],
|
||||
note: 'Provider registration and selection plus normalized query execution over exactly four operations; the seam offers no protocol escape hatch, so a backend translates into the normalized request and result.',
|
||||
},
|
||||
{
|
||||
key: 'apiProxy',
|
||||
pkg: 'apiproxy',
|
||||
title: 'Host API dispatch',
|
||||
mode: 'core',
|
||||
consumers: ['connection'],
|
||||
note: 'The transport-agnostic host gateway face: it dispatches browser API calls, and each open host stream subscribes to the events it forwards rather than being pushed to through a broadcast verb.',
|
||||
},
|
||||
{
|
||||
key: 'dynamicCordisRunner',
|
||||
pkg: 'cordis-host-runner',
|
||||
title: 'Dynamic Cordis package host runner',
|
||||
mode: 'core',
|
||||
consumers: ['tool-cordis'],
|
||||
note: 'Owns the in-memory definition registry, the vm sandbox for host halves, and the request-run round trip; browser pages reach the same service over the wire through its remote namespace.',
|
||||
},
|
||||
{
|
||||
key: 'cordisInspect',
|
||||
pkg: 'cordis-host-runner',
|
||||
title: 'Dynamic Cordis inspect registry',
|
||||
mode: 'core',
|
||||
consumers: ['tool-cordis'],
|
||||
note: 'Registers host inspect providers, mirrors the client provider manifest, and routes client queries through the dynamic Cordis transport.',
|
||||
},
|
||||
]
|
||||
|
||||
function generatedHeader(title: string): string[] {
|
||||
@@ -1265,7 +1298,7 @@ function renderLifecycle(): string {
|
||||
'',
|
||||
'The `assistant/message` event records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history, while the durable event keeps usage and `sourceEventSeqs` listing the exact `assistant/chunk` events, including an explicit empty list.',
|
||||
'',
|
||||
'`dsh-compact-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
|
||||
'`dsh-compaction-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
|
||||
'',
|
||||
'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later claim operation takes their next-step batch.',
|
||||
'',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { parseJsDoc, pointer, rawJsDoc, reportViolations } from './jsdoc.ts'
|
||||
import { githubSlug } from './verify-md-links.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const OUT = 'docs/persistence-catalog.md'
|
||||
@@ -341,7 +342,8 @@ function typeLinks(payload: string): string {
|
||||
|
||||
/** Render one log event entry. */
|
||||
function renderEvent(e: AnnotatedLogEventEntry): string[] {
|
||||
const out = [`#### \`${e.name}\` — ${e.surface ? 'surface' : 'log-only'}`, '']
|
||||
const heading = `${e.name} — ${e.surface ? 'surface' : 'log-only'}`
|
||||
const out = [`<a id="${githubSlug(heading)}"></a>`, '', `#### \`${e.name}\` — ${e.surface ? 'surface' : 'log-only'}`, '']
|
||||
out.push('```' + FENCE, e.declaration, '```', '')
|
||||
const links = typeLinks(e.payload)
|
||||
if (links) out.push(links, '')
|
||||
|
||||
@@ -46,8 +46,8 @@ describe('tierExternalDeps', () => {
|
||||
const { manifests, names } = workspace({
|
||||
// Root tooling and test infrastructure never ship, whichever section declares them.
|
||||
'package.json': { dependencies: { 'root-runtime-looking': '^1' }, devDependencies: { 'lint-tool': '^1' } },
|
||||
'packages/support/loader-smoke/package.json': { name: '@deepseek-ai/dsh-loader-smoke', dependencies: { 'smoke-helper': '^1' } },
|
||||
'packages/client/test-runtime/package.json': { name: '@deepseek-ai/dsh-client-test-runtime', dependencies: { 'test-lib': '^1' } },
|
||||
'packages/test-support/loader-smoke/package.json': { name: '@deepseek-ai/dsh-loader-smoke', dependencies: { 'smoke-helper': '^1' } },
|
||||
'packages/test-support/client-runtime/package.json': { name: '@deepseek-ai/dsh-client-test-runtime', dependencies: { 'test-lib': '^1' } },
|
||||
'website/package.json': { devDependencies: { 'site-tool': '^1' } },
|
||||
// A plugin package's runtime dependency ships even when no app mounts it by default.
|
||||
'packages/mcp/mcp-client/package.json': { name: '@deepseek-ai/dsh-mcp-client', dependencies: { 'protocol-sdk': '^1' }, devDependencies: { 'protocol-fixture-server': '^1' } },
|
||||
|
||||
@@ -32,8 +32,8 @@ const ALL_KINDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'p
|
||||
*/
|
||||
const DEV_ONLY_AREAS = [
|
||||
'package.json',
|
||||
'packages/support/',
|
||||
'packages/client/test-runtime/',
|
||||
'packages/test-support/',
|
||||
'packages/test-support/client-runtime/',
|
||||
'website/',
|
||||
'examples/',
|
||||
'native/',
|
||||
@@ -691,7 +691,7 @@ export function render(): string {
|
||||
|
||||
# Third-Party Notices
|
||||
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
|
||||
This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude platform payload closure. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
|
||||
|
||||
|
||||
@@ -15,53 +15,55 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { createScope } from '@deepseek-ai/dsh-scope'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import SessionQuerySqlite from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import GoalService from '@deepseek-ai/dsh-goal'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
|
||||
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
|
||||
import * as BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
|
||||
import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
|
||||
import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||
import { AttachmentStore } from '@deepseek-ai/dsh-attachment'
|
||||
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
|
||||
import WebService from '@deepseek-ai/dsh-web'
|
||||
import UserQuestionService from '@deepseek-ai/dsh-user-questions'
|
||||
import PlanModeController from '@deepseek-ai/dsh-plan-mode'
|
||||
import WebRuntime from '@deepseek-ai/dsh-web'
|
||||
import * as WebSearchExa from '@deepseek-ai/dsh-web-search-exa'
|
||||
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-local'
|
||||
import SubagentService from '@deepseek-ai/dsh-subagent'
|
||||
import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-http'
|
||||
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentProvider, SubagentReportDelivery } from '@deepseek-ai/dsh-subagent'
|
||||
import * as ToolSubagentControl from '@deepseek-ai/dsh-tool-subagent-control'
|
||||
import * as ToolSubagentListAgents from '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
||||
import * as ToolSubagentReport from '@deepseek-ai/dsh-tool-subagent-report'
|
||||
import SkillService from '@deepseek-ai/dsh-skill'
|
||||
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
|
||||
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
|
||||
import SkillRegistry from '@deepseek-ai/dsh-skill'
|
||||
import * as SkillFileSystem from '@deepseek-ai/dsh-skill-filesystem'
|
||||
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
|
||||
import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
|
||||
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import * as ToolPwsh from '@deepseek-ai/dsh-tool-pwsh'
|
||||
import * as ToolBashPersistent from '@deepseek-ai/dsh-tool-bash-persistent'
|
||||
import CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner'
|
||||
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
import * as ToolFsSearch from '@deepseek-ai/dsh-tool-fs-search'
|
||||
import * as ToolStrReplaceEditor from '@deepseek-ai/dsh-tool-str-replace-editor'
|
||||
import PtyService from '@deepseek-ai/dsh-pty'
|
||||
import * as ToolPty from '@deepseek-ai/dsh-tool-pty'
|
||||
import TerminalSessionService from '@deepseek-ai/dsh-terminal'
|
||||
import * as ToolPty from '@deepseek-ai/dsh-tool-terminal'
|
||||
import * as ToolGoal from '@deepseek-ai/dsh-tool-goal'
|
||||
import * as ToolSchedule from '@deepseek-ai/dsh-tool-schedule'
|
||||
import * as ToolSchedule from '@deepseek-ai/dsh-schedule'
|
||||
import Lsp from '@deepseek-ai/dsh-lsp'
|
||||
import * as ToolLsp from '@deepseek-ai/dsh-tool-lsp'
|
||||
import * as ToolSkill from '@deepseek-ai/dsh-tool-skill'
|
||||
import * as ToolSessionQuery from '@deepseek-ai/dsh-tool-session-query'
|
||||
import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks'
|
||||
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
|
||||
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
|
||||
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
|
||||
import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
|
||||
import VmWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
|
||||
import VmWorkflowEngine from '@deepseek-ai/dsh-workflow-worker-thread'
|
||||
import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph'
|
||||
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
|
||||
import { githubSlug } from './verify-md-links.ts'
|
||||
|
||||
/** Attachment seam marker that makes the attachments-conditional `read_image` schema harvestable. */
|
||||
class CatalogAttachmentStore extends AttachmentStore {
|
||||
@@ -135,7 +137,7 @@ async function mountCatalogChildScope(
|
||||
* prompt and registry; each recipe supplies only package-specific seams and
|
||||
* config, while `dir` participates in the completeness check.
|
||||
*/
|
||||
interface ToolPackage {
|
||||
export interface ToolPackage {
|
||||
/** The npm package name, used as the catalog section heading. */
|
||||
pkg: string
|
||||
/** The `packages/<group>/<dir>` leaf name — matched by the completeness guard. */
|
||||
@@ -158,7 +160,7 @@ interface ToolPackage {
|
||||
/** Agent-like scope key whose tool view is catalogued instead of the global view. */
|
||||
scope?: (ctx: Context) => Agent
|
||||
/**
|
||||
* Config for the caller's `ToolRegistry` mount. The registry itself ships a
|
||||
* Config for the caller's `ToolRuntime` mount. The registry itself ships a
|
||||
* model-facing tool (`run_code`, registered under a non-native `mode`), so
|
||||
* ITS catalog entry boots the registry in the mode that exposes it;
|
||||
* every other entry uses the default (native) registry.
|
||||
@@ -184,10 +186,10 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-ask-user',
|
||||
dir: 'tool-ask-user',
|
||||
source: 'packages/interaction/tool-ask-user/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.userInteraction'],
|
||||
requires: ['ctx.tools', 'ctx.userQuestions'],
|
||||
writes: ['tool/call', 'tool/result after a UI/provider answers the question'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(UserInteractionService)
|
||||
await ctx.plugin(UserQuestionService)
|
||||
await ctx.plugin(ToolAskUser)
|
||||
},
|
||||
note:
|
||||
@@ -211,67 +213,68 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-plan-mode',
|
||||
dir: 'plan-mode',
|
||||
source: 'packages/plan/plan-mode/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.userInteraction (execution time, opportunistic)'],
|
||||
requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.userQuestions (execution time, opportunistic)'],
|
||||
writes: ['tool/call', 'plan/mode inactive on an approved review', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(PlanModeService, { section: 'Tool catalog schema harvest.' })
|
||||
await ctx.plugin(PlanModeController, { section: 'Tool catalog schema harvest.' })
|
||||
},
|
||||
note:
|
||||
'exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-interaction seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary.',
|
||||
'exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-bash',
|
||||
dir: 'tool-bash',
|
||||
source: 'packages/bash/tool-bash/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.bash', 'ctx.systemPrompt', 'ctx.bashEnv', 'ctx.tasks at call time for run_in_background'],
|
||||
source: 'packages/shell/tool-bash/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.shell', 'ctx.systemPrompt', 'ctx.shellEnv', 'ctx.jobs at call time for run_in_background'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
await ctx.plugin(BashEnvPlugin)
|
||||
await ctx.plugin(LocalBashExecutor)
|
||||
await ctx.plugin(ToolBash)
|
||||
},
|
||||
note:
|
||||
'The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.tasks` runtime and is collected/stopped through the `task_*` tools from `@deepseek-ai/dsh-tool-tasks`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled.',
|
||||
'The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.jobs` runtime and is collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-pwsh',
|
||||
dir: 'tool-pwsh',
|
||||
source: 'packages/bash/tool-pwsh/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.bash', 'ctx.systemPrompt', 'ctx.bashEnv', 'ctx.tasks at call time for run_in_background'],
|
||||
source: 'packages/shell/tool-pwsh/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.shell', 'ctx.systemPrompt', 'ctx.shellEnv', 'ctx.jobs at call time for run_in_background'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
// The pwsh tool consumes the bash executor seam; the schema harvest
|
||||
// mounts the pwsh-local implementation so the inject resolves without
|
||||
// executing anything (registration never spawns a process).
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
await ctx.plugin(BashEnvPlugin)
|
||||
await ctx.plugin(PwshLocalExecutor)
|
||||
await ctx.plugin(ToolPwsh)
|
||||
},
|
||||
note:
|
||||
'The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.bash`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.tasks` runtime and are collected/stopped through the `task_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-bash-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\\...` paths and `$env:NAME` variables.',
|
||||
'The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.shell`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.jobs` runtime and are collected/stopped through the `job_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-shell-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\\...` paths and `$env:NAME` variables.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-cordis',
|
||||
dir: 'tool-cordis',
|
||||
source: 'packages/self-modification/tool-cordis/src/index.ts',
|
||||
requires: ['ctx.tools'],
|
||||
writes: ['tool/call', 'tool/result', 'process-local temporary Plugin lifecycle'],
|
||||
source: 'packages/extensions/tool-cordis/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.dynamicCordisRunner'],
|
||||
writes: ['tool/call', 'tool/result', 'process-local dynamic package lifecycle'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(CordisHostRunner)
|
||||
await ctx.plugin(ToolCordis)
|
||||
},
|
||||
note:
|
||||
'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.',
|
||||
'Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-bash-persistent',
|
||||
dir: 'tool-bash-persistent',
|
||||
source: 'packages/pty/tool-bash-persistent/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.pty', 'an owning Agent at execution time'],
|
||||
source: 'packages/shell/tool-bash-persistent/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.terminals', 'an owning Agent at execution time'],
|
||||
writes: ['tool/call', 'PTY shell state', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(PtyService)
|
||||
await ctx.plugin(TerminalSessionService)
|
||||
await ctx.plugin(ToolBashPersistent)
|
||||
},
|
||||
note:
|
||||
@@ -305,7 +308,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
await ctx.plugin(ToolFs)
|
||||
},
|
||||
note:
|
||||
'The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. `read_image` is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input.',
|
||||
'The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-observation-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. `read_image` is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-fs-search',
|
||||
@@ -319,24 +322,24 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
// spawns, so the real local service is inert here. `ctx.spillStore` is
|
||||
// optional (read via ctx.get) and does not affect the schemas, so no
|
||||
// spill backend is mounted.
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
await ctx.plugin(ToolFsSearch, { sampleOverCapGlobResults: true })
|
||||
},
|
||||
note:
|
||||
'glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background tasks) — no host `rg` install and no shell layer. 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.',
|
||||
'glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host `rg` install and no shell layer. 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',
|
||||
dir: 'tool-pty',
|
||||
source: 'packages/pty/tool-pty/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.pty', 'ctx.systemPrompt', 'ctx.tasks at call time for run_in_background'],
|
||||
pkg: '@deepseek-ai/dsh-tool-terminal',
|
||||
dir: 'tool-terminal',
|
||||
source: 'packages/terminal/tool-terminal/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.terminals', 'ctx.systemPrompt', 'ctx.jobs at call time for run_in_background'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(PtyService)
|
||||
await ctx.plugin(TerminalSessionService)
|
||||
await ctx.plugin(ToolPty)
|
||||
},
|
||||
note:
|
||||
'The six terminal tools are opt-in and complement one-shot bash/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.tasks`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema.',
|
||||
'The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-goal',
|
||||
@@ -353,9 +356,9 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
'create, edit, pause, and resume require direct-human root authority; complete and blocked also accept the exact current goal round. The default blocked lower bound is three admitted rounds.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-schedule',
|
||||
dir: 'tool-schedule',
|
||||
source: 'packages/schedule/tool-schedule/src/tools.ts',
|
||||
pkg: '@deepseek-ai/dsh-schedule',
|
||||
dir: 'schedule',
|
||||
source: 'packages/schedule/schedule/src/tools.ts',
|
||||
requires: ['ctx.tools', 'ctx.sessions', 'Session persistence', 'a future live root Agent'],
|
||||
writes: ['tool/call', 'schedule/change create or delete', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
@@ -385,16 +388,16 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
await ctx.plugin(ToolLsp)
|
||||
},
|
||||
note:
|
||||
'The lsp tool keeps provider selection and language-server subprocesses behind ctx.lsp, so its model-visible schema stays stable across providers. Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-local`) at runtime; without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema.',
|
||||
'The lsp tool keeps provider selection and language-server subprocesses behind ctx.lsp, so its model-visible schema stays stable across providers. Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-stdio`) at runtime; without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-ralph',
|
||||
dir: 'tool-ralph',
|
||||
source: 'packages/workflow/tool-ralph/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.workflows', 'ctx.subagents', 'ctx.systemPrompt', 'a calling Agent (exec.agent parents every fresh round)'],
|
||||
requires: ['ctx.tools', 'ctx.workflowEngine', 'ctx.subagents', 'ctx.systemPrompt', 'a calling Agent (exec.agent parents every fresh round)'],
|
||||
writes: ['tool/call', 'tool/result', 'workflow and child session events during execution'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
registerCatalogSubagentProvider(ctx, 'mock')
|
||||
await ctx.plugin(VmWorkflowEngine, { provider: 'mock' })
|
||||
await ctx.plugin(ToolRalph, { subagentProvider: 'mock' })
|
||||
@@ -410,8 +413,8 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
writes: ['tool/call', 'tool/result', 'user/message replacement catalogs via agent.inject()'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(SkillService)
|
||||
await ctx.plugin(SkillLocal, {
|
||||
await ctx.plugin(SkillRegistry)
|
||||
await ctx.plugin(SkillFileSystem, {
|
||||
dshHome: resolve(root, '.tmp/tool-catalog/.dsh'),
|
||||
agentsHome: resolve(root, '.tmp/tool-catalog/.agents'),
|
||||
})
|
||||
@@ -426,7 +429,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionQuerySqlite, { path: ':memory:' })
|
||||
await ctx.plugin(SqliteSessionQueryEngine, { path: ':memory:' })
|
||||
await ctx.plugin(ToolSessionQuery)
|
||||
},
|
||||
note:
|
||||
@@ -440,7 +443,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
writes: ['tool/call', 'tool/result', 'child session events through the chosen provider'],
|
||||
shippedNames: ['subagent', 'subagent_fork'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
registerCatalogSubagentProvider(ctx, 'mock')
|
||||
await ctx.plugin(ToolSubagent, { provider: 'mock' })
|
||||
},
|
||||
@@ -458,8 +461,8 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
requires: ['ctx.tools', 'ctx.subagents', 'ctx.agents and ctx.sessionProjections (list_agents only)'],
|
||||
writes: ['tool/call', 'tool/result', 'child session events through ctx.subagents'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(LocalTaskService)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(LocalJobRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
@@ -477,7 +480,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
writes: ['tool/call', 'tool/result', 'a user-role message in the direct parent session'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
const { reportDelivery } = ToolSubagentReport.Config({}) as { reportDelivery: SubagentReportDelivery }
|
||||
await mountCatalogChildScope(ctx, (childCtx) => {
|
||||
ToolSubagentReport.installReportTool(childCtx, ctx, reportDelivery)
|
||||
@@ -491,17 +494,17 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
+ '`send_message` tool is installed independently.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-tasks',
|
||||
dir: 'tool-tasks',
|
||||
source: 'packages/tasks/tool-tasks/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.tasks', 'ctx.systemPrompt'],
|
||||
pkg: '@deepseek-ai/dsh-tool-jobs',
|
||||
dir: 'tool-jobs',
|
||||
source: 'packages/jobs/tool-jobs/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.jobs', 'ctx.systemPrompt'],
|
||||
writes: ['tool/call', 'tool/result', 'user/message via agent.inject() for background completion notices'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalTaskService)
|
||||
await ctx.plugin(LocalJobRegistry)
|
||||
await ctx.plugin(ToolTasks)
|
||||
},
|
||||
note:
|
||||
'The kind-agnostic background-task controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers\' `ctx.tasks.start()`.',
|
||||
'The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers\' `ctx.jobs.start()`.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-todo',
|
||||
@@ -519,13 +522,13 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-workflow',
|
||||
dir: 'tool-workflow',
|
||||
source: 'packages/workflow/tool-workflow/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.workflows', 'ctx.systemPrompt', 'a calling Agent (exec.agent parents the script children)'],
|
||||
requires: ['ctx.tools', 'ctx.workflowEngine', 'ctx.systemPrompt', 'a calling Agent (exec.agent parents the script children)'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
// The tool injects `workflows`; boot the vm engine over a scripted
|
||||
// subagent provider to satisfy it. The schema does not depend on which
|
||||
// provider backs the engine.
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
registerCatalogSubagentProvider(ctx, 'mock')
|
||||
await ctx.plugin(VmWorkflowEngine, { provider: 'mock' })
|
||||
await ctx.plugin(ToolWorkflow)
|
||||
@@ -540,7 +543,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
async mount(ctx) {
|
||||
// Mount search and fetch providers so both tools register. Their schemas
|
||||
// do not depend on provider identity or availability.
|
||||
await ctx.plugin(WebService)
|
||||
await ctx.plugin(WebRuntime)
|
||||
await ctx.plugin(WebSearchExa)
|
||||
await ctx.plugin(WebFetchLocal)
|
||||
await ctx.plugin(ToolWeb)
|
||||
@@ -587,6 +590,29 @@ export function assertManifestComplete(packages: ToolPackage[] = TOOL_PACKAGES,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert one manifest entry actually registered a tool.
|
||||
*
|
||||
* A tool package that boots without registering anything is a broken boot, not
|
||||
* an empty catalog section. The usual cause is an `inject` the entry's `mount`
|
||||
* does not satisfy: cordis leaves the plugin PENDING, every step here still
|
||||
* succeeds, and the generator writes a catalog missing that package's tools —
|
||||
* with the freshness gate green on it, because the omission is now what the
|
||||
* generator produces. {@link assertManifestComplete} cannot see this: the
|
||||
* package IS listed, it just contributed nothing.
|
||||
* @param entry - the manifest entry that was booted.
|
||||
* @param harvested - how many schemas its boot registered.
|
||||
* @throws when the boot registered no tool at all.
|
||||
*/
|
||||
export function assertToolsHarvested(entry: ToolPackage, harvested: number): void {
|
||||
if (harvested > 0) return
|
||||
throw new Error(
|
||||
`gen-tool-catalog: ${entry.pkg} booted without registering a single tool. `
|
||||
+ 'Its plugin is most likely PENDING on a service this manifest entry does not mount — '
|
||||
+ `compare the plugin's inject with mount() and requires: ${entry.requires.join(', ')}.`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot each tool package on a fresh Context and harvest its model-facing
|
||||
* schemas. A fresh Context per package keeps attribution clean (each entry's
|
||||
@@ -603,9 +629,10 @@ export async function collectToolCatalog(packages: ToolPackage[] = TOOL_PACKAGES
|
||||
// fiber) — the repo's "dispose must reach quiescence" rule.
|
||||
try {
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry, entry.toolsConfig ?? {})
|
||||
await ctx.plugin(ToolRuntime, entry.toolsConfig ?? {})
|
||||
await entry.mount(ctx)
|
||||
const schemas = ctx.tools.schemas(entry.scope?.(ctx)).sort((a, b) => a.name.localeCompare(b.name))
|
||||
assertToolsHarvested(entry, schemas.length)
|
||||
catalog.push({
|
||||
pkg: entry.pkg,
|
||||
sources: Object.fromEntries(schemas.map(schema => [
|
||||
@@ -678,7 +705,7 @@ export function render(catalog: ToolCatalog): string {
|
||||
'',
|
||||
]
|
||||
for (const entry of catalog) {
|
||||
lines.push(`## \`${entry.pkg}\``, '')
|
||||
lines.push(`<a id="${githubSlug(entry.pkg)}"></a>`, '', `## \`${entry.pkg}\``, '')
|
||||
for (const schema of entry.schemas) {
|
||||
// Collection validated that every harvested schema has a source.
|
||||
const source = entry.sources[schema.name] as string
|
||||
|
||||
@@ -16,6 +16,7 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { removeFixtureSafely, unlinkFixtureLinks } from './test-fixture-cleanup.ts'
|
||||
|
||||
const installer = fileURLToPath(new URL('./install-lefthook.mjs', import.meta.url))
|
||||
const pairingMergeDriver = 'scripts/merge-translation-pairing-driver.sh %O %A %B %P'
|
||||
@@ -40,7 +41,7 @@ interface CommandResult {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const fixture of fixtures.splice(0)) rmSync(fixture, { recursive: true, force: true })
|
||||
for (const fixture of fixtures.splice(0)) removeFixtureSafely(fixture)
|
||||
})
|
||||
|
||||
function commandResult(command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv): CommandResult {
|
||||
@@ -282,6 +283,10 @@ describe('worktree-local Lefthook installer', { timeout: 30_000 }, () => {
|
||||
expect(gitResult(fixture, fixture.main, ['config', '--file', commonConfig, '--get', 'core.bare']).status).toBe(1)
|
||||
|
||||
const mainHookBeforeRemoval = readFileSync(join(mainHooks, 'pre-commit'), 'utf8')
|
||||
// Windows Git follows the fixture's MOUNT_POINT junctions into their real
|
||||
// targets while removing a worktree; unlink them first so the removal
|
||||
// cannot delete the repository's scripts/ or tsx package.
|
||||
unlinkFixtureLinks(fixture.linked)
|
||||
git(fixture, fixture.main, ['worktree', 'remove', '--force', fixture.linked])
|
||||
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBeforeRemoval)
|
||||
expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy hook\n')
|
||||
|
||||
@@ -50,8 +50,8 @@ describe('Oxlint executable contract', () => {
|
||||
const suffix = randomUUID()
|
||||
const configPath = await writeContractConfig(suffix)
|
||||
const probes = [
|
||||
['host package source', 'packages/fs/fs-policy/src', 'packages/fs/fs-policy/tsconfig.json'],
|
||||
['host package test', 'packages/fs/fs-policy/tests', 'tsconfig.host.json'],
|
||||
['host package source', 'packages/fs/fs-observation-policy/src', 'packages/fs/fs-observation-policy/tsconfig.json'],
|
||||
['host package test', 'packages/fs/fs-observation-policy/tests', 'tsconfig.host.json'],
|
||||
['client package source', 'packages/client/ui-primitives/src', 'packages/client/ui-primitives/tsconfig.json'],
|
||||
// A test under packages/client states its face in the filename, so the
|
||||
// probe carries the Client suffix to reach the Client aggregate.
|
||||
|
||||
@@ -57,7 +57,7 @@ function fixture(options: {
|
||||
}
|
||||
writeFileSync(join(dir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
|
||||
references: options.invariantReference === false ? [] : [{ path: '../../support/invariants' }],
|
||||
references: options.invariantReference === false ? [] : [{ path: '../../runtime-diagnostics/invariants' }],
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(join(dir, 'src/invariant.ts'), options.source ?? handwrittenInvariant(packageName))
|
||||
writeFileSync(
|
||||
@@ -80,7 +80,7 @@ describe('package invariant gate', () => {
|
||||
references: [{ path: './tsconfig.host.json' }],
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(join(dir, 'tsconfig.host.json'), `${JSON.stringify({
|
||||
references: [{ path: '../../support/invariants' }],
|
||||
references: [{ path: '../../runtime-diagnostics/invariants' }],
|
||||
}, null, 2)}\n`)
|
||||
|
||||
expect(collectPackageInvariantViolations(root)).toEqual([])
|
||||
|
||||
@@ -123,7 +123,7 @@ function checkBuild(
|
||||
addViolation(
|
||||
violations,
|
||||
tsconfigPath,
|
||||
'TypeScript project references must include ../../support/invariants',
|
||||
'TypeScript project references must include ../../runtime-diagnostics/invariants',
|
||||
)
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ function checkBuild(
|
||||
|
||||
function projectReferencesInvariants(root: string, ownerDir: string, entryPath: string): boolean {
|
||||
const ownerRoot = resolve(root, ownerDir)
|
||||
const target = resolve(root, 'packages/support/invariants')
|
||||
const target = resolve(root, 'packages/runtime-diagnostics/invariants')
|
||||
const pending = [resolve(root, entryPath)]
|
||||
const visited = new Set<string>()
|
||||
while (pending.length > 0) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { basename, join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { docsPages, landingLink, routeLink, sectionSpec, type DocsPage } from '../website/docs.ts'
|
||||
import {
|
||||
addProjectionFrontmatter, projectedPageContent, publishableImage, rewriteMarkdown,
|
||||
addProjectionFrontmatter, projectedPageContent, publishableImage, resolveRepositoryRef, rewriteMarkdown,
|
||||
} from './project-doc-site.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
@@ -91,6 +91,16 @@ describe('publishableImage', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveRepositoryRef', () => {
|
||||
it('defaults to public master instead of a private workflow SHA', () => {
|
||||
expect(resolveRepositoryRef({ GITHUB_SHA: 'private-sha' })).toBe('master')
|
||||
})
|
||||
|
||||
it('accepts an explicit public repository ref', () => {
|
||||
expect(resolveRepositoryRef({ DOCS_REPOSITORY_REF: 'public-sha' })).toBe('public-sha')
|
||||
})
|
||||
})
|
||||
|
||||
describe('rewriteMarkdown', () => {
|
||||
it('maps published pages and pins unpublished source links', () => {
|
||||
const { root, pages } = fixture()
|
||||
|
||||
@@ -19,6 +19,16 @@ const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness'
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const generatedRoot = resolve(root, 'website/.generated')
|
||||
|
||||
/**
|
||||
* Resolve the public repository ref used by projected source links.
|
||||
*
|
||||
* @param environment Build environment containing an optional explicit public ref.
|
||||
* @returns The configured public ref, or `master`.
|
||||
*/
|
||||
export function resolveRepositoryRef(environment: NodeJS.ProcessEnv): string {
|
||||
return environment.DOCS_REPOSITORY_REF ?? 'master'
|
||||
}
|
||||
|
||||
interface Replacement {
|
||||
start: number
|
||||
end: number
|
||||
@@ -400,7 +410,7 @@ export function projectDocs(): void {
|
||||
const routes = new Set<string>()
|
||||
/** Projected path to the repository file that claimed it, pages and images alike. */
|
||||
const claimed = new Map<string, string>()
|
||||
const repositoryRef = process.env.GITHUB_SHA ?? 'master'
|
||||
const repositoryRef = resolveRepositoryRef(process.env)
|
||||
rmSync(generatedRoot, { recursive: true, force: true })
|
||||
|
||||
/** Reserve one projected path, refusing a second source for it. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
hasTypeRTRemoteNavigation,
|
||||
hasTypertRemoteNavigation,
|
||||
isForbiddenPublicationFile,
|
||||
validateTarballPayload,
|
||||
} from './publication-payload.ts'
|
||||
@@ -68,7 +68,7 @@ describe('publication payload policy', () => {
|
||||
})
|
||||
|
||||
it('recognizes only the canonical Host-for-Client export pair', () => {
|
||||
expect(hasTypeRTRemoteNavigation({
|
||||
expect(hasTypertRemoteNavigation({
|
||||
exports: {
|
||||
'./remote': {
|
||||
types: './lib/typert.remote-client.d.ts',
|
||||
@@ -76,6 +76,6 @@ describe('publication payload policy', () => {
|
||||
},
|
||||
},
|
||||
})).toBe(true)
|
||||
expect(hasTypeRTRemoteNavigation({ exports: { './remote': './lib/remote.js' } })).toBe(false)
|
||||
expect(hasTypertRemoteNavigation({ exports: { './remote': './lib/remote.js' } })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @param manifest - parsed package manifest to inspect.
|
||||
* @returns whether the canonical `./remote` export pair is present.
|
||||
*/
|
||||
export function hasTypeRTRemoteNavigation(manifest: unknown): boolean {
|
||||
export function hasTypertRemoteNavigation(manifest: unknown): boolean {
|
||||
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) return false
|
||||
const exportsField = (manifest as Record<string, unknown>).exports
|
||||
if (exportsField === null || typeof exportsField !== 'object' || Array.isArray(exportsField)) return false
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
*
|
||||
* The dsh family shares one version across its members and the workspace root:
|
||||
* `major`, `minor`, `patch`, or an explicit `x.y.z` (including a prerelease such
|
||||
* as `0.0.1-rc.1`). The vendored family has one version line per package and
|
||||
* publishes only what changed since that package's own `vendor-<package>-v*`
|
||||
* tag, which is the record of the commit it last published from.
|
||||
* as `0.0.1-rc.1`). The vendored family has one version line per package, but
|
||||
* every release advances and publishes the complete family so the next release
|
||||
* never reuses an unchanged member's existing version from a different
|
||||
* repository state.
|
||||
*
|
||||
* The version lands in the manifests, the lockfile follows, and a human creates
|
||||
* the tag after the commit merges. CI never writes to the repository.
|
||||
@@ -17,7 +18,7 @@ import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join, matchesGlob } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { releaseFamily, type ReleaseFamily, type ReleaseMember } from './families.ts'
|
||||
import { attempt, capture, isEntry } from './process.ts'
|
||||
import { capture, isEntry } from './process.ts'
|
||||
|
||||
/** Files npm publishes whether or not `files` lists them. */
|
||||
const ALWAYS_PUBLISHED = ['package.json', 'README*', 'LICENSE*', 'LICENCE*'] as const
|
||||
@@ -144,30 +145,33 @@ function nextSharedVersion(current: string, request: string): string {
|
||||
/**
|
||||
* The version a vendored package publishes next.
|
||||
*
|
||||
* The baseline is the higher of the manifest version and the last published
|
||||
* The baseline is the higher of the manifest version and the last tagged
|
||||
* version: a vendor re-sync restores upstream's version, which is lower than
|
||||
* what this repository already published, and incrementing that would name a
|
||||
* version the registry already carries.
|
||||
* the release version this repository already reserved, and incrementing that
|
||||
* would reuse an existing version.
|
||||
*
|
||||
* A prerelease does not consume its own release numbers. Publishing
|
||||
* `4.0.1-rc.1` leaves `4.0.1` free, so the next stable version is `4.0.1`
|
||||
* rather than `4.0.2`, and a second prerelease keeps those numbers too.
|
||||
* @param current - the package's manifest version.
|
||||
* @param published - the version its newest tag names, when it has one.
|
||||
* @param tagged - the version its newest tag names, when it has one.
|
||||
* @param prerelease - prerelease identifier to append, for a rehearsal publication.
|
||||
* @returns The target version.
|
||||
*/
|
||||
export function nextVendorVersion(
|
||||
current: string,
|
||||
published: string | undefined,
|
||||
tagged: string | undefined,
|
||||
prerelease?: string,
|
||||
): string {
|
||||
const ahead = published !== undefined && compareReleaseNumbers(published, current) > 0
|
||||
const baseline = ahead ? published : current
|
||||
const taggedOrder = tagged === undefined ? undefined : compareReleaseNumbers(tagged, current)
|
||||
const ahead = taggedOrder !== undefined && taggedOrder > 0
|
||||
const baseline = ahead && tagged !== undefined ? tagged : current
|
||||
const [major, minor, patch] = releaseNumbers(baseline)
|
||||
// Reuse the numbers when the published version that set them is a prerelease
|
||||
// Reuse the numbers when the tagged version that set them is a prerelease
|
||||
// of them; increment when a stable release already holds them.
|
||||
const reuse = ahead && published.includes('-')
|
||||
const taggedPrerelease = tagged !== undefined && prereleaseOf(tagged) !== undefined
|
||||
const sameReleasePrereleases = taggedOrder === 0 && prereleaseOf(current) !== undefined
|
||||
const reuse = taggedPrerelease && (ahead || sameReleasePrereleases)
|
||||
const numbers = reuse
|
||||
? `${String(major)}.${String(minor)}.${String(patch)}`
|
||||
: `${String(major)}.${String(minor)}.${String(patch + 1)}`
|
||||
@@ -191,12 +195,12 @@ export function reachesPayload(member: ReleaseMember, path: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* The newest version a member published, read from its tags.
|
||||
* The newest version a member tagged.
|
||||
* @param family - the member's family.
|
||||
* @param member - the member.
|
||||
* @returns The version, or undefined when the member never published.
|
||||
* @returns The version, or undefined when the member has no release tag.
|
||||
*/
|
||||
function lastPublishedVersion(family: ReleaseFamily, member: ReleaseMember): string | undefined {
|
||||
function lastTaggedVersion(family: ReleaseFamily, member: ReleaseMember): string | undefined {
|
||||
const prefix = family.tagPrefixFor(member)
|
||||
const versions = capture('git', ['tag', '--list', `${prefix}*`])
|
||||
.split('\n').filter(line => line !== '').map(tag => tag.slice(prefix.length))
|
||||
@@ -204,33 +208,6 @@ function lastPublishedVersion(family: ReleaseFamily, member: ReleaseMember): str
|
||||
return versions.reduce((newest, candidate) => compareVersions(candidate, newest) > 0 ? candidate : newest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the registry carries the version a tag names.
|
||||
*
|
||||
* A tag is a commit pointer, not proof of publication: a tag pushed for a
|
||||
* publication that then failed would otherwise read as "already published" and
|
||||
* skip the package indefinitely. Querying a private package needs credentials,
|
||||
* so an unauthenticated machine reports the gap instead of failing.
|
||||
* @param name - package name.
|
||||
* @param version - the version the tag names.
|
||||
*/
|
||||
function confirmPublished(name: string, version: string): void {
|
||||
const result = attempt('npm', ['view', `${name}@${version}`, 'version'])
|
||||
if (result.status === 0) return
|
||||
const output = `${result.stdout}${result.stderr}`
|
||||
if (output.includes('ENEEDAUTH') || output.includes('E401') || output.includes('E403')) {
|
||||
console.log(`release bump: cannot reach the registry for ${name}@${version}; skipping the tag check`)
|
||||
return
|
||||
}
|
||||
if (output.includes('E404') || output.includes('404 Not Found')) {
|
||||
throw new Error(
|
||||
`${name}@${version} is tagged but absent from the registry.`
|
||||
+ '\nThe tag was pushed for a publication that did not complete: re-run that publish, or delete the tag.',
|
||||
)
|
||||
}
|
||||
throw new Error(`npm view ${name}@${version} failed:\n${output}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a version into a manifest, preserving formatting and key order.
|
||||
* @param root - repository root.
|
||||
@@ -293,8 +270,8 @@ function planShared(
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan the vendored family's rewrite: every package whose payload changed since
|
||||
* it last published.
|
||||
* Plan the vendored family's rewrite: every package advances together while
|
||||
* retaining its own version line and tag.
|
||||
* @param family - the vendored family.
|
||||
* @param members - the family's members.
|
||||
* @param prerelease - prerelease identifier to append, for a rehearsal publication.
|
||||
@@ -307,15 +284,8 @@ function planPerPackage(
|
||||
): PlannedVersion[] {
|
||||
const planned: PlannedVersion[] = []
|
||||
for (const member of members) {
|
||||
const published = lastPublishedVersion(family, member)
|
||||
if (published !== undefined) {
|
||||
confirmPublished(member.name, published)
|
||||
const since = `${family.tagPrefixFor(member)}${published}`
|
||||
const changed = capture('git', ['diff', '--name-only', `${since}..HEAD`, '--', member.directory])
|
||||
.split('\n').filter(line => line !== '')
|
||||
if (!changed.some(path => reachesPayload(member, path))) continue
|
||||
}
|
||||
const to = nextVendorVersion(member.version, published, prerelease)
|
||||
const tagged = lastTaggedVersion(family, member)
|
||||
const to = nextVendorVersion(member.version, tagged, prerelease)
|
||||
planned.push({
|
||||
manifestPath: join(member.directory, 'package.json'),
|
||||
label: member.directory,
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('release families', () => {
|
||||
|
||||
it('rejects a family whose members disagree on the shared version', () => {
|
||||
const dsh = releaseFamily('dsh')
|
||||
const members = [member('apps/cli', '@deepseek-ai/dsh'), { ...member('apps/web', '@deepseek-ai/dsh-frontend'), version: '0.0.2' }]
|
||||
const members = [member('apps/cli', '@deepseek-ai/dsh'), { ...member('apps/web', '@deepseek-ai/dsh-web-frontend'), version: '0.0.2' }]
|
||||
|
||||
expect(() => { dsh.verifyVersions(members) }).toThrow(/must share one version/)
|
||||
expect(() => { dsh.verifyVersions([members[0]!]) }).not.toThrow()
|
||||
@@ -57,7 +57,7 @@ describe('release families', () => {
|
||||
member('packages/a/zebra', '@deepseek-ai/dsh-zebra'),
|
||||
]
|
||||
|
||||
expect(dsh.publishOrder(members).map(entry => entry.name)).toEqual([
|
||||
expect(dsh.publishOrder(members).order.map(entry => entry.name)).toEqual([
|
||||
'@deepseek-ai/dsh-library',
|
||||
'@deepseek-ai/dsh-consumer',
|
||||
'@deepseek-ai/dsh-zebra',
|
||||
@@ -74,6 +74,92 @@ describe('release families', () => {
|
||||
expect(() => { dsh.publishOrder(members) }).toThrow(/dependency cycle/)
|
||||
})
|
||||
|
||||
it('publishes a peer before its consumer', () => {
|
||||
const dsh = releaseFamily('dsh')
|
||||
const members = [
|
||||
member('packages/a/consumer', '@deepseek-ai/dsh-consumer', { peerDependencies: { '@deepseek-ai/dsh-zebra': 'workspace:^' } }),
|
||||
member('packages/a/zebra', '@deepseek-ai/dsh-zebra'),
|
||||
]
|
||||
|
||||
// Name order alone would place the consumer first; the peer edge moves it.
|
||||
expect(dsh.publishOrder(members).order.map(entry => entry.name)).toEqual([
|
||||
'@deepseek-ai/dsh-zebra',
|
||||
'@deepseek-ai/dsh-consumer',
|
||||
])
|
||||
})
|
||||
|
||||
it('orders around a peer cycle rather than refusing to publish, and reports the edge it dropped', () => {
|
||||
const dsh = releaseFamily('dsh')
|
||||
const members = [
|
||||
member('packages/a/left', '@deepseek-ai/dsh-left', { peerDependencies: { '@deepseek-ai/dsh-right': 'workspace:^' } }),
|
||||
member('packages/a/right', '@deepseek-ai/dsh-right', { peerDependencies: { '@deepseek-ai/dsh-left': 'workspace:^' } }),
|
||||
]
|
||||
|
||||
// Sibling packages declare each other as peers, and npm treats an unmet peer
|
||||
// as a warning, so this pair has to publish rather than fail the release.
|
||||
const plan = dsh.publishOrder(members)
|
||||
expect(plan.order.map(entry => entry.name)).toEqual([
|
||||
'@deepseek-ai/dsh-right',
|
||||
'@deepseek-ai/dsh-left',
|
||||
])
|
||||
// One of the two edges has to give, and which one it is belongs in the log.
|
||||
expect(plan.droppedPeerEdges).toEqual([
|
||||
{ consumer: '@deepseek-ai/dsh-right', peer: '@deepseek-ai/dsh-left' },
|
||||
])
|
||||
})
|
||||
|
||||
it('honours an install edge even when a peer cycle surrounds it', () => {
|
||||
const dsh = releaseFamily('dsh')
|
||||
const members = [
|
||||
member('packages/a/base', '@deepseek-ai/dsh-base', { peerDependencies: { '@deepseek-ai/dsh-consumer': 'workspace:^' } }),
|
||||
member('packages/a/consumer', '@deepseek-ai/dsh-consumer', {
|
||||
dependencies: { '@deepseek-ai/dsh-base': 'workspace:^' },
|
||||
peerDependencies: { '@deepseek-ai/dsh-base': 'workspace:^' },
|
||||
}),
|
||||
]
|
||||
|
||||
// The install edge is absolute: base publishes first, and the peer edge that
|
||||
// would reverse it is the one dropped.
|
||||
const plan = dsh.publishOrder(members)
|
||||
expect(plan.order.map(entry => entry.name)).toEqual([
|
||||
'@deepseek-ai/dsh-base',
|
||||
'@deepseek-ai/dsh-consumer',
|
||||
])
|
||||
expect(plan.droppedPeerEdges).toEqual([
|
||||
{ consumer: '@deepseek-ai/dsh-base', peer: '@deepseek-ai/dsh-consumer' },
|
||||
])
|
||||
})
|
||||
|
||||
it('refuses an order that would publish a consumer before a dependency it installs', () => {
|
||||
const dsh = releaseFamily('dsh')
|
||||
const members = [
|
||||
member('packages/a/alpha', '@deepseek-ai/dsh-alpha', { peerDependencies: { '@deepseek-ai/dsh-bravo': 'workspace:^' } }),
|
||||
member('packages/a/bravo', '@deepseek-ai/dsh-bravo', { peerDependencies: { '@deepseek-ai/dsh-charlie': 'workspace:^' } }),
|
||||
member('packages/a/charlie', '@deepseek-ai/dsh-charlie', { dependencies: { '@deepseek-ai/dsh-alpha': 'workspace:^' } }),
|
||||
]
|
||||
|
||||
// A cycle of two peer edges closed by one install edge: dropping a peer edge
|
||||
// would order this, and the traversal drops the install edge instead. That
|
||||
// order would publish charlie before the alpha it installs, so it is refused
|
||||
// here rather than published.
|
||||
expect(() => { dsh.publishOrder(members) }).toThrow(/no publish order honours @deepseek-ai\/dsh-charlie -> @deepseek-ai\/dsh-alpha/)
|
||||
})
|
||||
|
||||
it('ignores devDependencies when ordering', () => {
|
||||
const dsh = releaseFamily('dsh')
|
||||
const members = [
|
||||
member('packages/a/alpha', '@deepseek-ai/dsh-alpha', { devDependencies: { '@deepseek-ai/dsh-zebra': 'workspace:^' } }),
|
||||
member('packages/a/zebra', '@deepseek-ai/dsh-zebra'),
|
||||
]
|
||||
|
||||
// A dev dependency is absent from the published package, so it must not move
|
||||
// the consumer behind it.
|
||||
expect(dsh.publishOrder(members).order.map(entry => entry.name)).toEqual([
|
||||
'@deepseek-ai/dsh-alpha',
|
||||
'@deepseek-ai/dsh-zebra',
|
||||
])
|
||||
})
|
||||
|
||||
it('applies the harness payload policy to dsh and keeps upstream payloads for vendored packages', () => {
|
||||
const dsh = releaseFamily('dsh')
|
||||
const vendor = releaseFamily('vendor')
|
||||
|
||||
@@ -13,12 +13,47 @@ import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { validateTarballPayload } from '../publication-payload.ts'
|
||||
|
||||
/** Dependency sections that constrain publish order: a consumer must publish after its dependency. */
|
||||
const ORDER_SECTIONS = ['dependencies', 'optionalDependencies'] as const
|
||||
/**
|
||||
* Dependency sections a consumer must publish after, because npm resolves them
|
||||
* when the package is installed: publishing a consumer first would leave a
|
||||
* window where its own tree cannot be assembled.
|
||||
*/
|
||||
const INSTALL_SECTIONS = ['dependencies', 'optionalDependencies'] as const
|
||||
|
||||
/**
|
||||
* Peer declarations also order the publication, but they cannot constrain it.
|
||||
* npm never installs a peer on the package's behalf — an unmet peer is a
|
||||
* warning, not a resolution failure — and sibling packages legitimately declare
|
||||
* each other as peers, which makes these edges the ones that close cycles. They
|
||||
* order what they can and are dropped where they would deadlock.
|
||||
*/
|
||||
const PEER_SECTIONS = ['peerDependencies'] as const
|
||||
|
||||
/** The workspace root manifest, which is never a release member. */
|
||||
const WORKSPACE_ROOT_PACKAGE = '@deepseek-ai/dsh-root'
|
||||
|
||||
/** One peer declaration the publish order leaves unordered. */
|
||||
interface DroppedPeerEdge {
|
||||
/** Package declaring the peer. */
|
||||
readonly consumer: string
|
||||
/** The declared peer, which publishes after `consumer` or alongside it in a cycle. */
|
||||
readonly peer: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A family's publish order together with the ordering it could not honour.
|
||||
*
|
||||
* The dropped edges are part of the result rather than a detail of forming it:
|
||||
* a release drops real ordering constraints, and the operator reading the pack
|
||||
* log is the only one who can judge whether a newly dropped edge is expected.
|
||||
*/
|
||||
export interface PublishPlan {
|
||||
/** Members in publish order. */
|
||||
readonly order: readonly ReleaseMember[]
|
||||
/** Peer declarations left unordered, in the order the traversal reached them. */
|
||||
readonly droppedPeerEdges: readonly DroppedPeerEdge[]
|
||||
}
|
||||
|
||||
/** One publishable package of a release family. */
|
||||
export interface ReleaseMember {
|
||||
/** Repository-relative package directory, for example `packages/core/session`. */
|
||||
@@ -107,45 +142,122 @@ export abstract class ReleaseFamily {
|
||||
}
|
||||
|
||||
/**
|
||||
* Order members so every package publishes after the family members it depends on.
|
||||
* Order members so every package publishes after the family members it
|
||||
* depends on, which is what makes a partial publication self-consistent: an
|
||||
* interrupted run leaves a prefix whose packages never point at something
|
||||
* absent from the registry.
|
||||
*
|
||||
* Install edges are honoured absolutely — a cycle among them is a defect this
|
||||
* reports rather than works around. Peer edges order what they can and are
|
||||
* dropped where honouring one would deadlock: sibling packages declare each
|
||||
* other as peers, and npm treats an unmet peer as a warning rather than a
|
||||
* resolution failure ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
|
||||
* Every dropped edge is reported, because dropping one is a decision about a
|
||||
* real release rather than an implementation detail.
|
||||
* @param members - this family's members.
|
||||
* @returns The same members in publish order; ties break by name for determinism.
|
||||
* @returns The order, ties broken by name for determinism, and the peer edges it left unordered.
|
||||
*/
|
||||
publishOrder(members: readonly ReleaseMember[]): ReleaseMember[] {
|
||||
publishOrder(members: readonly ReleaseMember[]): PublishPlan {
|
||||
const byName = new Map(members.map(member => [member.name, member]))
|
||||
const ordered: ReleaseMember[] = []
|
||||
const placed = new Set<string>()
|
||||
const visiting = new Set<string>()
|
||||
const byNameSorted = [...members].sort((left, right) => left.name.localeCompare(right.name))
|
||||
const edges = (member: ReleaseMember, sections: readonly string[]): ReleaseMember[] =>
|
||||
this.orderEdges(member, byName, sections)
|
||||
|
||||
const visit = (member: ReleaseMember, path: readonly string[]): void => {
|
||||
if (placed.has(member.name)) return
|
||||
if (visiting.has(member.name)) {
|
||||
// Install edges alone must be acyclic, and that is checked on its own graph:
|
||||
// a peer edge leading into an install edge would otherwise read as a cycle
|
||||
// where the install edges are perfectly orderable.
|
||||
const installVisiting = new Set<string>()
|
||||
const installDone = new Set<string>()
|
||||
const checkInstall = (member: ReleaseMember, path: readonly string[]): void => {
|
||||
if (installDone.has(member.name)) return
|
||||
if (installVisiting.has(member.name)) {
|
||||
throw new Error(`dependency cycle in release family ${this.id}: ${[...path, member.name].join(' -> ')}`)
|
||||
}
|
||||
visiting.add(member.name)
|
||||
for (const dependency of this.orderEdges(member, byName)) {
|
||||
visit(dependency, [...path, member.name])
|
||||
installVisiting.add(member.name)
|
||||
for (const dependency of edges(member, INSTALL_SECTIONS)) checkInstall(dependency, [...path, member.name])
|
||||
installVisiting.delete(member.name)
|
||||
installDone.add(member.name)
|
||||
}
|
||||
for (const member of byNameSorted) checkInstall(member, [])
|
||||
|
||||
// Emit the order over both kinds of edge. A node already on the stack closes
|
||||
// a cycle, and that cycle carries at least one peer edge because the install
|
||||
// edges were just proved acyclic — but the back edge that reaches the stacked
|
||||
// node is not necessarily the peer one, so the post-condition below decides
|
||||
// whether the emitted order survived.
|
||||
const ordered: ReleaseMember[] = []
|
||||
const droppedPeerEdges: DroppedPeerEdge[] = []
|
||||
const placed = new Set<string>()
|
||||
const onStack = new Set<string>()
|
||||
// Members reachable from one member through install edges. A peer edge is
|
||||
// dropped when the peer installs the member declaring it: honouring it would
|
||||
// emit a package before something it installs, and the install edge wins.
|
||||
const installClosure = (member: ReleaseMember): Set<string> => {
|
||||
const reached = new Set<string>()
|
||||
const walk = (current: ReleaseMember): void => {
|
||||
for (const dependency of edges(current, INSTALL_SECTIONS)) {
|
||||
if (reached.has(dependency.name)) continue
|
||||
reached.add(dependency.name)
|
||||
walk(dependency)
|
||||
}
|
||||
}
|
||||
visiting.delete(member.name)
|
||||
walk(member)
|
||||
return reached
|
||||
}
|
||||
const visit = (member: ReleaseMember): void => {
|
||||
if (placed.has(member.name) || onStack.has(member.name)) return
|
||||
onStack.add(member.name)
|
||||
for (const dependency of edges(member, INSTALL_SECTIONS)) visit(dependency)
|
||||
for (const peer of edges(member, PEER_SECTIONS)) {
|
||||
if (installClosure(peer).has(member.name)) {
|
||||
droppedPeerEdges.push({ consumer: member.name, peer: peer.name })
|
||||
continue
|
||||
}
|
||||
// A peer already on the stack is an ancestor, so it publishes after this
|
||||
// member rather than before it: the edge is dropped, not honoured.
|
||||
if (onStack.has(peer.name)) droppedPeerEdges.push({ consumer: member.name, peer: peer.name })
|
||||
visit(peer)
|
||||
}
|
||||
onStack.delete(member.name)
|
||||
placed.add(member.name)
|
||||
ordered.push(member)
|
||||
}
|
||||
for (const member of byNameSorted) visit(member)
|
||||
|
||||
for (const member of [...members].sort((left, right) => left.name.localeCompare(right.name))) {
|
||||
visit(member, [])
|
||||
// A cycle mixing both kinds of edge can put an install edge's target on the
|
||||
// stack, where the traversal skips it like a peer edge and emits a consumer
|
||||
// before something it installs. Nothing downstream can detect that, and it
|
||||
// would only surface as an unresolvable install for whoever consumes the
|
||||
// published packages, so the emitted order is checked against the edges it
|
||||
// exists to honour.
|
||||
const position = new Map(ordered.map((entry, index) => [entry.name, index]))
|
||||
for (const [index, member] of ordered.entries()) {
|
||||
for (const dependency of edges(member, INSTALL_SECTIONS)) {
|
||||
const dependencyIndex = position.get(dependency.name)
|
||||
if (dependencyIndex !== undefined && dependencyIndex < index) continue
|
||||
throw new Error(
|
||||
`release family ${this.id}: no publish order honours ${member.name} -> ${dependency.name};`
|
||||
+ ' a cycle mixing peer and dependency declarations reaches this dependency through a peer edge',
|
||||
)
|
||||
}
|
||||
}
|
||||
return ordered
|
||||
return { order: ordered, droppedPeerEdges }
|
||||
}
|
||||
|
||||
/**
|
||||
* The family members one member depends on at runtime.
|
||||
* The family members one member declares in the given sections.
|
||||
* @param member - the dependent member.
|
||||
* @param byName - every family member by package name.
|
||||
* @returns Dependencies inside this family, sorted by name.
|
||||
* @param sections - manifest sections to read.
|
||||
* @returns Members of this family named there, sorted by name.
|
||||
*/
|
||||
private orderEdges(member: ReleaseMember, byName: ReadonlyMap<string, ReleaseMember>): ReleaseMember[] {
|
||||
private orderEdges(
|
||||
member: ReleaseMember,
|
||||
byName: ReadonlyMap<string, ReleaseMember>,
|
||||
sections: readonly string[],
|
||||
): ReleaseMember[] {
|
||||
const edges: ReleaseMember[] = []
|
||||
for (const section of ORDER_SECTIONS) {
|
||||
for (const section of sections) {
|
||||
const dependencies = member.manifest[section]
|
||||
if (dependencies === null || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue
|
||||
for (const name of Object.keys(dependencies)) {
|
||||
|
||||
@@ -45,7 +45,7 @@ function main(): void {
|
||||
const family = releaseFamily(values.family)
|
||||
const root = process.cwd()
|
||||
const destination = resolve(root, values.out ?? DEFAULT_OUTPUT)
|
||||
const members = family.publishOrder(family.members(root))
|
||||
const members = family.publishOrder(family.members(root)).order
|
||||
family.verifyVersions(members)
|
||||
|
||||
rmSync(destination, { recursive: true, force: true })
|
||||
|
||||
@@ -38,6 +38,40 @@ export function attempt(command: string, args: readonly string[], options: RunOp
|
||||
return { status: result.status, stdout: result.stdout, stderr: result.stderr }
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command, capture its output, and echo it once the command exits.
|
||||
*
|
||||
* A step that both shows what a command said and classifies its own failure
|
||||
* needs both halves: the output has to reach the workflow log, and the caller has
|
||||
* to read it to decide whether a failure is worth retrying.
|
||||
*
|
||||
* This is not live progress. `spawnSync` returns only after the child exits, so
|
||||
* nothing appears while the command runs, and the two streams are echoed one
|
||||
* after the other — all of stdout, then all of stderr — which loses their
|
||||
* interleaving. For an npm publish that matters in one visible way: `npm notice`
|
||||
* lines go to stderr while the `+ name@version` confirmation goes to stdout, so
|
||||
* the log shows the confirmation first. Live progress would need an
|
||||
* asynchronous spawn with data listeners.
|
||||
* @param command - executable name.
|
||||
* @param args - command arguments.
|
||||
* @param options - working directory and environment.
|
||||
* @returns The exit status and captured streams.
|
||||
*/
|
||||
export function attemptEchoed(command: string, args: readonly string[], options: RunOptions = {}): CommandResult {
|
||||
const result = spawnSync(command, [...args], {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
encoding: 'utf8',
|
||||
// 'inherit' would leave nothing to capture, so the streams are piped and
|
||||
// echoed instead.
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
})
|
||||
if (result.error !== undefined) throw result.error
|
||||
if (result.stdout !== '') process.stdout.write(result.stdout)
|
||||
if (result.stderr !== '') process.stderr.write(result.stderr)
|
||||
return { status: result.status, stdout: result.stdout, stderr: result.stderr }
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command, capture its standard output, and fail on a non-zero exit.
|
||||
* @param command - executable name.
|
||||
|
||||
@@ -15,19 +15,46 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { setTimeout as sleep } from 'node:timers/promises'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { releaseFamily } from './families.ts'
|
||||
import { attempt, isEntry, run } from './process.ts'
|
||||
import { attempt, attemptEchoed, isEntry } from './process.ts'
|
||||
import { packedIdentity, readPublishOrder } from './tarball.ts'
|
||||
|
||||
/** npm access level for every package this repository publishes. */
|
||||
const ACCESS = 'restricted'
|
||||
/**
|
||||
* Registry codes that answer a write which did not settle, rather than a
|
||||
* rejection of what was sent. `E409 Failed to save packument` is the one this
|
||||
* sequence actually hits: publishing several packages in a row can outrun the
|
||||
* registry's own processing. A rejected payload (`E403` over an existing
|
||||
* version, a malformed manifest) never clears on a retry and must surface.
|
||||
*/
|
||||
const TRANSIENT_PUBLISH_CODES = ['E409', 'E429', 'E500', 'E502', 'E503', 'E504', 'ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN'] as const
|
||||
|
||||
/** How many times one tarball's publish is attempted before the run fails. */
|
||||
const PUBLISH_ATTEMPTS = 4
|
||||
|
||||
/**
|
||||
* Shortest gap between two publishes, and the first retry backoff.
|
||||
*
|
||||
* The registry needs a moment to commit a packument before the next write; back
|
||||
* to back publishes are what produce `E409`.
|
||||
*/
|
||||
const PUBLISH_SPACING_MS = 2_000
|
||||
|
||||
/** What the registry knows about one version. */
|
||||
type RegistryState =
|
||||
| { readonly kind: 'absent' }
|
||||
| { readonly kind: 'present'; readonly integrity: string }
|
||||
|
||||
/**
|
||||
* Whether a failed publish is worth another attempt.
|
||||
* @param output - combined npm output.
|
||||
* @returns True when the registry reported a write it did not commit.
|
||||
*/
|
||||
function isTransientFailure(output: string): boolean {
|
||||
return TRANSIENT_PUBLISH_CODES.some(code => output.includes(`code ${code}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* The subresource integrity string npm records for a tarball.
|
||||
* @param tarball - absolute tarball path.
|
||||
@@ -57,8 +84,47 @@ function registryState(name: string, version: string): RegistryState {
|
||||
return { kind: 'present', integrity: parsed }
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish one tarball, retrying a registry write that did not settle.
|
||||
*
|
||||
* Every retry re-reads the registry first, because `E409` can answer a write
|
||||
* that landed anyway: republishing a version that now exists fails permanently,
|
||||
* so the same integrity appearing under the failed attempt counts as success.
|
||||
* @param tarball - absolute tarball path.
|
||||
* @param name - package name the tarball declares.
|
||||
* @param version - package version the tarball declares.
|
||||
*/
|
||||
async function publishTarball(tarball: string, name: string, version: string): Promise<void> {
|
||||
// A prerelease version never takes the latest dist-tag.
|
||||
const tagArgs = version.includes('-') ? ['--tag', 'next'] : []
|
||||
for (let tries = 1; tries <= PUBLISH_ATTEMPTS; tries += 1) {
|
||||
// No --access: the sequences do not share one access level, so a
|
||||
// command-line flag could not serve both and would override the manifest
|
||||
// that does. Each packed manifest decides, and
|
||||
// check-workspace-constraints holds every manifest to its sequence's level.
|
||||
const result = attemptEchoed('npm', ['publish', tarball, ...tagArgs])
|
||||
const output = `${result.stdout}${result.stderr}`
|
||||
if (result.status === 0) return
|
||||
|
||||
const settled = registryState(name, version)
|
||||
if (settled.kind === 'present' && settled.integrity === integrityOf(tarball)) {
|
||||
console.log(`release publish: ${name}@${version} landed despite a reported failure, continuing`)
|
||||
return
|
||||
}
|
||||
if (tries === PUBLISH_ATTEMPTS || !isTransientFailure(output)) {
|
||||
throw new Error(`npm publish ${name}@${version} failed:\n${output}`)
|
||||
}
|
||||
const backoff = PUBLISH_SPACING_MS * 2 ** (tries - 1)
|
||||
console.log(
|
||||
`release publish: ${name}@${version} hit a transient registry failure`
|
||||
+ ` (attempt ${String(tries)} of ${String(PUBLISH_ATTEMPTS)}), retrying in ${String(backoff)}ms`,
|
||||
)
|
||||
await sleep(backoff)
|
||||
}
|
||||
}
|
||||
|
||||
/** Publish the family named by `--family` from the directory named by `--from`. */
|
||||
function main(): void {
|
||||
async function main(): Promise<void> {
|
||||
const { values } = parseArgs({
|
||||
options: { family: { type: 'string' }, from: { type: 'string' } },
|
||||
allowPositionals: false,
|
||||
@@ -70,9 +136,15 @@ function main(): void {
|
||||
const family = releaseFamily(values.family)
|
||||
const directory = resolve(process.cwd(), values.from)
|
||||
|
||||
// Every entry in the order settles as either published or already present, so
|
||||
// one counter answers "how far along is this run" for whoever is watching a
|
||||
// release that takes minutes per family.
|
||||
const order = readPublishOrder(directory)
|
||||
const total = String(order.length)
|
||||
let published = 0
|
||||
let skipped = 0
|
||||
for (const filename of readPublishOrder(directory)) {
|
||||
for (const [index, filename] of order.entries()) {
|
||||
const progress = `[${String(index + 1)}/${total}]`
|
||||
const tarball = join(directory, filename)
|
||||
const { name, version } = packedIdentity(tarball)
|
||||
const state = registryState(name, version)
|
||||
@@ -85,17 +157,22 @@ function main(): void {
|
||||
+ '\nBump the version, or investigate why the build is not reproducible.',
|
||||
)
|
||||
}
|
||||
console.log(`release publish: ${name}@${version} already published, skipping`)
|
||||
console.log(`release publish: ${progress} ${name}@${version} already published, skipping`)
|
||||
skipped += 1
|
||||
continue
|
||||
}
|
||||
// A prerelease version never takes the latest dist-tag.
|
||||
const tagArgs = version.includes('-') ? ['--tag', 'next'] : []
|
||||
run('npm', ['publish', tarball, '--access', ACCESS, ...tagArgs])
|
||||
// Space out the writes: the gap belongs between publishes, so a run that
|
||||
// only skips does not wait at all.
|
||||
if (published > 0) await sleep(PUBLISH_SPACING_MS)
|
||||
await publishTarball(tarball, name, version)
|
||||
console.log(`release publish: ${progress} ${name}@${version} published`)
|
||||
published += 1
|
||||
}
|
||||
|
||||
console.log(`release publish: family ${family.id}, ${String(published)} published, ${String(skipped)} already present`)
|
||||
console.log(
|
||||
`release publish: family ${family.id}, ${total} member(s),`
|
||||
+ ` ${String(published)} published, ${String(skipped)} already present`,
|
||||
)
|
||||
}
|
||||
|
||||
if (isEntry(import.meta.url)) main()
|
||||
if (isEntry(import.meta.url)) await main()
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* Every tarball the installed tree needs comes from `--from`, so the only
|
||||
* registry traffic is for external dependencies. That matters beyond hermetic
|
||||
* verification: the harness packages declare the vendored framework as a peer,
|
||||
* and those packages live in another release sequence that this credential-free
|
||||
* job cannot fetch from a private registry — so a dsh verification passes the
|
||||
* those packages live in another release sequence, and this job must not depend
|
||||
* on the registry already carrying versions that match — one pull request may
|
||||
* bump both families before either publishes — so a dsh verification passes the
|
||||
* vendored family's pack output too, while publishing only its own
|
||||
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
|
||||
*
|
||||
|
||||
@@ -9,7 +9,33 @@
|
||||
|
||||
import { parseArgs } from 'node:util'
|
||||
import { isEntry } from './process.ts'
|
||||
import { releaseFamily, type ReleaseFamily, type ReleaseMember } from './families.ts'
|
||||
import { releaseFamily, type PublishPlan, type ReleaseFamily, type ReleaseMember } from './families.ts'
|
||||
|
||||
/**
|
||||
* Print the publish order the release will follow, and the peer declarations it
|
||||
* leaves unordered.
|
||||
*
|
||||
* The order is the release's own plan: an interrupted publication leaves exactly
|
||||
* a prefix of it, so reading it is how anyone judges what a partial run left on
|
||||
* the registry, and printing it on every pull request is what makes a change to
|
||||
* the order reviewable rather than only observable during a publication.
|
||||
* @param family - the release family.
|
||||
* @param plan - the resolved order and its dropped edges.
|
||||
*/
|
||||
function reportPublishOrder(family: ReleaseFamily, plan: PublishPlan): void {
|
||||
console.log(`release verify: publish order for family ${family.id}, ${String(plan.order.length)} member(s):`)
|
||||
const width = String(plan.order.length).length
|
||||
for (const [index, member] of plan.order.entries()) {
|
||||
console.log(` ${String(index + 1).padStart(width, ' ')} ${member.name}@${member.version}`)
|
||||
}
|
||||
if (plan.droppedPeerEdges.length === 0) return
|
||||
console.log(
|
||||
`release verify: ${String(plan.droppedPeerEdges.length)} peer declaration(s) publish unordered,`
|
||||
+ ' because the peer cannot precede the package declaring it without contradicting a dependency edge'
|
||||
+ ' or its own cycle. npm treats an unmet peer as a warning, so this orders nothing and blocks nothing:',
|
||||
)
|
||||
for (const edge of plan.droppedPeerEdges) console.log(` ${edge.consumer} -> ${edge.peer}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert every member may be published: npm refuses a `private` package.
|
||||
@@ -55,6 +81,16 @@ function main(): void {
|
||||
const family = releaseFamily(values.family)
|
||||
const members = family.members(process.cwd())
|
||||
family.verifyVersions(members)
|
||||
// Resolve the publish order here, before the build: an install-edge cycle
|
||||
// makes the order unrepresentable, and that has to surface at the first gate
|
||||
// rather than when pack is already writing tarballs.
|
||||
const plan = family.publishOrder(members)
|
||||
if (plan.order.length !== members.length) {
|
||||
throw new Error(
|
||||
`release family ${family.id}: publish order covers ${String(plan.order.length)} of ${String(members.length)} members`,
|
||||
)
|
||||
}
|
||||
reportPublishOrder(family, plan)
|
||||
|
||||
const publishing = process.env.RELEASE_PUBLISH === 'true'
|
||||
if (publishing) {
|
||||
@@ -64,7 +100,11 @@ function main(): void {
|
||||
|
||||
const versions = [...new Set(members.map(member => member.version))]
|
||||
const summary = versions.length === 1 ? versions[0] : `${String(versions.length)} versions`
|
||||
console.log(`release verify: family ${family.id}, ${String(members.length)} member(s), ${summary}${publishing ? ', publish gates passed' : ''}`)
|
||||
console.log(
|
||||
`release verify: family ${family.id}, ${String(members.length)} member(s), ${summary},`
|
||||
+ ` publish order resolved, ${String(plan.droppedPeerEdges.length)} peer declaration(s) unordered`
|
||||
+ (publishing ? ', publish gates passed' : ''),
|
||||
)
|
||||
}
|
||||
|
||||
if (isEntry(import.meta.url)) main()
|
||||
|
||||
@@ -409,9 +409,9 @@ const VENDORED_LIBRARY = /^@deepseek-ai\\/(cosmokit|schemastery)(\\/|$)/
|
||||
// repository's vendored copies; cosmokit comes along as cordis's own dependency.
|
||||
id: 'packed-install-vendored-peer',
|
||||
file: 'packages/sandbox/sandbox-local/tests/packed-install.e2e.ts',
|
||||
find: ` 'packages/support/invariants',
|
||||
find: ` 'packages/runtime-diagnostics/invariants',
|
||||
]`,
|
||||
replace: ` 'packages/support/invariants',
|
||||
replace: ` 'packages/runtime-diagnostics/invariants',
|
||||
// The framework and the vendored packages the closure declares outright:
|
||||
// rescoped into @deepseek-ai, so the consumer installs this repository's
|
||||
// copies. Schemastery is a hard dependency of three members above, not a
|
||||
|
||||
@@ -83,6 +83,15 @@ describe('gate graph validation', () => {
|
||||
expect(ids).toContain('public-repository-links')
|
||||
})
|
||||
|
||||
it.each(['ci-primary', 'ci-static', 'check-all'] as const)(
|
||||
'keeps the DSH package license policy in %s',
|
||||
(mode) => {
|
||||
const ids = withPnpmEntrypoint(() => gatesForMode(mode).map(subject => subject.id))
|
||||
|
||||
expect(ids).toContain('dsh-package-licenses')
|
||||
},
|
||||
)
|
||||
|
||||
it('keeps native Windows coverage blocking while portability inventory remains observational', () => {
|
||||
const gates = withPnpmEntrypoint(() => gatesForMode('ci-windows-complete'))
|
||||
const byId = new Map(gates.map(subject => [subject.id, subject]))
|
||||
@@ -179,7 +188,7 @@ describe('Oxlint gate', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('TypeRT contract preparation', () => {
|
||||
describe('Typert contract preparation', () => {
|
||||
it('prepares primary source consumers once before they run', () => {
|
||||
const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-primary')))
|
||||
|
||||
@@ -246,8 +246,12 @@ function ciSharedStaticGates(): Gate[] {
|
||||
return [
|
||||
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),
|
||||
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
||||
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
|
||||
pnpmScript('optional-dependency-imports', 'verify-optional-dependency-imports', {
|
||||
label: 'optional dependency imports',
|
||||
}),
|
||||
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
|
||||
]
|
||||
}
|
||||
@@ -307,7 +311,7 @@ function nodeCompatSmokeGates(options: { cliSmoke?: boolean } = {}): Gate[] {
|
||||
pnpmExec('source-worker-smoke', [
|
||||
'vitest',
|
||||
'run',
|
||||
'packages/workflow/workflow-workerthread/tests/source-worker.compat.spec.ts',
|
||||
'packages/workflow/workflow-worker-thread/tests/source-worker.compat.spec.ts',
|
||||
], { label: 'source worker smoke' }),
|
||||
pnpmExec('jsonl-zstd-smoke', [
|
||||
'vitest',
|
||||
@@ -455,7 +459,7 @@ function ciWindowsObservationalGates(): Gate[] {
|
||||
}
|
||||
|
||||
function typertContractsGate(): Gate {
|
||||
return pnpmScript('typert-contracts', 'build:lib:host', { label: 'TypeRT contracts' })
|
||||
return pnpmScript('typert-contracts', 'build:lib:host', { label: 'Typert contracts' })
|
||||
}
|
||||
|
||||
function lintGate(options: { needs?: string[] } = {}): Gate {
|
||||
@@ -570,12 +574,16 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
|
||||
pnpmScript('knip', 'knip'),
|
||||
pnpmScript('publint', 'publint', artifactOptions),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('dsh-package-licenses', 'verify-dsh-package-licenses', { label: 'DSH package licenses' }),
|
||||
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
||||
builtPackageInvariantsGate(options.artifactNeeds),
|
||||
pnpmScript('node-next-types', 'verify-node-next-types', {
|
||||
label: 'node-next types',
|
||||
...artifactOptions,
|
||||
}),
|
||||
pnpmScript('optional-dependency-imports', 'verify-optional-dependency-imports', {
|
||||
label: 'optional dependency imports',
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -594,6 +602,7 @@ function docSyncLeafGates(options: {
|
||||
? []
|
||||
: [pnpmScript('doc-typecheck', options.docTypecheckScript ?? 'doc-typecheck', docTypecheckOptions)],
|
||||
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),
|
||||
pnpmScript('client-catalog', 'verify-client-catalog', { label: 'client catalog' }),
|
||||
pnpmScript('export-jsdoc', 'verify-export-jsdoc', { label: 'export jsdoc' }),
|
||||
pnpmScript('tool-catalog', 'verify-tool-catalog', { label: 'tool catalog' }),
|
||||
pnpmScript('config-catalog', 'verify-config-catalog', { label: 'config catalog' }),
|
||||
@@ -616,8 +625,8 @@ function docSyncLeafGates(options: {
|
||||
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }),
|
||||
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
|
||||
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }),
|
||||
pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts'], {
|
||||
label: 'documentation projection',
|
||||
pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts', 'scripts/verify-doc-site-fragments.spec.ts'], {
|
||||
label: 'documentation site checks',
|
||||
}),
|
||||
// Keep the VitePress build itself in one gate because projection rewrites website/.generated.
|
||||
pnpmScript('docs-site-build', options.docsBuildScript ?? 'docs:build', { label: 'documentation build' }),
|
||||
@@ -642,9 +651,9 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
|
||||
// Built execution consumers: the only automated proof that package-name
|
||||
// imports reach their lib/ entrypoints under plain Node. The e2e lane runs
|
||||
// unbuilt, so these files self-skip there.
|
||||
'packages/workflow/workflow-workerthread/tests/built-worker.e2e.ts',
|
||||
'packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts',
|
||||
'packages/lsp/lsp-local/tests/built-lib.e2e.ts',
|
||||
'packages/workflow/workflow-worker-thread/tests/built-worker.e2e.ts',
|
||||
'packages/code-runtime/code-runtime-worker-thread/tests/built-lib.e2e.ts',
|
||||
'packages/lsp/lsp-stdio/tests/built-lib.e2e.ts',
|
||||
], {
|
||||
label: 'built-bin smoke',
|
||||
needs,
|
||||
|
||||
429
scripts/slot-walk.ts
Normal file
429
scripts/slot-walk.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* AST helpers for the client slot surface: the `SlotMap` declaration merges
|
||||
* that type every slot, and the `slots.register` call sites that say who
|
||||
* already occupies one. Both readings are lexical (no type-checker program):
|
||||
* the client catalog generator consumes them, and the same scan doubles as its
|
||||
* own exhaustiveness backstop because it reads every source file rather than a
|
||||
* reachable-export closure.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { dirname, join, resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
|
||||
/** The module whose `SlotMap` / standard-kit interfaces every slot owner merges into. */
|
||||
const SLOTS_MODULE = '@deepseek-ai/dsh-client-ui-slots'
|
||||
|
||||
/** Cheap textual prefilter for a slot-contract merge, quote-style agnostic. */
|
||||
const MERGE_HEAD = /declare module ['"]@deepseek-ai\/dsh-client-ui-slots['"]/
|
||||
|
||||
/** Cheap textual prefilter for a registration call site. */
|
||||
const REGISTER_HEAD = /\.register\(/
|
||||
|
||||
/** One `SlotMap` member: the slot's contract as its owning package declares it. */
|
||||
export interface SlotDeclaration {
|
||||
/** SlotMap key, e.g. `settings.section`. */
|
||||
key: string
|
||||
/** Cardinality literal (`single` / `list` / `keyed` / `chain`), or '' when not a literal. */
|
||||
kind: string
|
||||
/** Data-scope literal (`root` / `session` / `session-maybe`), or '' when not a literal. */
|
||||
scope: string
|
||||
/** Type name of the owner-supplied props share, absent when the slot declares none. */
|
||||
ownerType?: string
|
||||
/** Source text of the `keyProps` member (keyed slots), absent otherwise. */
|
||||
keyProps?: string
|
||||
/** Source text of the `hookContext` member, absent otherwise. */
|
||||
hookContext?: string
|
||||
/** Type name of the slot-level inject face, absent when the slot declares none. */
|
||||
injectType?: string
|
||||
/** The member's JSDoc with container indentation removed, '' when undocumented. */
|
||||
jsDoc: string
|
||||
/** Workspace package that declares the contract. */
|
||||
package: string
|
||||
/** Source pointer `packages/…/file.ts:line`. */
|
||||
source: string
|
||||
}
|
||||
|
||||
/** One `slots.register({ name, … }, Component)` call site. */
|
||||
export interface SlotRegistration {
|
||||
/** Target SlotMap key the entry contributes into. */
|
||||
key: string
|
||||
/** Workspace package that registers the entry. */
|
||||
package: string
|
||||
/** Component argument as written (identifier, or a trimmed expression). */
|
||||
component: string
|
||||
/** `id` literal of a list entry, absent otherwise. */
|
||||
id?: string
|
||||
/** `key` literal of a keyed entry, absent otherwise. */
|
||||
entryKey?: string
|
||||
/** SlotMap keys this registration declares as children (they exist while it is mounted). */
|
||||
children: string[]
|
||||
/** Source pointer `packages/…/file.ts:line`. */
|
||||
source: string
|
||||
}
|
||||
|
||||
/** One exported type declaration, retained with its JSDoc for catalog projection. */
|
||||
export interface TypeDeclaration {
|
||||
/** Declared name. */
|
||||
name: string
|
||||
/** Full declaration text INCLUDING its JSDoc (member docs are the teaching text). */
|
||||
text: string
|
||||
/** Source pointer `packages/…/file.ts:line`. */
|
||||
source: string
|
||||
}
|
||||
|
||||
/** One scanned source file with the artifacts the catalog reads from it. */
|
||||
export interface ScannedFile {
|
||||
/** Repo-relative, `/`-normalized path. */
|
||||
rel: string
|
||||
/** Workspace package name that owns the file. */
|
||||
package: string
|
||||
/** Parsed source file. */
|
||||
sf: ts.SourceFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse every file matching `patterns`, keeping the ones that carry a slot
|
||||
* contract merge or a registration call. Files without either are skipped so
|
||||
* the scan stays cheap over the whole workspace.
|
||||
* @param scanRoot - repository root the patterns resolve against.
|
||||
* @param patterns - glob(s) selecting the TypeScript/TSX files to scan.
|
||||
* @returns one entry per interesting file, in path order.
|
||||
*/
|
||||
export function scanSlotFiles(scanRoot: string, patterns: readonly string[]): ScannedFile[] {
|
||||
const out: ScannedFile[] = []
|
||||
const names = new Map<string, string>()
|
||||
const rels = [...new Set(globSync(patterns as string[], { cwd: scanRoot })
|
||||
.map(path => path.split(sep).join('/')))].sort()
|
||||
for (const rel of rels) {
|
||||
const abs = resolve(scanRoot, rel)
|
||||
const text = readFileSync(abs, 'utf8')
|
||||
if (!MERGE_HEAD.test(text) && !REGISTER_HEAD.test(text)) continue
|
||||
out.push({
|
||||
rel,
|
||||
package: packageNameOf(scanRoot, rel, names),
|
||||
sf: ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true, scriptKindOf(rel)),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Index every exported type declaration of the scanned packages, keeping JSDoc.
|
||||
* The catalog resolves owner-props and inject-face shapes through this index
|
||||
* instead of a type-checker program: the declaration text with its member
|
||||
* documentation IS the teaching material a registrant needs.
|
||||
* @param scanRoot - repository root the patterns resolve against.
|
||||
* @param patterns - glob(s) selecting the TypeScript/TSX files to index.
|
||||
* @returns name → declaration, with names declared more than once dropped as ambiguous.
|
||||
*/
|
||||
export function indexExportedTypes(scanRoot: string, patterns: readonly string[]): Map<string, TypeDeclaration> {
|
||||
const index = new Map<string, TypeDeclaration>()
|
||||
const ambiguous = new Set<string>()
|
||||
const rels = [...new Set(globSync(patterns as string[], { cwd: scanRoot })
|
||||
.map(path => path.split(sep).join('/')))].sort()
|
||||
for (const rel of rels) {
|
||||
const abs = resolve(scanRoot, rel)
|
||||
const sf = ts.createSourceFile(abs, readFileSync(abs, 'utf8'), ts.ScriptTarget.Latest, true, scriptKindOf(rel))
|
||||
for (const statement of sf.statements) {
|
||||
if (!ts.isInterfaceDeclaration(statement) && !ts.isTypeAliasDeclaration(statement)) continue
|
||||
if (!statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
|
||||
const name = statement.name.text
|
||||
if (index.has(name)) {
|
||||
ambiguous.add(name)
|
||||
continue
|
||||
}
|
||||
index.set(name, {
|
||||
name,
|
||||
text: declarationText(statement, sf),
|
||||
source: `${rel}:${String(lineOf(sf, statement))}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const name of ambiguous) index.delete(name)
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Read every `SlotMap` member declared in one scanned file.
|
||||
* @param file - a file returned by {@link scanSlotFiles}.
|
||||
* @returns the declared slots, in source order.
|
||||
*/
|
||||
export function slotDeclarations(file: ScannedFile): SlotDeclaration[] {
|
||||
const out: SlotDeclaration[] = []
|
||||
for (const body of slotModuleBodies(file.sf)) {
|
||||
for (const statement of body.statements) {
|
||||
if (!ts.isInterfaceDeclaration(statement) || statement.name.text !== 'SlotMap') continue
|
||||
for (const member of statement.members) {
|
||||
if (!ts.isPropertySignature(member) || member.type === undefined) continue
|
||||
const key = ts.isStringLiteral(member.name) || ts.isIdentifier(member.name)
|
||||
? member.name.text
|
||||
: member.name.getText(file.sf)
|
||||
const entry = ts.isTypeLiteralNode(member.type) ? member.type : undefined
|
||||
const ownerType = memberTypeText(entry, 'owner', file.sf)
|
||||
const keyProps = memberTypeText(entry, 'keyProps', file.sf)
|
||||
const hookContext = memberTypeText(entry, 'hookContext', file.sf)
|
||||
const injectType = memberTypeText(entry, 'inject', file.sf)
|
||||
out.push({
|
||||
key,
|
||||
kind: literalMember(entry, 'kind'),
|
||||
scope: literalMember(entry, 'scope'),
|
||||
...ownerType === undefined ? {} : { ownerType },
|
||||
...keyProps === undefined ? {} : { keyProps },
|
||||
...hookContext === undefined ? {} : { hookContext },
|
||||
...injectType === undefined ? {} : { injectType },
|
||||
jsDoc: jsDocOf(member, file.sf),
|
||||
package: file.package,
|
||||
source: `${file.rel}:${String(lineOf(file.sf, member))}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Read every registration call site in one scanned file: which slot it
|
||||
* occupies, with which component and cell identity, and which child slots it
|
||||
* declares. A call whose `name` is not a string literal is skipped — the
|
||||
* shipped composition always names its target literally, and a computed name
|
||||
* carries no catalog fact.
|
||||
* @param file - a file returned by {@link scanSlotFiles}.
|
||||
* @returns the registrations, in source order.
|
||||
*/
|
||||
export function slotRegistrations(file: ScannedFile): SlotRegistration[] {
|
||||
const out: SlotRegistration[] = []
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isCallExpression(node)
|
||||
&& ts.isPropertyAccessExpression(node.expression)
|
||||
&& node.expression.name.text === 'register'
|
||||
&& isSlotsReceiver(node.expression.expression, file.sf)
|
||||
&& node.arguments.length >= 1) {
|
||||
const options = node.arguments[0]
|
||||
if (options !== undefined && ts.isObjectLiteralExpression(options)) {
|
||||
const key = stringProperty(options, 'name')
|
||||
if (key !== undefined) {
|
||||
const id = stringProperty(options, 'id')
|
||||
const entryKey = stringProperty(options, 'key')
|
||||
out.push({
|
||||
key,
|
||||
package: file.package,
|
||||
component: componentText(node.arguments[1], file.sf),
|
||||
...id === undefined ? {} : { id },
|
||||
...entryKey === undefined ? {} : { entryKey },
|
||||
children: childKeys(options),
|
||||
source: `${file.rel}:${String(lineOf(file.sf, node))}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(file.sf)
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one standard-kit interface's members from the scanned files: the props
|
||||
* a slot component receives for free from the framework at a given scope.
|
||||
* @param files - scanned files to search.
|
||||
* @param interfaceName - `GlobalStandardProps`, `SessionStandardProps`, or `SessionMaybeStandardProps`.
|
||||
* @returns `member: type` texts in declaration order, merged across declaring files.
|
||||
*/
|
||||
export function standardKitMembers(files: readonly ScannedFile[], interfaceName: string): string[] {
|
||||
const out: string[] = []
|
||||
for (const file of files) {
|
||||
for (const body of slotModuleBodies(file.sf)) {
|
||||
for (const statement of body.statements) {
|
||||
if (!ts.isInterfaceDeclaration(statement) || statement.name.text !== interfaceName) continue
|
||||
for (const member of statement.members) {
|
||||
if (!ts.isPropertySignature(member)) continue
|
||||
const type = member.type === undefined ? 'unknown' : member.type.getText(file.sf)
|
||||
out.push(`${member.name.getText(file.sf)}${member.questionToken === undefined ? '' : '?'}: ${collapse(type)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Names in the type index that seed texts mention, word-bounded — ONE level, not
|
||||
* a transitive closure. The catalog expands an owner-props contract exactly one
|
||||
* step: the owner interface carries the interaction protocol in its own member
|
||||
* documentation, while the shapes its fields reference belong to the subsystems
|
||||
* that own them and would otherwise drag the entire session model into a single
|
||||
* slot's report.
|
||||
* @param seeds - declaration or signature texts to search.
|
||||
* @param index - the type index from {@link indexExportedTypes}.
|
||||
* @returns the mentioned names, sorted.
|
||||
*/
|
||||
export function referencedTypeNames(
|
||||
seeds: readonly string[],
|
||||
index: ReadonlyMap<string, TypeDeclaration>,
|
||||
): string[] {
|
||||
const found: string[] = []
|
||||
for (const name of index.keys()) {
|
||||
const pattern = new RegExp(`\\b${name}\\b`)
|
||||
if (seeds.some(text => pattern.test(text))) found.push(name)
|
||||
}
|
||||
return found.sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve declarations by name, dropping names the index does not hold.
|
||||
* @param names - type names to resolve.
|
||||
* @param index - the type index from {@link indexExportedTypes}.
|
||||
* @returns the resolved declarations, sorted by name.
|
||||
*/
|
||||
export function declaredTypes(
|
||||
names: readonly string[],
|
||||
index: ReadonlyMap<string, TypeDeclaration>,
|
||||
): TypeDeclaration[] {
|
||||
return [...names]
|
||||
.flatMap(name => index.get(name) ?? [])
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
}
|
||||
|
||||
/** Every slot-contract module block in one file, in source order. */
|
||||
function slotModuleBodies(sf: ts.SourceFile): ts.ModuleBlock[] {
|
||||
const bodies: ts.ModuleBlock[] = []
|
||||
for (const statement of sf.statements) {
|
||||
if (!ts.isModuleDeclaration(statement) || !ts.isStringLiteral(statement.name)) continue
|
||||
if (statement.name.text !== SLOTS_MODULE) continue
|
||||
if (statement.body !== undefined && ts.isModuleBlock(statement.body)) bodies.push(statement.body)
|
||||
}
|
||||
return bodies
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a `X.register(...)` receiver is the slots service. Every other
|
||||
* registry in the repo (`ctx.tools`, `ctx.commands`, `ctx.settings`, …) also
|
||||
* takes an options object with a `name`, so the receiver is what separates a
|
||||
* slot occupancy fact from an unrelated registration.
|
||||
*/
|
||||
function isSlotsReceiver(receiver: ts.Expression, sf: ts.SourceFile): boolean {
|
||||
const text = receiver.getText(sf)
|
||||
return text === 'slots' || text.endsWith('.slots')
|
||||
}
|
||||
|
||||
/** The workspace package name owning a repo-relative file, memoized per package root. */
|
||||
function packageNameOf(scanRoot: string, rel: string, cache: Map<string, string>): string {
|
||||
let dir = dirname(resolve(scanRoot, rel))
|
||||
while (dir.length > scanRoot.length) {
|
||||
const cached = cache.get(dir)
|
||||
if (cached !== undefined) return cached
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { name?: unknown }
|
||||
if (typeof manifest.name === 'string') {
|
||||
cache.set(dir, manifest.name)
|
||||
return manifest.name
|
||||
}
|
||||
} catch {
|
||||
// No manifest at this level: keep walking up to the owning package root.
|
||||
}
|
||||
dir = dirname(dir)
|
||||
}
|
||||
return '(unknown package)'
|
||||
}
|
||||
|
||||
/** TSX must parse as TSX; a `.ts` file with JSX-looking generics must not. */
|
||||
function scriptKindOf(rel: string): ts.ScriptKind {
|
||||
return rel.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
||||
}
|
||||
|
||||
/** 1-based line of a node's first character. */
|
||||
function lineOf(sf: ts.SourceFile, node: ts.Node): number {
|
||||
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1
|
||||
}
|
||||
|
||||
/** Declaration text including leading JSDoc, with container indentation removed. */
|
||||
function declarationText(statement: ts.Node, sf: ts.SourceFile): string {
|
||||
return dedent(sf.text.slice(statement.getStart(sf, true), statement.getEnd()))
|
||||
}
|
||||
|
||||
/** One member's JSDoc comment text, '' when the member has none. */
|
||||
function jsDocOf(member: ts.Node, sf: ts.SourceFile): string {
|
||||
// getStart(includeJsDoc) brackets exactly the doc comment: with it the range
|
||||
// opens at `/**`, without it at the member itself.
|
||||
const withDoc = member.getStart(sf, true)
|
||||
const withoutDoc = member.getStart(sf, false)
|
||||
if (withDoc >= withoutDoc) return ''
|
||||
return dedent(sf.text.slice(withDoc, withoutDoc).trimEnd())
|
||||
}
|
||||
|
||||
/** Strip the shared leading indentation of a multi-line source slice. */
|
||||
function dedent(text: string): string {
|
||||
const lines = text.split('\n')
|
||||
const indents = lines.slice(1).filter(line => line.trim() !== '')
|
||||
.map(line => (/^\s*/.exec(line) as RegExpExecArray)[0].length)
|
||||
const shared = indents.length === 0 ? 0 : Math.min(...indents)
|
||||
return [lines[0] ?? '', ...lines.slice(1).map(line => line.slice(shared))].join('\n').trimEnd()
|
||||
}
|
||||
|
||||
/** Collapse a type text to one line so catalog rows stay one row. */
|
||||
function collapse(text: string): string {
|
||||
return text.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
/** A type-literal member's string-literal type text, '' when absent or computed. */
|
||||
function literalMember(entry: ts.TypeLiteralNode | undefined, name: string): string {
|
||||
const member = namedMember(entry, name)
|
||||
if (member?.type === undefined) return ''
|
||||
return ts.isLiteralTypeNode(member.type) && ts.isStringLiteral(member.type.literal)
|
||||
? member.type.literal.text
|
||||
: ''
|
||||
}
|
||||
|
||||
/** A type-literal member's type text on one line, absent when the member is. */
|
||||
function memberTypeText(
|
||||
entry: ts.TypeLiteralNode | undefined,
|
||||
name: string,
|
||||
sf: ts.SourceFile,
|
||||
): string | undefined {
|
||||
const member = namedMember(entry, name)
|
||||
return member?.type === undefined ? undefined : collapse(member.type.getText(sf))
|
||||
}
|
||||
|
||||
/** One named property signature of a type literal. */
|
||||
function namedMember(entry: ts.TypeLiteralNode | undefined, name: string): ts.PropertySignature | undefined {
|
||||
if (entry === undefined) return undefined
|
||||
for (const member of entry.members) {
|
||||
if (ts.isPropertySignature(member) && memberName(member.name) === name) return member
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** A property name's text, quotes removed. */
|
||||
function memberName(name: ts.PropertyName): string {
|
||||
return ts.isStringLiteral(name) || ts.isIdentifier(name) ? name.text : name.getText()
|
||||
}
|
||||
|
||||
/** One string-literal property of an options object literal. */
|
||||
function stringProperty(options: ts.ObjectLiteralExpression, name: string): string | undefined {
|
||||
for (const property of options.properties) {
|
||||
if (!ts.isPropertyAssignment(property)) continue
|
||||
if (memberName(property.name) !== name) continue
|
||||
if (ts.isStringLiteral(property.initializer)) return property.initializer.text
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** The SlotMap keys a registration's `children` table declares. */
|
||||
function childKeys(options: ts.ObjectLiteralExpression): string[] {
|
||||
for (const property of options.properties) {
|
||||
if (!ts.isPropertyAssignment(property)) continue
|
||||
if (memberName(property.name) !== 'children') continue
|
||||
if (!ts.isObjectLiteralExpression(property.initializer)) return []
|
||||
return property.initializer.properties
|
||||
.flatMap(child => (child.name === undefined ? [] : [memberName(child.name)]))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/** The component argument as written; a non-identifier expression is collapsed. */
|
||||
function componentText(argument: ts.Expression | undefined, sf: ts.SourceFile): string {
|
||||
if (argument === undefined) return '(none)'
|
||||
const text = collapse(argument.getText(sf))
|
||||
return text.length > 60 ? `${text.slice(0, 57)}…` : text
|
||||
}
|
||||
@@ -42,7 +42,7 @@ SNAPSHOT_SESSION_ID = "advanced-executable"
|
||||
SNAPSHOT_DIRECT_CHILD_PROMPT = "Reply with exactly DIRECT_CHILD_OK and nothing else."
|
||||
SNAPSHOT_WORKFLOW_CHILD_PROMPT = "Reply with exactly WORKFLOW_CHILD_OK and nothing else."
|
||||
SNAPSHOT_FINAL_TEXT = "ADVANCED_EXECUTABLE_OK"
|
||||
SNAPSHOT_MOUNT_CODE = """\
|
||||
SNAPSHOT_PLUGIN_CODE = """\
|
||||
return (ctx) => {
|
||||
harness.registerTool(ctx, harness.defineTool({
|
||||
name: 'snapshot_double',
|
||||
@@ -70,8 +70,8 @@ SNAPSHOT_DIRECTORY = (
|
||||
)
|
||||
SNAPSHOT_FILENAMES = ("result.json", "session.jsonl", "session.1.jsonl", "session.2.jsonl")
|
||||
CUSTOM_CORDIS = """\
|
||||
- id: jsonrpc
|
||||
name: '@deepseek-ai/dsh-jsonrpc'
|
||||
- id: sdk-jsonrpc-server
|
||||
name: '@deepseek-ai/dsh-sdk-jsonrpc-server'
|
||||
- id: agent-core
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
@@ -87,11 +87,11 @@ CUSTOM_CORDIS = """\
|
||||
root: !!js process.env.DSH_SESSION_ROOT
|
||||
compression: 'none'
|
||||
- id: code-runtime
|
||||
name: '@deepseek-ai/dsh-code-runtime-worker'
|
||||
name: '@deepseek-ai/dsh-code-runtime-worker-thread'
|
||||
- id: subagents
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
- id: subagent-spawn
|
||||
name: '@deepseek-ai/dsh-subagent-spawn'
|
||||
- id: subagent-spawn-in-process
|
||||
name: '@deepseek-ai/dsh-subagent-spawn-in-process'
|
||||
config:
|
||||
providerName: spawn
|
||||
- id: subagent-tool
|
||||
@@ -99,11 +99,13 @@ CUSTOM_CORDIS = """\
|
||||
config:
|
||||
provider: spawn
|
||||
- id: workflow-engine
|
||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||
name: '@deepseek-ai/dsh-workflow-worker-thread'
|
||||
config:
|
||||
provider: spawn
|
||||
- id: workflow-tool
|
||||
name: '@deepseek-ai/dsh-tool-workflow'
|
||||
- id: cordis-host-runner
|
||||
name: '@deepseek-ai/dsh-cordis-host-runner'
|
||||
- id: cordis-tool
|
||||
name: '@deepseek-ai/dsh-tool-cordis'
|
||||
"""
|
||||
@@ -200,11 +202,16 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]:
|
||||
if prompt == SNAPSHOT_WORKFLOW_CHILD_PROMPT:
|
||||
return text_chunks("WORKFLOW_CHILD_OK")
|
||||
if prompt == SNAPSHOT_PROMPT:
|
||||
assert_advertised_tool(body, "cordis_mount")
|
||||
assert_advertised_tool(body, "cordis_define")
|
||||
return tool_call_chunks(
|
||||
"advanced-mount",
|
||||
"cordis_mount",
|
||||
{"code": SNAPSHOT_MOUNT_CODE},
|
||||
"advanced-define",
|
||||
"cordis_define",
|
||||
{
|
||||
"plugin": {"kind": "new", "idPrefix": "snap"},
|
||||
"name": "Snapshot Double",
|
||||
"purpose": "Expose a deterministic doubling tool for executable snapshot verification.",
|
||||
"code": {"host": SNAPSHOT_PLUGIN_CODE},
|
||||
},
|
||||
)
|
||||
if prompt == CODE_PROMPT:
|
||||
assert_advertised_tool(body, "run_code")
|
||||
@@ -289,9 +296,20 @@ def advanced_tool_followup(
|
||||
"""Advance the executable snapshot's deterministic parent tool chain."""
|
||||
if not call_id.startswith("advanced-"):
|
||||
return None
|
||||
if call_id == "advanced-mount" and tool_name == "cordis_mount":
|
||||
if "Temporary Plugin dyn-1 is running" not in tool_text:
|
||||
raise AssertionError(f"cordis_mount returned no temporary Plugin id: {tool_text}")
|
||||
if call_id == "advanced-define" and tool_name == "cordis_define":
|
||||
if "Defined snap-1/pkg-1 (Snapshot Double)" not in tool_text:
|
||||
raise AssertionError(f"cordis_define returned no dynamic Package ids: {tool_text}")
|
||||
if "snapshot_double" in advertised_tool_names(body):
|
||||
raise AssertionError("snapshot_double was advertised before cordis_run")
|
||||
assert_advertised_tool(body, "cordis_run")
|
||||
return tool_call_chunks(
|
||||
"advanced-run",
|
||||
"cordis_run",
|
||||
{"pluginId": "snap-1", "packageId": "pkg-1", "mode": "run"},
|
||||
)
|
||||
if call_id == "advanced-run" and tool_name == "cordis_run":
|
||||
if "snap-1/pkg-1 is running (run-1)" not in tool_text:
|
||||
raise AssertionError(f"cordis_run returned no running Package ids: {tool_text}")
|
||||
assert_advertised_tool(body, "run_code")
|
||||
assert_advertised_tool(body, "snapshot_double")
|
||||
return tool_call_chunks(
|
||||
@@ -332,17 +350,17 @@ def advanced_tool_followup(
|
||||
if call_id == "advanced-workflow" and tool_name == "workflow":
|
||||
if "WORKFLOW_CHILD_OK" not in tool_text:
|
||||
raise AssertionError(f"workflow returned no expected child value: {tool_text}")
|
||||
assert_advertised_tool(body, "cordis_unmount")
|
||||
assert_advertised_tool(body, "cordis_undefine")
|
||||
return tool_call_chunks(
|
||||
"advanced-unmount",
|
||||
"cordis_unmount",
|
||||
{"id": "dyn-1"},
|
||||
"advanced-undefine",
|
||||
"cordis_undefine",
|
||||
{"pluginId": "snap-1"},
|
||||
)
|
||||
if call_id == "advanced-unmount" and tool_name == "cordis_unmount":
|
||||
if "Temporary Plugin dyn-1 was unmounted and removed." not in tool_text:
|
||||
raise AssertionError(f"cordis_unmount returned no unmount result: {tool_text}")
|
||||
if call_id == "advanced-undefine" and tool_name == "cordis_undefine":
|
||||
if "Removed dynamic Plugin snap-1 and all of its Packages." not in tool_text:
|
||||
raise AssertionError(f"cordis_undefine returned no removal result: {tool_text}")
|
||||
if "snapshot_double" in advertised_tool_names(body):
|
||||
raise AssertionError("snapshot_double remained advertised after cordis_unmount")
|
||||
raise AssertionError("snapshot_double remained advertised after cordis_undefine")
|
||||
return text_chunks(SNAPSHOT_FINAL_TEXT)
|
||||
raise AssertionError(f"unexpected advanced tool follow-up: {call_id} {tool_name}: {tool_text}")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":6,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":7,"time":0,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}}
|
||||
{"type":"request/header","seq":8,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}}
|
||||
{"type":"request/header","seq":8,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
|
||||
{"type":"request/context","seq":9,"time":0,"data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":6,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":7,"time":0,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}}
|
||||
{"type":"request/header","seq":8,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}}
|
||||
{"type":"request/header","seq":8,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}}
|
||||
{"type":"request/context","seq":9,"time":0,"data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}}
|
||||
|
||||
@@ -5,71 +5,81 @@
|
||||
{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}
|
||||
{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":5,"time":0,"data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[4],"source":{"kind":"fallback"}}}
|
||||
{"type":"request/header","seq":6,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","subagent","task_kill","task_list","task_output","workflow"]},"reason":"initial"}}
|
||||
{"type":"request/header","seq":6,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}}
|
||||
{"type":"request/context","seq":7,"time":0,"data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}}
|
||||
{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-mount","name":"cordis_mount","argumentsDelta":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}}
|
||||
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-define","name":"cordis_define","argumentsDelta":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}}
|
||||
{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}}}
|
||||
{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":14,"time":0,"data":{"turn":1,"step":1,"callId":"advanced-mount","name":"cordis_mount","arguments":"{\"code\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}"}}
|
||||
{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-mount"},"content":[{"type":"tool-result","toolCallId":"advanced-mount","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"<anonymous>\"; available until unmounted or DSH restarts)."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":14,"time":0,"data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}
|
||||
{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}
|
||||
{"type":"request/header","seq":18,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","snapshot_double","subagent","task_kill","task_list","task_output","workflow"]},"reason":"change"}}
|
||||
{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}
|
||||
{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":25,"time":0,"data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}
|
||||
{"type":"tool/code-dispatch-start","seq":26,"time":0,"data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}}
|
||||
{"type":"tool/code-dispatch","seq":27,"time":0,"data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}}
|
||||
{"type":"tool/result","seq":28,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":29,"time":0,"data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","seq":30,"time":0,"data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}
|
||||
{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":34,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":36,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[31,32,33,34,35],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":37,"time":0,"data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
|
||||
{"type":"tool/result","seq":38,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[37],"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-run","name":"cordis_run","argumentsDelta":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}}
|
||||
{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":23,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":24,"time":0,"data":{"turn":1,"step":2,"callId":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}
|
||||
{"type":"tool/result","seq":25,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[24],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":26,"time":0,"data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","seq":27,"time":0,"data":{"turn":1,"step":3}}
|
||||
{"type":"request/header","seq":28,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"change"}}
|
||||
{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}
|
||||
{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":34,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":35,"time":0,"data":{"turn":1,"step":3,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}
|
||||
{"type":"tool/code-dispatch-start","seq":36,"time":0,"data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}}
|
||||
{"type":"tool/code-dispatch","seq":37,"time":0,"data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}}
|
||||
{"type":"tool/result","seq":38,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":39,"time":0,"data":{"turn":1,"step":3}}
|
||||
{"type":"step/start","seq":40,"time":0,"data":{"turn":1,"step":4}}
|
||||
{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}
|
||||
{"type":"assistant/chunk","seq":43,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}}
|
||||
{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}
|
||||
{"type":"assistant/chunk","seq":43,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":44,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":45,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":46,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[41,42,43,44,45],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":47,"time":0,"data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}
|
||||
{"type":"tool-workflow/run-start","seq":48,"time":0,"data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}}
|
||||
{"type":"tool-workflow/agent-start","seq":49,"time":0,"data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}}
|
||||
{"type":"tool-workflow/agent-end","seq":50,"time":0,"data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}}
|
||||
{"type":"tool-workflow/run-end","seq":51,"time":0,"data":{"runId":"{{workflow-run}}","stopReason":"completed"}}
|
||||
{"type":"tool/result","seq":52,"time":0,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[47],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":53,"time":0,"data":{"turn":1,"step":4}}
|
||||
{"type":"step/start","seq":54,"time":0,"data":{"turn":1,"step":5}}
|
||||
{"type":"assistant/chunk","seq":55,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":56,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-unmount","name":"cordis_unmount","argumentsDelta":"{\"id\": \"dyn-1\"}"}}}
|
||||
{"type":"assistant/chunk","seq":57,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":58,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":59,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":60,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[55,56,57,58,59],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":61,"time":0,"data":{"turn":1,"step":5,"callId":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\": \"dyn-1\"}"}}
|
||||
{"type":"tool/result","seq":62,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-unmount"},"content":[{"type":"tool-result","toolCallId":"advanced-unmount","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[61],"surfaceOp":"append"}
|
||||
{"type":"assistant/message","seq":46,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[41,42,43,44,45],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":47,"time":0,"data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}
|
||||
{"type":"tool/result","seq":48,"time":0,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[47],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":49,"time":0,"data":{"turn":1,"step":4}}
|
||||
{"type":"step/start","seq":50,"time":0,"data":{"turn":1,"step":5}}
|
||||
{"type":"assistant/chunk","seq":51,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":52,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}
|
||||
{"type":"assistant/chunk","seq":53,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}}
|
||||
{"type":"assistant/chunk","seq":54,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":55,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":56,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[51,52,53,54,55],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":57,"time":0,"data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}
|
||||
{"type":"tool-workflow/run-start","seq":58,"time":0,"data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}}
|
||||
{"type":"tool-workflow/agent-start","seq":59,"time":0,"data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}}
|
||||
{"type":"tool-workflow/agent-end","seq":60,"time":0,"data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}}
|
||||
{"type":"tool-workflow/run-end","seq":61,"time":0,"data":{"runId":"{{workflow-run}}","stopReason":"completed"}}
|
||||
{"type":"tool/result","seq":62,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[57],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":63,"time":0,"data":{"turn":1,"step":5}}
|
||||
{"type":"step/start","seq":64,"time":0,"data":{"turn":1,"step":6}}
|
||||
{"type":"request/header","seq":65,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_inspect","cordis_mount","cordis_unmount","run_code","subagent","task_kill","task_list","task_output","workflow"]},"reason":"change"}}
|
||||
{"type":"assistant/chunk","seq":66,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":67,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}}
|
||||
{"type":"assistant/chunk","seq":68,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}}
|
||||
{"type":"assistant/chunk","seq":69,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":70,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":71,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[66,67,68,69,70],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":72,"time":0,"data":{"turn":1,"step":6}}
|
||||
{"type":"turn/end","seq":73,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"assistant/chunk","seq":65,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":66,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-undefine","name":"cordis_undefine","argumentsDelta":"{\"pluginId\": \"snap-1\"}"}}}
|
||||
{"type":"assistant/chunk","seq":67,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":68,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":69,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":70,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[65,66,67,68,69],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":71,"time":0,"data":{"turn":1,"step":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}
|
||||
{"type":"tool/result","seq":72,"time":0,"data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[71],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":73,"time":0,"data":{"turn":1,"step":6}}
|
||||
{"type":"step/start","seq":74,"time":0,"data":{"turn":1,"step":7}}
|
||||
{"type":"request/header","seq":75,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change"}}
|
||||
{"type":"assistant/chunk","seq":76,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":77,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}}
|
||||
{"type":"assistant/chunk","seq":78,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}}
|
||||
{"type":"assistant/chunk","seq":79,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
|
||||
{"type":"assistant/chunk","seq":80,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":81,"time":0,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[76,77,78,79,80],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":82,"time":0,"data":{"turn":1,"step":7}}
|
||||
{"type":"turn/end","seq":83,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
|
||||
File diff suppressed because one or more lines are too long
49
scripts/test-fixture-cleanup.ts
Normal file
49
scripts/test-fixture-cleanup.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Junction-safe fixture cleanup for Windows. Test fixtures junction the REAL
|
||||
* `scripts/`, `node_modules`, and tsx package directories so installer probes
|
||||
* resolve through them; Windows recursive deletion — both Node's `rmSync` and
|
||||
* Git's `worktree remove` — follows MOUNT_POINT junctions into their targets
|
||||
* and would delete the repository's own directories. POSIX `unlink`/`rm`
|
||||
* already remove symlinks without following them, so the walk is a no-op
|
||||
* there.
|
||||
*/
|
||||
|
||||
import { lstatSync, readdirSync, rmSync, unlinkSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
/**
|
||||
* Recursively unlink every symbolic link (junction) under `path`.
|
||||
* @param path - the fixture tree whose reparse points are unlinked.
|
||||
*/
|
||||
export function unlinkFixtureLinks(path: string): void {
|
||||
const visit = (entry: string): void => {
|
||||
let stat: ReturnType<typeof lstatSync>
|
||||
try {
|
||||
stat = lstatSync(entry)
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
||||
throw error
|
||||
}
|
||||
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
||||
if (stat.isSymbolicLink()) unlinkSync(entry)
|
||||
return
|
||||
}
|
||||
for (const child of readdirSync(entry)) visit(join(entry, child))
|
||||
}
|
||||
visit(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one fixture tree after its junctions are unlinked (see
|
||||
* {@link unlinkFixtureLinks}). Retries the removal: Windows releases child
|
||||
* process and antivirus file handles asynchronously, and an unretried
|
||||
* `rmSync` fails immediately with EPERM under load. A 10-second retry window
|
||||
* (50 attempts × 200 ms) covers the failover pool's slow handle release;
|
||||
* release is one-shot (a terminated child's handles drain, not reacquired),
|
||||
* so a bounded window suffices and never pins afterEach cleanup.
|
||||
* @param path - the fixture tree to remove.
|
||||
*/
|
||||
export function removeFixtureSafely(path: string): void {
|
||||
unlinkFixtureLinks(path)
|
||||
rmSync(path, { recursive: true, force: true, maxRetries: 50, retryDelay: 200 })
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context, FiberState, Service, ValidationError } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
import { packageInvariantOwners } from './package-invariants.ts'
|
||||
import {
|
||||
@@ -154,7 +154,7 @@ describe('global test invariant host', () => {
|
||||
it('recognizes focused invariant suites without a package inventory', () => {
|
||||
expect(usesManualInvariantTree('/repo/packages/core/session/tests/invariant.spec.ts')).toBe(true)
|
||||
expect(usesManualInvariantTree('/repo/packages/core/session/tests/request-invariant-hmr.spec.ts')).toBe(true)
|
||||
expect(usesManualInvariantTree('C:\\repo\\packages\\support\\invariants\\tests\\service.spec.ts')).toBe(true)
|
||||
expect(usesManualInvariantTree('C:\\repo\\packages\\runtime-diagnostics\\invariants\\tests\\service.spec.ts')).toBe(true)
|
||||
expect(usesManualInvariantTree('/repo/packages/examples/agent-spine-demo/tests/agent-core.spec.ts')).toBe(true)
|
||||
expect(usesManualInvariantTree('/repo/packages/core/session/tests/session.spec.ts')).toBe(false)
|
||||
})
|
||||
@@ -297,9 +297,9 @@ describe('global test invariant host', () => {
|
||||
expect(order.at(-1)).toBe('nested')
|
||||
|
||||
if (delayedCompanion === undefined) throw new Error('delayed companion did not load')
|
||||
await ctx.plugin(InvariantService, { enabled: true })
|
||||
await ctx.plugin(InvariantRegistry, { enabled: true })
|
||||
await ctx.plugin(delayedCompanion)
|
||||
expect(ctx.registry.get(InvariantService)?.fibers).toHaveLength(1)
|
||||
expect(ctx.registry.get(InvariantRegistry)?.fibers).toHaveLength(1)
|
||||
expect(ctx.registry.get(delayedCompanion)?.fibers).toHaveLength(1)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
SaveImageAttachment,
|
||||
StoredImageAttachment,
|
||||
} from '@deepseek-ai/dsh-attachment'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
@@ -47,7 +47,7 @@ export const testInvariantCompanions: Readonly<Record<string, () => Promise<Test
|
||||
|
||||
/** Manual-topology suites whose names cannot follow the focused invariant convention. */
|
||||
const MANUAL_INVARIANT_TEST_EXCEPTIONS = [
|
||||
'/packages/support/invariants/tests/service.spec.ts',
|
||||
'/packages/runtime-diagnostics/invariants/tests/service.spec.ts',
|
||||
'/packages/examples/agent-spine-demo/tests/agent-core.spec.ts',
|
||||
] as const
|
||||
|
||||
@@ -174,7 +174,7 @@ function startInvariantHost(root: Context): InvariantHost {
|
||||
// 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 serviceFiber = mount(InvariantRegistry, { enabled: true })
|
||||
const testPath = expect.getState().testPath ?? ''
|
||||
const companionPaths = testInvariantCompanionPaths(testPath)
|
||||
const ready = requireActive(serviceFiber, 'invariant service').then(async () => {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
/** Integration coverage for automatic and explicit pairing-record conflict resolution. */
|
||||
|
||||
import { execFileSync, spawnSync } from 'node:child_process'
|
||||
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import {
|
||||
chmodSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -15,6 +22,7 @@ import {
|
||||
renderTranslationPairingRecord,
|
||||
translationPairPaths,
|
||||
} from './translation-pairing-record.ts'
|
||||
import { removeFixtureSafely } from './test-fixture-cleanup.ts'
|
||||
|
||||
const driver = fileURLToPath(new URL('./merge-translation-pairing.ts', import.meta.url))
|
||||
const driverLauncher = fileURLToPath(new URL('./merge-translation-pairing-driver.sh', import.meta.url))
|
||||
@@ -28,7 +36,7 @@ interface Fixture {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const fixture of fixtures.splice(0)) rmSync(fixture, { recursive: true, force: true })
|
||||
for (const fixture of fixtures.splice(0)) removeFixtureSafely(fixture)
|
||||
})
|
||||
|
||||
function git(fixture: Fixture, args: string[]): string {
|
||||
|
||||
@@ -11,6 +11,12 @@ interface ProjectGraph {
|
||||
options: ts.CompilerOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* A compiler face: the two aggregates a repository-wide program may seed from.
|
||||
* The root solution is never one of them.
|
||||
*/
|
||||
export type CompilerFace = 'host' | 'client'
|
||||
|
||||
/** TypeScript config host shared by repository scripts. */
|
||||
export const repositoryConfigHost: ts.ParseConfigFileHost = {
|
||||
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
|
||||
@@ -24,12 +30,12 @@ export const repositoryConfigHost: ts.ParseConfigFileHost = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the host aggregate tsconfig and flatten all referenced projects into one
|
||||
* Parse one face aggregate tsconfig and flatten all referenced projects into one
|
||||
* semantic graph. Never seed the root solution: flattening host+client into one
|
||||
* program collides the cordis Context merges.
|
||||
*/
|
||||
function loadProjectGraph(projectRoot: string): ProjectGraph {
|
||||
const rootConfigPath = resolve(projectRoot, 'tsconfig.host.json')
|
||||
function loadProjectGraph(projectRoot: string, face: CompilerFace): ProjectGraph {
|
||||
const rootConfigPath = resolve(projectRoot, `tsconfig.${face}.json`)
|
||||
const rootConfig = parseConfig(rootConfigPath)
|
||||
const rootNames = new Set<string>()
|
||||
const visited = new Set<string>()
|
||||
@@ -81,8 +87,12 @@ export class TypeScriptProject {
|
||||
/** The checker shared by every semantic query in this project. */
|
||||
readonly checker: ts.TypeChecker
|
||||
|
||||
constructor(private readonly projectRoot: string) {
|
||||
const graph = loadProjectGraph(projectRoot)
|
||||
/**
|
||||
* @param projectRoot - repository root the program is seeded and reported from.
|
||||
* @param face - which compiler face aggregate to flatten.
|
||||
*/
|
||||
constructor(readonly projectRoot: string, face: CompilerFace = 'host') {
|
||||
const graph = loadProjectGraph(projectRoot, face)
|
||||
this.program = ts.createProgram(graph.rootNames, semanticCompilerOptions(graph.options))
|
||||
this.checker = this.program.getTypeChecker()
|
||||
}
|
||||
|
||||
@@ -224,82 +224,82 @@
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "AfterScheduleRecord",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "AtScheduleRecord",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "EveryScheduleRecord",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "LocalAtInput",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "AtInput",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "OneShotScheduleRecord",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "ScheduleRecord",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "ScheduleCreateChange",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "ScheduleDeleteChange",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "OneShotScheduleDispatchChange",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "EveryScheduleDispatchChange",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "ScheduleDispatchChange",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "ScheduleChange",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "ScheduleState",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "ScheduleDeliveryMode",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/schedule.md",
|
||||
"symbol": "ScheduleView",
|
||||
"source": "packages/schedule/tool-schedule/src/types.ts"
|
||||
"source": "packages/schedule/schedule/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/commands.md",
|
||||
@@ -806,44 +806,44 @@
|
||||
"source": "packages/core/tools/src/json-schema.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/user-interaction.md",
|
||||
"doc": "docs/subsystems/user-questions.md",
|
||||
"symbol": "AskUserQuestionOption",
|
||||
"source": "packages/interaction/user-interaction/src/types.ts"
|
||||
"source": "packages/interaction/user-questions/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/user-interaction.md",
|
||||
"doc": "docs/subsystems/user-questions.md",
|
||||
"symbol": "AskUserQuestionIntent",
|
||||
"source": "packages/interaction/user-interaction/src/types.ts"
|
||||
"source": "packages/interaction/user-questions/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/user-interaction.md",
|
||||
"doc": "docs/subsystems/user-questions.md",
|
||||
"symbol": "AskUserQuestionItem",
|
||||
"source": "packages/interaction/user-interaction/src/types.ts"
|
||||
"source": "packages/interaction/user-questions/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/user-interaction.md",
|
||||
"doc": "docs/subsystems/user-questions.md",
|
||||
"symbol": "AskUserQuestionRequest",
|
||||
"source": "packages/interaction/user-interaction/src/index.ts"
|
||||
"source": "packages/interaction/user-questions/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/user-interaction.md",
|
||||
"doc": "docs/subsystems/user-questions.md",
|
||||
"symbol": "AskUserQuestionAnswerItem",
|
||||
"source": "packages/interaction/user-interaction/src/types.ts"
|
||||
"source": "packages/interaction/user-questions/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/user-interaction.md",
|
||||
"doc": "docs/subsystems/user-questions.md",
|
||||
"symbol": "AskUserQuestionAnswer",
|
||||
"source": "packages/interaction/user-interaction/src/types.ts"
|
||||
"source": "packages/interaction/user-questions/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/user-interaction.md",
|
||||
"symbol": "UserInteractionProvider",
|
||||
"source": "packages/interaction/user-interaction/src/index.ts"
|
||||
"doc": "docs/subsystems/user-questions.md",
|
||||
"symbol": "UserQuestionProvider",
|
||||
"source": "packages/interaction/user-questions/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/user-interaction.md",
|
||||
"symbol": "UserInteractionError",
|
||||
"source": "packages/interaction/user-interaction/src/index.ts"
|
||||
"doc": "docs/subsystems/user-questions.md",
|
||||
"symbol": "UserQuestionError",
|
||||
"source": "packages/interaction/user-questions/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/approval.md",
|
||||
@@ -891,94 +891,94 @@
|
||||
"source": "packages/attachment/attachment/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/bash.md",
|
||||
"symbol": "BashExecRequest",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
"doc": "docs/subsystems/shell.md",
|
||||
"symbol": "ShellExecRequest",
|
||||
"source": "packages/shell/shell/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/bash.md",
|
||||
"symbol": "BashExecSpec",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
"doc": "docs/subsystems/shell.md",
|
||||
"symbol": "ShellExecSpec",
|
||||
"source": "packages/shell/shell/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/bash.md",
|
||||
"symbol": "BashRunResult",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
"doc": "docs/subsystems/shell.md",
|
||||
"symbol": "ShellRunResult",
|
||||
"source": "packages/shell/shell/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/bash.md",
|
||||
"symbol": "BashSandboxInfo",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
"doc": "docs/subsystems/shell.md",
|
||||
"symbol": "ShellSandboxInfo",
|
||||
"source": "packages/shell/shell/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/bash.md",
|
||||
"symbol": "BashProcess",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
"doc": "docs/subsystems/shell.md",
|
||||
"symbol": "ShellProcess",
|
||||
"source": "packages/shell/shell/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/bash.md",
|
||||
"symbol": "BashProcessRead",
|
||||
"source": "packages/bash/bash/src/types.ts"
|
||||
"doc": "docs/subsystems/shell.md",
|
||||
"symbol": "ShellProcessRead",
|
||||
"source": "packages/shell/shell/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tasks.md",
|
||||
"symbol": "TaskKindMap",
|
||||
"source": "packages/tasks/tasks/src/types.ts"
|
||||
"doc": "docs/subsystems/jobs.md",
|
||||
"symbol": "JobKindMap",
|
||||
"source": "packages/jobs/jobs/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tasks.md",
|
||||
"symbol": "TaskStart",
|
||||
"source": "packages/tasks/tasks/src/types.ts"
|
||||
"doc": "docs/subsystems/jobs.md",
|
||||
"symbol": "JobStart",
|
||||
"source": "packages/jobs/jobs/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tasks.md",
|
||||
"symbol": "TaskHooks",
|
||||
"source": "packages/tasks/tasks/src/types.ts"
|
||||
"doc": "docs/subsystems/jobs.md",
|
||||
"symbol": "JobHooks",
|
||||
"source": "packages/jobs/jobs/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tasks.md",
|
||||
"symbol": "TaskOutcome",
|
||||
"source": "packages/tasks/tasks/src/types.ts"
|
||||
"doc": "docs/subsystems/jobs.md",
|
||||
"symbol": "JobOutcome",
|
||||
"source": "packages/jobs/jobs/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tasks.md",
|
||||
"symbol": "TaskSnapshot",
|
||||
"source": "packages/tasks/tasks/src/types.ts"
|
||||
"doc": "docs/subsystems/jobs.md",
|
||||
"symbol": "JobSnapshot",
|
||||
"source": "packages/jobs/jobs/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/tasks.md",
|
||||
"symbol": "TaskRead",
|
||||
"source": "packages/tasks/tasks/src/types.ts"
|
||||
"doc": "docs/subsystems/jobs.md",
|
||||
"symbol": "JobRead",
|
||||
"source": "packages/jobs/jobs/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/pty.md",
|
||||
"symbol": "PtyWaitReason",
|
||||
"source": "packages/pty/pty/src/types.ts"
|
||||
"doc": "docs/subsystems/terminal.md",
|
||||
"symbol": "TerminalWaitReason",
|
||||
"source": "packages/terminal/terminal/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/pty.md",
|
||||
"symbol": "PtySessionStatus",
|
||||
"source": "packages/pty/pty/src/types.ts"
|
||||
"doc": "docs/subsystems/terminal.md",
|
||||
"symbol": "TerminalSessionStatus",
|
||||
"source": "packages/terminal/terminal/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/pty.md",
|
||||
"symbol": "PtyBackend",
|
||||
"source": "packages/pty/pty/src/types.ts"
|
||||
"doc": "docs/subsystems/terminal.md",
|
||||
"symbol": "TerminalBackend",
|
||||
"source": "packages/terminal/terminal/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/pty.md",
|
||||
"symbol": "PtyBackendSession",
|
||||
"source": "packages/pty/pty/src/types.ts"
|
||||
"doc": "docs/subsystems/terminal.md",
|
||||
"symbol": "TerminalBackendSession",
|
||||
"source": "packages/terminal/terminal/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/pty.md",
|
||||
"symbol": "PtySendOperation",
|
||||
"source": "packages/pty/pty/src/types.ts"
|
||||
"doc": "docs/subsystems/terminal.md",
|
||||
"symbol": "TerminalSendOperation",
|
||||
"source": "packages/terminal/terminal/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/pty.md",
|
||||
"symbol": "PtySendResult",
|
||||
"source": "packages/pty/pty/src/types.ts"
|
||||
"doc": "docs/subsystems/terminal.md",
|
||||
"symbol": "TerminalSendResult",
|
||||
"source": "packages/terminal/terminal/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/sandbox.md",
|
||||
@@ -1117,8 +1117,8 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/filesystem.md",
|
||||
"symbol": "FsPolicyExec",
|
||||
"source": "packages/fs/fs-policy/src/types.ts"
|
||||
"symbol": "FsObservationActor",
|
||||
"source": "packages/fs/fs-observation-policy/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/filesystem.md",
|
||||
@@ -1198,27 +1198,27 @@
|
||||
{
|
||||
"doc": "docs/subsystems/compaction.md",
|
||||
"symbol": "CompactionResult",
|
||||
"source": "packages/compact/compact/src/types.ts"
|
||||
"source": "packages/compaction/compaction/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/compaction.md",
|
||||
"symbol": "CompactionTrigger",
|
||||
"source": "packages/compact/compact/src/index.ts"
|
||||
"source": "packages/compaction/compaction/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/compaction.md",
|
||||
"symbol": "ManualCompactionErrorCode",
|
||||
"source": "packages/compact/compact/src/index.ts"
|
||||
"source": "packages/compaction/compaction/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/compaction.md",
|
||||
"symbol": "PrunedEntry",
|
||||
"source": "packages/compact/compact-tool-result-prune/src/types.ts"
|
||||
"source": "packages/compaction/compaction-tool-result-pruner/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/compaction.md",
|
||||
"symbol": "PruneResult",
|
||||
"source": "packages/compact/compact-tool-result-prune/src/types.ts"
|
||||
"source": "packages/compaction/compaction-tool-result-pruner/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/subagent.md",
|
||||
@@ -1616,14 +1616,14 @@
|
||||
"source": "packages/settings/settings/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/permission.md",
|
||||
"doc": "docs/subsystems/permission-presets.md",
|
||||
"symbol": "Config",
|
||||
"source": "packages/interaction/permission/src/index.ts"
|
||||
"source": "packages/interaction/permission-presets/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/permission.md",
|
||||
"doc": "docs/subsystems/permission-presets.md",
|
||||
"symbol": "PresetOption",
|
||||
"source": "packages/interaction/permission/src/types.ts"
|
||||
"source": "packages/interaction/permission-presets/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/plan.md",
|
||||
@@ -1633,30 +1633,30 @@
|
||||
{
|
||||
"doc": "docs/subsystems/invariants.md",
|
||||
"symbol": "Config",
|
||||
"source": "packages/support/invariants/src/index.ts"
|
||||
"source": "packages/runtime-diagnostics/invariants/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/invariants.md",
|
||||
"symbol": "InvariantFailure",
|
||||
"source": "packages/support/invariants/src/index.ts"
|
||||
"source": "packages/runtime-diagnostics/invariants/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/invariants.md",
|
||||
"symbol": "InvariantInstaller",
|
||||
"source": "packages/support/invariants/src/index.ts"
|
||||
"source": "packages/runtime-diagnostics/invariants/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/http-server.md",
|
||||
"doc": "docs/subsystems/web-server.md",
|
||||
"symbol": "WebRouteKind",
|
||||
"source": "packages/host/webserver/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/http-server.md",
|
||||
"doc": "docs/subsystems/web-server.md",
|
||||
"symbol": "WebRoute",
|
||||
"source": "packages/host/webserver/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/http-server.md",
|
||||
"doc": "docs/subsystems/web-server.md",
|
||||
"symbol": "Config",
|
||||
"source": "packages/host/webserver/src/index.ts"
|
||||
},
|
||||
@@ -1711,64 +1711,64 @@
|
||||
"source": "packages/client/modules/src/client/manifest.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/telemetry.md",
|
||||
"symbol": "TelemetrySharingStatus",
|
||||
"doc": "docs/subsystems/session-telemetry.md",
|
||||
"symbol": "SessionTelemetrySharingStatus",
|
||||
"source": "packages/session/session-telemetry/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/telemetry.md",
|
||||
"symbol": "TelemetrySeverity",
|
||||
"doc": "docs/subsystems/session-telemetry.md",
|
||||
"symbol": "SessionTelemetrySeverity",
|
||||
"source": "packages/session/session-telemetry/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/telemetry.md",
|
||||
"symbol": "TelemetryRecord",
|
||||
"doc": "docs/subsystems/session-telemetry.md",
|
||||
"symbol": "SessionTelemetryRecord",
|
||||
"source": "packages/session/session-telemetry/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/telemetry.md",
|
||||
"symbol": "TelemetryBackend",
|
||||
"doc": "docs/subsystems/session-telemetry.md",
|
||||
"symbol": "SessionTelemetrySink",
|
||||
"source": "packages/session/session-telemetry/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
"symbol": "TypeRTLookupMap",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
"symbol": "TypertLookupMap",
|
||||
"source": "packages/typert/protocol/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
"symbol": "TypeRTContextMap",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
"symbol": "TypertContextMap",
|
||||
"source": "packages/typert/protocol/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
"symbol": "TypeRTLookupDefinition",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
"symbol": "TypertLookupDefinition",
|
||||
"source": "packages/typert/protocol/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
"symbol": "TypeRTCodec",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
"symbol": "TypertCodec",
|
||||
"source": "packages/typert/protocol/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
"symbol": "InvocationParameterDescriptor",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
"source": "packages/typert/protocol/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
"symbol": "InvocationDescriptor",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
"source": "packages/typert/protocol/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
"symbol": "TypeRTService",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
"symbol": "TypertRegistryContract",
|
||||
"source": "packages/typert/protocol/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
"symbol": "TypeRTRemoteNamespaceMap",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
"symbol": "TypertRemoteNamespaceMap",
|
||||
"source": "packages/typert/protocol/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
@@ -1787,8 +1787,8 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/typert.md",
|
||||
"symbol": "TypeRTClientRemote",
|
||||
"source": "packages/typert/type-meta/src/types.ts"
|
||||
"symbol": "TypertClientRemote",
|
||||
"source": "packages/typert/protocol/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/credentials.md",
|
||||
@@ -1796,9 +1796,9 @@
|
||||
"source": "packages/credentials/credentials/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/permission.md",
|
||||
"doc": "docs/subsystems/permission-presets.md",
|
||||
"symbol": "PresetSpec",
|
||||
"source": "packages/interaction/permission/src/index.ts"
|
||||
"source": "packages/interaction/permission-presets/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/session-projection.md",
|
||||
|
||||
@@ -53,7 +53,7 @@ const CHOOSER_PACKAGE = '@deepseek-ai/dsh-host-directory-picker-auto'
|
||||
const CHOOSER_BACKEND_PACKAGES = [
|
||||
'@deepseek-ai/dsh-host-directory-picker-native',
|
||||
'@deepseek-ai/dsh-host-directory-picker-browse',
|
||||
'@deepseek-ai/dsh-client-ui-directory-picker',
|
||||
'@deepseek-ai/dsh-client-ui-directory-picker-browse',
|
||||
'@deepseek-ai/dsh-client-ui-directory-picker-native',
|
||||
]
|
||||
const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
@@ -137,7 +137,7 @@ function validateClientHalvesDeclared(): string[] {
|
||||
* contributor to that service reaches nobody; a row that registers into a host
|
||||
* singleton registers once per live session, so the second one collides.
|
||||
*
|
||||
* Both have happened. `bash-env` in a preset realm left `DSH_WEB_URL` reaching
|
||||
* Both have happened. `shell-env` in a preset realm left `DSH_WEB_URL` reaching
|
||||
* no shell, and `tool-subagent-report` handed every child `report` once per live
|
||||
* session until the second registration threw. Neither changes a tool catalog,
|
||||
* so no catalog assertion can see them — and the shipped presets are near-copies
|
||||
|
||||
87
scripts/verify-doc-site-fragments.spec.ts
Normal file
87
scripts/verify-doc-site-fragments.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/** Tests for built-site fragment validation. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { inspectSiteFragments } from './verify-doc-site-fragments.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function fixture(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-doc-fragments-'))
|
||||
roots.push(root)
|
||||
mkdirSync(join(root, 'guide'), { recursive: true })
|
||||
writeFileSync(join(root, 'index.html'), '<a id="home"></a><a href="/guide/start#ready">start</a>')
|
||||
writeFileSync(join(root, 'guide/start.html'), [
|
||||
'<h1 id="ready">Ready</h1>',
|
||||
'<a name="legacy"></a>',
|
||||
'<a href="#ready">same page</a>',
|
||||
'<a href="./start.html#legacy">html alias</a>',
|
||||
'<a href="../#home">root</a>',
|
||||
'<a href="https://example.com/page#missing">external</a>',
|
||||
].join(''))
|
||||
return root
|
||||
}
|
||||
|
||||
describe('inspectSiteFragments', () => {
|
||||
it('rejects a directory with no built pages', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-doc-fragments-empty-'))
|
||||
roots.push(root)
|
||||
|
||||
expect(() => inspectSiteFragments(root)).toThrow('no HTML files found')
|
||||
})
|
||||
|
||||
it('resolves clean, encoded, and same-page routes', () => {
|
||||
const root = fixture()
|
||||
writeFileSync(
|
||||
join(root, 'guide/encoded.html'),
|
||||
'<h1 id="a b">Encoded</h1><h2 id="%">Literal</h2><a href="./encoded#a%20b">encoded</a><a href="#%">literal</a>',
|
||||
)
|
||||
|
||||
expect(inspectSiteFragments(root)).toEqual({ checked: 6, broken: [] })
|
||||
})
|
||||
|
||||
it('rejects ambiguous built routes', () => {
|
||||
const root = fixture()
|
||||
writeFileSync(join(root, 'guide.html'), '<h1 id="flat">Flat</h1>')
|
||||
writeFileSync(join(root, 'guide/index.html'), '<h1 id="index">Index</h1>')
|
||||
|
||||
expect(() => inspectSiteFragments(root)).toThrow('share route "/guide"')
|
||||
})
|
||||
|
||||
it('rejects malformed fragment hrefs', () => {
|
||||
const root = fixture()
|
||||
writeFileSync(join(root, 'guide/invalid.html'), '<a href="http://[invalid]#fragment">invalid</a>')
|
||||
|
||||
expect(() => inspectSiteFragments(root)).toThrow(
|
||||
'guide/invalid.html has invalid fragment href "http://[invalid]#fragment"',
|
||||
)
|
||||
})
|
||||
|
||||
it('reports missing ids and missing built routes', () => {
|
||||
const root = fixture()
|
||||
writeFileSync(join(root, 'guide/broken.html'), [
|
||||
'<a href="./start#missing">id</a>',
|
||||
'<a href="./absent#missing">route</a>',
|
||||
].join(''))
|
||||
|
||||
expect(inspectSiteFragments(root).broken).toEqual([
|
||||
{
|
||||
source: 'guide/broken.html',
|
||||
href: './start#missing',
|
||||
target: 'guide/start.html',
|
||||
fragment: 'missing',
|
||||
},
|
||||
{
|
||||
source: 'guide/broken.html',
|
||||
href: './absent#missing',
|
||||
fragment: 'missing',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
157
scripts/verify-doc-site-fragments.ts
Normal file
157
scripts/verify-doc-site-fragments.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Verify fragment links against the HTML emitted by VitePress. Markdown and
|
||||
* VitePress use different heading-slug algorithms, so source-link validation
|
||||
* alone cannot prove that a published fragment exists.
|
||||
*
|
||||
* This runs as part of `docs:build` and can also run directly after a build
|
||||
* with `tsx scripts/verify-doc-site-fragments.ts`.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import { JSDOM } from 'jsdom'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** One fragment reference that does not resolve in the built site. */
|
||||
export interface BrokenSiteFragment {
|
||||
/** HTML file containing the link. */
|
||||
source: string
|
||||
/** Link value as emitted by VitePress. */
|
||||
href: string
|
||||
/** Built HTML target, or `undefined` when the route was not emitted. */
|
||||
target?: string
|
||||
/** Decoded fragment id requested by the link. */
|
||||
fragment: string
|
||||
}
|
||||
|
||||
/** Result of checking every fragment-bearing anchor in a built site. */
|
||||
export interface SiteFragmentReport {
|
||||
/** Number of internal fragment references inspected. */
|
||||
checked: number
|
||||
/** References whose route or fragment id is absent. */
|
||||
broken: BrokenSiteFragment[]
|
||||
}
|
||||
|
||||
interface BuiltPage {
|
||||
file: string
|
||||
route: string
|
||||
ids: Set<string>
|
||||
document: Document
|
||||
}
|
||||
|
||||
function posixPath(path: string): string {
|
||||
return path.split(sep).join('/')
|
||||
}
|
||||
|
||||
function routeFor(file: string): string {
|
||||
if (file === 'index.html') return '/'
|
||||
if (file.endsWith('/index.html')) return `/${file.slice(0, -'index.html'.length)}`
|
||||
return `/${file.slice(0, -'.html'.length)}`
|
||||
}
|
||||
|
||||
function aliasesFor(page: BuiltPage): string[] {
|
||||
if (page.route === '/') return ['/', '/index', '/index.html']
|
||||
if (page.route.endsWith('/')) {
|
||||
const stem = page.route.slice(0, -1)
|
||||
return [page.route, stem, `${stem}/index`, `${stem}/index.html`]
|
||||
}
|
||||
return [page.route, `${page.route}.html`]
|
||||
}
|
||||
|
||||
function decodedFragment(hash: string): string {
|
||||
try {
|
||||
return decodeURIComponent(hash.slice(1))
|
||||
} catch (error) {
|
||||
if (!(error instanceof URIError)) throw error
|
||||
// URIError means malformed percent encoding; preserve the literal id for comparison.
|
||||
return hash.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check fragment-bearing links in a VitePress output directory.
|
||||
*
|
||||
* @param distRoot - Directory containing generated HTML files.
|
||||
* @returns Counted internal links and every unresolved target.
|
||||
*/
|
||||
export function inspectSiteFragments(distRoot: string): SiteFragmentReport {
|
||||
const files = globSync('**/*.html', { cwd: distRoot }).map(posixPath).sort()
|
||||
if (files.length === 0) {
|
||||
throw new Error(`verify-doc-site-fragments: no HTML files found under ${distRoot}; run docs:build first.`)
|
||||
}
|
||||
const pages: BuiltPage[] = files.map((file) => {
|
||||
const document = new JSDOM(readFileSync(resolve(distRoot, file), 'utf8')).window.document
|
||||
const ids = new Set<string>()
|
||||
for (const element of document.querySelectorAll<HTMLElement>('[id]')) ids.add(element.id)
|
||||
for (const element of document.querySelectorAll<HTMLAnchorElement>('a[name]')) {
|
||||
const name = element.getAttribute('name')
|
||||
if (name !== null) ids.add(name)
|
||||
}
|
||||
return { file, route: routeFor(file), ids, document }
|
||||
})
|
||||
|
||||
const byRoute = new Map<string, BuiltPage>()
|
||||
for (const page of pages) {
|
||||
for (const alias of aliasesFor(page)) {
|
||||
const existing = byRoute.get(alias)
|
||||
if (existing !== undefined && existing !== page) {
|
||||
throw new Error(
|
||||
`verify-doc-site-fragments: built pages ${existing.file} and ${page.file} share route ${JSON.stringify(alias)}.`,
|
||||
)
|
||||
}
|
||||
byRoute.set(alias, page)
|
||||
}
|
||||
}
|
||||
|
||||
const origin = 'https://dsh-docs.invalid'
|
||||
const broken: BrokenSiteFragment[] = []
|
||||
let checked = 0
|
||||
for (const page of pages) {
|
||||
for (const anchor of page.document.querySelectorAll<HTMLAnchorElement>('a[href]')) {
|
||||
const href = anchor.getAttribute('href')
|
||||
if (href === null || !href.includes('#')) continue
|
||||
let targetUrl: URL
|
||||
try {
|
||||
targetUrl = new URL(href, `${origin}${page.route}`)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`verify-doc-site-fragments: ${page.file} has invalid fragment href ${JSON.stringify(href)}.`,
|
||||
{ cause: error },
|
||||
)
|
||||
}
|
||||
if (targetUrl.origin !== origin || targetUrl.hash === '') continue
|
||||
const fragment = decodedFragment(targetUrl.hash)
|
||||
if (fragment === '') continue
|
||||
checked++
|
||||
const target = byRoute.get(targetUrl.pathname)
|
||||
if (target === undefined || !target.ids.has(fragment)) {
|
||||
broken.push({
|
||||
source: page.file,
|
||||
href,
|
||||
...(target === undefined ? {} : { target: target.file }),
|
||||
fragment,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return { checked, broken }
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
const distRoot = resolve(root, 'website/.dist')
|
||||
const report = inspectSiteFragments(distRoot)
|
||||
if (report.broken.length === 0) {
|
||||
console.log(`verify-doc-site-fragments: ${report.checked} internal fragment reference(s) resolve.`)
|
||||
return 0
|
||||
}
|
||||
|
||||
console.error(`verify-doc-site-fragments: ${report.broken.length} broken fragment reference(s):`)
|
||||
for (const item of report.broken) {
|
||||
const target = item.target === undefined ? 'target route was not built' : `${item.target} has no id ${JSON.stringify(item.fragment)}`
|
||||
console.error(` ${item.source}: ${JSON.stringify(item.href)} (${target})`)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
if (import.meta.main) process.exitCode = main()
|
||||
59
scripts/verify-dsh-package-licenses.spec.ts
Normal file
59
scripts/verify-dsh-package-licenses.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { inspectDshPackageLicenses } from './verify-dsh-package-licenses.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function writeManifest(root: string, file: string, manifest: Record<string, unknown>): void {
|
||||
const path = join(root, file)
|
||||
mkdirSync(dirname(path), { recursive: true })
|
||||
writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
}
|
||||
|
||||
function createWorkspace(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-package-licenses-'))
|
||||
roots.push(root)
|
||||
writeManifest(root, 'package.json', {
|
||||
name: '@deepseek-ai/dsh-root',
|
||||
license: 'MIT',
|
||||
workspaces: ['apps/*', 'packages/*/*', 'vendor/*'],
|
||||
})
|
||||
return root
|
||||
}
|
||||
|
||||
describe('DSH package license gate', () => {
|
||||
it('checks root, unhyphenated CLI, and dsh-prefixed package names while ignoring other families', () => {
|
||||
const root = createWorkspace()
|
||||
writeManifest(root, 'apps/cli/package.json', { name: '@deepseek-ai/dsh', license: 'MIT' })
|
||||
writeManifest(root, 'packages/core/agent/package.json', {
|
||||
name: '@deepseek-ai/dsh-agent',
|
||||
license: 'BSD-3-Clause',
|
||||
})
|
||||
writeManifest(root, 'vendor/cordis/package.json', {
|
||||
name: '@deepseek-ai/cordis',
|
||||
license: 'BSD-3-Clause',
|
||||
})
|
||||
|
||||
expect(inspectDshPackageLicenses(root)).toEqual({
|
||||
packageCount: 3,
|
||||
failures: [
|
||||
'packages/core/agent/package.json: @deepseek-ai/dsh-agent must declare "license": "MIT"; found "BSD-3-Clause".',
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a missing license declaration', () => {
|
||||
const root = createWorkspace()
|
||||
writeManifest(root, 'packages/core/agent/package.json', { name: '@deepseek-ai/dsh-agent' })
|
||||
|
||||
expect(inspectDshPackageLicenses(root).failures).toEqual([
|
||||
'packages/core/agent/package.json: @deepseek-ai/dsh-agent must declare "license": "MIT"; found undefined.',
|
||||
])
|
||||
})
|
||||
})
|
||||
89
scripts/verify-dsh-package-licenses.ts
Normal file
89
scripts/verify-dsh-package-licenses.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Enforce the MIT license declaration for repository-owned DSH npm packages.
|
||||
* @module scripts/verify-dsh-package-licenses
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, '..')
|
||||
const DSH_PACKAGE_NAME = /^@deepseek-ai\/dsh(?:-|$)/
|
||||
|
||||
/** Result of checking every DSH package reachable through the root workspace list. */
|
||||
export interface DshPackageLicenseReport {
|
||||
/** Number of DSH package manifests checked. */
|
||||
packageCount: number
|
||||
/** Repository-relative diagnostics for non-MIT declarations. */
|
||||
failures: string[]
|
||||
}
|
||||
|
||||
function readManifest(root: string, file: string): Record<string, unknown> {
|
||||
const parsed: unknown = JSON.parse(readFileSync(resolve(root, file), 'utf8'))
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error(`verify-dsh-package-licenses: ${file} must contain a JSON object.`)
|
||||
}
|
||||
return parsed as Record<string, unknown>
|
||||
}
|
||||
|
||||
function isStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && value.every((entry: unknown) => typeof entry === 'string')
|
||||
}
|
||||
|
||||
function workspaceManifestPaths(root: string): string[] {
|
||||
const rootManifest = readManifest(root, 'package.json')
|
||||
const workspaces = rootManifest.workspaces
|
||||
if (!isStringArray(workspaces)) {
|
||||
throw new Error('verify-dsh-package-licenses: package.json workspaces must be a string array.')
|
||||
}
|
||||
|
||||
const files = new Set(['package.json'])
|
||||
for (const pattern of workspaces) {
|
||||
for (const file of globSync(`${pattern}/package.json`, { cwd: root })) {
|
||||
files.add(file)
|
||||
}
|
||||
}
|
||||
return [...files].sort()
|
||||
}
|
||||
|
||||
function printable(value: unknown): string {
|
||||
return value === undefined ? 'undefined' : JSON.stringify(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check every DSH npm package declared by the repository workspace.
|
||||
* @param root - absolute repository root containing the workspace package.json.
|
||||
* @returns the checked package count and every non-MIT declaration.
|
||||
*/
|
||||
export function inspectDshPackageLicenses(root: string): DshPackageLicenseReport {
|
||||
let packageCount = 0
|
||||
const failures: string[] = []
|
||||
|
||||
for (const file of workspaceManifestPaths(root)) {
|
||||
const manifest = readManifest(root, file)
|
||||
const name = manifest.name
|
||||
if (typeof name !== 'string' || !DSH_PACKAGE_NAME.test(name)) continue
|
||||
|
||||
packageCount++
|
||||
if (manifest.license !== 'MIT') {
|
||||
const normalizedFile = file.split(sep).join('/')
|
||||
failures.push(
|
||||
`${normalizedFile}: ${name} must declare "license": "MIT"; found ${printable(manifest.license)}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return { packageCount, failures }
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
|
||||
const report = inspectDshPackageLicenses(ROOT)
|
||||
if (report.failures.length > 0) {
|
||||
process.stderr.write('verify-dsh-package-licenses: non-MIT DSH package declarations found:\n')
|
||||
for (const failure of report.failures) process.stderr.write(` ${failure}\n`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
process.stdout.write(
|
||||
`verify-dsh-package-licenses: ${String(report.packageCount)} DSH package(s) checked; all declare MIT.\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
130
scripts/verify-optional-dependency-imports.spec.ts
Normal file
130
scripts/verify-optional-dependency-imports.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Tests for the optional-dependency load gate: which import and re-export forms
|
||||
* survive emit, and therefore load a package the installed tree may not carry.
|
||||
*
|
||||
* The expectations here match what `tsc` emits with `verbatimModuleSyntax` off:
|
||||
* `import type`, `import {}`, an inline `type` specifier, and a named binding
|
||||
* that resolves to a type all disappear; a bare import, a value binding, and a
|
||||
* star re-export remain.
|
||||
*/
|
||||
|
||||
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 { TypeScriptProject } from './ts-project.ts'
|
||||
import { collectOptionalImportViolations } from './verify-optional-dependency-imports.ts'
|
||||
|
||||
const FIXTURE: Record<string, string> = {
|
||||
'tsconfig.host.json': JSON.stringify({
|
||||
compilerOptions: {
|
||||
target: 'es2022',
|
||||
module: 'esnext',
|
||||
moduleResolution: 'bundler',
|
||||
noEmit: true,
|
||||
skipLibCheck: true,
|
||||
types: [],
|
||||
paths: {
|
||||
'@f/opt': ['./packages/f/opt/src/index.ts'],
|
||||
'@f/hard': ['./packages/f/hard/src/index.ts'],
|
||||
},
|
||||
},
|
||||
include: ['packages/**/*.ts'],
|
||||
}),
|
||||
|
||||
'packages/f/opt/package.json': JSON.stringify({ name: '@f/opt', version: '0.0.1' }),
|
||||
'packages/f/opt/src/index.ts': [
|
||||
'export interface Shape { a: number }',
|
||||
'export const runtimeValue = 1',
|
||||
'',
|
||||
].join('\n'),
|
||||
|
||||
'packages/f/hard/package.json': JSON.stringify({ name: '@f/hard', version: '0.0.1' }),
|
||||
'packages/f/hard/src/index.ts': 'export const hardValue = 2\n',
|
||||
|
||||
// The consumer allows @f/opt to be absent and requires @f/hard.
|
||||
'packages/f/consumer/package.json': JSON.stringify({
|
||||
name: '@f/consumer',
|
||||
version: '0.0.1',
|
||||
dependencies: { '@f/hard': '*' },
|
||||
peerDependencies: { '@f/opt': '*' },
|
||||
peerDependenciesMeta: { '@f/opt': { optional: true } },
|
||||
}),
|
||||
|
||||
// Elided by the compiler, so each of these is allowed.
|
||||
'packages/f/consumer/src/allowed-type-only.ts': [
|
||||
"import type {} from '@f/opt'",
|
||||
'export const a = 1',
|
||||
'',
|
||||
].join('\n'),
|
||||
'packages/f/consumer/src/allowed-empty.ts': [
|
||||
"import {} from '@f/opt'",
|
||||
'export const b = 1',
|
||||
'',
|
||||
].join('\n'),
|
||||
'packages/f/consumer/src/allowed-inline-type.ts': [
|
||||
"import { type Shape } from '@f/opt'",
|
||||
'export const c: Shape = { a: 1 }',
|
||||
'',
|
||||
].join('\n'),
|
||||
'packages/f/consumer/src/allowed-type-binding.ts': [
|
||||
"import { Shape } from '@f/opt'",
|
||||
'export const d: Shape = { a: 1 }',
|
||||
'',
|
||||
].join('\n'),
|
||||
'packages/f/consumer/src/allowed-type-reexport.ts': [
|
||||
"export type { Shape } from '@f/opt'",
|
||||
'',
|
||||
].join('\n'),
|
||||
// A hard dependency may be loaded at module scope; only optional ones may not.
|
||||
'packages/f/consumer/src/allowed-hard-dependency.ts': [
|
||||
"import { hardValue } from '@f/hard'",
|
||||
'export const e = hardValue',
|
||||
'',
|
||||
].join('\n'),
|
||||
|
||||
// Kept by the compiler, so each of these loads a package that may be absent.
|
||||
'packages/f/consumer/src/rejected-bare.ts': [
|
||||
"import '@f/opt'",
|
||||
'export const f = 1',
|
||||
'',
|
||||
].join('\n'),
|
||||
'packages/f/consumer/src/rejected-value.ts': [
|
||||
"import { runtimeValue } from '@f/opt'",
|
||||
'export const g = runtimeValue',
|
||||
'',
|
||||
].join('\n'),
|
||||
'packages/f/consumer/src/rejected-star-reexport.ts': [
|
||||
"export * from '@f/opt'",
|
||||
'',
|
||||
].join('\n'),
|
||||
}
|
||||
|
||||
const root = mkdtempSync(join(tmpdir(), 'optional-imports-'))
|
||||
for (const [rel, content] of Object.entries(FIXTURE)) {
|
||||
mkdirSync(dirname(join(root, rel)), { recursive: true })
|
||||
writeFileSync(join(root, rel), content)
|
||||
}
|
||||
const violations = collectOptionalImportViolations(new TypeScriptProject(root))
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('optional dependency loads', () => {
|
||||
it('reports every form the compiler keeps, and nothing else', () => {
|
||||
expect(violations.map(violation => violation.split(' loads ')[0])).toEqual([
|
||||
'packages/f/consumer/src/rejected-bare.ts:1',
|
||||
'packages/f/consumer/src/rejected-star-reexport.ts:1',
|
||||
'packages/f/consumer/src/rejected-value.ts:1',
|
||||
])
|
||||
})
|
||||
|
||||
it('names the package, the declaration that made it optional, and the way out', () => {
|
||||
expect(violations[0]).toBe(
|
||||
'packages/f/consumer/src/rejected-bare.ts:1 loads @f/opt at module scope,'
|
||||
+ ' declared optional in peerDependenciesMeta; import it as a type,'
|
||||
+ ' or restructure so module scope does not need it',
|
||||
)
|
||||
})
|
||||
})
|
||||
214
scripts/verify-optional-dependency-imports.ts
Normal file
214
scripts/verify-optional-dependency-imports.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Reject a static value import of an optional dependency.
|
||||
*
|
||||
* A dependency declared in `optionalDependencies`, or as a peer carrying
|
||||
* `peerDependenciesMeta.<name>.optional`, may be absent from an installed tree —
|
||||
* that absence is what "optional" promises a consumer. A static import is
|
||||
* evaluated when the importing module loads, so one absent package turns
|
||||
* "this capability is unavailable" into a load failure for everything that
|
||||
* reaches the importing module.
|
||||
*
|
||||
* The way out, in order: import it as a type, which emits nothing and is all
|
||||
* that declaration merging needs; or restructure so nothing at module scope
|
||||
* needs the package. A dynamic `import()` only moves the failure to first use,
|
||||
* so it belongs to a caller that genuinely requires the package and handles its
|
||||
* absence — it is a last resort, not the default answer, and reaching for it is
|
||||
* a sign the dependency is not optional.
|
||||
*
|
||||
* Value-vs-type is decided against a bound Program rather than the import
|
||||
* syntax, because `verbatimModuleSyntax` is off: a named import used only in
|
||||
* type positions is elided and does not load anything. The decision is
|
||||
* deliberately conservative in one direction — a value binding the compiler
|
||||
* would elide because nothing references it in a value position is still
|
||||
* reported, and the fix it asks for (`import type`, or dropping the binding) is
|
||||
* what the published package wants regardless. Both compiler faces are scanned,
|
||||
* and only files that ship — a published package's `src` — are subject.
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { TypeScriptProject, type CompilerFace } from './ts-project.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** Directories whose `src` ships as a published package. */
|
||||
const PUBLISHED_SOURCE = /^(?:packages\/[^/]+\/[^/]+|apps\/[^/]+)\/src\//
|
||||
|
||||
/** How a manifest marked a dependency optional, for the violation message. */
|
||||
type OptionalKind = 'optionalDependencies' | 'peerDependenciesMeta'
|
||||
|
||||
/**
|
||||
* The package name a module specifier resolves to.
|
||||
* @param specifier - an import specifier, possibly a subpath.
|
||||
* @returns The bare package name, keeping a leading scope.
|
||||
*/
|
||||
function packageOf(specifier: string): string {
|
||||
const parts = specifier.split('/')
|
||||
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] ?? specifier
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a manifest field as a record.
|
||||
* @param manifest - parsed manifest.
|
||||
* @param field - field name.
|
||||
* @returns The field value, or an empty record.
|
||||
*/
|
||||
function record(manifest: Record<string, unknown>, field: string): Record<string, unknown> {
|
||||
const value = manifest[field]
|
||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* The dependencies one manifest allows to be absent.
|
||||
* @param manifest - parsed manifest.
|
||||
* @returns Each optional package name and how it was marked.
|
||||
*/
|
||||
function optionalDependencies(manifest: Record<string, unknown>): Map<string, OptionalKind> {
|
||||
const optional = new Map<string, OptionalKind>()
|
||||
for (const name of Object.keys(record(manifest, 'optionalDependencies'))) {
|
||||
optional.set(name, 'optionalDependencies')
|
||||
}
|
||||
const peers = record(manifest, 'peerDependencies')
|
||||
for (const [name, meta] of Object.entries(record(manifest, 'peerDependenciesMeta'))) {
|
||||
if (meta === null || typeof meta !== 'object') continue
|
||||
if ((meta as Record<string, unknown>).optional !== true) continue
|
||||
// A meta entry for an undeclared peer is check-workspace-constraints' business.
|
||||
if (!(name in peers)) continue
|
||||
optional.set(name, 'peerDependenciesMeta')
|
||||
}
|
||||
return optional
|
||||
}
|
||||
|
||||
/** One package directory's optional dependencies, resolved once per directory. */
|
||||
const optionalByDirectory = new Map<string, Map<string, OptionalKind>>()
|
||||
|
||||
/**
|
||||
* The optional dependencies of the package owning a source file.
|
||||
* @param projectRoot - root the relative path is resolved against.
|
||||
* @param relativePath - repository-relative path of a source file.
|
||||
* @returns That package's optional dependencies, empty when it declares none.
|
||||
*/
|
||||
function optionalFor(projectRoot: string, relativePath: string): Map<string, OptionalKind> {
|
||||
const directory = resolve(projectRoot, relativePath.slice(0, relativePath.indexOf('/src/')))
|
||||
const cached = optionalByDirectory.get(directory)
|
||||
if (cached !== undefined) return cached
|
||||
const manifestPath = resolve(directory, 'package.json')
|
||||
const parsed: unknown = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, 'utf8')) : {}
|
||||
const manifest = parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? parsed as Record<string, unknown>
|
||||
: {}
|
||||
const optional = optionalDependencies(manifest)
|
||||
optionalByDirectory.set(directory, optional)
|
||||
return optional
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether one binding of an import or re-export names a value.
|
||||
* @param name - the local binding name node.
|
||||
* @param checker - the program's checker.
|
||||
* @returns True when the binding carries value meaning, and on an unresolved
|
||||
* symbol, so an unresolvable binding fails closed.
|
||||
*/
|
||||
function bindsValue(name: ts.Identifier | ts.StringLiteral, checker: ts.TypeChecker): boolean {
|
||||
const symbol = checker.getSymbolAtLocation(name)
|
||||
if (symbol === undefined) return true
|
||||
const target = (symbol.flags & ts.SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol)
|
||||
return (target.flags & ts.SymbolFlags.Value) !== 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an import declaration loads its module at run time.
|
||||
* @param declaration - the import declaration.
|
||||
* @param checker - the program's checker.
|
||||
* @returns True when the emitted module keeps the import.
|
||||
*/
|
||||
function importLoadsModule(declaration: ts.ImportDeclaration, checker: ts.TypeChecker): boolean {
|
||||
const clause = declaration.importClause
|
||||
// A bare `import 'x'` is kept for its side effects.
|
||||
if (clause === undefined) return true
|
||||
// Only the type phase erases the import. `import defer` still resolves and
|
||||
// links the module, deferring evaluation alone, so an absent package fails
|
||||
// exactly as it would without the modifier.
|
||||
if (clause.phaseModifier === ts.SyntaxKind.TypeKeyword) return false
|
||||
if (clause.name !== undefined) return true
|
||||
const bindings = clause.namedBindings
|
||||
if (bindings === undefined || ts.isNamespaceImport(bindings)) return true
|
||||
return bindings.elements.some(element => !element.isTypeOnly && bindsValue(element.name, checker))
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a re-export loads its module at run time.
|
||||
* @param declaration - the export declaration, which carries a module specifier.
|
||||
* @param checker - the program's checker.
|
||||
* @returns True when the emitted module keeps the re-export.
|
||||
*/
|
||||
function exportLoadsModule(declaration: ts.ExportDeclaration, checker: ts.TypeChecker): boolean {
|
||||
if (declaration.isTypeOnly) return false
|
||||
const clause = declaration.exportClause
|
||||
// `export * from 'x'` re-exports whatever values the module has.
|
||||
if (clause === undefined || ts.isNamespaceExport(clause)) return true
|
||||
return clause.elements.some(element => !element.isTypeOnly && bindsValue(element.name, checker))
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect every static value import of an optional dependency in one face.
|
||||
* @param project - a bound repository project.
|
||||
* @returns One message per violation, sorted by location.
|
||||
*/
|
||||
export function collectOptionalImportViolations(project: TypeScriptProject): string[] {
|
||||
const checker = project.checker
|
||||
const violations: string[] = []
|
||||
for (const sourceFile of project.sourceFiles()) {
|
||||
if (sourceFile.isDeclarationFile) continue
|
||||
const relativePath = project.relativePath(sourceFile)
|
||||
if (!PUBLISHED_SOURCE.test(relativePath)) continue
|
||||
const optional = optionalFor(project.projectRoot, relativePath)
|
||||
if (optional.size === 0) continue
|
||||
|
||||
for (const statement of sourceFile.statements) {
|
||||
const isImport = ts.isImportDeclaration(statement)
|
||||
if (!isImport && !ts.isExportDeclaration(statement)) continue
|
||||
const specifierNode = statement.moduleSpecifier
|
||||
if (specifierNode === undefined || !ts.isStringLiteral(specifierNode)) continue
|
||||
const kind = optional.get(packageOf(specifierNode.text))
|
||||
if (kind === undefined) continue
|
||||
const loads = isImport
|
||||
? importLoadsModule(statement, checker)
|
||||
: exportLoadsModule(statement, checker)
|
||||
if (!loads) continue
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile))
|
||||
violations.push(
|
||||
`${relativePath}:${String(line + 1)} loads ${specifierNode.text} at module scope,`
|
||||
+ ` declared optional in ${kind}; import it as a type, or restructure so module scope does not need it`,
|
||||
)
|
||||
}
|
||||
}
|
||||
return violations.sort((left, right) => left.localeCompare(right))
|
||||
}
|
||||
|
||||
/** CLI entry: list every violation and exit 1, or confirm the invariant holds. */
|
||||
function main(): void {
|
||||
const faces: readonly CompilerFace[] = ['host', 'client']
|
||||
const violations = new Set<string>()
|
||||
for (const face of faces) {
|
||||
for (const violation of collectOptionalImportViolations(new TypeScriptProject(root, face))) {
|
||||
violations.add(violation)
|
||||
}
|
||||
}
|
||||
if (violations.size === 0) {
|
||||
console.log('verify-optional-dependency-imports: no optional dependency is loaded at module scope.')
|
||||
return
|
||||
}
|
||||
console.error(`verify-optional-dependency-imports: ${String(violations.size)} optional dependency load(s) at module scope:`)
|
||||
for (const violation of [...violations].sort((left, right) => left.localeCompare(right))) {
|
||||
console.error(` ${violation}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Run only when invoked as a script, not when imported by a test.
|
||||
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
|
||||
main()
|
||||
}
|
||||
@@ -32,8 +32,8 @@ interface SentenceContract {
|
||||
const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
|
||||
'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.',
|
||||
'packages/util/brand': 'The package is a type-only primitive erased at compile time.',
|
||||
'packages/util/paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
|
||||
'packages/util/environment': 'The package only resolves host environment values; model-facing consumers own any rendered use.',
|
||||
'packages/util/home-paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
|
||||
'packages/util/launch-environment': 'The package only resolves host environment values; model-facing consumers own any rendered use.',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,13 +44,13 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
|
||||
const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/attachment/attachment': { kind: 'indirect', reason: 'The storage seam delegates model request rendering to provider adapters.' },
|
||||
'packages/attachment/attachment-local': { kind: 'indirect', reason: 'The local backend delegates model request rendering to provider adapters.' },
|
||||
'packages/bash/bash': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },
|
||||
'packages/bash/bash-env': { kind: 'indirect', reason: 'The env service exposes managed DSH_* facts through the shell tools (dsh-tool-bash/dsh-tool-pwsh); it registers no prompt or schema of its own.' },
|
||||
'packages/bash/bash-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-bash.' },
|
||||
'packages/bash/pwsh-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-pwsh.' },
|
||||
'packages/shell/shell': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },
|
||||
'packages/shell/shell-env': { kind: 'indirect', reason: 'The env service exposes managed DSH_* facts through the shell tools (dsh-tool-bash/dsh-tool-pwsh); it registers no prompt or schema of its own.' },
|
||||
'packages/shell/bash-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-bash.' },
|
||||
'packages/shell/pwsh-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-pwsh.' },
|
||||
'packages/code-runtime/code-runtime': { kind: 'indirect', reason: 'The service interface delegates model rendering to Code Mode in dsh-tools.' },
|
||||
'packages/core/agent-tool-mode': { kind: 'indirect', reason: 'The row only selects between the two projections dsh-tools owns; it registers no prompt, schema, or result of its own.' },
|
||||
'packages/code-runtime/code-runtime-worker': { kind: 'indirect', reason: 'The worker backend delegates model rendering to Code Mode in dsh-tools.' },
|
||||
'packages/core/agent-tool-presentation': { kind: 'indirect', reason: 'The row only selects between the two projections dsh-tools owns; it registers no prompt, schema, or result of its own.' },
|
||||
'packages/code-runtime/code-runtime-worker-thread': { kind: 'indirect', reason: 'The worker backend delegates model rendering to Code Mode in dsh-tools.' },
|
||||
'packages/client/ui-agent-preset': { kind: 'indirect', reason: 'Browser-side settings row; the preset it selects owns every model-facing effect.' },
|
||||
'packages/core/agent-default-model': { kind: 'indirect', reason: 'The service supplies a ModelSelection; request assembly and adapters own the model-visible request.' },
|
||||
'packages/preset/agent-presets': { kind: 'indirect', reason: 'The mount installs a preset\'s own plugins, which own every model-facing registration it makes visible.' },
|
||||
@@ -59,7 +59,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/e2b/e2b': { kind: 'none', reason: 'The shared remote-runtime owner registers no model context; provider adapters and consumers own rendered effects.' },
|
||||
'packages/client/hmr': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/modules': { kind: 'none', reason: 'Browser-side module-loading kernel machinery; registers nothing model-facing.' },
|
||||
'packages/client/test-runtime': { kind: 'none', reason: 'Browser-side test infrastructure (jsdom bench); registers nothing model-facing.' },
|
||||
'packages/test-support/client-runtime': { kind: 'none', reason: 'Browser-side test infrastructure (jsdom bench); registers nothing model-facing.' },
|
||||
'packages/client/ui-slots': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-attachment': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-primitives': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
@@ -71,28 +71,28 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-feedback': { kind: 'none', reason: 'Browser-side controls over the message-feedback sidecar; ratings and notes never enter the Session log, model context, or telemetry.' },
|
||||
'packages/client/ui-message-feedback': { kind: 'none', reason: 'Browser-side controls over the message-feedback sidecar; ratings and notes never enter the Session log, model context, or telemetry.' },
|
||||
'packages/client/ui-tool': { kind: 'none', reason: 'Browser-side Tool presentation layer; renders logged calls without changing model context.' },
|
||||
'packages/client/ui-deliverables': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-task': { kind: 'none', reason: 'Browser-side read-only projection of ctx.tasks records; dsh-tool-tasks owns the model-facing behavior.' },
|
||||
'packages/client/ui-jobs': { kind: 'none', reason: 'Browser-side read-only projection of ctx.jobs records; dsh-tool-jobs owns the model-facing behavior.' },
|
||||
'packages/client/ui-workflow-run': { kind: 'none', reason: 'Browser-side UI plugin layer; renders durable workflow records without changing model context.' },
|
||||
'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-command': { kind: 'indirect', reason: 'The dispatch paths trigger the host command.execute RPC; each command handler\'s host package owns any model-visible effect.' },
|
||||
'packages/client/ui-model': { kind: 'indirect', reason: 'Selection routes session.selectModel; the Host snapshots the selection at the next prompt-assembly boundary and owns the model-visible effect.' },
|
||||
'packages/client/ui-input-trigger': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-commands': { kind: 'indirect', reason: 'The dispatch paths trigger the host command.execute RPC; each command handler\'s host package owns any model-visible effect.' },
|
||||
'packages/client/ui-model-selection': { kind: 'indirect', reason: 'Selection routes session.selectModel; the Host snapshots the selection at the next prompt-assembly boundary and owns the model-visible effect.' },
|
||||
'packages/client/ui-goal': { kind: 'indirect', reason: 'The strip verbs route goal.* mutations; the host GoalService owns the model-visible goal/change context message.' },
|
||||
'packages/client/ui-permission': { kind: 'indirect', reason: 'The picker submits the host /permission command; the knob events it appends own the model-visible effect through the sandbox/approval consumers.' },
|
||||
'packages/client/ui-plugin-config': { kind: 'none', reason: 'Browser-side settings surface; registers no model surface.' },
|
||||
'packages/extensions/ui-cordis': { kind: 'indirect', reason: 'The definition card drives the host dynamic run/stop verbs that the model\'s cordis_run/cordis_stop tools also reach; the runner owns any model-visible effect.' },
|
||||
'packages/client/ui-permission-presets': { kind: 'indirect', reason: 'The picker submits the host /permission command; the knob events it appends own the model-visible effect through the sandbox/approval consumers.' },
|
||||
'packages/client/ui-settings-plugins': { kind: 'none', reason: 'Browser-side settings surface; registers no model surface.' },
|
||||
'packages/client/ui-plan': { kind: 'indirect', reason: 'The chip dispatches /plan off; dsh-plan-mode owns the model-visible policy, exit tool, and logged state.' },
|
||||
'packages/client/ui-question': { kind: 'indirect', reason: 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' },
|
||||
'packages/client/ui-user-questions': { kind: 'indirect', reason: 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' },
|
||||
'packages/client/ui-trajectory': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-workspace': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-directory-picker': { kind: 'none', reason: 'Browser-side directory-browsing surface; registers nothing model-facing.' },
|
||||
'packages/client/ui-directory-picker-browse': { kind: 'none', reason: 'Browser-side directory-browsing surface; registers nothing model-facing.' },
|
||||
'packages/client/ui-directory-picker-native': { kind: 'none', reason: 'Browser-side surface driving the host OS chooser; registers nothing model-facing.' },
|
||||
'packages/client/ui-theme': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-settings': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-settings-general': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-models': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-plugins': { kind: 'none', reason: 'Browser-side inventory projection; registers nothing model-facing.' },
|
||||
'packages/client/ui-settings-models': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/ui-settings-plugin-inventory': { kind: 'none', reason: 'Browser-side inventory projection; registers nothing model-facing.' },
|
||||
'packages/client/locale': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/client/web': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
|
||||
'packages/examples/agent-spine-demo': { kind: 'indirect', reason: 'The bundle only mounts model-facing child plugins.' },
|
||||
@@ -113,53 +113,54 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
|
||||
'packages/llm/token-meter': { kind: 'indirect', reason: 'The measurement service leaves model-visible changes to its consumers.' },
|
||||
'packages/lsp/lsp': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-lsp.' },
|
||||
'packages/lsp/lsp-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-lsp.' },
|
||||
'packages/lsp/lsp-stdio': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-lsp.' },
|
||||
'packages/subprocess/subprocess': { kind: 'indirect', reason: 'The seam delegates all model rendering to consumer seams such as the bash executor family.' },
|
||||
'packages/e2b/subprocess-e2b': { kind: 'indirect', reason: 'The remote spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
|
||||
'packages/subprocess/subprocess-local': { kind: 'indirect', reason: 'The spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
|
||||
'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' },
|
||||
'packages/sandbox/sandbox-windows-acl': { kind: 'indirect', reason: 'The provider backend delegates model rendering to the bash/pwsh sandbox executors and their tools.' },
|
||||
'packages/sandbox/sandbox-windows-acl': { kind: 'indirect', reason: 'The provider backend delegates model rendering to the shell/pwsh sandbox executors and their tools.' },
|
||||
'packages/sdk/client': { kind: 'none', reason: 'Client-process library; model-facing behavior lives in the spawned runtime\'s composed plugins.' },
|
||||
'packages/sdk/protocol': { kind: 'none', reason: 'Client-facing wire library; the runtime plugins behind the serving entry own model-facing behavior.' },
|
||||
'packages/session/session-projection': { kind: 'none', reason: 'The projection registry serves client-facing read models of already-logged session state and registers nothing model-facing.' },
|
||||
'packages/session/session-projection-cache': { kind: 'none', reason: 'The persisted cache accelerates host-side cold reads of projection state and registers nothing model-facing.' },
|
||||
'packages/session/session-stats': { kind: 'none', reason: 'The sessionStats unit folds already-logged step boundaries into a client-facing read model and registers nothing model-facing.' },
|
||||
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers nothing model-facing.' },
|
||||
'packages/session-query/session-query-sqlite': { kind: 'none', reason: 'The search backend returns hits only to callers and registers nothing model-facing.' },
|
||||
'packages/settings/settings': { kind: 'indirect', reason: 'The seam stores and resolves user settings; consumer plugins own any model-facing content fed by a value.' },
|
||||
'packages/settings/settings-local': { kind: 'indirect', reason: 'The file provider stores and publishes namespace sections; consumers of ctx.settings own any model-facing behavior.' },
|
||||
'packages/settings/settings-file': { kind: 'indirect', reason: 'The file provider stores and publishes namespace sections; consumers of ctx.settings own any model-facing behavior.' },
|
||||
'packages/credentials/credentials': { kind: 'indirect', reason: 'The seam resolves credential references; the consuming adapter owns every model-facing use a value authorizes.' },
|
||||
'packages/credentials/credentials-local': { kind: 'indirect', reason: 'The file/environment provider stores credential values; consumers of ctx.credentials own any model-facing behavior.' },
|
||||
'packages/util/atomic-write': { kind: 'none', reason: 'Pure filesystem write primitive; registers nothing model-facing.' },
|
||||
'packages/session/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers nothing model-facing.' },
|
||||
'packages/session/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers nothing model-facing.' },
|
||||
'packages/session/user-id': { kind: 'none', reason: 'The shared identifier reaches DeepSeek only as model-hidden HTTP metadata; it registers nothing model-facing.' },
|
||||
'packages/identity/anonymous-user-id': { kind: 'none', reason: 'The shared identifier reaches DeepSeek only as model-hidden HTTP metadata; it registers nothing model-facing.' },
|
||||
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },
|
||||
'packages/skill/skill-badge': { kind: 'indirect', reason: 'The bundled provider delegates model rendering to dsh-tool-skill.' },
|
||||
'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
|
||||
'packages/skill/skill-filesystem': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
|
||||
'packages/spill/spill': { kind: 'indirect', reason: 'The storage seam delegates model rendering to spill consumers.' },
|
||||
'packages/spill/spill-local': { kind: 'indirect', reason: 'The storage backend delegates model rendering to spill consumers.' },
|
||||
'packages/support/acp-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' },
|
||||
'packages/support/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model requests.' },
|
||||
'packages/support/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' },
|
||||
'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness submits an ordinary user task but delegates prompt and tool composition to the loaded tree.' },
|
||||
'packages/support/llm-mock-server': { kind: 'none', reason: 'The test server substitutes provider wire behavior without invoking a real model.' },
|
||||
'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
|
||||
'packages/test-support/acp-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' },
|
||||
'packages/test-support/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model requests.' },
|
||||
'packages/runtime-diagnostics/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' },
|
||||
'packages/test-support/loader-smoke': { kind: 'none', reason: 'The test harness submits an ordinary user task but delegates prompt and tool composition to the loaded tree.' },
|
||||
'packages/test-support/llm-mock-server': { kind: 'none', reason: 'The test server substitutes provider wire behavior without invoking a real model.' },
|
||||
'packages/test-support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
|
||||
'packages/api/gateway': { kind: 'none', reason: 'Remote dispatch infrastructure; invoked business methods own any model-visible effect.' },
|
||||
'packages/typert/type-meta': { kind: 'none', reason: 'Compiler-independent Remote protocol declarations; registers nothing model-facing.' },
|
||||
'packages/typert/protocol': { kind: 'none', reason: 'Compiler-independent Remote protocol declarations; registers nothing model-facing.' },
|
||||
'packages/typert/generator': { kind: 'none', reason: 'The build-time generator runs outside any agent runtime and touches no model request.' },
|
||||
'packages/tasks/tasks': { kind: 'indirect', reason: 'Producer and controller plugins own all model rendering over the task registry.' },
|
||||
'packages/tasks/tasks-local': { kind: 'indirect', reason: 'The registry backend delegates model rendering to producer plugins and dsh-tool-tasks.' },
|
||||
'packages/jobs/jobs': { kind: 'indirect', reason: 'Producer and controller plugins own all model rendering over the job registry.' },
|
||||
'packages/jobs/jobs-local': { kind: 'indirect', reason: 'The registry backend delegates model rendering to producer plugins and dsh-tool-jobs.' },
|
||||
'packages/examples/acp-demo': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-spine-demo and dsh-acp.' },
|
||||
'packages/boot/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' },
|
||||
'packages/boot/cmdline': { kind: 'none', reason: 'Resolves the process command line before any session exists; configured rows own every model-visible consequence.' },
|
||||
'packages/examples/jsonrpc-demo': { kind: 'indirect', reason: 'Only the externally configured plugin tree contributes model context.' },
|
||||
'packages/interaction/permission': { kind: 'indirect', reason: 'The service writes mechanism events rendered by dsh-user-approval and dsh-tool-bash.' },
|
||||
'packages/interaction/user-interaction': { kind: 'indirect', reason: 'Model-facing consumers render provider answers and seam errors.' },
|
||||
'packages/interaction/permission-presets': { kind: 'indirect', reason: 'The service writes mechanism events rendered by dsh-user-approval and dsh-tool-bash.' },
|
||||
'packages/interaction/user-questions': { kind: 'indirect', reason: 'Model-facing consumers render provider answers and seam errors.' },
|
||||
'packages/util/timeout': { kind: 'indirect', reason: 'Only timeout consumers render timeout outcomes.' },
|
||||
'packages/util/retention': { kind: 'indirect', reason: 'Only retention consumers render retained content and omission metadata.' },
|
||||
'packages/util/output-retention': { kind: 'indirect', reason: 'Only retention consumers render retained content and omission metadata.' },
|
||||
'packages/util/native-command': { kind: 'none', reason: 'The host-side subprocess runner registers nothing model-facing.' },
|
||||
'packages/web/web': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-web.' },
|
||||
'packages/web/web-fetch-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },
|
||||
'packages/web/web-fetch-http': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },
|
||||
'packages/web/web-search-exa': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-web.' },
|
||||
'packages/workflow/workflow': { kind: 'indirect', reason: 'The service delegates parent and child model rendering to its consumer and engine.' },
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user