mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/doc/host-client-group-readmes' into feat/directory-picker
This commit is contained in:
33
scripts/ci-workflow.spec.ts
Normal file
33
scripts/ci-workflow.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const runnerPrivatePnpmDestination = '${{ runner.temp }}/setup-pnpm'
|
||||
|
||||
describe('CI workflow', () => {
|
||||
it('isolates every pnpm action setup destination per runner', () => {
|
||||
const workflow: unknown = yaml.load(readFileSync(resolve(root, '.github/workflows/ci.yml'), 'utf8'))
|
||||
if (!isRecord(workflow) || !isRecord(workflow.jobs)) throw new TypeError('CI workflow must define jobs')
|
||||
|
||||
const setups = Object.entries(workflow.jobs).flatMap(([jobName, job]) => {
|
||||
if (!isRecord(job) || !Array.isArray(job.steps)) return []
|
||||
return job.steps.flatMap((step) => {
|
||||
if (!isRecord(step) || typeof step.uses !== 'string' || !step.uses.startsWith('pnpm/action-setup@')) return []
|
||||
return [{ jobName, step }]
|
||||
})
|
||||
})
|
||||
|
||||
expect(setups.length).toBeGreaterThan(0)
|
||||
for (const { jobName, step } of setups) {
|
||||
expect(step, `${jobName} must not share pnpm/action-setup's default destination`).toMatchObject({
|
||||
with: { dest: runnerPrivatePnpmDestination },
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
@@ -7,7 +7,14 @@ import { spawn } from 'node:child_process'
|
||||
|
||||
// Each UI's node invocation matches its base demo script plus the overlay config.
|
||||
const UIS = new Map([
|
||||
['tui', ['--import', 'tsx', 'apps/cli/src/bin.ts', '--config', 'examples/tui-agent/code-mode.cordis.yml']],
|
||||
['tui', [
|
||||
'--experimental-transform-types',
|
||||
'--import',
|
||||
'./scripts/tspath-loader.ts',
|
||||
'apps/cli/src/bin.ts',
|
||||
'--config',
|
||||
'examples/tui-agent/code-mode.cordis.yml',
|
||||
]],
|
||||
['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', 'examples/acp-agent/code-mode.cordis.yml']],
|
||||
])
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"AGENTS.md": 1705,
|
||||
"AGENTS.md": 1750,
|
||||
"docs/AGENTS.md": 1150,
|
||||
"docs/architecture.md": 1800,
|
||||
"docs/cordis-primer.md": 600,
|
||||
|
||||
@@ -209,6 +209,7 @@ const FOUNDATION_TYPE_NAMES = new Set([
|
||||
'AsyncIterable',
|
||||
'Context',
|
||||
'Error',
|
||||
'Partial',
|
||||
'Pick',
|
||||
'Promise',
|
||||
'Readonly',
|
||||
@@ -238,6 +239,7 @@ const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
SessionProjectionMap: 'merge-extensible projection key map is owned by packages/session-projection/session-projection/src/types.ts',
|
||||
ProjectionChangeListener: 'change-feed listener contract is owned by packages/session-projection/session-projection/src/index.ts',
|
||||
ProjectionSnapshot: 'watermark snapshot shape is owned by packages/session-projection/session-projection/src/index.ts',
|
||||
ProjectionCheckpoint: 'persisted checkpoint row map is owned by packages/session-projection/session-projection/src/index.ts',
|
||||
CommandExecution: 'executor return contract is owned by packages/ui/commands/src/index.ts',
|
||||
InvariantInstaller: 'service-local contribution contract is owned by packages/support/invariants/README.md',
|
||||
LocaleDict: 'service-local dictionary shape is owned by packages/client/i18n/src/index.ts',
|
||||
|
||||
@@ -244,6 +244,14 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
consumers: ['tool-todo', 'session-title', 'host-apiproxy'],
|
||||
note: 'Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values.',
|
||||
},
|
||||
{
|
||||
key: 'sessionProjectionCache',
|
||||
pkg: 'session-projection-cache',
|
||||
title: 'Persisted projection cache',
|
||||
mode: 'core',
|
||||
consumers: ['host-apiproxy'],
|
||||
note: 'Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs.',
|
||||
},
|
||||
{
|
||||
key: 'tui',
|
||||
pkg: 'tui',
|
||||
|
||||
14
scripts/tspath-loader.ts
Normal file
14
scripts/tspath-loader.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/** Register source-only tsconfig paths resolution before a TypeScript entry loads. */
|
||||
|
||||
import { register } from 'node:module'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const tsconfigPath = process.env.TSX_TSCONFIG_PATH === undefined
|
||||
? fileURLToPath(new URL('../tsconfig.json', import.meta.url))
|
||||
: resolve(process.env.TSX_TSCONFIG_PATH)
|
||||
|
||||
register(new URL('../apps/cli/src/tsconfig-paths-loader.ts', import.meta.url), {
|
||||
parentURL: import.meta.url,
|
||||
data: { tsconfigPath },
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Validate Cordis Loader entry metadata and example package resolution.
|
||||
* Validate Cordis Loader entry metadata and package resolution.
|
||||
*
|
||||
* The Loader interpolates only a plugin entry's `config`; expression objects in
|
||||
* fields such as `disabled` remain truthy data and silently change composition.
|
||||
* Example configs run from built packages, so every named package must resolve
|
||||
* from the examples workspace and every local package must be in the root
|
||||
* Example configs and the dsh Web composition resolve named plugins from their
|
||||
* owning workspace manifests. Local example packages must also be in the root
|
||||
* TypeScript project graph.
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,7 @@ const schema = yaml.JSON_SCHEMA.extend(jsExprType)
|
||||
|
||||
const files = cordisConfigFiles(root)
|
||||
const errors: string[] = []
|
||||
const examplePluginReferences: PluginReference[] = []
|
||||
const pluginReferences: PluginReference[] = []
|
||||
|
||||
for (const file of files) {
|
||||
const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema })
|
||||
@@ -56,9 +56,10 @@ for (const file of files) {
|
||||
}
|
||||
|
||||
errors.push(...validateExampleResolution())
|
||||
errors.push(...validateAppResolution())
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('verify-cordis-config: invalid Loader metadata or example package resolution:')
|
||||
console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:')
|
||||
for (const error of errors) console.error(`- ${error}`)
|
||||
process.exitCode = 1
|
||||
} else {
|
||||
@@ -70,7 +71,7 @@ function validateEntry(value: unknown, file: string, path: string): void {
|
||||
errors.push(`${file}${path}: entry must be an object`)
|
||||
return
|
||||
}
|
||||
recordExamplePlugin(value, file)
|
||||
recordPlugin(value, file)
|
||||
validateMetadata(value, file, path)
|
||||
if ((value.group === true || value.name === '@cordisjs/plugin-group') && isUnknownArray(value.config)) {
|
||||
for (let index = 0; index < value.config.length; index++) {
|
||||
@@ -84,7 +85,7 @@ function validateEntry(value: unknown, file: string, path: string): void {
|
||||
const patch = config.patches[index]
|
||||
const patchPath = `${path}.config.patches[${index}]`
|
||||
if (!isRecord(patch)) continue
|
||||
recordExamplePlugin(patch, file)
|
||||
recordPlugin(patch, file)
|
||||
validateMetadata(patch, file, patchPath)
|
||||
if (!isUnknownArray(patch.insert)) continue
|
||||
for (let insertIndex = 0; insertIndex < patch.insert.length; insertIndex++) {
|
||||
@@ -93,10 +94,8 @@ function validateEntry(value: unknown, file: string, path: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function recordExamplePlugin(entry: Record<string, unknown>, file: string): void {
|
||||
if (file.startsWith('examples/') && typeof entry.name === 'string') {
|
||||
examplePluginReferences.push({ file, name: entry.name })
|
||||
}
|
||||
function recordPlugin(entry: Record<string, unknown>, file: string): void {
|
||||
if (typeof entry.name === 'string') pluginReferences.push({ file, name: entry.name })
|
||||
}
|
||||
|
||||
function validateExampleResolution(): string[] {
|
||||
@@ -105,25 +104,13 @@ function validateExampleResolution(): string[] {
|
||||
const dependencies = exampleManifest.dependencies ?? {}
|
||||
const localPackages = localPackageDirectories()
|
||||
const rootReferences = rootProjectReferences()
|
||||
const requiredPackages = new Map<string, Set<string>>()
|
||||
|
||||
for (const reference of examplePluginReferences) {
|
||||
const packageName = packageNameFromSpecifier(reference.name)
|
||||
if (packageName === undefined) continue
|
||||
const locations = requiredPackages.get(packageName) ?? new Set<string>()
|
||||
locations.add(reference.file)
|
||||
requiredPackages.set(packageName, locations)
|
||||
}
|
||||
|
||||
for (const [packageName, locations] of requiredPackages) {
|
||||
if (!(packageName in dependencies)) {
|
||||
violations.push(`${[...locations].join(', ')}: ${packageName} must be declared in examples/package.json dependencies`)
|
||||
}
|
||||
}
|
||||
const exampleReferences = pluginReferences.filter(reference => reference.file.startsWith('examples/'))
|
||||
violations.push(...missingPluginDependencies(exampleReferences, dependencies, 'examples/package.json'))
|
||||
const requiredPackages = new Set(exampleReferences.map(reference => packageNameFromSpecifier(reference.name)))
|
||||
|
||||
const localExamplePackages = new Set([
|
||||
...Object.keys(dependencies),
|
||||
...requiredPackages.keys(),
|
||||
...[...requiredPackages].filter(packageName => packageName !== undefined),
|
||||
])
|
||||
for (const packageName of localExamplePackages) {
|
||||
const packageDirectory = localPackages.get(packageName)
|
||||
@@ -135,6 +122,30 @@ function validateExampleResolution(): string[] {
|
||||
return violations
|
||||
}
|
||||
|
||||
function validateAppResolution(): string[] {
|
||||
const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
|
||||
const references = pluginReferences.filter(reference => reference.file === 'apps/cli/cordis.yml')
|
||||
return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
|
||||
}
|
||||
|
||||
function missingPluginDependencies(
|
||||
references: readonly PluginReference[],
|
||||
dependencies: Readonly<Record<string, string>>,
|
||||
manifestPath: string,
|
||||
): string[] {
|
||||
const requiredPackages = new Map<string, Set<string>>()
|
||||
for (const reference of references) {
|
||||
const packageName = packageNameFromSpecifier(reference.name)
|
||||
if (packageName === undefined) continue
|
||||
const locations = requiredPackages.get(packageName) ?? new Set<string>()
|
||||
locations.add(reference.file)
|
||||
requiredPackages.set(packageName, locations)
|
||||
}
|
||||
return [...requiredPackages].flatMap(([packageName, locations]) => packageName in dependencies
|
||||
? []
|
||||
: `${[...locations].join(', ')}: ${packageName} must be declared in ${manifestPath} dependencies`)
|
||||
}
|
||||
|
||||
function readManifest(path: string): PackageManifest {
|
||||
return JSON.parse(readFileSync(resolve(root, path), 'utf8')) as PackageManifest
|
||||
}
|
||||
@@ -177,7 +188,7 @@ function rootProjectReferences(): Set<string> {
|
||||
}
|
||||
|
||||
function packageNameFromSpecifier(specifier: string): string | undefined {
|
||||
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:')) return undefined
|
||||
if (specifier.startsWith('.') || specifier.startsWith('/') || /^[a-z][a-z+.-]*:/i.test(specifier)) return undefined
|
||||
const segments = specifier.split('/')
|
||||
if (specifier.startsWith('@')) {
|
||||
return segments.length >= 2 ? `${segments[0]}/${segments[1]}` : undefined
|
||||
|
||||
@@ -47,6 +47,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/code-runtime/code-runtime-worker': { kind: 'indirect', reason: 'The worker backend delegates model rendering to Code Mode in dsh-tools.' },
|
||||
'packages/client/hmr': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/modules': { kind: 'none', reason: 'Browser-side module-loading kernel machinery; registers no model surface.' },
|
||||
'packages/client/test-runtime': { kind: 'none', reason: 'Browser-side test infrastructure (jsdom bench); registers no model surface.' },
|
||||
'packages/client/ui-slots': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-primitives': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/web-react': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
@@ -92,6 +93,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/sdk/sdk-protocol': { kind: 'none', reason: 'Client-facing wire library; the runtime plugins behind the serving entry own the model surface.' },
|
||||
'packages/sdk/telemetry': { kind: 'none', reason: 'The launcher-side reporter sends developer-cycle telemetry and registers no live agent or model surface.' },
|
||||
'packages/session-projection/session-projection': { kind: 'none', reason: 'The projection registry serves client-facing read models of already-logged session state and registers no model surface.' },
|
||||
'packages/session-projection/session-projection-cache': { kind: 'none', reason: 'The persisted cache accelerates host-side cold reads of projection state and registers no model surface.' },
|
||||
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers no model surface.' },
|
||||
'packages/session-query/session-query-sqlite': { kind: 'none', reason: 'The search backend returns hits only to callers and registers no model surface.' },
|
||||
'packages/telemetry/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers no model surface.' },
|
||||
|
||||
Reference in New Issue
Block a user