mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge branch 'master' into worktree/pr-to-issue-lifecycle
This commit is contained in:
BIN
scripts/attribute-chunk-bytes.mjs
Normal file
BIN
scripts/attribute-chunk-bytes.mjs
Normal file
Binary file not shown.
@@ -7,6 +7,7 @@
|
||||
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||
import { join, relative, resolve } from 'node:path'
|
||||
import { hasTypeRTRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
// vendor/* is single-level; packages/<group>/<pkg> nests one level deeper
|
||||
@@ -14,6 +15,7 @@ const root = resolve(import.meta.dirname, '..')
|
||||
const workspaceGlobs = [
|
||||
{ dir: 'vendor', depth: 1 },
|
||||
{ dir: 'packages', depth: 2 },
|
||||
{ dir: 'apps', depth: 1 },
|
||||
] as const
|
||||
const vendoredPackages = new Set([
|
||||
'cordis',
|
||||
@@ -28,6 +30,10 @@ const vendoredPackages = new Set([
|
||||
])
|
||||
|
||||
const localArtifactDirs = new Set(['node_modules'])
|
||||
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
|
||||
'@deepseek-ai/dsh': ['lib/*.js', 'config'],
|
||||
'@deepseek-ai/dsh-frontend': ['dist'],
|
||||
}
|
||||
|
||||
/** The subset of package.json fields this constraint check cares about. */
|
||||
interface PackageManifest {
|
||||
@@ -96,7 +102,13 @@ function workspaceManifests(): WorkspaceManifest[] {
|
||||
}
|
||||
|
||||
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// Profile bundles publish their dsh.bundle.patch layer beside the lib.
|
||||
'@deepseek-ai/dsh-base': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
|
||||
'@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-scripts': [
|
||||
'lib/dev/tsdown-config.js',
|
||||
'lib/local-plugin-loader-hooks.js',
|
||||
@@ -110,6 +122,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
|
||||
@@ -133,11 +146,37 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
||||
// declarations.
|
||||
...usesEmittedTreeDefaults(manifest) ? ['lib/types/**/*.js'] : [],
|
||||
'lib/types/**/*.d.ts',
|
||||
'lib/types/**/*.d.ts.map',
|
||||
'src',
|
||||
...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]
|
||||
@@ -164,7 +203,25 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
return errors
|
||||
}
|
||||
|
||||
if (manifest.name?.startsWith('@deepseek-ai/dsh-') && manifest.name !== '@deepseek-ai/dsh-root') {
|
||||
if (manifest.name?.startsWith('@deepseek-ai/')) {
|
||||
const publicationPolicy = { typeRTRemoteNavigation: hasTypeRTRemoteNavigation(manifest) }
|
||||
for (const file of manifest.files ?? []) {
|
||||
if (isForbiddenPublicationFile(file, publicationPolicy)) {
|
||||
errors.push(`${label}: package.json files must not publish ${JSON.stringify(file)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dir.startsWith('apps/') && manifest.name?.startsWith('@deepseek-ai/')) {
|
||||
const expectedFiles = appPackageFiles[manifest.name]
|
||||
if (expectedFiles === undefined) {
|
||||
errors.push(`${label}: app package has no publication files policy`)
|
||||
} else if (!sameStringList(manifest.files, expectedFiles)) {
|
||||
errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (dir.startsWith('packages/') && manifest.name?.startsWith('@deepseek-ai/dsh-')) {
|
||||
const peer = manifest.peerDependencies?.cordis
|
||||
const dev = manifest.devDependencies?.cordis
|
||||
|
||||
|
||||
@@ -59,6 +59,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/)
|
||||
|
||||
108
scripts/coverage-uncovered-locations.cjs
Normal file
108
scripts/coverage-uncovered-locations.cjs
Normal file
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Istanbul coverage reporter printing one clickable `path:line:col` record per
|
||||
* uncovered statement, branch path, and function. Vitest's per-file threshold
|
||||
* failures name only the file; this reporter supplies the exact locations,
|
||||
* printed just above those ERROR lines (reports run before threshold checks).
|
||||
* Files at 100% print nothing, so a green run stays silent.
|
||||
*
|
||||
* CommonJS by requirement: istanbul-reports loads custom reporters with a bare
|
||||
* require() outside the tsx/ESM pipeline (istanbul-reports index.js create()),
|
||||
* so this file can be neither TypeScript nor ESM. Wired into vitest.config.ts
|
||||
* by absolute path — require() would resolve a relative specifier against
|
||||
* istanbul-reports' own directory.
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
const { ReportBase } = require('istanbul-lib-report');
|
||||
|
||||
/**
|
||||
* Editor-convention `line:column` of an istanbul location start (istanbul
|
||||
* columns are 0-based; editors and terminal link handlers expect 1-based).
|
||||
*/
|
||||
function pos(loc) {
|
||||
return `${loc.start.line}:${loc.start.column + 1}`;
|
||||
}
|
||||
|
||||
/** Whether a location carries a usable 1-based start line. */
|
||||
function usable(loc) {
|
||||
return Boolean(loc && loc.start && Number.isFinite(loc.start.line) && loc.start.line >= 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* ` (to line:col)` suffix when the range end adds information beyond the
|
||||
* start. v8-remapped whole-line statements carry end.column = Infinity; those
|
||||
* degrade to a line-only suffix, or to nothing on a single line.
|
||||
*/
|
||||
function endSuffix(loc) {
|
||||
const end = loc.end;
|
||||
if (!end || !Number.isFinite(end.line) || end.line < 1) return '';
|
||||
if (!Number.isFinite(end.column)) {
|
||||
return end.line === loc.start.line ? '' : ` (to ${end.line})`;
|
||||
}
|
||||
if (end.line === loc.start.line && end.column === loc.start.column) return '';
|
||||
return ` (to ${end.line}:${end.column + 1})`;
|
||||
}
|
||||
|
||||
class UncoveredLocationsReport extends ReportBase {
|
||||
constructor(opts = {}) {
|
||||
super(opts);
|
||||
// Vitest passes the resolved config root alongside reporter options.
|
||||
this.projectRoot = opts.projectRoot || process.cwd();
|
||||
this.records = [];
|
||||
}
|
||||
|
||||
onStart() {
|
||||
this.records = [];
|
||||
}
|
||||
|
||||
onDetail(node) {
|
||||
const fc = node.getFileCoverage();
|
||||
const rel = path.relative(this.projectRoot, fc.path).split(path.sep).join('/');
|
||||
const items = [];
|
||||
const add = (loc, text) => items.push({ line: loc.start.line, column: loc.start.column, text });
|
||||
|
||||
for (const id of Object.keys(fc.statementMap)) {
|
||||
if (fc.s[id] !== 0) continue;
|
||||
const loc = fc.statementMap[id];
|
||||
if (!usable(loc)) continue;
|
||||
add(loc, `${rel}:${pos(loc)} uncovered statement${endSuffix(loc)}`);
|
||||
}
|
||||
|
||||
for (const id of Object.keys(fc.fnMap)) {
|
||||
if (fc.f[id] !== 0) continue;
|
||||
const fn = fc.fnMap[id];
|
||||
const loc = usable(fn.decl) ? fn.decl : fn.loc;
|
||||
if (!usable(loc)) continue;
|
||||
const name = fn.name ? ` ${fn.name}` : '';
|
||||
add(loc, `${rel}:${pos(loc)} uncovered function${name}`);
|
||||
}
|
||||
|
||||
for (const id of Object.keys(fc.branchMap)) {
|
||||
const counts = fc.b[id];
|
||||
const branch = fc.branchMap[id];
|
||||
for (let i = 0; i < counts.length; i += 1) {
|
||||
if (counts[i] !== 0) continue;
|
||||
// Implicit arms (e.g. a missing else) may carry an empty location;
|
||||
// fall back to the branch's own span so the record stays clickable.
|
||||
const loc = usable(branch.locations && branch.locations[i]) ? branch.locations[i] : branch.loc;
|
||||
if (!usable(loc)) continue;
|
||||
add(loc, `${rel}:${pos(loc)} uncovered branch (${branch.type}, path ${i + 1}/${counts.length})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0) return;
|
||||
items.sort((a, b) => a.line - b.line || a.column - b.column);
|
||||
for (const item of items) this.records.push(item.text);
|
||||
}
|
||||
|
||||
onEnd() {
|
||||
if (this.records.length === 0) return;
|
||||
console.log(`\nUncovered locations (per-file 100% gate): ${this.records.length}`);
|
||||
for (const record of this.records) console.log(record);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UncoveredLocationsReport;
|
||||
@@ -6,7 +6,7 @@ import { spawn } from 'node:child_process'
|
||||
const SURFACES = new Map([
|
||||
// The browser surface with the cordis toolset layered on: `dsh web --config`
|
||||
// applies this overlay over the shipped web composition; it owns port 3081.
|
||||
['web', ['--import', 'tsx', 'apps/cli/src/bin.ts', 'web', '--config', 'examples/web-cordis/cordis.yml']],
|
||||
['web', ['--import', 'tsx', 'apps/cli/src/bin.ts', 'web', '--patch', 'examples/web-cordis/cordis.yml']],
|
||||
['acp', ['--import', 'tsx', 'packages/examples/acp-demo/src/bin.ts', '--config', 'examples/acp-agent/cordis-tools.cordis.yml']],
|
||||
])
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"AGENTS.md": 1775,
|
||||
"docs/AGENTS.md": 1150,
|
||||
"AGENTS.md": 1782,
|
||||
"docs/AGENTS.md": 1320,
|
||||
"docs/architecture.md": 2160,
|
||||
"docs/cordis-primer.md": 600,
|
||||
"docs/defensive-patterns.md": 550,
|
||||
"docs/testing.md": 1100,
|
||||
"docs/testing.md": 1150,
|
||||
"examples/AGENTS.md": 310,
|
||||
"packages/AGENTS.md": 675,
|
||||
"packages/README.md": 920
|
||||
"packages/README.md": 936
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -28,12 +28,11 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ContinuationDecision: 'core.md',
|
||||
ContinuationStop: 'core.md',
|
||||
GenerateOptions: 'core.md',
|
||||
InboxItem: 'core.md',
|
||||
InboxPlacement: 'core.md',
|
||||
MessageId: 'core.md',
|
||||
HookContext: 'core.md',
|
||||
SettleReason: 'core.md',
|
||||
AdapterRegistrationHandle: 'core.md',
|
||||
DirectoryRegistrationHandle: 'core.md',
|
||||
LlmCallConfig: 'core.md',
|
||||
LlmModelContext: 'core.md',
|
||||
LlmModelReasoningInfo: 'core.md',
|
||||
@@ -42,13 +41,16 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
LlmModelInfo: 'core.md',
|
||||
LlmProviderInfo: 'core.md',
|
||||
LlmConfigurableProvider: 'core.md',
|
||||
LlmModelDiscoveryRequest: 'core.md',
|
||||
LlmDiscoveredModel: 'core.md',
|
||||
ResolvedRetryPolicy: 'llm-streaming.md',
|
||||
Message: 'core.md',
|
||||
MessageSource: 'core.md',
|
||||
UserMessage: 'session.md',
|
||||
PromptDecision: 'core.md',
|
||||
RequestError: 'core.md',
|
||||
PreStepDecision: 'core.md',
|
||||
PreStepContext: 'core.md',
|
||||
RequestErrorAction: 'core.md',
|
||||
RequestFailureContext: 'core.md',
|
||||
PreparedReferencedMessage: 'session-reference.md',
|
||||
SessionReferenceCandidate: 'session-reference.md',
|
||||
SessionReferenceInput: 'session-reference.md',
|
||||
@@ -88,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',
|
||||
@@ -103,8 +106,11 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
StreamChunk: 'llm-streaming.md',
|
||||
SkillProviderControl: 'skills.md',
|
||||
CreateSessionOptions: 'persistence.md',
|
||||
PrepareSessionOptions: 'persistence.md',
|
||||
SessionHeader: 'persistence.md',
|
||||
SessionInspection: 'persistence.md',
|
||||
SessionLocation: 'persistence.md',
|
||||
SessionPreparation: 'persistence.md',
|
||||
SessionPersistenceSnapshot: 'persistence.md',
|
||||
ConfinedArgv: 'sandbox.md',
|
||||
SandboxExecutionPolicy: 'sandbox.md',
|
||||
@@ -271,15 +277,19 @@ 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',
|
||||
LocaleDict: 'service-local dictionary shape is owned by packages/client/i18n/src/index.ts',
|
||||
WebBootGraph: 'web boot graph wire shape is owned by packages/client/modules/src/client/index.ts',
|
||||
WebRoute: 'route registration contract is owned by packages/host/webserver/src/index.ts',
|
||||
WebUpgradeRoute:
|
||||
'upgrade route registration contract is owned by packages/host/webserver/src/index.ts',
|
||||
ThemeTokens: 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts',
|
||||
Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts',
|
||||
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',
|
||||
|
||||
@@ -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',
|
||||
@@ -320,24 +327,25 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
title: 'Subprocess seam',
|
||||
mode: 'seam',
|
||||
implementations: ['subprocess-local'],
|
||||
consumers: ['bash-local', 'bash-sandbox', 'lsp-local', 'subagent-acp'],
|
||||
note: 'The bash executors, the LSP host, and the ACP subagent backend spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation.',
|
||||
consumers: ['bash-local', 'bash-sandbox', 'lsp-local', 'subagent-acp', 'subagent-codex', 'subagent-claude-code'],
|
||||
note: 'The bash executors, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation.',
|
||||
},
|
||||
{
|
||||
key: 'bash',
|
||||
pkg: 'bash',
|
||||
title: 'Bash executor seam',
|
||||
mode: 'seam',
|
||||
implementations: ['bash-local', 'bash-sandbox'],
|
||||
consumers: ['tool-bash', 'hooks-claude', 'hooks-codex'],
|
||||
note: 'The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them.',
|
||||
implementations: ['bash-local', 'bash-sandbox', 'pwsh-local'],
|
||||
consumers: ['tool-bash', 'tool-pwsh', 'hooks-claude', 'hooks-codex'],
|
||||
note: 'The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them.',
|
||||
},
|
||||
{
|
||||
key: 'bashEnv',
|
||||
pkg: 'tool-bash',
|
||||
pkg: 'bash-env',
|
||||
title: 'Managed bash environment registry',
|
||||
mode: 'core',
|
||||
note: 'Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace.',
|
||||
consumers: ['tool-bash', 'tool-pwsh'],
|
||||
note: 'Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace.',
|
||||
},
|
||||
{
|
||||
key: 'pty',
|
||||
@@ -416,7 +424,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'subagent',
|
||||
title: 'Subagent provider and continuation service',
|
||||
mode: 'seam',
|
||||
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp'],
|
||||
implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-codex', 'subagent-claude-code', 'subagent-dsh-sdk'],
|
||||
consumers: ['tool-subagent', 'tool-subagent-control', 'tool-ralph'],
|
||||
note: 'Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route.',
|
||||
},
|
||||
@@ -597,7 +605,8 @@ function parseExampleCordis(rel: string): ExamplePlugin[] {
|
||||
if (current?.name) plugins.push({ id: current.id, name: current.name })
|
||||
}
|
||||
for (const line of text.split('\n')) {
|
||||
const id = /^-\s+id:\s+(.+?)\s*$/.exec(line)
|
||||
// Top-level rows (`- id:`) and bundle-patch insert rows (` - id:`).
|
||||
const id = /^\s*-\s+id:\s+(.+?)\s*$/.exec(line)
|
||||
if (id?.[1] !== undefined) {
|
||||
flush()
|
||||
current = { id: stripYamlScalar(id[1]) }
|
||||
@@ -619,9 +628,9 @@ const APP_EXAMPLES = [
|
||||
id: 'dsh_base',
|
||||
rel: 'apps/cli/composition.md',
|
||||
title: 'DSH Base Composition',
|
||||
label: 'apps/cli/config/base.cordis.yml',
|
||||
config: 'apps/cli/config/base.cordis.yml',
|
||||
summary: 'The raw CLI applies one required caller-selected patch list over this shared base; Web and headless apply their own shipped overlays.',
|
||||
label: 'packages/bundle/base/cordis.patch.yml',
|
||||
config: 'packages/bundle/base/cordis.patch.yml',
|
||||
summary: 'The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app, dsh-headless) and the user\'s profile layer patch over it.',
|
||||
},
|
||||
{
|
||||
id: 'headless',
|
||||
@@ -1152,20 +1161,22 @@ function renderLifecycle(): string {
|
||||
' participant Session',
|
||||
' participant SDK as UI or SDK listener',
|
||||
' User->>Agent: followup(content)',
|
||||
` Agent-->>SDK: ${mermaidCode('agent/inbox/enqueue')}`,
|
||||
` Agent-->>SDK: ${mermaidCode('agent/inbox/spliced')}`,
|
||||
` Agent-->>SDK: ${mermaidCode('agent/inbox/inserted')} { message }`,
|
||||
' Agent->>Driver: queued work wakes driver',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/status')} running`,
|
||||
' Note over Agent,Driver: next-step acceptance window opens',
|
||||
` Driver->>Hooks: ${mermaidCode('agent/prompt-submit')} waterfall`,
|
||||
' Hooks-->>Driver: authoritative allow, block, or add context',
|
||||
' alt prompt blocked or admission failed',
|
||||
' Driver-->>Driver: append context-only batch or keep steering boundary pending',
|
||||
' else prompt allowed',
|
||||
' Note over Agent,Driver: claim pending next-step input plus one queued prompt',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/inbox/spliced')} pure deletion`,
|
||||
` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`,
|
||||
' Hooks-->>Driver: authoritative reject or enter(messages)',
|
||||
' alt proposed step rejected or pre-step failed',
|
||||
' Driver-->>Driver: claimed batch stays removed, no turn opens',
|
||||
' else enter proposed step',
|
||||
` Driver->>Session: ${mermaidCode('turn/start')}`,
|
||||
` Driver->>Session: ${mermaidCode('user/message')}`,
|
||||
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
|
||||
` Driver-->>Driver: ${mermaidCode('agent/step')} serial checkpoint`,
|
||||
` Driver->>Session: ${mermaidCode('step/start')}`,
|
||||
` Driver->>Session: ${mermaidCode('user/message')} per entered message`,
|
||||
` Driver->>Prompt: ${mermaidCode('system-prompt/assemble')} waterfall`,
|
||||
` Driver->>LLM: ${mermaidCode('agent/request')} waterfall, then ${mermaidCode('llm/stream')} waterfall`,
|
||||
' LLM-->>Driver: StreamChunk*',
|
||||
` Driver->>Session: ${mermaidCode('assistant/chunk')}*`,
|
||||
@@ -1188,11 +1199,17 @@ function renderLifecycle(): string {
|
||||
` Driver->>Session: ${mermaidCode('tool/result')}`,
|
||||
' end',
|
||||
' end',
|
||||
' Driver->>Session: post-tool context and steering (no prompt-submit)',
|
||||
` Driver->>Session: ${mermaidCode('step/end')}`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`,
|
||||
' opt natural stop and next-step inbox empty',
|
||||
` Driver->>Hooks: ${mermaidCode('agent/turn-stopping')} serial terminal checkpoint`,
|
||||
' end',
|
||||
' opt next-step input is pending',
|
||||
' Driver-->>Driver: claim pending next-step input',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/inbox/claimed')} { message, turn } per message`,
|
||||
` Driver->>Hooks: ${mermaidCode('agent/pre-step')} waterfall`,
|
||||
' Hooks-->>Driver: authoritative reject or enter(messages)',
|
||||
' end',
|
||||
' end',
|
||||
' Note over Agent,Driver: next-step acceptance window closes',
|
||||
` Driver->>Session: ${mermaidCode('turn/end')}`,
|
||||
' end',
|
||||
` Driver-->>SDK: ${mermaidCode('agent/status')} idle`,
|
||||
@@ -1200,9 +1217,9 @@ function renderLifecycle(): string {
|
||||
'',
|
||||
'The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.',
|
||||
'',
|
||||
'`dsh-compact-basic` uses `agent/step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
|
||||
'`dsh-compact-basic` uses `agent/pre-step` for pressure before request derivation and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.',
|
||||
'',
|
||||
'The returned `agent/prompt-submit` allow is authoritative; listeners wrapping `next()` preserve downstream content and additional contexts unless replacement is intentional. Steering bypasses that waterfall and joins at its durable checkpoint.',
|
||||
'The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later boundary claims their next-step batch.',
|
||||
'',
|
||||
'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.',
|
||||
'',
|
||||
|
||||
@@ -2,7 +2,20 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSyn
|
||||
import { join, resolve } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { collectPythonDependencies, isPermissive, type Manifest, manifestPatterns, parsePyprojectRequirements, parseVendoredRows, render, tierExternalDeps, virtualManifest } from './gen-third-party-notices.ts'
|
||||
import {
|
||||
CLAUDE_AGENT_SDK_PACKAGE,
|
||||
claudeDistributionFromManifest,
|
||||
collectPythonDependencies,
|
||||
isOwnerAuthorizedRuntime,
|
||||
isPermissive,
|
||||
type Manifest,
|
||||
manifestPatterns,
|
||||
parsePyprojectRequirements,
|
||||
parseVendoredRows,
|
||||
render,
|
||||
tierExternalDeps,
|
||||
virtualManifest,
|
||||
} from './gen-third-party-notices.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
@@ -12,7 +25,9 @@ describe('THIRD_PARTY_NOTICES.md', () => {
|
||||
// Pre-commit regenerates the file whenever a manifest is staged, so reaching
|
||||
// this assertion means the notices were committed without that hook.
|
||||
it('matches what the generator produces from the current manifests', () => {
|
||||
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(render())
|
||||
const generated = render()
|
||||
expect(generated).toContain('It depends on the third-party software listed below.')
|
||||
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(generated)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -223,7 +238,14 @@ describe('collectPythonDependencies', () => {
|
||||
describe('isPermissive', () => {
|
||||
it('accepts the licenses this project ships and rejects copyleft or unknown ones', () => {
|
||||
expect(['MIT', 'ISC', 'BSD-3-Clause', 'Apache-2.0', 'MIT / Apache-2.0', '(MIT OR CC0-1.0)'].every(isPermissive)).toBe(true)
|
||||
expect(['LGPL-3.0-only', 'MPL-2.0', 'GPL-3.0-or-later', 'SEE LICENSE IN LICENSE'].some(isPermissive)).toBe(false)
|
||||
expect([
|
||||
'LGPL-3.0-only',
|
||||
'MPL-2.0',
|
||||
'GPL-3.0-or-later',
|
||||
'SEE LICENSE IN LICENSE',
|
||||
'SEE LICENSE IN README.md',
|
||||
'SEE LICENSE IN LICENSE.md',
|
||||
].some(isPermissive)).toBe(false)
|
||||
})
|
||||
|
||||
it('requires every operand of an AND, so a copyleft conjunct cannot ride along', () => {
|
||||
@@ -245,6 +267,66 @@ describe('isPermissive', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('official Claude distribution authorization', () => {
|
||||
it('authorizes only the direct SDK identity without relabeling its license', () => {
|
||||
expect(isOwnerAuthorizedRuntime(CLAUDE_AGENT_SDK_PACKAGE)).toBe(true)
|
||||
expect(isOwnerAuthorizedRuntime(`${CLAUDE_AGENT_SDK_PACKAGE}-linux-x64`))
|
||||
.toBe(false)
|
||||
expect(isOwnerAuthorizedRuntime('@anthropic-ai/unrelated')).toBe(false)
|
||||
expect(isPermissive('SEE LICENSE IN README.md')).toBe(false)
|
||||
})
|
||||
|
||||
it('derives version-independent platform payloads from the official SDK manifest', () => {
|
||||
expect(claudeDistributionFromManifest({
|
||||
name: CLAUDE_AGENT_SDK_PACKAGE,
|
||||
version: '9.8.7',
|
||||
license: 'future declared terms',
|
||||
claudeCodeVersion: '6.5.4',
|
||||
optionalDependencies: {
|
||||
[`${CLAUDE_AGENT_SDK_PACKAGE}-linux-x64`]: '9.8.7',
|
||||
[`${CLAUDE_AGENT_SDK_PACKAGE}-darwin-arm64`]: '9.8.7',
|
||||
},
|
||||
})).toEqual({
|
||||
sdkVersion: '9.8.7',
|
||||
claudeCodeVersion: '6.5.4',
|
||||
payloads: [
|
||||
{
|
||||
name: `${CLAUDE_AGENT_SDK_PACKAGE}-darwin-arm64`,
|
||||
version: '9.8.7',
|
||||
},
|
||||
{
|
||||
name: `${CLAUDE_AGENT_SDK_PACKAGE}-linux-x64`,
|
||||
version: '9.8.7',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a wrong SDK identity, missing payloads, and unrelated optionals', () => {
|
||||
expect(() => claudeDistributionFromManifest({
|
||||
name: '@anthropic-ai/unrelated',
|
||||
version: '1.0.0',
|
||||
claudeCodeVersion: '1.0.0',
|
||||
optionalDependencies: {
|
||||
[`${CLAUDE_AGENT_SDK_PACKAGE}-linux-x64`]: '1.0.0',
|
||||
},
|
||||
})).toThrow(`expected ${CLAUDE_AGENT_SDK_PACKAGE} manifest`)
|
||||
expect(() => claudeDistributionFromManifest({
|
||||
name: CLAUDE_AGENT_SDK_PACKAGE,
|
||||
version: '1.0.0',
|
||||
claudeCodeVersion: '1.0.0',
|
||||
})).toThrow('declares no optional platform payloads')
|
||||
expect(() => claudeDistributionFromManifest({
|
||||
name: CLAUDE_AGENT_SDK_PACKAGE,
|
||||
version: '1.0.0',
|
||||
claudeCodeVersion: '1.0.0',
|
||||
optionalDependencies: {
|
||||
'@anthropic-ai/unrelated': '1.0.0',
|
||||
},
|
||||
})).toThrow('outside its authorized platform-payload identity')
|
||||
})
|
||||
})
|
||||
|
||||
describe('manifestPatterns', () => {
|
||||
it('derives globs from the declared members, so a new member area is read', () => {
|
||||
expect(manifestPatterns(['packages/*/*', 'tools/*'], ['packages/*'])).toEqual([
|
||||
|
||||
@@ -49,6 +49,21 @@ const FIRST_PARTY = new Set([
|
||||
'node-addon-landlock-run-linux-x64',
|
||||
])
|
||||
|
||||
/** Official SDK identity covered by the project's narrow owner authorization. */
|
||||
export const CLAUDE_AGENT_SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk'
|
||||
const CLAUDE_PLATFORM_PACKAGE_PREFIX = `${CLAUDE_AGENT_SDK_PACKAGE}-`
|
||||
const CLAUDE_PLATFORM_DECLARED_LICENSE = 'SEE LICENSE IN LICENSE.md'
|
||||
|
||||
/**
|
||||
* Whether a non-permissive runtime declaration has an identity-scoped owner
|
||||
* authorization. This does not reclassify its terms as permissive.
|
||||
* @param name - exact npm package identity.
|
||||
* @returns true only for the official Claude Agent SDK package.
|
||||
*/
|
||||
export function isOwnerAuthorizedRuntime(name: string): boolean {
|
||||
return name === CLAUDE_AGENT_SDK_PACKAGE
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata overrides where the installed manifest is wrong or unreachable.
|
||||
* Each entry documents why the store cannot answer.
|
||||
@@ -92,6 +107,7 @@ const BUILD_TIME_TOOLS = [
|
||||
/** The `package.json` fields this generator reads. */
|
||||
export interface Manifest {
|
||||
name?: string
|
||||
version?: string
|
||||
private?: boolean
|
||||
license?: string
|
||||
dependencies?: Record<string, string>
|
||||
@@ -164,7 +180,74 @@ function loadWorkspaceManifests(): { manifests: Map<string, Manifest>; names: Se
|
||||
return { manifests, names }
|
||||
}
|
||||
|
||||
type VirtualManifest = Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }
|
||||
type VirtualManifest = Manifest & {
|
||||
claudeCodeVersion?: string
|
||||
license?: string
|
||||
repository?: string | { url?: string }
|
||||
homepage?: string
|
||||
}
|
||||
|
||||
/** One platform payload declared by the official Claude Agent SDK. */
|
||||
export interface ClaudePlatformPayload {
|
||||
readonly name: string
|
||||
readonly version: string
|
||||
}
|
||||
|
||||
/** Current SDK and CLI distribution facts derived from the installed SDK manifest. */
|
||||
export interface ClaudeDistribution {
|
||||
readonly sdkVersion: string
|
||||
readonly claudeCodeVersion: string
|
||||
readonly payloads: ClaudePlatformPayload[]
|
||||
}
|
||||
|
||||
function requiredManifestString(
|
||||
value: string | undefined,
|
||||
field: string,
|
||||
): string {
|
||||
if (value === undefined || value.length === 0) {
|
||||
throw new Error(`gen-third-party-notices: ${CLAUDE_AGENT_SDK_PACKAGE} has no ${field}.`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the official platform payload set without a version or platform
|
||||
* allowlist. Only identities in the SDK's own package namespace are covered.
|
||||
* @param manifest - installed official SDK manifest.
|
||||
* @returns current SDK, CLI, and optional platform payload facts.
|
||||
*/
|
||||
export function claudeDistributionFromManifest(
|
||||
manifest: VirtualManifest,
|
||||
): ClaudeDistribution {
|
||||
if (manifest.name !== CLAUDE_AGENT_SDK_PACKAGE) {
|
||||
throw new Error(
|
||||
`gen-third-party-notices: expected ${CLAUDE_AGENT_SDK_PACKAGE} manifest, got ${JSON.stringify(manifest.name)}.`,
|
||||
)
|
||||
}
|
||||
const sdkVersion = requiredManifestString(manifest.version, 'version')
|
||||
const claudeCodeVersion = requiredManifestString(
|
||||
manifest.claudeCodeVersion,
|
||||
'claudeCodeVersion',
|
||||
)
|
||||
const entries = Object.entries(manifest.optionalDependencies ?? {})
|
||||
if (entries.length === 0) {
|
||||
throw new Error(
|
||||
`gen-third-party-notices: ${CLAUDE_AGENT_SDK_PACKAGE} declares no optional platform payloads.`,
|
||||
)
|
||||
}
|
||||
const payloads = entries.map(([name, version]) => {
|
||||
if (!name.startsWith(CLAUDE_PLATFORM_PACKAGE_PREFIX)) {
|
||||
throw new Error(
|
||||
`gen-third-party-notices: ${CLAUDE_AGENT_SDK_PACKAGE} optional dependency ${name} is outside its authorized platform-payload identity.`,
|
||||
)
|
||||
}
|
||||
return {
|
||||
name,
|
||||
version: requiredManifestString(version, `${name} optional dependency version`),
|
||||
}
|
||||
}).sort((left, right) => left.name.localeCompare(right.name))
|
||||
return { sdkVersion, claudeCodeVersion, payloads }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one package's manifest inside a pnpm virtual store. The prefix scan
|
||||
@@ -193,9 +276,8 @@ export function virtualManifest(virtual: string, name: string): VirtualManifest
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** License and repository URL for an installed external package, from the pnpm store. */
|
||||
function installedMetadata(name: string): { license: string; repo: string } {
|
||||
const override = OVERRIDES[name]
|
||||
/** 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.
|
||||
@@ -210,6 +292,13 @@ function installedMetadata(name: string): { license: string; repo: string } {
|
||||
manifest = virtualManifest(virtual, name)
|
||||
if (manifest !== undefined) break
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
|
||||
/** License and repository URL for an installed external package, from the pnpm store. */
|
||||
function installedMetadata(name: string): { license: string; repo: string } {
|
||||
const override = OVERRIDES[name]
|
||||
const manifest = installedManifest(name)
|
||||
const license = override?.license ?? manifest?.license
|
||||
const rawRepo = typeof manifest?.repository === 'string' ? manifest.repository : manifest?.repository?.url ?? manifest?.homepage
|
||||
const repo = override?.repo ?? normalizeRepo(rawRepo)
|
||||
@@ -219,6 +308,37 @@ function installedMetadata(name: string): { license: string; repo: string } {
|
||||
return { license, repo }
|
||||
}
|
||||
|
||||
function collectClaudeDistribution(): ClaudeDistribution {
|
||||
const manifest = installedManifest(CLAUDE_AGENT_SDK_PACKAGE)
|
||||
if (manifest === undefined) {
|
||||
throw new Error(
|
||||
`gen-third-party-notices: cannot resolve ${CLAUDE_AGENT_SDK_PACKAGE}; run \`pnpm install\`.`,
|
||||
)
|
||||
}
|
||||
const distribution = claudeDistributionFromManifest(manifest)
|
||||
let installedPayloads = 0
|
||||
for (const payload of distribution.payloads) {
|
||||
const installed = installedManifest(payload.name)
|
||||
if (installed === undefined) continue
|
||||
installedPayloads += 1
|
||||
if (
|
||||
installed.name !== payload.name
|
||||
|| installed.version !== payload.version
|
||||
|| installed.license !== CLAUDE_PLATFORM_DECLARED_LICENSE
|
||||
) {
|
||||
throw new Error(
|
||||
`gen-third-party-notices: installed ${payload.name} does not match its SDK-declared version and ${CLAUDE_PLATFORM_DECLARED_LICENSE} license field.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (installedPayloads === 0) {
|
||||
throw new Error(
|
||||
'gen-third-party-notices: no SDK-declared Claude platform payload is installed; install optional dependencies before regenerating.',
|
||||
)
|
||||
}
|
||||
return distribution
|
||||
}
|
||||
|
||||
/** Normalize a manifest repository/homepage value to a browsable https URL. */
|
||||
function normalizeRepo(raw: string | undefined): string | undefined {
|
||||
if (raw === undefined || raw === '') return undefined
|
||||
@@ -519,6 +639,26 @@ function renderNpmTable(deps: ExternalDep[]): string {
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function renderClaudeDistribution(
|
||||
distribution: ClaudeDistribution | undefined,
|
||||
): string {
|
||||
if (distribution === undefined) return ''
|
||||
const rows = distribution.payloads.map(payload =>
|
||||
`| [\`${payload.name}\`](https://www.npmjs.com/package/${payload.name}) | ${payload.version} | ${CLAUDE_PLATFORM_DECLARED_LICENSE} |`,
|
||||
)
|
||||
return `
|
||||
## Official Claude Code platform payloads
|
||||
|
||||
The project owner authorizes distribution of every version of the official \`${CLAUDE_AGENT_SDK_PACKAGE}\` package and the official Claude Code CLI/platform payloads that each version declares through \`optionalDependencies\`. This identity-scoped authorization does not classify their declared terms as permissive and does not cover any unrelated runtime package; version, declared-license, and payload-set changes still require the ordinary dependency, lockfile, compatibility, terms, and notices review.
|
||||
|
||||
The installed SDK ${distribution.sdkVersion} declares the following optional platform packages. Each carries the official Claude Code ${distribution.claudeCodeVersion} executable; the package identities and versions come from the SDK manifest, while the declared license field is verified against the platform payload installed for the current host.
|
||||
|
||||
| Optional platform package | Version | Declared license |
|
||||
| --- | --- | --- |
|
||||
${rows.join('\n')}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the complete notices document.
|
||||
* @returns the exact bytes `THIRD_PARTY_NOTICES.md` must hold.
|
||||
@@ -531,11 +671,19 @@ export function render(): string {
|
||||
const vendored = collectVendored()
|
||||
const python = collectPython()
|
||||
const patched = collectPatched()
|
||||
const claudeDistribution = runtimeDeps.some(
|
||||
dep => dep.name === CLAUDE_AGENT_SDK_PACKAGE,
|
||||
)
|
||||
? collectClaudeDistribution()
|
||||
: undefined
|
||||
|
||||
const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license))
|
||||
// A copyleft license reaching a shipped surface is a distribution decision,
|
||||
// not a rendering detail; the notices cannot quietly absorb it.
|
||||
const nonPermissiveRuntime = runtimeDeps.filter(dep => !isPermissive(dep.license))
|
||||
const nonPermissiveRuntime = runtimeDeps.filter(dep =>
|
||||
!isPermissive(dep.license)
|
||||
&& !isOwnerAuthorizedRuntime(dep.name),
|
||||
)
|
||||
if (nonPermissiveRuntime.length > 0) {
|
||||
throw new Error(`gen-third-party-notices: runtime ${nonPermissiveRuntime.map(dep => `${dep.name} (${dep.license})`).join(', ')} is not a permissive license; review the distribution terms and record the decision before regenerating.`)
|
||||
}
|
||||
@@ -546,9 +694,9 @@ export function render(): string {
|
||||
|
||||
# Third-Party Notices
|
||||
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party open-source software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
|
||||
This file lists **direct** dependencies declared by the workspace. 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.
|
||||
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).
|
||||
|
||||
@@ -562,13 +710,14 @@ ${vendored.map(row => `| \`${row.npmName}\` | [${row.upstream.replace('https://'
|
||||
|
||||
## Runtime npm dependencies
|
||||
|
||||
External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI/TUI, the Web UI, and the Python SDK runtime load by default.
|
||||
External packages that a workspace package resolves at runtime. \`scripts/install.sh\` installs this repository itself, so the tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default.
|
||||
|
||||
${renderNpmTable(runtimeDeps)}
|
||||
|
||||
pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification:
|
||||
|
||||
${patchedLines.join('\n')}
|
||||
${renderClaudeDistribution(claudeDistribution)}
|
||||
|
||||
## Development-only npm dependencies
|
||||
|
||||
|
||||
@@ -14,11 +14,14 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { createScope } from '@deepseek-ai/dsh-scope'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import SessionQuerySqlite from '@deepseek-ai/dsh-session-query-sqlite'
|
||||
import GoalService from '@deepseek-ai/dsh-goal'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
|
||||
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
|
||||
import * as BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
|
||||
import { PwshLocalExecutor } from '@deepseek-ai/dsh-pwsh-local'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
@@ -36,6 +39,7 @@ import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
|
||||
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
|
||||
import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
|
||||
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import * as ToolPwsh from '@deepseek-ai/dsh-tool-pwsh'
|
||||
import * as ToolBashPersistent from '@deepseek-ai/dsh-tool-bash-persistent'
|
||||
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
@@ -172,7 +176,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
toolsConfig: { mode: 'code' },
|
||||
async mount() {},
|
||||
note:
|
||||
'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.',
|
||||
'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime\'s language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-plan-mode',
|
||||
@@ -190,16 +194,35 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-bash',
|
||||
dir: 'tool-bash',
|
||||
source: 'packages/bash/tool-bash/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.bash', 'ctx.tasks at call time for run_in_background'],
|
||||
requires: ['ctx.tools', 'ctx.bash', 'ctx.systemPrompt', 'ctx.bashEnv', 'ctx.tasks at call time for run_in_background'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
await ctx.plugin(BashEnvPlugin)
|
||||
await ctx.plugin(LocalBashExecutor)
|
||||
await ctx.plugin(ToolBash)
|
||||
},
|
||||
note:
|
||||
'The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.tasks` runtime and is collected/stopped through the `task_*` tools from `@deepseek-ai/dsh-tool-tasks`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-pwsh',
|
||||
dir: 'tool-pwsh',
|
||||
source: 'packages/bash/tool-pwsh/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.bash', 'ctx.systemPrompt', 'ctx.bashEnv', 'ctx.tasks at call time for run_in_background'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
// The pwsh tool consumes the bash executor seam; the schema harvest
|
||||
// mounts the pwsh-local implementation so the inject resolves without
|
||||
// executing anything (registration never spawns a process).
|
||||
await ctx.plugin(LocalSubprocessService)
|
||||
await ctx.plugin(BashEnvPlugin)
|
||||
await ctx.plugin(PwshLocalExecutor)
|
||||
await ctx.plugin(ToolPwsh)
|
||||
},
|
||||
note:
|
||||
'The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.bash`); it mirrors the bash tool call-for-call minus the sandbox surface — `run_in_background` runs register with the generic `ctx.tasks` runtime and are collected/stopped through the `task_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-bash-env`. Each call runs in a fresh process (no persistent PTY session; ConPTY is roadmap work), with native `C:\\...` paths and `$env:NAME` variables.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-cordis',
|
||||
dir: 'tool-cordis',
|
||||
@@ -289,7 +312,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
dir: 'tool-goal',
|
||||
source: 'packages/goal/tool-goal/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.agents', 'ctx.goals', 'ctx.systemPrompt', 'a calling Agent in an authorized open turn'],
|
||||
writes: ['tool/call', 'user/message goal snapshot for mutations', 'tool/result'],
|
||||
writes: ['tool/call', 'goal/change for mutations', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(GoalService)
|
||||
@@ -370,7 +393,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
await ctx.plugin(ToolSubagent, { provider: 'mock' })
|
||||
},
|
||||
note:
|
||||
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `apps/cli/config/base.cordis.yml` and `examples/acp-agent/cordis.yml`.',
|
||||
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-subagent-control',
|
||||
@@ -379,19 +402,19 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
list_agents: 'packages/subagent/tool-subagent-control/src/list-agents.ts',
|
||||
send_message: 'packages/subagent/tool-subagent-control/src/index.ts',
|
||||
},
|
||||
requires: ['ctx.tools', 'ctx.subagents', 'ctx.sessionQuery (list_agents only)'],
|
||||
requires: ['ctx.tools', 'ctx.subagents', 'ctx.sessionProjections (list_agents catalog rows)'],
|
||||
writes: ['tool/call', 'tool/result', 'child session events through ctx.subagents'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(SubagentService)
|
||||
await ctx.plugin(LocalTaskService)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionQuerySqlite, { path: ':memory:' })
|
||||
await ctx.plugin(SessionProjectionRegistry)
|
||||
await ctx.plugin(ToolSubagentControl)
|
||||
await ctx.plugin(ToolSubagentListAgents)
|
||||
},
|
||||
note:
|
||||
'The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` once, plus `list_agents` from its separately loaded `/list-agents` plugin (which additionally requires session query).',
|
||||
'The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows are served through the sessionProjections registry).',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-subagent-report',
|
||||
@@ -432,10 +455,10 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
requires: ['ctx.tools', 'owning Agent session'],
|
||||
writes: ['tool/call', 'todo/write', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(ToolTodo)
|
||||
await ctx.plugin(ToolTodo, { allowParallelInProgress: true })
|
||||
},
|
||||
note:
|
||||
'todo_write is session-owned state; UIs render the latest todo/write event as a checklist.',
|
||||
'todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-workflow',
|
||||
@@ -588,7 +611,7 @@ export function render(catalog: ToolCatalog): string {
|
||||
'',
|
||||
'This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator\'s boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog Agent Note](../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md).',
|
||||
'',
|
||||
'Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config. The registered tool NAME can be a load-time config (e.g. `tool-subagent`\'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog\'s packages-only scope.',
|
||||
'Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config, except where a Config field is REQUIRED with no default — there the generator must choose, and the per-package note records which branch this page shows. The registered tool NAME can be a load-time config (e.g. `tool-subagent`\'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog\'s packages-only scope.',
|
||||
'',
|
||||
'## Tool Package Map',
|
||||
'',
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
import lefthookPackage from 'lefthook/package.json' with { type: 'json' }
|
||||
|
||||
const MINIMUM_GIT = [2, 26, 0]
|
||||
const HOOKS_DIRECTORY = 'dsh-hooks'
|
||||
@@ -596,6 +597,7 @@ function refuseScopedHooksPath(entry) {
|
||||
|
||||
async function main() {
|
||||
if (process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true') return
|
||||
if (typeof lefthookPackage.bin?.lefthook !== 'string') return
|
||||
const probe = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' })
|
||||
if (probe.status !== 0) return
|
||||
const root = stripGitLineTerminator(probe.stdout)
|
||||
|
||||
@@ -19,6 +19,9 @@ import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const installer = fileURLToPath(new URL('./install-lefthook.mjs', import.meta.url))
|
||||
const fixtures: string[] = []
|
||||
// Multi-worktree cases spawn several Git and Node subprocesses; coverage concurrency can
|
||||
// legitimately exceed Vitest's default deadline without changing the installer behavior.
|
||||
const MULTI_PROCESS_TEST_TIMEOUT_MS = 20_000
|
||||
|
||||
interface Fixture {
|
||||
container: string
|
||||
@@ -260,7 +263,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
git(fixture, fixture.main, ['worktree', 'remove', '--force', fixture.linked])
|
||||
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBeforeRemoval)
|
||||
expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy hook\n')
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it('replaces the owned hook path Git copies into a newly added worktree', async () => {
|
||||
const fixture = createFixture()
|
||||
@@ -284,7 +287,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
'# config=late-linked-worktree-config',
|
||||
)
|
||||
expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBefore)
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it('serializes concurrent installs and keeps repeated output stable', async () => {
|
||||
const fixture = createFixture()
|
||||
@@ -305,7 +308,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
expect(readFileSync(mainHookPath, 'utf8')).toBe(initialHook)
|
||||
expect(existsSync(join(commonDirectory(fixture), 'dsh-lefthook-install.lock'))).toBe(false)
|
||||
expect(existsSync(join(hooksPath(fixture, fixture.main), '.fake-lefthook-running'))).toBe(false)
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it('waits for a concurrent installer to finish publishing its lock record', async () => {
|
||||
const fixture = createFixture()
|
||||
@@ -343,7 +346,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
expect(readFileSync(join(movedHooks, '.dsh-lefthook-owned'), 'utf8')).toContain(
|
||||
JSON.stringify(movedHooks),
|
||||
)
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it.skipIf(process.platform === 'win32')('refuses a multiply linked ownership marker before relocation rewrites it', async () => {
|
||||
const fixture = createFixture()
|
||||
@@ -384,7 +387,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => {
|
||||
expect(result.stderr).toContain('non-regular or multiply linked hook entry')
|
||||
expect(readFileSync(externalHook, 'utf8')).toBe(externalContent)
|
||||
}
|
||||
})
|
||||
}, MULTI_PROCESS_TEST_TIMEOUT_MS)
|
||||
|
||||
it('restores the marker-backed stale hook path when relocation reinstall fails', async () => {
|
||||
const fixture = createFixture()
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
# DSH_MASTER master clone directory (default: $DSH_SOURCE/master)
|
||||
# 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 the personal config (default: ~/.dsh)
|
||||
# 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
|
||||
|
||||
@@ -47,7 +47,7 @@ function fixture(options: {
|
||||
default: './lib/invariant.js',
|
||||
},
|
||||
},
|
||||
files: ['lib/index.js', 'lib/invariant.js', 'src'],
|
||||
files: ['lib/index.js', 'lib/invariant.js'],
|
||||
peerDependencies: options.invariantDependency === false ? {} : {
|
||||
'@deepseek-ai/dsh-invariants': '^0.0.1',
|
||||
},
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/** Tests for the documentation website projection adapter. */
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { docsPages, type DocsPage } from '../website/docs.ts'
|
||||
import { addProjectionFrontmatter, projectedPageContent, rewriteMarkdown } from './project-doc-site.ts'
|
||||
import {
|
||||
addProjectionFrontmatter, projectedPageContent, publishableImage, rewriteMarkdown,
|
||||
} from './project-doc-site.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const repositoryRoot = resolve(import.meta.dirname, '..')
|
||||
@@ -63,6 +65,32 @@ describe('website source layout', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('publishableImage', () => {
|
||||
it('accepts a regular file inside the repository', () => {
|
||||
const { root } = fixture()
|
||||
const real = realpathSync(join(root, 'packages/logo.svg'))
|
||||
expect(publishableImage(join(root, 'packages/logo.svg'), realpathSync(root))).toBe(real)
|
||||
})
|
||||
|
||||
it('refuses a target whose real path escapes the repository', () => {
|
||||
// Publication copies the bytes onto the site, so a reference reaching a
|
||||
// build-machine file must not be treated as an image the repository owns.
|
||||
const { root } = fixture()
|
||||
const outside = mkdtempSync(join(tmpdir(), 'dsh-doc-site-outside-'))
|
||||
roots.push(outside)
|
||||
writeFileSync(join(outside, 'secret.png'), 'not really a png\n')
|
||||
symlinkSync(join(outside, 'secret.png'), join(root, 'packages/linked.png'))
|
||||
|
||||
expect(publishableImage(join(root, 'packages/linked.png'), realpathSync(root))).toBeUndefined()
|
||||
expect(publishableImage(join(outside, 'secret.png'), realpathSync(root))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('refuses a directory', () => {
|
||||
const { root } = fixture()
|
||||
expect(publishableImage(join(root, 'packages'), realpathSync(root))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('rewriteMarkdown', () => {
|
||||
it('maps published pages and pins unpublished source links', () => {
|
||||
const { root, pages } = fixture()
|
||||
@@ -93,7 +121,7 @@ describe('rewriteMarkdown', () => {
|
||||
})).toBe('[B](./reference-root/b.md)\n')
|
||||
})
|
||||
|
||||
it('uses raw GitHub content for unpublished images', () => {
|
||||
it('uses raw GitHub content for unpublished images when nothing places them', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('\n', {
|
||||
locale: 'en',
|
||||
@@ -105,6 +133,57 @@ describe('rewriteMarkdown', () => {
|
||||
})).toBe('\n')
|
||||
})
|
||||
|
||||
it('hands an image to the placer and uses the URL it returns', () => {
|
||||
// A raw GitHub URL cannot serve a private repository, so the site build
|
||||
// carries images itself; the placer is what puts them there. The stand-in
|
||||
// derives its URL the way the real one does, so a placer that stopped
|
||||
// returning the basename would fail here rather than pass on a constant.
|
||||
const { root, pages } = fixture()
|
||||
const placed: string[] = []
|
||||
expect(rewriteMarkdown('\n', {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
placeImage: (absPath) => {
|
||||
const name = absPath.split('/').pop() ?? ''
|
||||
placed.push(name)
|
||||
return `./${name}`
|
||||
},
|
||||
})).toBe('\n')
|
||||
expect(placed).toEqual(['logo.svg'])
|
||||
})
|
||||
|
||||
it('keeps a placed image\u2019s query or fragment', () => {
|
||||
// An SVG view fragment and a Vite query both change what the reference
|
||||
// means, and the GitHub branch has always carried them.
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('\n', {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
placeImage: absPath => `./${absPath.split('/').pop() ?? ''}`,
|
||||
})).toBe('\n')
|
||||
})
|
||||
|
||||
it('leaves a published page link to the route even when a placer exists', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('[B](b.md)\n', {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
placeImage: () => { throw new Error('a page link must not be placed as an asset') },
|
||||
})).toBe('[B](./reference/b.md)\n')
|
||||
})
|
||||
|
||||
it('does not rewrite Markdown-looking text inside code fences', () => {
|
||||
const { root, pages } = fixture()
|
||||
const source = '```md\n[B](b.md)\n```\n'
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
* tier, while this adapter rewrites cross-source links for the public site.
|
||||
*/
|
||||
|
||||
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, extname, posix, relative, resolve, sep } from 'node:path'
|
||||
import {
|
||||
copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { basename, dirname, extname, posix, relative, resolve, sep } from 'node:path'
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
||||
import { gfm } from 'micromark-extension-gfm'
|
||||
@@ -38,6 +40,15 @@ export interface RewriteMarkdownOptions {
|
||||
pages: DocsPage[]
|
||||
repoRoot: string
|
||||
repositoryRef: string
|
||||
/**
|
||||
* Place one referenced image beside the projected page and return the URL to
|
||||
* reach it from that page. A GitHub raw URL cannot serve this repository —
|
||||
* `raw.githubusercontent.com` answers 404 for a private one, and no reader of
|
||||
* the site is authenticated to it — so an image travels into the generated
|
||||
* tree and Vite bundles it like any other site asset. Omitted by callers that
|
||||
* only rewrite text, which then leave images pointing at the repository.
|
||||
*/
|
||||
placeImage?: (absPath: string) => string
|
||||
}
|
||||
|
||||
function repoPath(absPath: string, repoRoot: string): string {
|
||||
@@ -222,9 +233,13 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
|
||||
? options.locale === 'root' ? 'en' : 'root'
|
||||
: options.locale
|
||||
const page = published.get(targetPath)?.get(targetLocale)
|
||||
const nextUrl = page === undefined
|
||||
? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
|
||||
: routeTarget(options.route, page.route, suffix)
|
||||
const nextUrl = page !== undefined
|
||||
? routeTarget(options.route, page.route, suffix)
|
||||
: node.type === 'image' && options.placeImage !== undefined
|
||||
// The suffix rides along exactly as the GitHub branch keeps it: an SVG
|
||||
// view fragment or a Vite query changes what the reference means.
|
||||
? `${options.placeImage(absPath)}${suffix}`
|
||||
: githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
|
||||
|
||||
const start = node.position?.start.offset
|
||||
const end = node.position?.end.offset
|
||||
@@ -291,17 +306,78 @@ export function projectedPageContent(markdown: string, page: DocsPage): string {
|
||||
return markdown.slice(0, closing + closingDelimiter.length)
|
||||
}
|
||||
|
||||
/** Canonical Markdown files watched by the local VitePress dev server. */
|
||||
/**
|
||||
* The repository file one image reference resolves to, or `undefined` when the
|
||||
* target is not a local file this build may publish.
|
||||
* @param absPath - resolved image target.
|
||||
* @param repoRoot - repository root every published image must stay inside.
|
||||
* @returns the file's real path, or `undefined` when it must not be copied.
|
||||
*
|
||||
* Only a regular file whose real path stays inside the repository qualifies.
|
||||
* Publication copies the bytes into the site, so a reference escaping the
|
||||
* repository — `../../.ssh/id_rsa`, or a symlink pointing out of the tree —
|
||||
* would put a build-machine file on the site; `existsSync` alone, which is all
|
||||
* link resolution needs, does not answer that.
|
||||
*/
|
||||
export function publishableImage(absPath: string, repoRoot: string): string | undefined {
|
||||
const real = realpathSync(absPath)
|
||||
const inside = real === repoRoot || real.startsWith(`${repoRoot}${sep}`)
|
||||
return inside && statSync(real).isFile() ? real : undefined
|
||||
}
|
||||
|
||||
/** Every local image a published page references, resolved to its repository file. */
|
||||
function referencedImages(): string[] {
|
||||
const found = new Set<string>()
|
||||
for (const page of docsPages) {
|
||||
const sourceAbs = resolve(root, page.source)
|
||||
if (!existsSync(sourceAbs)) continue
|
||||
rewriteMarkdown(readFileSync(sourceAbs, 'utf8'), {
|
||||
sourcePath: page.source,
|
||||
locale: page.locale,
|
||||
route: page.route,
|
||||
pages: docsPages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'master',
|
||||
placeImage: (absPath) => {
|
||||
const real = publishableImage(absPath, root)
|
||||
if (real !== undefined) found.add(real)
|
||||
return ''
|
||||
},
|
||||
})
|
||||
}
|
||||
return [...found]
|
||||
}
|
||||
|
||||
/**
|
||||
* Files watched by the local VitePress dev server: every canonical Markdown
|
||||
* source, plus the images they publish. Without the images, replacing a
|
||||
* screenshot leaves the previous copy in the generated tree until something
|
||||
* touches the Markdown beside it.
|
||||
*/
|
||||
export function docsSourceFiles(): string[] {
|
||||
return [...new Set(docsPages.map(page => resolve(root, page.source)))]
|
||||
return [...new Set([...docsPages.map(page => resolve(root, page.source)), ...referencedImages()])]
|
||||
}
|
||||
|
||||
/** Rebuild the disposable VitePress source tree from the publication manifest. */
|
||||
export function projectDocs(): void {
|
||||
const routes = new Set<string>()
|
||||
/** Projected path to the repository file that claimed it, pages and images alike. */
|
||||
const claimed = new Map<string, string>()
|
||||
const repositoryRef = process.env.GITHUB_SHA ?? 'master'
|
||||
rmSync(generatedRoot, { recursive: true, force: true })
|
||||
|
||||
/** Reserve one projected path, refusing a second source for it. */
|
||||
const claim = (target: string, sourceAbs: string): void => {
|
||||
const holder = claimed.get(target)
|
||||
if (holder !== undefined && holder !== sourceAbs) {
|
||||
throw new Error(
|
||||
`project-doc-site: ${repoPath(sourceAbs, root)} and ${repoPath(holder, root)}`
|
||||
+ ` both project to ${relative(generatedRoot, target).split(sep).join('/')}.`,
|
||||
)
|
||||
}
|
||||
claimed.set(target, sourceAbs)
|
||||
}
|
||||
|
||||
for (const page of docsPages) {
|
||||
if (routes.has(page.route)) throw new Error(`project-doc-site: duplicate route ${JSON.stringify(page.route)}.`)
|
||||
routes.add(page.route)
|
||||
@@ -310,6 +386,9 @@ export function projectDocs(): void {
|
||||
throw new Error(`project-doc-site: source ${JSON.stringify(page.source)} does not exist or is not a file.`)
|
||||
}
|
||||
const output = resolve(generatedRoot, page.route)
|
||||
// Claimed before the images are placed: a page and an image landing on one
|
||||
// path would otherwise overwrite each other in whichever order they ran.
|
||||
claim(output, sourceAbs)
|
||||
mkdirSync(dirname(output), { recursive: true })
|
||||
const markdown = readFileSync(sourceAbs, 'utf8')
|
||||
const projected = rewriteMarkdown(markdown, {
|
||||
@@ -319,6 +398,25 @@ export function projectDocs(): void {
|
||||
pages: docsPages,
|
||||
repoRoot: root,
|
||||
repositoryRef,
|
||||
placeImage: (absPath) => {
|
||||
const real = publishableImage(absPath, root)
|
||||
if (real === undefined) {
|
||||
throw new Error(
|
||||
`project-doc-site: ${page.source} references image ${repoPath(absPath, root)},`
|
||||
+ ' which is not a regular file inside the repository.',
|
||||
)
|
||||
}
|
||||
// Beside the page that references it, under its own basename: each
|
||||
// locale's route tree gets its own copy, so one relative URL is correct
|
||||
// from both.
|
||||
const name = basename(real)
|
||||
const target = resolve(dirname(output), name)
|
||||
claim(target, real)
|
||||
copyFileSync(real, target)
|
||||
// Encoded because the destination is a Markdown inline target, where an
|
||||
// unescaped space would end it early.
|
||||
return `./${encodeURI(name)}`
|
||||
},
|
||||
})
|
||||
writeFileSync(output, addProjectionFrontmatter(projectedPageContent(projected, page), page))
|
||||
}
|
||||
|
||||
83
scripts/publication-payload.spec.ts
Normal file
83
scripts/publication-payload.spec.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
hasTypeRTRemoteNavigation,
|
||||
isForbiddenPublicationFile,
|
||||
validateTarballPayload,
|
||||
} from './publication-payload.ts'
|
||||
|
||||
function validateFixtureTarball(files: readonly string[]): () => void {
|
||||
return () => {
|
||||
validateTarballPayload(files, 'fixture.tgz')
|
||||
}
|
||||
}
|
||||
|
||||
describe('publication payload policy', () => {
|
||||
it.each([
|
||||
'lib/index.js',
|
||||
'lib/types/index.d.ts',
|
||||
'lib/styles/base.css',
|
||||
])('accepts %s', (file) => {
|
||||
expect(isForbiddenPublicationFile(file)).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src',
|
||||
'./src',
|
||||
'src/',
|
||||
'src/index.ts',
|
||||
'./src/index.ts',
|
||||
String.raw`src\index.ts`,
|
||||
'lib/types/index.d.ts.map',
|
||||
'./lib/types/index.d.ts.map',
|
||||
])('rejects static manifest path %s', (file) => {
|
||||
expect(isForbiddenPublicationFile(file)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects source members in packed tarballs', () => {
|
||||
expect(validateFixtureTarball([
|
||||
'package/package.json',
|
||||
'package/src/index.ts',
|
||||
])).toThrow('fixture.tgz publishes source file package/src/index.ts')
|
||||
})
|
||||
|
||||
it('rejects declaration maps in packed tarballs', () => {
|
||||
expect(validateFixtureTarball([
|
||||
'package/package.json',
|
||||
'package/lib/types/index.d.ts.map',
|
||||
])).toThrow('fixture.tgz publishes declaration map package/lib/types/index.d.ts.map')
|
||||
})
|
||||
|
||||
it('accepts a clean packed tarball', () => {
|
||||
expect(validateFixtureTarball([
|
||||
'package/package.json',
|
||||
'package/lib/index.js',
|
||||
'package/lib/types/index.d.ts',
|
||||
'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)
|
||||
})
|
||||
})
|
||||
57
scripts/publication-payload.ts
Normal file
57
scripts/publication-payload.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/** 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(/\/+$/, '')
|
||||
return normalized.startsWith('package/') ? normalized.slice('package/'.length) : normalized
|
||||
}
|
||||
|
||||
/** Whether a package payload path exposes source or declaration-map intermediates. */
|
||||
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,
|
||||
policy: PublicationPayloadPolicy = {},
|
||||
): void {
|
||||
for (const file of files) {
|
||||
if (!isForbiddenPublicationFile(file, policy)) continue
|
||||
const normalized = payloadPath(file)
|
||||
if (normalized === 'src' || normalized.startsWith('src/')) {
|
||||
throw new Error(`${context} publishes source file ${file}`)
|
||||
}
|
||||
throw new Error(`${context} publishes declaration map ${file}`)
|
||||
}
|
||||
}
|
||||
1085
scripts/publish-npm-baseline.ts
Normal file
1085
scripts/publish-npm-baseline.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -189,6 +189,12 @@ describe('Node 24 lane ownership', () => {
|
||||
expect(subject.find(item => item.id === 'doc-typecheck')?.env).toEqual({
|
||||
DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1',
|
||||
})
|
||||
expect(subject.find(item => item.id === 'built-bin-smoke')?.args).toEqual(
|
||||
expect.arrayContaining([
|
||||
'packages/subagent/subagent-codex/tests/loader-composition.e2e.ts',
|
||||
'packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts',
|
||||
]),
|
||||
)
|
||||
expect(subject.find(item => item.id === 'web-snapshot')).toMatchObject({
|
||||
displayCommand: 'DSH_SNAPSHOT=replay pnpm run test:web:built',
|
||||
env: { DSH_SNAPSHOT: 'replay' },
|
||||
|
||||
@@ -602,7 +602,11 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
|
||||
'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',
|
||||
'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
@@ -1,4 +1,4 @@
|
||||
/** Unit tests for the prompt-v4 renderer and three-section response parser. */
|
||||
/** Unit tests for the prompt-v7 content and unchanged three-section protocol. */
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
@@ -15,6 +15,20 @@ const root = resolve(import.meta.dirname, '..')
|
||||
const document = readFileSync(join(root, 'docs/i18n/translation-prompt.md'), 'utf8')
|
||||
const terminology = '| English | 中文 |\n|---|---|\n| agent | agent |'
|
||||
|
||||
const retainedExamples = [
|
||||
['### Colloquial verb → Professional verb', 'The repo pins pnpm@11.7.0 in package.json', '该仓库在 package.json 中固定使用 pnpm@11.7.0'],
|
||||
['### Run-on sentence → Natural phrasing with pause', 'Read docs/architecture.md before changing anything under packages/.', '在修改 packages/ 目录下的任何内容之前,请先阅读 docs/architecture.md。'],
|
||||
['### Stiff passive voice → Active and natural', 'a green gate means the pair was confirmed consistent at these exact contents, not that the confirmation was sound.', '门禁通过意味着这组文档在当前内容上的一致性得到了确认,不代表确认本身正确可靠。'],
|
||||
['### Invented word → Natural expression', 'A sidecar record of both blob hashes makes consistency checkable', '伴随记录保存两侧 blob hash,使一致性可检查'],
|
||||
['### Em-dash → Colon/period', 'FIXME — an issue that should block a new release.', 'FIXME:应当阻塞新版本发布的问题。'],
|
||||
['### Overly literal → Meaningful rendering', 'awkward phrasing is easier to hear without the source anchoring you', '不对照原文时,更容易察觉别扭的表达'],
|
||||
['### Terminology — do not translate what should be kept in English', 'typed service seams, and explicit extension points', '类型化的服务 seam 与显式扩展点'],
|
||||
['### Slang/jargon → Professional phrasing', 'The committed agent workflow lives in .agents/skills/dsh-translate-docs', '仓库内置的 agent 工作流见 .agents/skills/dsh-translate-docs'],
|
||||
['### "For humans" — translate the intent, not the word', 'For humans, start with the development guide', '面向开发者:请先阅读开发指南'],
|
||||
['### Code block comments — NEVER translate', '# full-screen TUI coding agent (needs DEEPSEEK_API_KEY)', 'keep exactly as-is, byte-for-byte'],
|
||||
['### Language switcher — flip direction', 'English | [中文](README.zh.md)', '[English](README.md) | 中文'],
|
||||
]
|
||||
|
||||
describe('translation prompt rendering', () => {
|
||||
it('renders both directions with every placeholder resolved', () => {
|
||||
const en = renderTranslationPrompt(document, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })
|
||||
@@ -22,15 +36,31 @@ describe('translation prompt rendering', () => {
|
||||
expect(en).toContain(terminology)
|
||||
expect(en).not.toContain('{{')
|
||||
expect(en).toContain('plain source stays plain (必须)')
|
||||
expect(en).toContain('When the target language is English, use the "English" column without a Chinese gloss')
|
||||
expect(en).toContain('for a Chinese target, use an established Chinese rendering')
|
||||
expect(en).toContain('for an English target, use the established English technical term')
|
||||
expect(en).toContain('does an English target use established English terminology')
|
||||
expect(en).toContain('For an English target, use the established English technical term')
|
||||
expect(en).toContain('does a Chinese target use an established Chinese rendering')
|
||||
expect(en).toContain('does an English target use the established English technical term')
|
||||
expect(en).toContain('The parser removes exactly one framing escape')
|
||||
const zh = renderTranslationPrompt(document, { sourceLanguage: 'Chinese', sourceFilename: 'guide.zh.md', terminology })
|
||||
expect(zh).toContain('from Chinese to English')
|
||||
})
|
||||
|
||||
it('retains every v4 embedded example', () => {
|
||||
for (const example of retainedExamples) {
|
||||
for (const fragment of example) expect(document).toContain(fragment)
|
||||
}
|
||||
})
|
||||
|
||||
it('states the selected v7 safeguards', () => {
|
||||
const rendered = renderTranslationPrompt(document, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })
|
||||
expect(rendered).toContain('## Priority')
|
||||
expect(rendered).toContain('### Faithfulness')
|
||||
expect(rendered).toContain('do not invent a filename or switcher')
|
||||
expect(rendered).toContain('Markdown emphasis markers do not create a word boundary')
|
||||
expect(rendered).toContain('Never invent responsibility merely to avoid a passive construction')
|
||||
expect(rendered).toContain('Never vary a terminology-table form, defined concept, or contract verb merely for stylistic variety')
|
||||
expect(rendered).toContain('Return exactly three raw XML sections')
|
||||
})
|
||||
|
||||
it('rejects a template with unknown or missing placeholders', () => {
|
||||
const alien = document.replaceAll('{{terminology}}', '{{terms_prompt}}')
|
||||
expect(() => renderTranslationPrompt(alien, { sourceLanguage: 'English', sourceFilename: 'guide.md', terminology })).toThrow(/unsupported placeholder/)
|
||||
|
||||
@@ -26,6 +26,21 @@
|
||||
"symbol": "MessageSourceMap",
|
||||
"source": "packages/llm/llm/src/message.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "ContextForm",
|
||||
"source": "packages/llm/llm/src/message.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "ContextSnapshotSection",
|
||||
"source": "packages/llm/llm/src/message.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "ContextFormed",
|
||||
"source": "packages/llm/llm/src/message.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "FinishReasonMap",
|
||||
@@ -46,6 +61,16 @@
|
||||
"symbol": "LlmModelInfo",
|
||||
"source": "packages/llm/llm/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmModelDiscoveryRequest",
|
||||
"source": "packages/llm/llm/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmDiscoveredModel",
|
||||
"source": "packages/llm/llm/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmModelContext",
|
||||
@@ -98,33 +123,8 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SendTarget",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxPlacement",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxItem",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxAction",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "InboxActionResult",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SendOptions",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"symbol": "InboxTarget",
|
||||
"source": "packages/core/agent/src/inbox.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
@@ -134,7 +134,7 @@
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "AgentCancelCause",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
@@ -143,7 +143,7 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "PromptDecision",
|
||||
"symbol": "PreStepDecision",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
@@ -151,11 +151,6 @@
|
||||
"symbol": "RequestErrorAction",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "RequestError",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SessionStartSource",
|
||||
@@ -204,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",
|
||||
@@ -224,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",
|
||||
@@ -370,7 +365,7 @@
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/session.md",
|
||||
"symbol": "TurnTriggerMap",
|
||||
"symbol": "TurnEndCancelCause",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
@@ -424,6 +419,32 @@
|
||||
"symbol": "CreateSessionOptions",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "RestoredSessionOptions",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "PrepareSessionOptions",
|
||||
"source": "packages/core/session/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "SessionPreparationOptions",
|
||||
"source": "packages/core/session/src/preparation.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "SessionPreparation",
|
||||
"source": "packages/core/session/src/preparation.ts",
|
||||
"projection": "public-api"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "SessionInspection",
|
||||
"source": "packages/session-persistence/session-persistence/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/persistence.md",
|
||||
"symbol": "SessionLocation",
|
||||
@@ -889,6 +910,11 @@
|
||||
"symbol": "SandboxPolicyRequest",
|
||||
"source": "packages/sandbox/sandbox-policy/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/sandbox.md",
|
||||
"symbol": "RunnerFailureRule",
|
||||
"source": "packages/sandbox/sandbox/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/sandbox.md",
|
||||
"symbol": "ConfinedArgv",
|
||||
@@ -1468,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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -149,11 +149,33 @@ function validateExampleResolution(): string[] {
|
||||
}
|
||||
|
||||
function validateAppResolution(): string[] {
|
||||
const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
|
||||
const violations: string[] = []
|
||||
// App overlays (and any config left under apps/cli/config) resolve from the
|
||||
// dsh app's own dependency surface — the profile module fallback mirrors it.
|
||||
const appDependencies = {
|
||||
...readManifest('apps/cli/package.json').dependencies,
|
||||
// The fallback also links every bundle's own dependencies (healProfilesModuleFallback).
|
||||
...Object.fromEntries(globSync('packages/bundle/*/package.json', { cwd: root })
|
||||
.flatMap(file => Object.entries(readManifest(file).dependencies ?? {}))),
|
||||
}
|
||||
const shipped = new Set(globSync('*.cordis.yml', { cwd: resolve(root, 'apps/cli/config') })
|
||||
.map(file => `apps/cli/config/${file}`))
|
||||
const references = pluginReferences.filter(reference => shipped.has(reference.file) || appOverlayFiles.has(reference.file))
|
||||
return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
|
||||
const appReferences = pluginReferences.filter(reference => shipped.has(reference.file) || appOverlayFiles.has(reference.file))
|
||||
violations.push(...missingPluginDependencies(appReferences, appDependencies, 'apps/cli/package.json or a bundle manifest'))
|
||||
// Each bundle's patch rows must resolve from that bundle's own dependencies:
|
||||
// per-layer resolution anchors on the bundle package directory.
|
||||
for (const manifestPath of globSync('packages/bundle/*/package.json', { cwd: root })) {
|
||||
const bundleDir = manifestPath.replace(/\/package\.json$/, '')
|
||||
const manifest = readManifest(manifestPath)
|
||||
const references = pluginReferences.filter(reference => reference.file.startsWith(`${bundleDir}/`))
|
||||
violations.push(...missingPluginDependencies(
|
||||
// A bundle may mount its own package (the web-app runtime row).
|
||||
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
|
||||
manifest.dependencies ?? {},
|
||||
manifestPath,
|
||||
))
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,9 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
|
||||
*/
|
||||
const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/bash/bash': { kind: 'indirect', reason: 'The service interface delegates all model rendering to dsh-tool-bash.' },
|
||||
'packages/bash/bash-env': { kind: 'indirect', reason: 'The env service surfaces managed DSH_* facts through the shell tools (dsh-tool-bash/dsh-tool-pwsh); it registers no prompt or schema of its own.' },
|
||||
'packages/bash/bash-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-bash.' },
|
||||
'packages/bash/pwsh-local': { kind: 'indirect', reason: 'The executor backend delegates model rendering to dsh-tool-pwsh.' },
|
||||
'packages/code-runtime/code-runtime': { kind: 'indirect', reason: 'The service interface delegates model rendering to Code Mode in dsh-tools.' },
|
||||
'packages/code-runtime/code-runtime-worker': { kind: 'indirect', reason: 'The worker backend delegates model rendering to Code Mode in dsh-tools.' },
|
||||
'packages/typert/registry': { kind: 'none', reason: 'Runtime type registry; consumers (cordis_inspect, wire faces, gates) own any model-visible projection of registry contents.' },
|
||||
@@ -55,10 +57,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.' },
|
||||
@@ -84,6 +88,9 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/host/directory-picker-browse': { kind: 'none', reason: 'The GUI-host picking backend registers no model surface.' },
|
||||
'packages/host/directory-picker-native': { kind: 'none', reason: 'The GUI-host picking backend registers no model surface.' },
|
||||
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers no model surface.' },
|
||||
'packages/host/frontend-static': { kind: 'none', reason: 'The SPA dist server answers browser asset requests and registers no model surface.' },
|
||||
'packages/bundle/base': { kind: 'indirect', reason: 'The bundle is a patch-list carrier; each inserted row\'s package owns its model surface.' },
|
||||
'packages/bundle/headless': { kind: 'none', reason: 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base/web bundles.' },
|
||||
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
|
||||
'packages/llm/token-meter': { kind: 'indirect', reason: 'The measurement service leaves model-visible changes to its consumers.' },
|
||||
'packages/lsp/lsp': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-lsp.' },
|
||||
@@ -119,6 +126,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.' },
|
||||
|
||||
@@ -204,11 +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: generate Host contracts
|
||||
# before either aggregate typecheck, then bundle the completed workspace.
|
||||
# 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/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/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $?
|
||||
wine_node "$scratch/logs/tsdown.log" "$tsdown_js"
|
||||
}
|
||||
site_gate() {
|
||||
@@ -235,7 +238,12 @@ 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 (contract prepass, tsc, tsdown)' "$build_status" \
|
||||
"$scratch/logs/contracts-tsc.log" \
|
||||
"$scratch/logs/contracts-tsdown.log" \
|
||||
"$scratch/logs/host-tsc.log" \
|
||||
"$scratch/logs/client-tsc.log" \
|
||||
"$scratch/logs/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