Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml # docs/core-data-structures/core.i18n.yaml # docs/module-graph.md # packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx # packages/client/ui-conversation/src/client/index.ts # packages/compact/compact-basic/README.i18n.yaml
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||
import { join, relative, resolve } from 'node:path'
|
||||
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
|
||||
@@ -15,6 +16,8 @@ const root = resolve(import.meta.dirname, '..')
|
||||
const workspaceGlobs = [
|
||||
{ dir: 'vendor', depth: 1 },
|
||||
{ dir: 'packages', depth: 2 },
|
||||
{ dir: 'native', depth: 1 },
|
||||
{ dir: 'native/landlock-run/packages', depth: 1 },
|
||||
{ dir: 'apps', depth: 1 },
|
||||
] as const
|
||||
const vendoredPackages = new Set([
|
||||
@@ -28,6 +31,16 @@ const vendoredPackages = new Set([
|
||||
'@cordisjs/plugin-hmr',
|
||||
'@cordisjs/plugin-logger-console',
|
||||
])
|
||||
const publicLandlockPackages = new Set([
|
||||
'@deepseek-ai/node-addon-landlock-run',
|
||||
'@deepseek-ai/node-addon-landlock-run-linux-arm64',
|
||||
'@deepseek-ai/node-addon-landlock-run-linux-x64',
|
||||
])
|
||||
/** Deliberate source payloads whose exact bytes are part of the package's audit surface. */
|
||||
const publicationSourceAllowlist: Readonly<Record<string, readonly string[]>> = {
|
||||
'@deepseek-ai/node-addon-landlock-run': ['src/main.c'],
|
||||
}
|
||||
const repositoryUrl = 'git+https://github.com/deepseek-harness/deepseek-harness.git'
|
||||
|
||||
const localArtifactDirs = new Set(['node_modules'])
|
||||
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
|
||||
@@ -55,6 +68,8 @@ interface PackageManifest {
|
||||
| undefined
|
||||
>
|
||||
files?: string[]
|
||||
publishConfig?: { access?: string }
|
||||
repository?: { type?: string; url?: string; directory?: string }
|
||||
peerDependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
}
|
||||
@@ -71,6 +86,8 @@ function readJson(path: string): PackageManifest {
|
||||
|
||||
const rootManifest = readJson(join(root, 'package.json'))
|
||||
const repositoryVersion = rootManifest.version
|
||||
const landlockWorkspaceManifest = readJson(join(root, 'native/landlock-run/package.json'))
|
||||
const landlockVersion = landlockWorkspaceManifest.version
|
||||
|
||||
/** Repo-relative dirs holding a package.json, walked to the configured depth. */
|
||||
function packageDirs(base: string, depth: number): string[] {
|
||||
@@ -79,12 +96,12 @@ function packageDirs(base: string, depth: number): string[] {
|
||||
.filter(entry => entry.isDirectory())
|
||||
.filter(entry => !localArtifactDirs.has(entry.name))
|
||||
.filter(entry => existsSync(join(root, base, entry.name, 'package.json')))
|
||||
.map(entry => join(base, entry.name))
|
||||
.map(entry => `${base}/${entry.name}`)
|
||||
}
|
||||
return readdirSync(join(root, base), { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory())
|
||||
.filter(entry => !localArtifactDirs.has(entry.name))
|
||||
.flatMap(group => packageDirs(join(base, group.name), depth - 1))
|
||||
.flatMap(group => packageDirs(`${base}/${group.name}`, depth - 1))
|
||||
}
|
||||
|
||||
function workspaceManifests(): WorkspaceManifest[] {
|
||||
@@ -109,6 +126,7 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
|
||||
'@deepseek-ai/dsh-helper': ['lib/assets'],
|
||||
'@deepseek-ai/dsh-pty-local': ['scripts/ensure-spawn-helper.mjs'],
|
||||
'@deepseek-ai/dsh-skill-badge': ['assets'],
|
||||
'@deepseek-ai/dsh-scripts': [
|
||||
'lib/dev/tsdown-config.js',
|
||||
'lib/local-plugin-loader-hooks.js',
|
||||
@@ -194,8 +212,25 @@ function usesEmittedTreeDefaults(manifest: PackageManifest): boolean {
|
||||
function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
const errors: string[] = []
|
||||
const label = manifest.name ?? dir
|
||||
const isLandlockPackageDir = dir.startsWith('native/landlock-run/packages/')
|
||||
const isPublicLandlockPackage = isLandlockPackageDir
|
||||
&& manifest.name !== undefined
|
||||
&& publicLandlockPackages.has(manifest.name)
|
||||
|
||||
if (manifest.private !== true) {
|
||||
if (isPublicLandlockPackage) {
|
||||
if (manifest.private === true) {
|
||||
errors.push(`${label}: published Landlock package must not set "private": true`)
|
||||
}
|
||||
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'
|
||||
|| manifest.repository.url !== repositoryUrl
|
||||
|| manifest.repository.directory !== expectedDirectory) {
|
||||
errors.push(`${label}: published Landlock package repository must use ${repositoryUrl} with directory ${expectedDirectory} for trusted publishing`)
|
||||
}
|
||||
} else if (manifest.private !== true) {
|
||||
errors.push(`${label}: package.json must set "private": true`)
|
||||
}
|
||||
|
||||
@@ -204,9 +239,10 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
}
|
||||
|
||||
if (manifest.name?.startsWith('@deepseek-ai/')) {
|
||||
const allowedSources = publicationSourceAllowlist[manifest.name] ?? []
|
||||
const publicationPolicy = { typeRTRemoteNavigation: hasTypeRTRemoteNavigation(manifest) }
|
||||
for (const file of manifest.files ?? []) {
|
||||
if (isForbiddenPublicationFile(file, publicationPolicy)) {
|
||||
if (isForbiddenPublicationFile(file, publicationPolicy) && !allowedSources.includes(file)) {
|
||||
errors.push(`${label}: package.json files must not publish ${JSON.stringify(file)}`)
|
||||
}
|
||||
}
|
||||
@@ -221,6 +257,15 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
if (isLandlockPackageDir) {
|
||||
if (!isPublicLandlockPackage) {
|
||||
errors.push(`${label}: unexpected package in the public Landlock package family`)
|
||||
}
|
||||
if (manifest.version !== landlockVersion) {
|
||||
errors.push(`${label}: package.json version must match Landlock workspace version ${landlockVersion ?? '(missing)'}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (dir.startsWith('packages/') && manifest.name?.startsWith('@deepseek-ai/dsh-')) {
|
||||
const peer = manifest.peerDependencies?.cordis
|
||||
const dev = manifest.devDependencies?.cordis
|
||||
@@ -305,6 +350,7 @@ const errors = [
|
||||
...checkRepositoryVersion(),
|
||||
...workspaceManifests().flatMap(checkWorkspace),
|
||||
...checkHierarchyShape(),
|
||||
...collectProjectReferenceFaceViolations(root),
|
||||
]
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.join('\n'))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+16
-2
@@ -18,10 +18,10 @@ function write(path: string, content = ''): void {
|
||||
writeFileSync(path, content)
|
||||
}
|
||||
|
||||
function addProject(root: string, path: string): void {
|
||||
function addProject(root: string, path: string, outDir = 'lib/types'): void {
|
||||
write(join(root, 'tsconfig.json'), JSON.stringify({ files: [], references: [{ path }] }))
|
||||
write(join(root, path, 'tsconfig.json'), JSON.stringify({
|
||||
compilerOptions: { composite: true, outDir: 'lib/types' },
|
||||
compilerOptions: { composite: true, outDir },
|
||||
include: ['src'],
|
||||
}))
|
||||
write(join(root, path, 'src/index.ts'), 'export {}\n')
|
||||
@@ -60,6 +60,20 @@ describe('RepositoryCleaner', () => {
|
||||
expect(existsSync(join(root, 'products/shell/lib'))).toBe(true)
|
||||
})
|
||||
|
||||
it('removes the native Landlock entry output and solution build info', async () => {
|
||||
const root = fixture()
|
||||
const entry = 'native/landlock-run/packages/entry'
|
||||
addProject(root, entry, 'lib')
|
||||
write(join(root, entry, 'lib/index.js'))
|
||||
write(join(root, 'native/landlock-run/tsconfig.tsbuildinfo'))
|
||||
|
||||
await new RepositoryCleaner(root).clean()
|
||||
|
||||
expect(existsSync(join(root, entry, 'lib'))).toBe(false)
|
||||
expect(existsSync(join(root, entry, 'src/index.ts'))).toBe(true)
|
||||
expect(existsSync(join(root, 'native/landlock-run/tsconfig.tsbuildinfo'))).toBe(false)
|
||||
})
|
||||
|
||||
it('refuses project outputs reached through a symlink outside the repository', async () => {
|
||||
const root = fixture()
|
||||
const externalProject = fixture()
|
||||
|
||||
+12
-2
@@ -72,6 +72,11 @@ export class RepositoryCleaner {
|
||||
for (const entry of await readdir(this.root, { withFileTypes: true })) {
|
||||
if (entry.isFile() && entry.name.endsWith('.tsbuildinfo')) targets.add(join(this.root, entry.name))
|
||||
}
|
||||
await this.addIfPresent(
|
||||
targets,
|
||||
join(this.root, 'native/landlock-run/tsconfig.tsbuildinfo'),
|
||||
canonicalRoot,
|
||||
)
|
||||
|
||||
// The root project-reference graph is the source of truth for live build targets.
|
||||
// Each emitting project declares lib/types as outDir; its parent lib also owns
|
||||
@@ -114,6 +119,7 @@ export class RepositoryCleaner {
|
||||
const outputs = new Set<string>()
|
||||
const pending = [join(this.root, 'tsconfig.json')]
|
||||
const visited = new Set<string>()
|
||||
const nativeEntryOutput = join(this.root, 'native/landlock-run/packages/entry/lib')
|
||||
|
||||
while (pending.length > 0) {
|
||||
const nextConfigPath = pending.pop()
|
||||
@@ -125,10 +131,14 @@ export class RepositoryCleaner {
|
||||
const parsed = parseConfig(configPath)
|
||||
if (parsed.options.outDir !== undefined) {
|
||||
const typesDirectory = resolve(parsed.options.outDir)
|
||||
if (basename(typesDirectory) !== 'types') {
|
||||
const outputDirectory = basename(typesDirectory) === 'types'
|
||||
? dirname(typesDirectory)
|
||||
: typesDirectory === nativeEntryOutput
|
||||
? typesDirectory
|
||||
: undefined
|
||||
if (outputDirectory === undefined) {
|
||||
throw new Error(`clean: expected TypeScript outDir to end in /types: ${repositoryPath(this.root, typesDirectory)}`)
|
||||
}
|
||||
const outputDirectory = dirname(typesDirectory)
|
||||
this.assertRepositoryTarget(outputDirectory)
|
||||
outputs.add(outputDirectory)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
@@ -87,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')
|
||||
@@ -105,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')
|
||||
@@ -116,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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -134,7 +134,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'session',
|
||||
title: 'In-memory session store',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'agent', 'cli-demo', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
|
||||
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
|
||||
note: 'Owns append-only Session instances and emits the durable session event feed.',
|
||||
},
|
||||
{
|
||||
@@ -304,7 +304,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'skill',
|
||||
title: 'Skill provider registry',
|
||||
mode: 'seam',
|
||||
implementations: ['skill-local'],
|
||||
implementations: ['skill-badge', 'skill-local'],
|
||||
consumers: ['tool-skill'],
|
||||
note: 'Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies.',
|
||||
},
|
||||
@@ -313,7 +313,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'agent',
|
||||
title: 'Agent service',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'acp', 'cli-demo', 'subagent-inprocess'],
|
||||
consumers: ['agent-loop', 'acp', 'subagent-inprocess'],
|
||||
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
|
||||
},
|
||||
{
|
||||
@@ -645,10 +645,10 @@ const APP_EXAMPLES = [
|
||||
{
|
||||
id: 'headless',
|
||||
rel: 'examples/headless-agent/composition.md',
|
||||
title: 'Headless Agent App Composition',
|
||||
title: 'Headless Agent Snapshot Composition',
|
||||
label: 'examples/headless-agent',
|
||||
config: 'examples/headless-agent/cordis.yml',
|
||||
summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
|
||||
summary: 'The headless snapshot composition combines the real DeepSeek adapter and coding capabilities with one explicitly configured persisted top-level agent; its JSONL driver is test-only.',
|
||||
},
|
||||
{
|
||||
id: 'acp',
|
||||
@@ -667,9 +667,7 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
|
||||
const jsonl = nodeId('bundle', 'jsonl')
|
||||
lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`)
|
||||
lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`)
|
||||
if (pluginName === '@deepseek-ai/dsh-cli-demo') {
|
||||
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'cli')}["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"]`)
|
||||
} else if (pluginName === '@deepseek-ai/dsh-acp-demo') {
|
||||
if (pluginName === '@deepseek-ai/dsh-acp-demo') {
|
||||
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]`)
|
||||
}
|
||||
lines.push(
|
||||
@@ -695,7 +693,7 @@ function renderAppComposition(example: AppExample): string {
|
||||
const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
|
||||
lines.push(` ${pluginNode}["${escLabel(plugin.id)}<br/>${escLabel(plugin.name)}"]`)
|
||||
lines.push(` cfg --> ${pluginNode}`)
|
||||
if (plugin.name === '@deepseek-ai/dsh-cli-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') {
|
||||
if (plugin.name === '@deepseek-ai/dsh-acp-demo') {
|
||||
renderAppExpansion(lines, pluginNode, plugin.name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,13 +329,13 @@ describe('official Claude distribution authorization', () => {
|
||||
|
||||
describe('manifestPatterns', () => {
|
||||
it('derives globs from the declared members, so a new member area is read', () => {
|
||||
expect(manifestPatterns(['packages/*/*', 'tools/*'], ['packages/*'])).toEqual([
|
||||
expect(manifestPatterns(['packages/*/*', 'tools/*', 'native/landlock-run', 'native/landlock-run/packages/*'])).toEqual([
|
||||
'package.json',
|
||||
'packages/*/*/package.json',
|
||||
'tools/*/package.json',
|
||||
'examples/*/package.json',
|
||||
'native/landlock-run/package.json',
|
||||
'native/landlock-run/packages/*/package.json',
|
||||
'examples/*/package.json',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -39,14 +39,11 @@ const DEV_ONLY_AREAS = [
|
||||
'native/',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* First-party packages released from sibling repositories under the project's
|
||||
* own license: reachable from workspace manifests but not third-party.
|
||||
*/
|
||||
/** First-party public native packages: reachable at runtime but not third-party. */
|
||||
const FIRST_PARTY = new Set([
|
||||
'node-addon-landlock-run',
|
||||
'node-addon-landlock-run-linux-arm64',
|
||||
'node-addon-landlock-run-linux-x64',
|
||||
'@deepseek-ai/node-addon-landlock-run',
|
||||
'@deepseek-ai/node-addon-landlock-run-linux-arm64',
|
||||
'@deepseek-ai/node-addon-landlock-run-linux-x64',
|
||||
])
|
||||
|
||||
/** Official SDK identity covered by the project's narrow owner authorization. */
|
||||
@@ -135,16 +132,13 @@ function readManifest(rel: string): Manifest {
|
||||
* here, so a new member area (`tools/*`) is read the day it is declared.
|
||||
* @returns one glob per manifest-bearing location, repository-relative.
|
||||
*/
|
||||
export function manifestPatterns(rootMembers: readonly string[], nativeMembers: readonly string[]): string[] {
|
||||
export function manifestPatterns(rootMembers: readonly string[]): string[] {
|
||||
return [
|
||||
'package.json',
|
||||
...rootMembers.map(member => `${member}/package.json`),
|
||||
// The demo leaves join the workspace through `examples/package.json`, so
|
||||
// their own manifests are members of nothing and no glob above reaches them.
|
||||
'examples/*/package.json',
|
||||
// `native/landlock-run` is a nested workspace with its own lock file.
|
||||
'native/landlock-run/package.json',
|
||||
...nativeMembers.map(member => `native/landlock-run/${member}/package.json`),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -165,7 +159,7 @@ function workspaceMembers(rel: string): string[] {
|
||||
* would silently push dev-area manifests into the runtime tier.
|
||||
*/
|
||||
function loadWorkspaceManifests(): { manifests: Map<string, Manifest>; names: Set<string> } {
|
||||
const patterns = manifestPatterns(workspaceMembers('pnpm-workspace.yaml'), workspaceMembers('native/landlock-run/pnpm-workspace.yaml'))
|
||||
const patterns = manifestPatterns(workspaceMembers('pnpm-workspace.yaml'))
|
||||
const manifests = new Map<string, Manifest>()
|
||||
const names = new Set<string>()
|
||||
for (const pattern of patterns) {
|
||||
@@ -279,8 +273,8 @@ export function virtualManifest(virtual: string, name: string): VirtualManifest
|
||||
/** Resolve one installed external package manifest from either pnpm store. */
|
||||
function installedManifest(name: string): VirtualManifest | undefined {
|
||||
let manifest: (Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }) | undefined
|
||||
// The nested Landlock workspace installs into its own store, so a package
|
||||
// only that workspace depends on is unreachable from the root one.
|
||||
// Workspace-local link farms can expose a dependency that is not linked at
|
||||
// the repository root; both are backed by the root workspace's lockfile.
|
||||
for (const store of ['node_modules', 'native/landlock-run/node_modules']) {
|
||||
const direct = resolve(root, store, name, 'package.json')
|
||||
if (existsSync(direct)) {
|
||||
@@ -303,7 +297,7 @@ function installedMetadata(name: string): { license: string; repo: string } {
|
||||
const rawRepo = typeof manifest?.repository === 'string' ? manifest.repository : manifest?.repository?.url ?? manifest?.homepage
|
||||
const repo = override?.repo ?? normalizeRepo(rawRepo)
|
||||
if (license === undefined || repo === undefined) {
|
||||
throw new Error(`gen-third-party-notices: cannot resolve ${license === undefined ? 'license' : 'repository'} for ${name}; run \`pnpm install\` (or, for a Landlock-only dependency, \`pnpm --dir native/landlock-run install\`), or add an OVERRIDES entry.`)
|
||||
throw new Error(`gen-third-party-notices: cannot resolve ${license === undefined ? 'license' : 'repository'} for ${name}; run \`pnpm install\`, or add an OVERRIDES entry.`)
|
||||
}
|
||||
return { license, repo }
|
||||
}
|
||||
@@ -698,7 +692,7 @@ DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the th
|
||||
|
||||
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.
|
||||
|
||||
The complete npm transitive closure, with exact pinned versions, is recorded in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python closure is recorded in [\`python/sdk/uv.lock\`](python/sdk/uv.lock), and the Landlock launcher workspace keeps its own in [\`native/landlock-run/pnpm-lock.yaml\`](native/landlock-run/pnpm-lock.yaml).
|
||||
The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python closure is recorded separately in [\`python/sdk/uv.lock\`](python/sdk/uv.lock).
|
||||
|
||||
## Vendored source (\`vendor/\`)
|
||||
|
||||
@@ -740,9 +734,9 @@ ${python.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.license} | ${dep.r
|
||||
| --- | --- | --- |
|
||||
${BUILD_TIME_TOOLS.map(tool => `| [\`${tool.name}\`](${tool.repo}) | ${tool.license} | ${tool.role} |`).join('\n')}
|
||||
|
||||
## First-party sibling releases
|
||||
## First-party native packages
|
||||
|
||||
\`node-addon-landlock-run\` (and its platform packages) is released from a DeepSeek Harness sibling repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party.
|
||||
\`@deepseek-ai/node-addon-landlock-run\` (and its platform packages) is built and released from this repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party.
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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('\n')
|
||||
})).toBe('\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',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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'))
|
||||
|
||||
+34
-19
@@ -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'),
|
||||
@@ -233,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' }),
|
||||
]
|
||||
@@ -254,19 +255,23 @@ function ciSharedStaticGates(): Gate[] {
|
||||
function ciPrimaryGates(): Gate[] {
|
||||
return [
|
||||
...ciSharedStaticGates(),
|
||||
pnpmScript('typecheck', 'typecheck'),
|
||||
lintGate(),
|
||||
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',
|
||||
@@ -355,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',
|
||||
@@ -385,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' },
|
||||
}),
|
||||
@@ -447,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
|
||||
@@ -554,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> = {}
|
||||
@@ -562,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' }),
|
||||
@@ -572,6 +587,7 @@ 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' }),
|
||||
@@ -601,7 +617,6 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
|
||||
'vitest.e2e.config.ts',
|
||||
'examples/headless-agent/tests/keyless-smoke.e2e.ts',
|
||||
'apps/cli/tests/built-bin.e2e.ts',
|
||||
'packages/examples/cli-demo/tests/built-bin.e2e.ts',
|
||||
'packages/examples/acp-demo/tests/built-bin.e2e.ts',
|
||||
'packages/host/directory-picker-native/tests/built-worker.e2e.ts',
|
||||
'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -65,6 +65,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'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-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 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.' },
|
||||
@@ -119,6 +120,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/telemetry/session-telemetry': { kind: 'none', reason: 'The seam observes the session stream and hands redacted copies outward; it registers no model surface.' },
|
||||
'packages/telemetry/session-telemetry-otel': { kind: 'none', reason: 'The backend forwards seam records into the OTel SDK pipeline and registers no model surface.' },
|
||||
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },
|
||||
'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/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.' },
|
||||
@@ -126,7 +128,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'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 observes child-process streams without changing live requests.' },
|
||||
'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/api/gateway': { kind: 'none', reason: 'Remote dispatch infrastructure; invoked business methods own any model-visible effect.' },
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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('/', '/')
|
||||
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 },
|
||||
])
|
||||
})
|
||||
|
||||
it('allows only the exact audited trusted-publishing repository declarations', () => {
|
||||
const internalOwner = ['deepseek', 'harness'].join('-')
|
||||
const internalRepository = [internalOwner, internalOwner].join('/')
|
||||
const repositoryUrl = `git+https://github.com/${internalRepository}.git`
|
||||
const manifestLine = ` "url": "${repositoryUrl}",`
|
||||
const constraintLine = `const repositoryUrl = '${repositoryUrl}'`
|
||||
const allowedDeclarations = [
|
||||
['native/landlock-run/packages/entry/package.json', manifestLine],
|
||||
['native/landlock-run/packages/linux-arm64/package.json', manifestLine],
|
||||
['native/landlock-run/packages/linux-x64/package.json', manifestLine],
|
||||
['scripts/check-workspace-constraints.ts', constraintLine],
|
||||
] as const
|
||||
|
||||
for (const [file, source] of allowedDeclarations) {
|
||||
expect(findInternalRepositoryReferences(file, source)).toEqual([])
|
||||
}
|
||||
|
||||
const wrongFile = 'native/landlock-run/package.json'
|
||||
expect(findInternalRepositoryReferences(wrongFile, manifestLine)).toEqual([{ file: wrongFile, line: 1 }])
|
||||
|
||||
const manifestFile = 'native/landlock-run/packages/entry/package.json'
|
||||
const wrongField = ` "homepage": "${repositoryUrl}",`
|
||||
expect(findInternalRepositoryReferences(manifestFile, wrongField)).toEqual([{ file: manifestFile, line: 1 }])
|
||||
|
||||
const encodedLine = manifestLine.replace('github.com/', 'github.com\\/')
|
||||
expect(findInternalRepositoryReferences(manifestFile, encodedLine)).toEqual([{ file: manifestFile, line: 1 }])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
/** Reject tracked files that expose the internal repository identity outside audited publishing declarations. */
|
||||
|
||||
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 trustedPublishingRepositoryUrl = `git+https://github.com/${internalRepository}.git`
|
||||
|
||||
/** Exact declarations that intentionally expose the source repository for trusted publishing. */
|
||||
const allowedInternalRepositoryLineByFile: Readonly<Record<string, string>> = {
|
||||
'native/landlock-run/packages/entry/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
|
||||
'native/landlock-run/packages/linux-arm64/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
|
||||
'native/landlock-run/packages/linux-x64/package.json': `"url": "${trustedPublishingRepositoryUrl}",`,
|
||||
'scripts/check-workspace-constraints.ts': `const repositoryUrl = '${trustedPublishingRepositoryUrl}'`,
|
||||
}
|
||||
|
||||
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 unaudited 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)
|
||||
const isAllowedPublishingDeclaration = line.trim() === allowedInternalRepositoryLineByFile[file]
|
||||
if (!isAllowedPublishingDeclaration
|
||||
&& (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 unexpected internal repository identity.')
|
||||
} else {
|
||||
console.error('verify-public-repository-links: unexpected internal repository references found:')
|
||||
for (const reference of references) console.error(` ${reference.file}:${String(reference.line)}`)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -204,15 +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 build preserves the face order from package.json: generate Host contracts
|
||||
# before either aggregate typecheck, then bundle the completed workspace.
|
||||
# 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/contracts-tsc.log" "$tsc_js" -b packages/typert/generator --pretty false || return $?
|
||||
wine_node "$scratch/logs/contracts-tsdown.log" "$tsdown_js" --config tsdown.typert-host.config.ts || return $?
|
||||
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/tsdown.log" "$tsdown_js"
|
||||
wine_node "$scratch/logs/client-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE client
|
||||
}
|
||||
site_gate() {
|
||||
cd website
|
||||
@@ -238,12 +237,11 @@ report() {
|
||||
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
|
||||
fi
|
||||
}
|
||||
report 'build (contract prepass, tsc, tsdown)' "$build_status" \
|
||||
"$scratch/logs/contracts-tsc.log" \
|
||||
"$scratch/logs/contracts-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/tsdown.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"
|
||||
Reference in New Issue
Block a user