Merge origin/master into codex/dsh-badge-plugin

This commit is contained in:
Tianyi Cui
2026-08-08 15:33:41 +08:00
900 changed files with 26967 additions and 3578 deletions

View File

@@ -7,7 +7,8 @@
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
import { 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
@@ -123,6 +124,7 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
const typeRTRemoteNavigation = hasTypeRTRemoteNavigation(manifest)
return [
'lib/index.js',
// Every package publishes its invariant ownership companion as a separate
@@ -146,9 +148,37 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
// declarations.
...usesEmittedTreeDefaults(manifest) ? ['lib/types/**/*.js'] : [],
'lib/types/**/*.d.ts',
...hasExportPair(manifest, './typert', './lib/typert.host.d.ts', './lib/typert.host.js')
? ['lib/typert.host.js', 'lib/typert.host.d.ts']
: [],
...hasExportPair(manifest, './client/typert', './lib/typert.client.d.ts', './lib/typert.client.js')
? ['lib/typert.client.js', 'lib/typert.client.d.ts']
: [],
...typeRTRemoteNavigation
? [
'lib/typert.remote-client.js',
'lib/typert.remote-client.d.ts',
'lib/typert.remote-client.d.ts.map',
'src',
]
: [],
]
}
/** Whether one conditional export exactly names the generated runtime and declaration pair. */
function hasExportPair(
manifest: PackageManifest,
subpath: string,
types: string,
runtime: string,
): boolean {
const entry = manifest.exports?.[subpath]
return typeof entry === 'object'
&& entry !== null
&& entry.types === types
&& entry.default === runtime
}
/** Runtime target of an export entry: conditional `default`, or the bare-string shorthand. */
function exportDefault(manifest: PackageManifest, subpath: string): string | undefined {
const entry = manifest.exports?.[subpath]
@@ -176,8 +206,9 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
}
if (manifest.name?.startsWith('@deepseek-ai/')) {
const publicationPolicy = { typeRTRemoteNavigation: hasTypeRTRemoteNavigation(manifest) }
for (const file of manifest.files ?? []) {
if (isForbiddenPublicationFile(file)) {
if (isForbiddenPublicationFile(file, publicationPolicy)) {
errors.push(`${label}: package.json files must not publish ${JSON.stringify(file)}`)
}
}
@@ -276,6 +307,7 @@ const errors = [
...checkRepositoryVersion(),
...workspaceManifests().flatMap(checkWorkspace),
...checkHierarchyShape(),
...collectProjectReferenceFaceViolations(root),
]
if (errors.length > 0) {
console.error(errors.join('\n'))

View File

@@ -28,6 +28,34 @@ describe('CI workflow', () => {
})
})
describe('Issue lifecycle workflow', () => {
it('uses review signals instead of rerunning when a draft becomes ready', () => {
const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')
const lifecyclePullRequest = workflowEvent(lifecycle, 'pull_request')
const lifecycleReview = workflowEvent(lifecycle, 'pull_request_review')
const policy = loadWorkflow('.github/workflows/issue-policy.yml')
const policyPullRequest = workflowEvent(policy, 'pull_request')
expect(lifecyclePullRequest.types).not.toContain('ready_for_review')
expect(lifecyclePullRequest.types).toContain('review_requested')
expect(lifecycleReview.types).toContain('submitted')
expect(policyPullRequest.types).toContain('ready_for_review')
})
})
function loadWorkflow(path: string): Record<string, unknown> {
const workflow: unknown = yaml.load(readFileSync(resolve(root, path), 'utf8'))
if (!isRecord(workflow)) throw new TypeError(`${path} must define a workflow`)
return workflow
}
function workflowEvent(workflow: Record<string, unknown>, event: string): Record<string, unknown> {
if (!isRecord(workflow.on) || !isRecord(workflow.on[event])) {
throw new TypeError(`workflow must define the ${event} event`)
}
return workflow.on[event]
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

View File

@@ -15,8 +15,13 @@ interface CssPlugin {
}
function cssPlugin(): CssPlugin {
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
const plugins = (configs[1] as { plugins: CssPlugin[] }).plugins
const configs = clientBundle(
'@deepseek-ai/dsh-client-test',
['lib/types/index.js', 'lib/types/invariant.js'],
)({ env: { DSH_BUILD_FACE: 'client' } })
const client = configs.find(config => config.platform === 'browser')
if (client === undefined) throw new Error('client config missing')
const plugins = (client as { plugins: CssPlugin[] }).plugins
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
if (plugin === undefined) throw new Error('CSS Modules plugin missing from client config')
return plugin

View File

@@ -14,6 +14,24 @@ interface CssModulePlugin {
load?: (this: { addWatchFile: (id: string) => void }, id: string) => Promise<unknown>
}
function clientConfigs(id = '@deepseek-ai/dsh-client-test') {
return clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])(
{ env: { DSH_BUILD_FACE: 'client' } },
).filter(config => config.platform === 'browser')
}
describe('client bundle build faces', () => {
it('watches source in development and consumes emitted JavaScript in the Client build', () => {
const bundle = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js'])
const development = bundle({ env: {} }).find(config => config.platform === 'browser')
const artifact = bundle({ env: { DSH_BUILD_FACE: 'client' } })
.find(config => config.platform === 'browser')
expect(development?.entry).toEqual({ client: 'src/client/index.ts' })
expect(artifact?.entry).toEqual({ client: 'lib/types/client/index.js' })
})
})
function clientSourceMapPath(packagePath: string): string {
return fileURLToPath(new URL(`../packages/${packagePath}/lib/client.js.map`, import.meta.url))
}
@@ -21,16 +39,16 @@ function clientSourceMapPath(packagePath: string): string {
function purityResolveId(): ResolveId {
// libEntry is spelled at every call site (no default) so the
// package-invariants text check can see the invariant entry per package.
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
const plugins = (configs[1] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
const configs = clientConfigs()
const plugins = (configs[0] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
const gate = plugins.find(p => p.name === 'dsh-client-bundle-purity')
if (gate?.resolveId === undefined) throw new Error('purity plugin missing from client config')
return gate.resolveId as ResolveId
}
function cssModulePlugin(): CssModulePlugin {
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
const plugins = (configs[1] as { plugins: CssModulePlugin[] }).plugins
const configs = clientConfigs()
const plugins = (configs[0] as { plugins: CssModulePlugin[] }).plugins
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
if (plugin?.resolveId === undefined || plugin.load === undefined) {
throw new Error('CSS Modules plugin missing from client config')
@@ -59,6 +77,13 @@ describe('client bundle purity gate', () => {
expect(resolveId('@deepseek-ai/dsh-brand')).toBeNull()
})
it('lets exact generated Remote contributions inline without admitting their package implementation', () => {
expect(resolveId('@deepseek-ai/dsh-goal/remote')).toBeNull()
expect(() => resolveId('@deepseek-ai/dsh-goal')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-goal/client')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-goal/remote/nested')).toThrow(/purity/)
})
it('throws on any other @deepseek-ai leak', () => {
expect(() => resolveId('@deepseek-ai/dsh-agent')).toThrow(/purity/)
expect(() => resolveId('@deepseek-ai/dsh-client-web')).toThrow(/purity/)
@@ -80,13 +105,13 @@ describe('client bundle purity gate', () => {
describe('client bundle debug artifacts', () => {
it('emits source maps for plugin TS and TSX outside the Vite module graph', () => {
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
expect(configs[1]?.sourcemap).toBe(true)
const configs = clientConfigs()
expect(configs[0]?.sourcemap).toBe(true)
})
it('maps first-party sources to their repository package paths', () => {
const configs = clientBundle('@deepseek-ai/dsh-client-ui-goal', ['lib/types/index.js', 'lib/types/invariant.js'])
const outputOptions = configs[1]?.outputOptions
const configs = clientConfigs('@deepseek-ai/dsh-client-ui-goal')
const outputOptions = configs[0]?.outputOptions
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
const transform = outputOptions.sourcemapPathTransform
if (transform === undefined) throw new Error('client sourcemap path transform missing')
@@ -98,8 +123,8 @@ describe('client bundle debug artifacts', () => {
})
it('maps dual-face host sources to the host package group', () => {
const configs = clientBundle('@deepseek-ai/dsh-host-directory-picker-native', ['lib/types/index.js'])
const outputOptions = configs[1]?.outputOptions
const configs = clientConfigs('@deepseek-ai/dsh-host-directory-picker-native')
const outputOptions = configs[0]?.outputOptions
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
const transform = outputOptions.sourcemapPathTransform
if (transform === undefined) throw new Error('client sourcemap path transform missing')
@@ -109,8 +134,8 @@ describe('client bundle debug artifacts', () => {
})
it('maps inlined workspace sources to packages and leaves dependencies outside it unchanged', () => {
const configs = clientBundle('@deepseek-ai/dsh-client-connection', ['lib/types/index.js'])
const outputOptions = configs[1]?.outputOptions
const configs = clientConfigs('@deepseek-ai/dsh-client-connection')
const outputOptions = configs[0]?.outputOptions
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
const transform = outputOptions.sourcemapPathTransform
if (transform === undefined) throw new Error('client sourcemap path transform missing')

View File

@@ -22,13 +22,7 @@ export default defineConfig({
const bundlePath = join(root, 'lib/client.js')
await writeFile(sourcePath, 'export const version = "watch-v1"\n')
bundles = await watchClientPlugins(root, ['.'], 50)
await expect.poll(async () => {
try {
return (await readFile(bundlePath, 'utf8')).includes('watch-v1')
} catch {
return false
}
}, { timeout: 10_000 }).toBe(true)
expect(await readFile(bundlePath, 'utf8')).toContain('watch-v1')
await new Promise(resolve => setTimeout(resolve, 1_000))
await writeFile(sourcePath, `export const version = "watch-v2-${'x'.repeat(100)}"\n`)

View File

@@ -47,21 +47,40 @@ export function discoverPluginDirs(root = repoRoot): string[] {
* @param root - repository or fixture root passed to tsdown.
* @param pluginDirs - workspace-relative package directories to watch.
* @param pollInterval - optional source-watcher polling interval in milliseconds.
* @returns live bundles whose async disposers stop every watcher.
* @returns live bundles after every watcher has completed its initial build.
*/
export async function watchClientPlugins(
root: string,
pluginDirs: readonly string[],
pollInterval?: number,
): Promise<TsdownBundle[]> {
return build({
let resolveInitialBuilds: (() => void) | undefined
const initialBuilds = new Promise<void>((resolve) => { resolveInitialBuilds = resolve })
const initialized = new WeakSet<object>()
const readiness: { expectedBuilds?: number; initializedBuilds: number } = { initializedBuilds: 0 }
const bundles = await build({
cwd: root,
workspace: [...pluginDirs],
watch: true,
hooks: {
'build:done': ({ options }) => {
if (initialized.has(options)) return
initialized.add(options)
readiness.initializedBuilds += 1
if (
readiness.expectedBuilds !== undefined
&& readiness.initializedBuilds >= readiness.expectedBuilds
) resolveInitialBuilds?.()
},
},
...pollInterval !== undefined
? { inputOptions: { watch: { watcher: { usePolling: true, pollInterval } } } }
: {},
})
readiness.expectedBuilds = bundles.length
if (readiness.initializedBuilds >= readiness.expectedBuilds) resolveInitialBuilds?.()
await initialBuilds
return bundles
}
const invokedPath = process.argv[1]

View File

@@ -1,5 +1,5 @@
{
"AGENTS.md": 1775,
"AGENTS.md": 1782,
"docs/AGENTS.md": 1320,
"docs/architecture.md": 2160,
"docs/cordis-primer.md": 600,
@@ -7,5 +7,5 @@
"docs/testing.md": 1150,
"examples/AGENTS.md": 310,
"packages/AGENTS.md": 675,
"packages/README.md": 920
"packages/README.md": 936
}

View File

@@ -136,10 +136,9 @@ function formatDiagnostics(diagnostics: readonly ts.Diagnostic[], blocks: Block[
}
/**
* Reuse the host-aggregate references from a temp project one directory below
* root. Doc fragments speak the host vocabulary, so the standalone project
* seeds tsconfig.host.json (never the root solution: flattening host+client
* into one program collides the cordis Context merges).
* Reuse the Host aggregate references from a temp project one directory below
* root. Generated Client API examples opt out because their declarations do
* not exist until Host tsdown has run.
*/
function workspaceReferences(): { path: string }[] {
const file = join(root, 'tsconfig.host.json')

View File

@@ -126,12 +126,13 @@ function loadFile(abs: string, rel: string, cache: Map<string, FileCtx>): FileCt
}
/** A type declaration a paste can contain. */
type TypeDecl = ts.InterfaceDeclaration | ts.TypeAliasDeclaration
type TypeDecl = ts.InterfaceDeclaration | ts.TypeAliasDeclaration | ts.EnumDeclaration
/** Find an interface/type-alias declaration by name in a file, or null. */
/** Find a pasteable type declaration by name in a file, or null. */
function findTypeDecl(ctx: FileCtx, name: string): TypeDecl | null {
for (const stmt of ctx.sf.statements) {
if ((ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) && stmt.name.text === name) return stmt
if ((ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt) || ts.isEnumDeclaration(stmt))
&& stmt.name.text === name) return stmt
}
return null
}
@@ -207,7 +208,7 @@ function checkMemberDocs(ctx: FileCtx, decl: TypeDecl, violations: string[]): vo
else ts.forEachChild(type, (n) => { walkNested(n, path) })
}
if (ts.isInterfaceDeclaration(decl)) walkMembers(decl.members, decl.name.text)
else walkNested(decl.type, decl.name.text)
else if (ts.isTypeAliasDeclaration(decl)) walkNested(decl.type, decl.name.text)
}
/** Cross-file resolution context for the schema-path check. */

View File

@@ -90,6 +90,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
FsWriteIntent: 'filesystem.md',
FsWriteOutcome: 'filesystem.md',
CreateGoalRequest: 'goal.md',
CreateGoalResult: 'goal.md',
EditGoalRequest: 'goal.md',
GoalBlockReason: 'goal.md',
GoalChanged: 'goal.md',
@@ -276,6 +277,7 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
TypertPackageRecord: 'registry package record is owned by packages/typert/registry/README.md',
TypertSchemaFilter: 'registry schema query filter is owned by packages/typert/registry/README.md',
TypertSchemaRecord: 'registry schema record is owned by packages/typert/registry/README.md',
TypeRTDisposer: 'TypeRT lifecycle contract is owned by packages/typert/type-meta/README.md',
'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',
InvariantInstaller: 'service-local contribution contract is owned by packages/support/invariants/README.md',
@@ -287,6 +289,7 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
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',
InvariantRegistration: 'service-local lifecycle handle is owned by packages/support/invariants/README.md',
InvokeRemoteRequest: 'gateway invocation contract is owned by packages/api/gateway/README.md',
PresetOption: 'deployment menu metadata is owned by packages/ui/permission/README.md',
PresetSpec: 'deployment preset composition is owned by packages/ui/permission/README.md',
KnobState: 'projection unit state shape is owned by packages/ui/permission/README.md',

View File

@@ -140,8 +140,15 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'typert-registry',
title: 'Runtime type registry',
mode: 'core',
consumers: ['typert-loader'],
note: 'Plugins register live zod contributions directly or through dsh-typert-loader; runtime consumers query schemas and reflection metadata at their own edges.',
consumers: ['typert-loader', 'api-gateway'],
note: 'Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges.',
},
{
key: 'typertGateway',
pkg: 'api-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.',
},
{
key: 'sessionPersistence',

View File

@@ -1,7 +1,7 @@
#!/bin/sh
# dsh one-line installer.
#
# curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh
# curl -fsSL https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/master/scripts/install.sh | sh
#
# It clones the harness under ~/.dsh/source (the master clone at
# ~/.dsh/source/master), adds a per-install staging worktree at
@@ -47,12 +47,10 @@
# DSH_CURRENT stable symlink to the active worktree (default: $DSH_SOURCE/current)
# DSH_BIN_DIR directory the `dsh` symlink lands in (default: ~/.local/bin)
# DSH_HOME Harness home holding profiles and user patches (default: ~/.dsh)
# FIXME(install-ts): Move the post-checkout workflow into a tested TypeScript
# entrypoint; keep this POSIX shell file as the curl/source bootstrap.
set -eu
DSH_REF=${DSH_REF:-master}
DSH_REPO=${DSH_REPO:-https://github.com/deepseek-harness/deepseek-harness.git}
DSH_REPO=${DSH_REPO:-https://github.com/deepseek-ai/deepseek-harness-sdk.git}
# DSH_SOURCE is the staging-worktree container and the default home of `current`.
# DSH_MASTER names the main clone: clone mode defaults it inside DSH_SOURCE,
# while adoption discovers an existing clone anywhere on disk. Remember whether

View File

@@ -221,6 +221,39 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
expect(result.status, normalizedOutput(result)).toBe(0)
})
it('keeps staged validation project-free while preserving source rules', async () => {
const configPath = join(repositoryRoot, '.oxlintrc.staged.json')
const result = parseConfigFileTextToJson(configPath, await readFile(configPath, 'utf8'))
if (result.error !== undefined) {
throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
}
expect(result.config).toMatchObject({
extends: ['./.oxlintrc.json'],
options: { typeAware: false },
})
const suffix = randomUUID()
const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
try {
await writeFile(path, 'export const value={answer:1};\n')
const lint = runOxlint([
'--config',
relative(repositoryRoot, configPath),
'--format',
'unix',
relative(repositoryRoot, path),
])
const output = normalizedOutput(lint)
expect(lint.error).toBeUndefined()
expect(lint.status, output).toBe(1)
expect(output).toContain('@stylistic')
expect(output).not.toContain('typescript(')
} finally {
await rm(path, { force: true })
}
})
it('applies staged stylistic fixes before Oxlint validation', async () => {
const suffix = randomUUID()
const configPath = await writeContractConfig(suffix)

View File

@@ -72,6 +72,20 @@ describe('package invariant gate', () => {
expect(collectPackageInvariantViolations(fixture())).toEqual([])
})
it('accepts an invariant reference owned by a package-local leaf project', () => {
const root = fixture({ invariantReference: false })
const dir = join(root, 'packages/core/probe')
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
files: [],
references: [{ path: './tsconfig.host.json' }],
}, null, 2)}\n`)
writeFileSync(join(dir, 'tsconfig.host.json'), `${JSON.stringify({
references: [{ path: '../../support/invariants' }],
}, null, 2)}\n`)
expect(collectPackageInvariantViolations(root)).toEqual([])
})
it('rejects missing publication metadata and build output', () => {
const violations = collectPackageInvariantViolations(fixture({
invariantExport: false,

View File

@@ -118,11 +118,8 @@ function checkBuild(
violations: PackageInvariantViolation[],
): void {
const tsconfigPath = `${owner.dir}/tsconfig.json`
const tsconfig = JSON.parse(readFileSync(resolve(root, tsconfigPath), 'utf8')) as {
references?: Array<{ path?: string }>
}
if (owner.packageName !== '@deepseek-ai/dsh-invariants'
&& !tsconfig.references?.some(reference => reference.path === '../../support/invariants')) {
&& !projectReferencesInvariants(root, owner.dir, tsconfigPath)) {
addViolation(
violations,
tsconfigPath,
@@ -138,6 +135,31 @@ function checkBuild(
}
}
function projectReferencesInvariants(root: string, ownerDir: string, entryPath: string): boolean {
const ownerRoot = resolve(root, ownerDir)
const target = resolve(root, 'packages/support/invariants')
const pending = [resolve(root, entryPath)]
const visited = new Set<string>()
while (pending.length > 0) {
const configPath = pending.pop()
if (configPath === undefined) break
if (visited.has(configPath)) continue
visited.add(configPath)
const config = JSON.parse(readFileSync(configPath, 'utf8')) as {
references?: Array<{ path?: string }>
}
for (const reference of config.references ?? []) {
if (reference.path === undefined) continue
const referenced = resolve(dirname(configPath), reference.path)
if (referenced === target) return true
if (!referenced.startsWith(`${ownerRoot}${sep}`)) continue
const childConfig = referenced.endsWith('.json') ? referenced : resolve(referenced, 'tsconfig.json')
if (existsSync(childConfig)) pending.push(childConfig)
}
}
return false
}
function checkSource(
owner: PackageInvariantOwner,
root: string,

View File

@@ -104,7 +104,7 @@ describe('rewriteMarkdown', () => {
repositoryRef: 'abc123',
})).toBe(
'[B](./reference/b.md#part) '
+ '[source](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/packages/tool.ts#L2) '
+ '[source](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/packages/tool.ts#L2) '
+ '[web](https://example.com)\n',
)
})
@@ -130,7 +130,7 @@ describe('rewriteMarkdown', () => {
pages,
repoRoot: root,
repositoryRef: 'abc123',
})).toBe('![logo](https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/abc123/packages/logo.svg)\n')
})).toBe('![logo](https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/abc123/packages/logo.svg)\n')
})
it('hands an image to the placer and uses the URL it returns', () => {
@@ -209,7 +209,7 @@ describe('rewriteMarkdown', () => {
repositoryRef: 'abc123',
})).toBe(
'[title](./reference/b.md "b.md") '
+ '[escaped](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/docs/x(y).md)\n',
+ '[escaped](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/docs/x(y).md)\n',
)
})

View File

@@ -15,7 +15,7 @@ import { gfm } from 'micromark-extension-gfm'
import type { Nodes } from 'mdast'
import { docsPages, type DocsLocale, type DocsPage } from '../website/docs.ts'
const REPOSITORY_URL = 'https://github.com/deepseek-harness/deepseek-harness'
const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness-sdk'
const root = resolve(import.meta.dirname, '..')
const generatedRoot = resolve(root, 'website/.generated')
@@ -203,7 +203,7 @@ function githubTarget(
image: boolean,
): string {
const path = repoPath(absPath, repoRoot)
if (image) return `https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/${repositoryRef}/${path}${suffix}`
if (image) return `https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/${repositoryRef}/${path}${suffix}`
const kind = lstatSync(absPath).isDirectory() ? 'tree' : 'blob'
const lineSuffix = line === undefined ? suffix : `#L${line}`
return `${REPOSITORY_URL}/${kind}/${repositoryRef}/${path}${lineSuffix}`

View File

@@ -0,0 +1,100 @@
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 { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function writeJson(path: string, value: unknown): void {
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`)
}
function workspaceFixture(options: {
readonly host: readonly string[]
readonly client: readonly string[]
}): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-project-reference-faces-'))
roots.push(root)
const shared = join(root, 'packages/core/shared')
const split = join(root, 'packages/api/split')
mkdirSync(shared, { recursive: true })
mkdirSync(split, { recursive: true })
writeJson(join(root, 'tsconfig.base.json'), {})
writeJson(join(root, 'tsconfig.base.client.json'), { extends: './tsconfig.base.json' })
writeJson(join(shared, 'package.json'), { name: '@deepseek-ai/dsh-shared' })
writeJson(join(shared, 'tsconfig.json'), {
extends: '../../../tsconfig.base.json',
references: [],
})
writeJson(join(split, 'package.json'), { name: '@deepseek-ai/dsh-split' })
writeJson(join(split, 'tsconfig.json'), {
files: [],
references: [{ path: './tsconfig.host.json' }, { path: './tsconfig.client.json' }],
})
writeJson(join(split, 'tsconfig.host.json'), { references: [{ path: '../../core/shared' }] })
writeJson(join(split, 'tsconfig.client.json'), { references: [{ path: '../../core/shared' }] })
writeJson(join(root, 'tsconfig.host.json'), {
references: options.host.map(path => ({ path })),
})
writeJson(join(root, 'tsconfig.client.json'), {
references: options.client.map(path => ({ path })),
})
return root
}
describe('Project Reference compiler faces', () => {
it('allows neutral projects in either graph and matching split leaves', () => {
const root = workspaceFixture({
host: ['./packages/core/shared', './packages/api/split/tsconfig.host.json'],
client: ['./packages/core/shared', './packages/api/split/tsconfig.client.json'],
})
expect(collectProjectReferenceFaceViolations(root)).toEqual([])
})
it('rejects the opposite leaf and the solution root of a split project', () => {
const root = workspaceFixture({
host: [
'./packages/api/split/tsconfig.host.json',
'./packages/api/split/tsconfig.client.json',
],
client: ['./packages/api/split'],
})
expect(collectProjectReferenceFaceViolations(root)).toEqual([
'tsconfig.client.json: Project Reference "./packages/api/split" enters split project packages/api/split from a Client config; reference "packages/api/split/tsconfig.client.json" instead',
'tsconfig.host.json: Project Reference "./packages/api/split/tsconfig.client.json" enters split project packages/api/split from a Host config; reference "packages/api/split/tsconfig.host.json" instead',
])
})
it('uses the referencing project face throughout the reachable graph', () => {
const root = workspaceFixture({
host: ['./packages/core/host-consumer'],
client: ['./packages/core/client-consumer'],
})
const hostConsumer = join(root, 'packages/core/host-consumer')
mkdirSync(hostConsumer, { recursive: true })
writeJson(join(hostConsumer, 'package.json'), { name: '@deepseek-ai/dsh-host-consumer' })
writeJson(join(hostConsumer, 'tsconfig.json'), {
extends: '../../../tsconfig.base.json',
references: [{ path: '../../api/split/tsconfig.client.json' }],
})
const clientConsumer = join(root, 'packages/core/client-consumer')
mkdirSync(clientConsumer, { recursive: true })
writeJson(join(clientConsumer, 'package.json'), { name: '@deepseek-ai/dsh-client-consumer' })
writeJson(join(clientConsumer, 'tsconfig.json'), {
extends: '../../../tsconfig.base.client.json',
references: [{ path: '../../api/split/tsconfig.host.json' }],
})
expect(collectProjectReferenceFaceViolations(root)).toEqual([
'packages/core/client-consumer/tsconfig.json: Project Reference "../../api/split/tsconfig.host.json" enters split project packages/api/split from a Client config; reference "packages/api/split/tsconfig.client.json" instead',
'packages/core/host-consumer/tsconfig.json: Project Reference "../../api/split/tsconfig.client.json" enters split project packages/api/split from a Host config; reference "packages/api/split/tsconfig.host.json" instead',
])
})
})

View File

@@ -0,0 +1,129 @@
/** Validate compiler-face isolation across workspace Project Reference graphs. */
import { existsSync, globSync } from 'node:fs'
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path'
import ts from 'typescript'
type ProjectFace = 'host' | 'client'
interface ProjectReferenceConfig {
readonly extends?: unknown
readonly references?: ReadonlyArray<{ readonly path?: unknown }>
}
const WORKSPACE_MANIFESTS = [
'packages/*/*/package.json',
'apps/*/package.json',
'vendor/*/package.json',
] as const
/**
* Find references that enter the wrong leaf of a split Host/Client project.
*
* A single-config project is neutral and may participate in either graph. Once
* a package declares both face configs, every reachable reference must name
* the leaf matching the aggregate from which traversal began.
*
* @param root - Repository root containing both aggregate tsconfigs.
* @returns Repo-relative diagnostics for every mismatched reference edge.
*/
export function collectProjectReferenceFaceViolations(root: string): string[] {
const splitRoots = splitProjectRoots(root)
const violations: string[] = []
const pending = [resolve(root, 'tsconfig.host.json'), resolve(root, 'tsconfig.client.json')]
const visited = new Set<string>()
for (let configPath = pending.pop(); configPath !== undefined; configPath = pending.pop()) {
if (visited.has(configPath) || !existsSync(configPath)) continue
visited.add(configPath)
const config = projectConfig(root, configPath)
const face = projectFace(root, configPath, config)
for (const reference of projectReferences(config)) {
const targetConfig = referenceConfigPath(configPath, reference)
const splitRoot = containingSplitRoot(splitRoots, targetConfig)
if (splitRoot !== undefined) {
if (face === undefined) {
violations.push(
`${repoPath(root, configPath)}: Project Reference ${JSON.stringify(reference)} enters split project ${repoPath(root, splitRoot)} from a config with no Host/Client face`,
)
continue
}
const expected = resolve(splitRoot, `tsconfig.${face}.json`)
if (targetConfig !== expected) {
violations.push(
`${repoPath(root, configPath)}: Project Reference ${JSON.stringify(reference)} enters split project ${repoPath(root, splitRoot)} from a ${faceLabel(face)} config; reference ${JSON.stringify(repoPath(root, expected))} instead`,
)
continue
}
}
pending.push(targetConfig)
}
}
return violations.sort()
}
function splitProjectRoots(root: string): string[] {
return globSync(WORKSPACE_MANIFESTS, { cwd: root })
.map(manifest => resolve(root, dirname(manifest)))
.filter(dir => existsSync(resolve(dir, 'tsconfig.host.json'))
&& existsSync(resolve(dir, 'tsconfig.client.json')))
.sort((left, right) => right.length - left.length)
}
function projectConfig(root: string, configPath: string): ProjectReferenceConfig {
const read = ts.readConfigFile(configPath, path => ts.sys.readFile(path))
if (read.error !== undefined) {
const message = ts.flattenDiagnosticMessageText(read.error.messageText, '\n')
throw new Error(`${repoPath(root, configPath)}: ${message}`)
}
return read.config as ProjectReferenceConfig
}
function projectReferences(config: ProjectReferenceConfig): string[] {
return (config.references ?? [])
.map(reference => reference.path)
.filter((path): path is string => typeof path === 'string')
}
function projectFace(
root: string,
configPath: string,
config: ProjectReferenceConfig,
seen = new Set<string>(),
): ProjectFace | undefined {
if (basename(configPath) === 'tsconfig.host.json') return 'host'
if (basename(configPath) === 'tsconfig.client.json') return 'client'
if (configPath === resolve(root, 'tsconfig.base.json')) return 'host'
if (configPath === resolve(root, 'tsconfig.base.client.json')) return 'client'
if (seen.has(configPath)) return undefined
seen.add(configPath)
const parent = localExtendsConfig(configPath, config.extends)
if (parent === undefined || !existsSync(parent)) return undefined
return projectFace(root, parent, projectConfig(root, parent), seen)
}
function localExtendsConfig(configPath: string, value: unknown): string | undefined {
if (typeof value !== 'string' || !value.startsWith('.')) return undefined
const target = resolve(dirname(configPath), value)
return target.endsWith('.json') ? target : `${target}.json`
}
function referenceConfigPath(sourceConfig: string, reference: string): string {
const target = resolve(dirname(sourceConfig), reference)
return target.endsWith('.json') ? target : resolve(target, 'tsconfig.json')
}
function containingSplitRoot(splitRoots: readonly string[], targetConfig: string): string | undefined {
return splitRoots.find((root) => {
const path = relative(root, targetConfig)
return path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path)
})
}
function repoPath(root: string, path: string): string {
return relative(root, path).split(sep).join('/')
}
function faceLabel(face: ProjectFace): string {
return face === 'host' ? 'Host' : 'Client'
}

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
import { isForbiddenPublicationFile, validateTarballPayload } from './publication-payload.ts'
import {
hasTypeRTRemoteNavigation,
isForbiddenPublicationFile,
validateTarballPayload,
} from './publication-payload.ts'
function validateFixtureTarball(files: readonly string[]): () => void {
return () => {
@@ -51,4 +55,29 @@ describe('publication payload policy', () => {
'package/lib/styles/base.css',
])).not.toThrow()
})
it('allows only the TypeRT declaration map and its navigable source tree when requested', () => {
const policy = { typeRTRemoteNavigation: true }
expect(isForbiddenPublicationFile('src/index.ts', policy)).toBe(false)
expect(isForbiddenPublicationFile('lib/typert.remote-client.d.ts.map', policy)).toBe(false)
expect(isForbiddenPublicationFile('lib/types/index.d.ts.map', policy)).toBe(true)
expect(() => {
validateTarballPayload([
'package/lib/typert.remote-client.d.ts.map',
'package/src/index.ts',
], 'fixture.tgz', policy)
}).not.toThrow()
})
it('recognizes only the canonical Host-for-Client export pair', () => {
expect(hasTypeRTRemoteNavigation({
exports: {
'./remote': {
types: './lib/typert.remote-client.d.ts',
default: './lib/typert.remote-client.js',
},
},
})).toBe(true)
expect(hasTypeRTRemoteNavigation({ exports: { './remote': './lib/remote.js' } })).toBe(false)
})
})

View File

@@ -1,5 +1,22 @@
/** Publication payload policy shared by static manifests and packed tarballs. */
/** Publication exceptions required for TypeRT declaration-map navigation. */
export interface PublicationPayloadPolicy {
readonly typeRTRemoteNavigation?: boolean
}
/** Whether a package manifest exports generated Host-for-Client metadata with source navigation. */
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
const remote = (exportsField as Record<string, unknown>)['./remote']
if (remote === null || typeof remote !== 'object' || Array.isArray(remote)) return false
const entry = remote as Record<string, unknown>
return entry.types === './lib/typert.remote-client.d.ts'
&& entry.default === './lib/typert.remote-client.js'
}
/** Normalize a package manifest path or npm tarball member to its payload-relative path. */
function payloadPath(file: string): string {
const normalized = file.replaceAll('\\', '/').replace(/^\.\/+/, '').replace(/\/+$/, '')
@@ -7,17 +24,30 @@ function payloadPath(file: string): string {
}
/** Whether a package payload path exposes source or declaration-map intermediates. */
export function isForbiddenPublicationFile(file: string): boolean {
export function isForbiddenPublicationFile(
file: string,
policy: PublicationPayloadPolicy = {},
): boolean {
const normalized = payloadPath(file)
if (policy.typeRTRemoteNavigation === true
&& (normalized === 'src'
|| normalized.startsWith('src/')
|| normalized === 'lib/typert.remote-client.d.ts.map')) {
return false
}
return normalized === 'src'
|| normalized.startsWith('src/')
|| normalized.endsWith('.d.ts.map')
}
/** Reject source and declaration-map members in a packed npm tarball. */
export function validateTarballPayload(files: readonly string[], context: string): void {
export function validateTarballPayload(
files: readonly string[],
context: string,
policy: PublicationPayloadPolicy = {},
): void {
for (const file of files) {
if (!isForbiddenPublicationFile(file)) continue
if (!isForbiddenPublicationFile(file, policy)) continue
const normalized = payloadPath(file)
if (normalized === 'src' || normalized.startsWith('src/')) {
throw new Error(`${context} publishes source file ${file}`)

View File

@@ -18,7 +18,7 @@ import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep
import { createInterface } from 'node:readline/promises'
import { pathToFileURL } from 'node:url'
import { parseArgs } from 'node:util'
import { validateTarballPayload } from './publication-payload.ts'
import { hasTypeRTRemoteNavigation, validateTarballPayload } from './publication-payload.ts'
const DEFAULT_REGISTRY = 'https://registry.npm.harnessment.com'
const DEFAULT_OUTPUT_DIRECTORY = '.artifacts/npm-baseline'
@@ -320,7 +320,11 @@ class ReleaseBundle {
if (expected === undefined || !missingNames.delete(artifact.name)) {
throw new Error(`unexpected or duplicate packed package: ${artifact.name}`)
}
if (expected.origin === 'harness') validateTarballPayload(artifact.files, tarball)
if (expected.origin === 'harness') {
validateTarballPayload(artifact.files, tarball, {
typeRTRemoteNavigation: hasTypeRTRemoteNavigation(artifact.manifest),
})
}
if (artifact.version !== version) {
throw new Error(`${tarball} has version ${artifact.version}; expected ${version}`)
}
@@ -394,7 +398,11 @@ class ReleaseBundle {
throw new Error(`tarball checksum mismatch: ${pkg.tarball}`)
}
const artifact = inspectTarball(path, runner)
if (pkg.origin === 'harness') validateTarballPayload(artifact.files, pkg.tarball)
if (pkg.origin === 'harness') {
validateTarballPayload(artifact.files, pkg.tarball, {
typeRTRemoteNavigation: hasTypeRTRemoteNavigation(artifact.manifest),
})
}
if (artifact.name !== pkg.name || artifact.version !== this.manifest.version) {
throw new Error(`tarball identity mismatch: ${pkg.tarball}`)
}
@@ -463,13 +471,6 @@ class InstalledBundleSmoke {
+ `expected ${this.bundle.manifest.version}`,
)
}
const config = this.runner.capture(
process.execPath,
[bin, '--dump-default-config'],
consumerRoot,
environment,
)
if (config === '') throw new Error('installed dsh --dump-default-config returned no output')
this.probeWeb(bin, consumerRoot, environment)
console.log('publish-npm-baseline: installed dsh entry and Web startup probes passed')
} finally {

View File

@@ -59,7 +59,7 @@ describe('gate graph validation', () => {
'ci-primary',
'ci-linux-primary',
'ci-static',
'ci-lint',
'ci-lint-contracts-ready',
'ci-coverage',
'ci-snapshot',
'ci-artifacts',
@@ -77,6 +77,12 @@ describe('gate graph validation', () => {
await expect(runGates(subject, subject.length, execute)).resolves.toHaveLength(subject.length)
})
it('keeps the public repository link policy in the documentation gate', () => {
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
expect(ids).toContain('public-repository-links')
})
it.each([
['empty', [], /gate graph has no gates/],
['duplicate ids', [gate('same'), gate('same')], /duplicate gate id "same"/],
@@ -112,29 +118,77 @@ describe('gate graph validation', () => {
describe('Oxlint gate', () => {
it('uses the package script when no worker bound is configured', () => {
const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
withPnpmEntrypoint(() => gatesForMode('ci-lint')[0]))
withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready')[0]))
expect(subject).toMatchObject({
id: 'lint',
displayCommand: 'pnpm run lint',
displayCommand: 'pnpm run lint:contracts-ready',
command: process.execPath,
args: ['/private/pnpm.cjs', 'run', 'lint'],
args: ['/private/pnpm.cjs', 'run', 'lint:contracts-ready'],
})
})
it('surfaces the configured worker bound on the shared package script', () => {
const subject = withEnv('DSH_OXLINT_THREADS', '4', () =>
withPnpmEntrypoint(() => gatesForMode('ci-lint')[0]))
withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready')[0]))
expect(subject).toMatchObject({
id: 'lint',
displayCommand: 'DSH_OXLINT_THREADS=4 pnpm run lint',
displayCommand: 'DSH_OXLINT_THREADS=4 pnpm run lint:contracts-ready',
command: process.execPath,
args: ['/private/pnpm.cjs', 'run', 'lint'],
args: ['/private/pnpm.cjs', 'run', 'lint:contracts-ready'],
})
})
})
describe('TypeRT contract preparation', () => {
it('prepares primary source consumers once before they run', () => {
const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
withPnpmEntrypoint(() => gatesForMode('ci-primary')))
expect(subject.find(item => item.id === 'typert-contracts')).toMatchObject({
displayCommand: 'pnpm run build:lib:host',
args: ['/private/pnpm.cjs', 'run', 'build:lib:host'],
})
for (const [id, script] of [
['typecheck', 'typecheck:contracts-ready'],
['lint', 'lint:contracts-ready'],
['doc-typecheck', 'doc-typecheck:contracts-ready'],
] as const) {
expect(subject.find(item => item.id === id)).toMatchObject({
displayCommand: `pnpm run ${script}`,
args: ['/private/pnpm.cjs', 'run', script],
needs: ['typert-contracts'],
})
}
expect(subject.find(item => item.id === 'build')?.needs).toEqual([
'typecheck',
'lint',
'doc-typecheck',
])
})
it('reuses contracts from the validated consumer build', () => {
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
expect(subject.find(item => item.id === 'lint-and-duplication')).toMatchObject({
displayCommand: 'pnpm run check:ci:lint:contracts-ready',
args: ['/private/pnpm.cjs', 'run', 'check:ci:lint:contracts-ready'],
})
expect(subject.find(item => item.id === 'doc-typecheck')).toMatchObject({
displayCommand: 'pnpm run doc-typecheck:contracts-ready',
args: ['/private/pnpm.cjs', 'run', 'doc-typecheck:contracts-ready'],
})
})
it('keeps standalone doc sync responsible for preparation', () => {
const docTypecheck = withPnpmEntrypoint(() =>
gatesForMode('doc-sync').find(item => item.id === 'doc-typecheck'))
expect(docTypecheck?.displayCommand).toBe('pnpm run doc-typecheck')
})
})
describe('Node compatibility graph', () => {
it('runs the jsdom environment smoke on every advertised Node line', () => {
const subject = withPnpmEntrypoint(() => gatesForMode('node-compat'))

View File

@@ -16,7 +16,7 @@ export type Mode =
| 'ci-primary'
| 'ci-linux-primary'
| 'ci-static'
| 'ci-lint'
| 'ci-lint-contracts-ready'
| 'ci-coverage'
| 'ci-snapshot'
| 'ci-artifacts'
@@ -101,7 +101,7 @@ function parseMode(raw: string | undefined): Mode {
case 'ci-primary':
case 'ci-linux-primary':
case 'ci-static':
case 'ci-lint':
case 'ci-lint-contracts-ready':
case 'ci-coverage':
case 'ci-snapshot':
case 'ci-artifacts':
@@ -115,7 +115,7 @@ function parseMode(raw: string | undefined): Mode {
return raw
default:
throw new Error(
`run-gates: expected mode ci-primary | ci-linux-primary | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | ci-consumers | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | check-all | doc-sync, got ${JSON.stringify(raw)}.`,
`run-gates: expected mode ci-primary | ci-linux-primary | ci-static | ci-lint-contracts-ready | ci-coverage | ci-snapshot | ci-artifacts | ci-consumers | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | check-all | doc-sync, got ${JSON.stringify(raw)}.`,
)
}
}
@@ -197,7 +197,7 @@ export function gatesForMode(selected: Mode): Gate[] {
return [...ciPrimaryGates(), webSnapshotGate(['built-package-invariants'])]
case 'ci-static':
return ciStaticGates({ ownsBuild: false })
case 'ci-lint':
case 'ci-lint-contracts-ready':
return [
lintGate(),
pnpmScript('duplication', 'duplication'),
@@ -224,6 +224,7 @@ export function gatesForMode(selected: Mode): Gate[] {
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
pnpmScript('client-domain-graph', 'verify-client-domain-graph', { label: 'client domain graph' }),
pnpmScript('test', 'test'),
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
pnpmScript('duplication', 'duplication'),
snapshotGate(),
pnpmScript('build', 'build'),
@@ -232,6 +233,7 @@ export function gatesForMode(selected: Mode): Gate[] {
...docSyncLeafGates({
docTypecheckNeeds: ['build'],
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
docTypecheckScript: 'doc-typecheck:contracts-ready',
}),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
]
@@ -240,25 +242,36 @@ export function gatesForMode(selected: Mode): Gate[] {
}
}
function ciPrimaryGates(): Gate[] {
function ciSharedStaticGates(): Gate[] {
return [
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('constraints', 'constraints'),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
pnpmScript('typecheck', 'typecheck'),
lintGate(),
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
]
}
function ciPrimaryGates(): Gate[] {
return [
...ciSharedStaticGates(),
typertContractsGate(),
pnpmScript('typecheck', 'typecheck:contracts-ready', { needs: ['typert-contracts'] }),
lintGate({ needs: ['typert-contracts'] }),
pnpmScript('duplication', 'duplication'),
...coverageGates(),
...nodeCompatSmokeGates(),
snapshotGate(),
...docSyncLeafGates(),
...docSyncLeafGates({
docTypecheckNeeds: ['typert-contracts'],
docTypecheckScript: 'doc-typecheck:contracts-ready',
}),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
pnpmScript('knip', 'knip'),
// typecheck and build now drive the same root solution graph; without the
// dependency two concurrent `tsc -b` runs race the same tsbuildinfo files.
// The tsc step is an incremental no-op after typecheck.
pnpmScript('build', 'build', { needs: ['typecheck'] }),
// The prepared typecheck and build both drive Client tsc, while build also
// repeats the Host contract pass. Wait for all three consumers so build
// neither races tsbuildinfo nor replaces declarations while they are read.
pnpmScript('build', 'build', { needs: ['typecheck', 'lint', 'doc-typecheck'] }),
pnpmScript('publint', 'publint', { needs: ['build'] }),
pnpmScript('node-next-types', 'verify-node-next-types', {
label: 'node-next types',
@@ -339,10 +352,7 @@ function runningNodeMajor(): number {
function ciStaticGates(options: { ownsBuild: boolean }): Gate[] {
return [
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('constraints', 'constraints'),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
...ciSharedStaticGates(),
...options.ownsBuild ? [pnpmScript('build', 'build')] : [],
...docSyncLeafGates({
includeDocTypecheck: options.ownsBuild,
@@ -350,6 +360,7 @@ function ciStaticGates(options: { ownsBuild: boolean }): Gate[] {
? {
docTypecheckNeeds: ['build'],
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
docTypecheckScript: 'doc-typecheck:contracts-ready',
}
: {},
docsBuildScript: 'docs:build:mpa',
@@ -380,13 +391,13 @@ function ciConsumerGates(): Gate[] {
pnpmScript('node-compat', 'check:node-compat', { label: 'Node compatibility' }),
pnpmScript('publint', 'publint', { needs: builtTree }),
builtPackageInvariantsGate(['publint']),
pnpmScript('lint-and-duplication', 'check:ci:lint', {
pnpmScript('lint-and-duplication', 'check:ci:lint:contracts-ready', {
label: 'lint and duplication',
needs: validatedBuild,
}),
snapshotGate(validatedBuild),
webSnapshotGate(validatedBuild),
pnpmScript('doc-typecheck', 'doc-typecheck', {
pnpmScript('doc-typecheck', 'doc-typecheck:contracts-ready', {
needs: validatedBuild,
env: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
}),
@@ -442,11 +453,19 @@ function ciWindowsObservationalGates(): Gate[] {
]
}
function lintGate(): Gate {
function typertContractsGate(): Gate {
return pnpmScript('typert-contracts', 'build:lib:host', { label: 'TypeRT contracts' })
}
function lintGate(options: { needs?: string[] } = {}): Gate {
const raw = process.env.DSH_OXLINT_THREADS
return pnpmScript('lint', 'lint', raw === undefined || raw === ''
? {}
: { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` })
const script = 'lint:contracts-ready'
return pnpmScript('lint', script, {
...raw === undefined || raw === ''
? {}
: { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run ${script}` },
...options.needs === undefined ? {} : { needs: options.needs },
})
}
// The heavy suites run uninstrumented beside the thresholded gate: their
@@ -549,6 +568,7 @@ function docSyncLeafGates(options: {
includeDocTypecheck?: boolean
docTypecheckNeeds?: string[]
docTypecheckEnv?: Record<string, string | undefined>
docTypecheckScript?: 'doc-typecheck' | 'doc-typecheck:contracts-ready'
docsBuildScript?: 'docs:build' | 'docs:build:mpa'
} = {}): Gate[] {
const docTypecheckOptions: Partial<Gate> = {}
@@ -557,7 +577,7 @@ function docSyncLeafGates(options: {
return [
...options.includeDocTypecheck === false
? []
: [pnpmScript('doc-typecheck', 'doc-typecheck', docTypecheckOptions)],
: [pnpmScript('doc-typecheck', options.docTypecheckScript ?? 'doc-typecheck', docTypecheckOptions)],
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),
pnpmScript('export-jsdoc', 'verify-export-jsdoc', { label: 'export jsdoc' }),
pnpmScript('tool-catalog', 'verify-tool-catalog', { label: 'tool catalog' }),
@@ -567,8 +587,10 @@ function docSyncLeafGates(options: {
pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }),
pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }),
pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }),
pnpmScript('public-repository-links', 'verify-public-repository-links', { label: 'public repository links' }),
pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }),
pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }),
pnpmScript('config-source-ownership', 'verify-config-source-ownership', { label: 'config source ownership' }),
pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience' }),
pnpmScript('mermaid', 'verify-mermaid'),
pnpmScript('agent-note-classification', 'verify-agent-note-classification', { label: 'agent note classification' }),
@@ -601,6 +623,7 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts',
'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts',
'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts',
'packages/api/remotes/tests/built-lib.e2e.ts',
// The worker-entry packages' built bundles: the only automated proof
// that lib/index.js resolves its sibling lib/worker.cjs under plain node
// (the e2e lane runs unbuilt, so these files self-skip there).

File diff suppressed because one or more lines are too long

View File

@@ -199,7 +199,7 @@
{
"doc": "docs/core-data-structures/goal.md",
"symbol": "GoalView",
"source": "packages/goal/goal/src/domain.ts"
"source": "packages/goal/goal/src/types.ts"
},
{
"doc": "docs/core-data-structures/goal.md",
@@ -219,12 +219,12 @@
{
"doc": "docs/core-data-structures/goal.md",
"symbol": "CreateGoalRequest",
"source": "packages/goal/goal/src/domain.ts"
"source": "packages/goal/goal/src/types.ts"
},
{
"doc": "docs/core-data-structures/goal.md",
"symbol": "EditGoalRequest",
"source": "packages/goal/goal/src/domain.ts"
"source": "packages/goal/goal/src/types.ts"
},
{
"doc": "docs/core-data-structures/goal.md",
@@ -1494,6 +1494,66 @@
"doc": "docs/core-data-structures/settings.md",
"symbol": "SettingsPathOp",
"source": "packages/settings/settings/src/index.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTLookupMap",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTContextMap",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTLookupDefinition",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTCodec",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "InvocationParameterDescriptor",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "InvocationDescriptor",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTService",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTRemoteNamespaceMap",
"source": "packages/typert/type-meta/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "InvokeRemoteRequest",
"source": "packages/api/gateway/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypertGatewayErrorCode",
"source": "packages/api/gateway/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypertGateway",
"source": "packages/api/gateway/src/types.ts"
},
{
"doc": "docs/core-data-structures/typert.md",
"symbol": "TypeRTClientRemote",
"source": "packages/typert/type-meta/src/types.ts"
}
]
}

View File

@@ -0,0 +1,30 @@
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 { collectConfigSourceOwnershipViolations } from './verify-config-source-ownership.ts'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
describe('configuration source ownership gate', () => {
it('rejects inline endpoints in shipped bundle patches', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-config-source-ownership-'))
roots.push(root)
const directory = join(root, 'packages/bundle/base')
mkdirSync(directory, { recursive: true })
writeFileSync(
join(directory, 'cordis.patch.yml'),
'config:\n baseURL: !!js process.env.DEEPSEEK_SEARCH_BASE_URL\n',
)
expect(collectConfigSourceOwnershipViolations(root)).toEqual([
'packages/bundle/base/cordis.patch.yml:2: inlines a credential or endpoint from the environment.'
+ ' The adapter resolves apiKeyEnv through ctx.credentials and the endpoint through the'
+ ' environment snapshot; inlining here bypasses both ladders.',
])
})
})

View File

@@ -0,0 +1,56 @@
/**
* Gate for forbidden credential or endpoint environment inlines in shipped
* Cordis configuration.
* @module scripts/verify-config-source-ownership
*/
import { globSync, readFileSync } from 'node:fs'
import { resolve, sep } from 'node:path'
const ROOT = resolve(import.meta.dirname, '..')
/** Shipped Cordis configuration these rules apply to. */
const SHIPPED_CONFIG_GLOBS = [
'apps/*/config/*.yml',
'examples/*/*.cordis.yml',
'examples/*/cordis.yml',
'packages/bundle/*/cordis.patch.yml',
// The Python runtime ships its own default composition inside the wheel.
'python/*/src/**/cordis.yml',
]
/** Ordinary single-line forms this narrow source-shape check rejects; not full YAML analysis. */
const INLINE_DENY = /^\s*(apiKey|baseURL|apiKeyEnv|authToken|headers)\s*:\s*!!js\b/
/** Return every forbidden inline environment form in shipped configuration. */
export function collectConfigSourceOwnershipViolations(root: string): string[] {
const failures: string[] = []
for (const glob of SHIPPED_CONFIG_GLOBS) {
for (const file of globSync(glob, { cwd: root })) {
const rel = file.split(sep).join('/')
readFileSync(resolve(root, rel), 'utf8').split('\n').forEach((line, index) => {
if (!INLINE_DENY.test(line)) return
failures.push(
`${rel}:${String(index + 1)}: inlines a credential or endpoint from the environment.`
+ ' The adapter resolves apiKeyEnv through ctx.credentials and the endpoint through the'
+ ' environment snapshot; inlining here bypasses both ladders.',
)
})
}
}
return failures
}
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
const failures = collectConfigSourceOwnershipViolations(ROOT)
if (failures.length > 0) {
process.stderr.write('verify-config-source-ownership: configuration source ownership violated:\n')
for (const failure of failures) process.stderr.write(` ${failure}\n`)
process.exit(1)
}
process.stdout.write(
'verify-config-source-ownership: no credential or endpoint uses the ordinary inline environment form'
+ ' in shipped configuration.\n',
)
}

View File

@@ -33,6 +33,7 @@ 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.',
}
/**
@@ -57,10 +58,12 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/web-react': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/schema-form': { kind: 'none', reason: 'Browser-side form-rendering library; registers no model surface.' },
'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/api/remotes': { kind: 'none', reason: 'The Remote BFF selects business methods and identity policy; selected services own any model-visible effect.' },
'packages/client/runtime': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-deliverables': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
'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 target at the next prompt-assembly boundary and owns the model-visible effect.' },
@@ -125,6 +128,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness observes child-process streams without changing live requests.' },
'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/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 no model surface.' },
'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 control-surface 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.' },

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import { findInternalRepositoryReferences } from './verify-public-repository-links.ts'
describe('public repository link policy', () => {
it('rejects encoded and case-varied internal identities without blocking public repositories', () => {
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const encodedRepository = internalRepository.replaceAll('-', '%2D').replace('/', '%2F')
const htmlEncodedRepository = internalRepository.replace('/', '&#x2f;')
const jsonEscapedRepository = internalRepository.replace('/', '\\/')
const unicodeEscapedRepository = internalRepository.replace('/', String.raw`\u002f`)
const source = [
'https://github.com/deepseek-ai/deepseek-harness-sdk',
`https://github.com/${internalOwner}/cordis`,
`https://github.com/${internalRepository.toUpperCase()}/issues/1`,
`https://github.com/${encodedRepository}/issues/2`,
`https://github.com/${htmlEncodedRepository}/issues/3`,
`"https:\\/\\/github.com\\/${jsonEscapedRepository}\\/issues\\/4"`,
`"https:\\/\\/github.com\\/${unicodeEscapedRepository}\\/issues\\/5"`,
`${internalOwner.toUpperCase()}#6`,
].join('\n')
expect(findInternalRepositoryReferences('subject.md', source)).toEqual([
{ file: 'subject.md', line: 3 },
{ file: 'subject.md', line: 4 },
{ file: 'subject.md', line: 5 },
{ file: 'subject.md', line: 6 },
{ file: 'subject.md', line: 7 },
{ file: 'subject.md', line: 8 },
])
})
})

View File

@@ -0,0 +1,90 @@
/** Reject tracked files that expose the internal repository identity. */
import { execFileSync } from 'node:child_process'
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
const root = resolve(import.meta.dirname, '..')
const internalOwner = ['deepseek', 'harness'].join('-')
const internalRepository = [internalOwner, internalOwner].join('/')
const internalIssueShorthand = `${internalOwner}#`
const namedReferenceCharacters: Readonly<Record<string, string>> = {
hyphen: '-',
num: '#',
sol: '/',
}
/** Normalize source spellings that render or decode to repository separators. */
function canonicalReferenceText(source: string): string {
return source
.replaceAll('\\/', '/')
.replace(/\\u(0023|002d|002f)/gi, (_match, code: string) => String.fromCodePoint(Number.parseInt(code, 16)))
.replace(/%(23|2d|2f)/gi, (_match, code: string) => String.fromCodePoint(Number.parseInt(code, 16)))
.replace(/&#(?:(\d+)|x([\da-f]+));/gi, (entity, decimal: string | undefined, hexadecimal: string | undefined) => {
const code = Number.parseInt(decimal ?? hexadecimal ?? '', decimal === undefined ? 16 : 10)
return code === 35 || code === 45 || code === 47 ? String.fromCodePoint(code) : entity
})
.replace(/&(hyphen|num|sol);/gi, (entity, name: string) => namedReferenceCharacters[name.toLowerCase()] ?? entity)
.normalize('NFKC')
.toLowerCase()
}
/** One tracked reference to the internal repository. */
export interface InternalRepositoryReference {
/** Repository-relative file path. */
file: string
/** One-based source line. */
line: number
}
/**
* Locate internal-repository references in one text file.
* @param file - Repository-relative path used in diagnostics.
* @param source - Text to inspect.
* @returns every matching source line.
*/
export function findInternalRepositoryReferences(file: string, source: string): InternalRepositoryReference[] {
const references: InternalRepositoryReference[] = []
for (const [index, line] of source.split('\n').entries()) {
const canonicalLine = canonicalReferenceText(line)
if (canonicalLine.includes(internalRepository) || canonicalLine.includes(internalIssueShorthand)) {
references.push({ file, line: index + 1 })
}
}
return references
}
function trackedFiles(repoRoot: string): string[] {
return execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8' })
.split('\0')
.filter(file => file !== '')
}
function scanRepository(repoRoot: string): InternalRepositoryReference[] {
const references: InternalRepositoryReference[] = []
for (const file of trackedFiles(repoRoot)) {
const path = resolve(repoRoot, file)
if (!existsSync(path)) continue
const stat = lstatSync(path)
if (!stat.isFile() && !stat.isSymbolicLink()) continue
const source = stat.isSymbolicLink() ? readlinkSync(path) : readFileSync(path, 'utf8')
if (source.includes('\0')) continue
references.push(...findInternalRepositoryReferences(file, source))
}
return references
}
const invokedPath = process.argv[1]
const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href
if (isMain) {
const references = scanRepository(root)
if (references.length === 0) {
console.log('verify-public-repository-links: tracked files expose no internal repository identity.')
} else {
console.error('verify-public-repository-links: internal repository references found:')
for (const reference of references) console.error(` ${reference.file}:${String(reference.line)}`)
process.exitCode = 1
}
}

View File

@@ -204,12 +204,14 @@ cat "$scratch/logs/smoke.log"
grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-gates: Windows Node smoke did not report win32 x64' >&2; exit 1; }
# ---- the two blocking surfaces, concurrently ------------------------------
# The same shape run-gates gives ci-windows-blocking on native Windows:
# `build` = tsc -b then tsdown, `production site` = the VitePress build. Both
# statuses are captured so one failure cannot hide the other's result.
# The build preserves the face order from package.json: compile and bundle the
# Host face before compiling and bundling the Client face.
# Both statuses are captured so one failure cannot hide the other's result.
build_gate() {
wine_node "$scratch/logs/tsc.log" "$tsc_js" -b --pretty false || return $?
wine_node "$scratch/logs/tsdown.log" "$tsdown_js"
wine_node "$scratch/logs/host-tsc.log" "$tsc_js" -b tsconfig.host.json --pretty false || return $?
wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $?
wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $?
wine_node "$scratch/logs/client-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE client
}
site_gate() {
cd website
@@ -235,7 +237,11 @@ report() {
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
fi
}
report 'build (tsc -b, tsdown)' "$build_status" "$scratch/logs/tsc.log" "$scratch/logs/tsdown.log"
report 'build (Host tsc/tsdown, Client tsc/tsdown)' "$build_status" \
"$scratch/logs/host-tsc.log" \
"$scratch/logs/host-tsdown.log" \
"$scratch/logs/client-tsc.log" \
"$scratch/logs/client-tsdown.log"
report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log"
if (( build_status != 0 )); then exit "$build_status"; fi
exit "$site_status"