mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into codex/unify-landlock-release
# Conflicts: # .agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml # scripts/check-workspace-constraints.ts
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,7 +7,8 @@
|
||||
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
||||
import { join, relative, resolve } from 'node:path'
|
||||
import { isForbiddenPublicationFile } from './publication-payload.ts'
|
||||
import { hasTypeRTRemoteNavigation, isForbiddenPublicationFile } from './publication-payload.ts'
|
||||
import { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
// vendor/* is single-level; packages/<group>/<pkg> nests one level deeper
|
||||
@@ -118,6 +119,10 @@ 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'],
|
||||
@@ -134,6 +139,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
|
||||
@@ -157,9 +163,37 @@ function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
||||
// declarations.
|
||||
...usesEmittedTreeDefaults(manifest) ? ['lib/types/**/*.js'] : [],
|
||||
'lib/types/**/*.d.ts',
|
||||
...hasExportPair(manifest, './typert', './lib/typert.host.d.ts', './lib/typert.host.js')
|
||||
? ['lib/typert.host.js', 'lib/typert.host.d.ts']
|
||||
: [],
|
||||
...hasExportPair(manifest, './client/typert', './lib/typert.client.d.ts', './lib/typert.client.js')
|
||||
? ['lib/typert.client.js', 'lib/typert.client.d.ts']
|
||||
: [],
|
||||
...typeRTRemoteNavigation
|
||||
? [
|
||||
'lib/typert.remote-client.js',
|
||||
'lib/typert.remote-client.d.ts',
|
||||
'lib/typert.remote-client.d.ts.map',
|
||||
'src',
|
||||
]
|
||||
: [],
|
||||
]
|
||||
}
|
||||
|
||||
/** Whether one conditional export exactly names the generated runtime and declaration pair. */
|
||||
function hasExportPair(
|
||||
manifest: PackageManifest,
|
||||
subpath: string,
|
||||
types: string,
|
||||
runtime: string,
|
||||
): boolean {
|
||||
const entry = manifest.exports?.[subpath]
|
||||
return typeof entry === 'object'
|
||||
&& entry !== null
|
||||
&& entry.types === types
|
||||
&& entry.default === runtime
|
||||
}
|
||||
|
||||
/** Runtime target of an export entry: conditional `default`, or the bare-string shorthand. */
|
||||
function exportDefault(manifest: PackageManifest, subpath: string): string | undefined {
|
||||
const entry = manifest.exports?.[subpath]
|
||||
@@ -205,8 +239,9 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
|
||||
if (manifest.name?.startsWith('@deepseek-ai/')) {
|
||||
const allowedSources = publicationSourceAllowlist[manifest.name] ?? []
|
||||
const publicationPolicy = { typeRTRemoteNavigation: hasTypeRTRemoteNavigation(manifest) }
|
||||
for (const file of manifest.files ?? []) {
|
||||
if (isForbiddenPublicationFile(file) && !allowedSources.includes(file)) {
|
||||
if (isForbiddenPublicationFile(file, publicationPolicy) && !allowedSources.includes(file)) {
|
||||
errors.push(`${label}: package.json files must not publish ${JSON.stringify(file)}`)
|
||||
}
|
||||
}
|
||||
@@ -314,6 +349,7 @@ const errors = [
|
||||
...checkRepositoryVersion(),
|
||||
...workspaceManifests().flatMap(checkWorkspace),
|
||||
...checkHierarchyShape(),
|
||||
...collectProjectReferenceFaceViolations(root),
|
||||
]
|
||||
if (errors.length > 0) {
|
||||
console.error(errors.join('\n'))
|
||||
|
||||
@@ -28,6 +28,34 @@ describe('CI workflow', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Issue lifecycle workflow', () => {
|
||||
it('uses review signals instead of rerunning when a draft becomes ready', () => {
|
||||
const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')
|
||||
const lifecyclePullRequest = workflowEvent(lifecycle, 'pull_request')
|
||||
const lifecycleReview = workflowEvent(lifecycle, 'pull_request_review')
|
||||
const policy = loadWorkflow('.github/workflows/issue-policy.yml')
|
||||
const policyPullRequest = workflowEvent(policy, 'pull_request')
|
||||
|
||||
expect(lifecyclePullRequest.types).not.toContain('ready_for_review')
|
||||
expect(lifecyclePullRequest.types).toContain('review_requested')
|
||||
expect(lifecycleReview.types).toContain('submitted')
|
||||
expect(policyPullRequest.types).toContain('ready_for_review')
|
||||
})
|
||||
})
|
||||
|
||||
function loadWorkflow(path: string): Record<string, unknown> {
|
||||
const workflow: unknown = yaml.load(readFileSync(resolve(root, path), 'utf8'))
|
||||
if (!isRecord(workflow)) throw new TypeError(`${path} must define a workflow`)
|
||||
return workflow
|
||||
}
|
||||
|
||||
function workflowEvent(workflow: Record<string, unknown>, event: string): Record<string, unknown> {
|
||||
if (!isRecord(workflow.on) || !isRecord(workflow.on[event])) {
|
||||
throw new TypeError(`workflow must define the ${event} event`)
|
||||
}
|
||||
return workflow.on[event]
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,13 @@ interface CssPlugin {
|
||||
}
|
||||
|
||||
function cssPlugin(): CssPlugin {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
const plugins = (configs[1] as { plugins: CssPlugin[] }).plugins
|
||||
const configs = clientBundle(
|
||||
'@deepseek-ai/dsh-client-test',
|
||||
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||
)({ env: { DSH_BUILD_FACE: 'client' } })
|
||||
const client = configs.find(config => config.platform === 'browser')
|
||||
if (client === undefined) throw new Error('client config missing')
|
||||
const plugins = (client as { plugins: CssPlugin[] }).plugins
|
||||
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
|
||||
if (plugin === undefined) throw new Error('CSS Modules plugin missing from client config')
|
||||
return plugin
|
||||
|
||||
@@ -14,6 +14,24 @@ interface CssModulePlugin {
|
||||
load?: (this: { addWatchFile: (id: string) => void }, id: string) => Promise<unknown>
|
||||
}
|
||||
|
||||
function clientConfigs(id = '@deepseek-ai/dsh-client-test') {
|
||||
return clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])(
|
||||
{ env: { DSH_BUILD_FACE: 'client' } },
|
||||
).filter(config => config.platform === 'browser')
|
||||
}
|
||||
|
||||
describe('client bundle build faces', () => {
|
||||
it('watches source in development and consumes emitted JavaScript in the Client build', () => {
|
||||
const bundle = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js'])
|
||||
const development = bundle({ env: {} }).find(config => config.platform === 'browser')
|
||||
const artifact = bundle({ env: { DSH_BUILD_FACE: 'client' } })
|
||||
.find(config => config.platform === 'browser')
|
||||
|
||||
expect(development?.entry).toEqual({ client: 'src/client/index.ts' })
|
||||
expect(artifact?.entry).toEqual({ client: 'lib/types/client/index.js' })
|
||||
})
|
||||
})
|
||||
|
||||
function clientSourceMapPath(packagePath: string): string {
|
||||
return fileURLToPath(new URL(`../packages/${packagePath}/lib/client.js.map`, import.meta.url))
|
||||
}
|
||||
@@ -21,16 +39,16 @@ function clientSourceMapPath(packagePath: string): string {
|
||||
function purityResolveId(): ResolveId {
|
||||
// libEntry is spelled at every call site (no default) so the
|
||||
// package-invariants text check can see the invariant entry per package.
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
const plugins = (configs[1] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
|
||||
const configs = clientConfigs()
|
||||
const plugins = (configs[0] as { plugins: { name: string; resolveId?: unknown }[] }).plugins
|
||||
const gate = plugins.find(p => p.name === 'dsh-client-bundle-purity')
|
||||
if (gate?.resolveId === undefined) throw new Error('purity plugin missing from client config')
|
||||
return gate.resolveId as ResolveId
|
||||
}
|
||||
|
||||
function cssModulePlugin(): CssModulePlugin {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
const plugins = (configs[1] as { plugins: CssModulePlugin[] }).plugins
|
||||
const configs = clientConfigs()
|
||||
const plugins = (configs[0] as { plugins: CssModulePlugin[] }).plugins
|
||||
const plugin = plugins.find(candidate => candidate.name === 'dsh-css-modules-inline')
|
||||
if (plugin?.resolveId === undefined || plugin.load === undefined) {
|
||||
throw new Error('CSS Modules plugin missing from client config')
|
||||
@@ -59,6 +77,13 @@ describe('client bundle purity gate', () => {
|
||||
expect(resolveId('@deepseek-ai/dsh-brand')).toBeNull()
|
||||
})
|
||||
|
||||
it('lets exact generated Remote contributions inline without admitting their package implementation', () => {
|
||||
expect(resolveId('@deepseek-ai/dsh-goal/remote')).toBeNull()
|
||||
expect(() => resolveId('@deepseek-ai/dsh-goal')).toThrow(/purity/)
|
||||
expect(() => resolveId('@deepseek-ai/dsh-goal/client')).toThrow(/purity/)
|
||||
expect(() => resolveId('@deepseek-ai/dsh-goal/remote/nested')).toThrow(/purity/)
|
||||
})
|
||||
|
||||
it('throws on any other @deepseek-ai leak', () => {
|
||||
expect(() => resolveId('@deepseek-ai/dsh-agent')).toThrow(/purity/)
|
||||
expect(() => resolveId('@deepseek-ai/dsh-client-web')).toThrow(/purity/)
|
||||
@@ -80,13 +105,13 @@ describe('client bundle purity gate', () => {
|
||||
|
||||
describe('client bundle debug artifacts', () => {
|
||||
it('emits source maps for plugin TS and TSX outside the Vite module graph', () => {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-test', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
expect(configs[1]?.sourcemap).toBe(true)
|
||||
const configs = clientConfigs()
|
||||
expect(configs[0]?.sourcemap).toBe(true)
|
||||
})
|
||||
|
||||
it('maps first-party sources to their repository package paths', () => {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-ui-goal', ['lib/types/index.js', 'lib/types/invariant.js'])
|
||||
const outputOptions = configs[1]?.outputOptions
|
||||
const configs = clientConfigs('@deepseek-ai/dsh-client-ui-goal')
|
||||
const outputOptions = configs[0]?.outputOptions
|
||||
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
||||
const transform = outputOptions.sourcemapPathTransform
|
||||
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
||||
@@ -98,8 +123,8 @@ describe('client bundle debug artifacts', () => {
|
||||
})
|
||||
|
||||
it('maps dual-face host sources to the host package group', () => {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-host-directory-picker-native', ['lib/types/index.js'])
|
||||
const outputOptions = configs[1]?.outputOptions
|
||||
const configs = clientConfigs('@deepseek-ai/dsh-host-directory-picker-native')
|
||||
const outputOptions = configs[0]?.outputOptions
|
||||
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
||||
const transform = outputOptions.sourcemapPathTransform
|
||||
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
||||
@@ -109,8 +134,8 @@ describe('client bundle debug artifacts', () => {
|
||||
})
|
||||
|
||||
it('maps inlined workspace sources to packages and leaves dependencies outside it unchanged', () => {
|
||||
const configs = clientBundle('@deepseek-ai/dsh-client-connection', ['lib/types/index.js'])
|
||||
const outputOptions = configs[1]?.outputOptions
|
||||
const configs = clientConfigs('@deepseek-ai/dsh-client-connection')
|
||||
const outputOptions = configs[0]?.outputOptions
|
||||
if (typeof outputOptions !== 'object' || outputOptions === null) throw new Error('client output options missing')
|
||||
const transform = outputOptions.sourcemapPathTransform
|
||||
if (transform === undefined) throw new Error('client sourcemap path transform missing')
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"AGENTS.md": 1775,
|
||||
"AGENTS.md": 1782,
|
||||
"docs/AGENTS.md": 1320,
|
||||
"docs/architecture.md": 2160,
|
||||
"docs/cordis-primer.md": 600,
|
||||
@@ -7,5 +7,5 @@
|
||||
"docs/testing.md": 1150,
|
||||
"examples/AGENTS.md": 310,
|
||||
"packages/AGENTS.md": 675,
|
||||
"packages/README.md": 920
|
||||
"packages/README.md": 936
|
||||
}
|
||||
|
||||
@@ -136,10 +136,9 @@ function formatDiagnostics(diagnostics: readonly ts.Diagnostic[], blocks: Block[
|
||||
}
|
||||
|
||||
/**
|
||||
* Reuse the host-aggregate references from a temp project one directory below
|
||||
* root. Doc fragments speak the host vocabulary, so the standalone project
|
||||
* seeds tsconfig.host.json (never the root solution: flattening host+client
|
||||
* into one program collides the cordis Context merges).
|
||||
* Reuse the Host aggregate references from a temp project one directory below
|
||||
* root. Generated Client API examples opt out because their declarations do
|
||||
* not exist until Host tsdown has run.
|
||||
*/
|
||||
function workspaceReferences(): { path: string }[] {
|
||||
const file = join(root, 'tsconfig.host.json')
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -32,6 +32,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
HookContext: 'core.md',
|
||||
SettleReason: 'core.md',
|
||||
AdapterRegistrationHandle: 'core.md',
|
||||
DirectoryRegistrationHandle: 'core.md',
|
||||
LlmCallConfig: 'core.md',
|
||||
LlmModelContext: 'core.md',
|
||||
LlmModelReasoningInfo: 'core.md',
|
||||
@@ -40,6 +41,8 @@ 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',
|
||||
@@ -87,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',
|
||||
@@ -273,6 +277,7 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
TypertPackageRecord: 'registry package record is owned by packages/typert/registry/README.md',
|
||||
TypertSchemaFilter: 'registry schema query filter is owned by packages/typert/registry/README.md',
|
||||
TypertSchemaRecord: 'registry schema record is owned by packages/typert/registry/README.md',
|
||||
TypeRTDisposer: 'TypeRT lifecycle contract is owned by packages/typert/type-meta/README.md',
|
||||
'z.core.JSONSchema.BaseSchema': 'zod projection output is owned by the zod v4 API',
|
||||
'z.core.ToJSONSchemaParams': 'zod projection parameters are owned by the zod v4 API',
|
||||
InvariantInstaller: 'service-local contribution contract is owned by packages/support/invariants/README.md',
|
||||
@@ -284,6 +289,7 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
ThemeTokens: 'service-local token dictionary is owned by packages/client/ui-theme/src/index.ts',
|
||||
Translate: 'service-local bound translator is owned by packages/client/i18n/src/index.ts',
|
||||
InvariantRegistration: 'service-local lifecycle handle is owned by packages/support/invariants/README.md',
|
||||
InvokeRemoteRequest: 'gateway invocation contract is owned by packages/api/gateway/README.md',
|
||||
PresetOption: 'deployment menu metadata is owned by packages/ui/permission/README.md',
|
||||
PresetSpec: 'deployment preset composition is owned by packages/ui/permission/README.md',
|
||||
KnobState: 'projection unit state shape is owned by packages/ui/permission/README.md',
|
||||
|
||||
@@ -124,7 +124,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'session',
|
||||
title: 'In-memory session store',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'agent', 'cli-demo', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
|
||||
consumers: ['agent-loop', 'agent', 'session-persistence', 'session-query', 'session-query-sqlite', 'subagent-inprocess', 'invariants'],
|
||||
note: 'Owns append-only Session instances and emits the durable session event feed.',
|
||||
},
|
||||
{
|
||||
@@ -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',
|
||||
@@ -296,7 +303,7 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
pkg: 'agent',
|
||||
title: 'Agent service',
|
||||
mode: 'core',
|
||||
consumers: ['agent-loop', 'acp', 'cli-demo', 'subagent-inprocess'],
|
||||
consumers: ['agent-loop', 'acp', 'subagent-inprocess'],
|
||||
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
|
||||
},
|
||||
{
|
||||
@@ -320,8 +327,8 @@ 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',
|
||||
@@ -417,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.',
|
||||
},
|
||||
@@ -598,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]) }
|
||||
@@ -620,17 +628,17 @@ 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',
|
||||
rel: 'examples/headless-agent/composition.md',
|
||||
title: 'Headless Agent App Composition',
|
||||
title: 'Headless Agent Snapshot Composition',
|
||||
label: 'examples/headless-agent',
|
||||
config: 'examples/headless-agent/cordis.yml',
|
||||
summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
|
||||
summary: 'The headless snapshot composition combines the real DeepSeek adapter and coding capabilities with one explicitly configured persisted top-level agent; its JSONL driver is test-only.',
|
||||
},
|
||||
{
|
||||
id: 'acp',
|
||||
@@ -649,9 +657,7 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
|
||||
const jsonl = nodeId('bundle', 'jsonl')
|
||||
lines.push(` ${appNode} --> ${agentCore}["@deepseek-ai/dsh-agent-spine-demo"]`)
|
||||
lines.push(` ${appNode} --> ${jsonl}["@deepseek-ai/dsh-session-persistence-jsonl"]`)
|
||||
if (pluginName === '@deepseek-ai/dsh-cli-demo') {
|
||||
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'cli')}["one-shot driver<br/>format-pure stdout<br/>fresh top-level agent"]`)
|
||||
} else if (pluginName === '@deepseek-ai/dsh-acp-demo') {
|
||||
if (pluginName === '@deepseek-ai/dsh-acp-demo') {
|
||||
lines.push(` ${appNode} --> ${nodeId('frontdoor', 'acp')}["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]`)
|
||||
}
|
||||
lines.push(
|
||||
@@ -677,7 +683,7 @@ function renderAppComposition(example: AppExample): string {
|
||||
const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
|
||||
lines.push(` ${pluginNode}["${escLabel(plugin.id)}<br/>${escLabel(plugin.name)}"]`)
|
||||
lines.push(` cfg --> ${pluginNode}`)
|
||||
if (plugin.name === '@deepseek-ai/dsh-cli-demo' || plugin.name === '@deepseek-ai/dsh-acp-demo') {
|
||||
if (plugin.name === '@deepseek-ai/dsh-acp-demo') {
|
||||
renderAppExpansion(lines, pluginNode, plugin.name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/*', 'native/landlock-run', 'native/landlock-run/packages/*'])).toEqual([
|
||||
|
||||
@@ -46,6 +46,21 @@ const FIRST_PARTY = new Set([
|
||||
'@deepseek-ai/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.
|
||||
@@ -89,6 +104,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>
|
||||
@@ -158,7 +174,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
|
||||
@@ -187,9 +270,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
|
||||
// Workspace-local link farms can expose a dependency that is not linked at
|
||||
// the repository root; both are backed by the root workspace's lockfile.
|
||||
@@ -204,6 +286,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)
|
||||
@@ -213,6 +302,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
|
||||
@@ -513,6 +633,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.
|
||||
@@ -525,11 +665,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.`)
|
||||
}
|
||||
@@ -540,9 +688,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, including the Landlock launcher workspace, is recorded with exact pinned versions in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python closure is recorded separately in [\`python/sdk/uv.lock\`](python/sdk/uv.lock).
|
||||
|
||||
@@ -563,6 +711,7 @@ ${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,6 +14,7 @@ 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'
|
||||
@@ -175,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',
|
||||
@@ -392,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',
|
||||
@@ -401,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',
|
||||
@@ -454,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',
|
||||
@@ -610,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',
|
||||
'',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
# dsh one-line installer.
|
||||
#
|
||||
# curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh
|
||||
# curl -fsSL https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/master/scripts/install.sh | sh
|
||||
#
|
||||
# It clones the harness under ~/.dsh/source (the master clone at
|
||||
# ~/.dsh/source/master), adds a per-install staging worktree at
|
||||
@@ -46,13 +46,11 @@
|
||||
# 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)
|
||||
# FIXME(install-ts): Move the post-checkout workflow into a tested TypeScript
|
||||
# entrypoint; keep this POSIX shell file as the curl/source bootstrap.
|
||||
# DSH_HOME Harness home holding profiles and user patches (default: ~/.dsh)
|
||||
set -eu
|
||||
|
||||
DSH_REF=${DSH_REF:-master}
|
||||
DSH_REPO=${DSH_REPO:-https://github.com/deepseek-harness/deepseek-harness.git}
|
||||
DSH_REPO=${DSH_REPO:-https://github.com/deepseek-ai/deepseek-harness-sdk.git}
|
||||
# DSH_SOURCE is the staging-worktree container and the default home of `current`.
|
||||
# DSH_MASTER names the main clone: clone mode defaults it inside DSH_SOURCE,
|
||||
# while adoption discovers an existing clone anywhere on disk. Remember whether
|
||||
|
||||
@@ -221,6 +221,39 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +
|
||||
expect(result.status, normalizedOutput(result)).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps staged validation project-free while preserving source rules', async () => {
|
||||
const configPath = join(repositoryRoot, '.oxlintrc.staged.json')
|
||||
const result = parseConfigFileTextToJson(configPath, await readFile(configPath, 'utf8'))
|
||||
if (result.error !== undefined) {
|
||||
throw new Error(flattenDiagnosticMessageText(result.error.messageText, '\n'))
|
||||
}
|
||||
expect(result.config).toMatchObject({
|
||||
extends: ['./.oxlintrc.json'],
|
||||
options: { typeAware: false },
|
||||
})
|
||||
|
||||
const suffix = randomUUID()
|
||||
const path = join(repositoryRoot, 'scripts', `staged-lint-probe-${suffix}.ts`)
|
||||
try {
|
||||
await writeFile(path, 'export const value={answer:1};\n')
|
||||
const lint = runOxlint([
|
||||
'--config',
|
||||
relative(repositoryRoot, configPath),
|
||||
'--format',
|
||||
'unix',
|
||||
relative(repositoryRoot, path),
|
||||
])
|
||||
const output = normalizedOutput(lint)
|
||||
|
||||
expect(lint.error).toBeUndefined()
|
||||
expect(lint.status, output).toBe(1)
|
||||
expect(output).toContain('@stylistic')
|
||||
expect(output).not.toContain('typescript(')
|
||||
} finally {
|
||||
await rm(path, { force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('applies staged stylistic fixes before Oxlint validation', async () => {
|
||||
const suffix = randomUUID()
|
||||
const configPath = await writeContractConfig(suffix)
|
||||
|
||||
@@ -72,6 +72,20 @@ describe('package invariant gate', () => {
|
||||
expect(collectPackageInvariantViolations(fixture())).toEqual([])
|
||||
})
|
||||
|
||||
it('accepts an invariant reference owned by a package-local leaf project', () => {
|
||||
const root = fixture({ invariantReference: false })
|
||||
const dir = join(root, 'packages/core/probe')
|
||||
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
|
||||
files: [],
|
||||
references: [{ path: './tsconfig.host.json' }],
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(join(dir, 'tsconfig.host.json'), `${JSON.stringify({
|
||||
references: [{ path: '../../support/invariants' }],
|
||||
}, null, 2)}\n`)
|
||||
|
||||
expect(collectPackageInvariantViolations(root)).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects missing publication metadata and build output', () => {
|
||||
const violations = collectPackageInvariantViolations(fixture({
|
||||
invariantExport: false,
|
||||
|
||||
@@ -118,11 +118,8 @@ function checkBuild(
|
||||
violations: PackageInvariantViolation[],
|
||||
): void {
|
||||
const tsconfigPath = `${owner.dir}/tsconfig.json`
|
||||
const tsconfig = JSON.parse(readFileSync(resolve(root, tsconfigPath), 'utf8')) as {
|
||||
references?: Array<{ path?: string }>
|
||||
}
|
||||
if (owner.packageName !== '@deepseek-ai/dsh-invariants'
|
||||
&& !tsconfig.references?.some(reference => reference.path === '../../support/invariants')) {
|
||||
&& !projectReferencesInvariants(root, owner.dir, tsconfigPath)) {
|
||||
addViolation(
|
||||
violations,
|
||||
tsconfigPath,
|
||||
@@ -138,6 +135,31 @@ function checkBuild(
|
||||
}
|
||||
}
|
||||
|
||||
function projectReferencesInvariants(root: string, ownerDir: string, entryPath: string): boolean {
|
||||
const ownerRoot = resolve(root, ownerDir)
|
||||
const target = resolve(root, 'packages/support/invariants')
|
||||
const pending = [resolve(root, entryPath)]
|
||||
const visited = new Set<string>()
|
||||
while (pending.length > 0) {
|
||||
const configPath = pending.pop()
|
||||
if (configPath === undefined) break
|
||||
if (visited.has(configPath)) continue
|
||||
visited.add(configPath)
|
||||
const config = JSON.parse(readFileSync(configPath, 'utf8')) as {
|
||||
references?: Array<{ path?: string }>
|
||||
}
|
||||
for (const reference of config.references ?? []) {
|
||||
if (reference.path === undefined) continue
|
||||
const referenced = resolve(dirname(configPath), reference.path)
|
||||
if (referenced === target) return true
|
||||
if (!referenced.startsWith(`${ownerRoot}${sep}`)) continue
|
||||
const childConfig = referenced.endsWith('.json') ? referenced : resolve(referenced, 'tsconfig.json')
|
||||
if (existsSync(childConfig)) pending.push(childConfig)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function checkSource(
|
||||
owner: PackageInvariantOwner,
|
||||
root: string,
|
||||
|
||||
@@ -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()
|
||||
@@ -76,7 +104,7 @@ describe('rewriteMarkdown', () => {
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe(
|
||||
'[B](./reference/b.md#part) '
|
||||
+ '[source](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/packages/tool.ts#L2) '
|
||||
+ '[source](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/packages/tool.ts#L2) '
|
||||
+ '[web](https://example.com)\n',
|
||||
)
|
||||
})
|
||||
@@ -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',
|
||||
@@ -102,7 +130,58 @@ describe('rewriteMarkdown', () => {
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe('\n')
|
||||
})).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', () => {
|
||||
@@ -130,7 +209,7 @@ describe('rewriteMarkdown', () => {
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe(
|
||||
'[title](./reference/b.md "b.md") '
|
||||
+ '[escaped](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/docs/x(y).md)\n',
|
||||
+ '[escaped](https://github.com/deepseek-ai/deepseek-harness-sdk/blob/abc123/docs/x(y).md)\n',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
* 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'
|
||||
import type { Nodes } from 'mdast'
|
||||
import { docsPages, type DocsLocale, type DocsPage } from '../website/docs.ts'
|
||||
|
||||
const REPOSITORY_URL = 'https://github.com/deepseek-harness/deepseek-harness'
|
||||
const REPOSITORY_URL = 'https://github.com/deepseek-ai/deepseek-harness-sdk'
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const generatedRoot = resolve(root, 'website/.generated')
|
||||
|
||||
@@ -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 {
|
||||
@@ -192,7 +203,7 @@ function githubTarget(
|
||||
image: boolean,
|
||||
): string {
|
||||
const path = repoPath(absPath, repoRoot)
|
||||
if (image) return `https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/${repositoryRef}/${path}${suffix}`
|
||||
if (image) return `https://raw.githubusercontent.com/deepseek-ai/deepseek-harness-sdk/${repositoryRef}/${path}${suffix}`
|
||||
const kind = lstatSync(absPath).isDirectory() ? 'tree' : 'blob'
|
||||
const lineSuffix = line === undefined ? suffix : `#L${line}`
|
||||
return `${REPOSITORY_URL}/${kind}/${repositoryRef}/${path}${lineSuffix}`
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
100
scripts/project-reference-faces.spec.ts
Normal file
100
scripts/project-reference-faces.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function writeJson(path: string, value: unknown): void {
|
||||
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`)
|
||||
}
|
||||
|
||||
function workspaceFixture(options: {
|
||||
readonly host: readonly string[]
|
||||
readonly client: readonly string[]
|
||||
}): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-project-reference-faces-'))
|
||||
roots.push(root)
|
||||
const shared = join(root, 'packages/core/shared')
|
||||
const split = join(root, 'packages/api/split')
|
||||
mkdirSync(shared, { recursive: true })
|
||||
mkdirSync(split, { recursive: true })
|
||||
writeJson(join(root, 'tsconfig.base.json'), {})
|
||||
writeJson(join(root, 'tsconfig.base.client.json'), { extends: './tsconfig.base.json' })
|
||||
writeJson(join(shared, 'package.json'), { name: '@deepseek-ai/dsh-shared' })
|
||||
writeJson(join(shared, 'tsconfig.json'), {
|
||||
extends: '../../../tsconfig.base.json',
|
||||
references: [],
|
||||
})
|
||||
writeJson(join(split, 'package.json'), { name: '@deepseek-ai/dsh-split' })
|
||||
writeJson(join(split, 'tsconfig.json'), {
|
||||
files: [],
|
||||
references: [{ path: './tsconfig.host.json' }, { path: './tsconfig.client.json' }],
|
||||
})
|
||||
writeJson(join(split, 'tsconfig.host.json'), { references: [{ path: '../../core/shared' }] })
|
||||
writeJson(join(split, 'tsconfig.client.json'), { references: [{ path: '../../core/shared' }] })
|
||||
writeJson(join(root, 'tsconfig.host.json'), {
|
||||
references: options.host.map(path => ({ path })),
|
||||
})
|
||||
writeJson(join(root, 'tsconfig.client.json'), {
|
||||
references: options.client.map(path => ({ path })),
|
||||
})
|
||||
return root
|
||||
}
|
||||
|
||||
describe('Project Reference compiler faces', () => {
|
||||
it('allows neutral projects in either graph and matching split leaves', () => {
|
||||
const root = workspaceFixture({
|
||||
host: ['./packages/core/shared', './packages/api/split/tsconfig.host.json'],
|
||||
client: ['./packages/core/shared', './packages/api/split/tsconfig.client.json'],
|
||||
})
|
||||
|
||||
expect(collectProjectReferenceFaceViolations(root)).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects the opposite leaf and the solution root of a split project', () => {
|
||||
const root = workspaceFixture({
|
||||
host: [
|
||||
'./packages/api/split/tsconfig.host.json',
|
||||
'./packages/api/split/tsconfig.client.json',
|
||||
],
|
||||
client: ['./packages/api/split'],
|
||||
})
|
||||
|
||||
expect(collectProjectReferenceFaceViolations(root)).toEqual([
|
||||
'tsconfig.client.json: Project Reference "./packages/api/split" enters split project packages/api/split from a Client config; reference "packages/api/split/tsconfig.client.json" instead',
|
||||
'tsconfig.host.json: Project Reference "./packages/api/split/tsconfig.client.json" enters split project packages/api/split from a Host config; reference "packages/api/split/tsconfig.host.json" instead',
|
||||
])
|
||||
})
|
||||
|
||||
it('uses the referencing project face throughout the reachable graph', () => {
|
||||
const root = workspaceFixture({
|
||||
host: ['./packages/core/host-consumer'],
|
||||
client: ['./packages/core/client-consumer'],
|
||||
})
|
||||
const hostConsumer = join(root, 'packages/core/host-consumer')
|
||||
mkdirSync(hostConsumer, { recursive: true })
|
||||
writeJson(join(hostConsumer, 'package.json'), { name: '@deepseek-ai/dsh-host-consumer' })
|
||||
writeJson(join(hostConsumer, 'tsconfig.json'), {
|
||||
extends: '../../../tsconfig.base.json',
|
||||
references: [{ path: '../../api/split/tsconfig.client.json' }],
|
||||
})
|
||||
const clientConsumer = join(root, 'packages/core/client-consumer')
|
||||
mkdirSync(clientConsumer, { recursive: true })
|
||||
writeJson(join(clientConsumer, 'package.json'), { name: '@deepseek-ai/dsh-client-consumer' })
|
||||
writeJson(join(clientConsumer, 'tsconfig.json'), {
|
||||
extends: '../../../tsconfig.base.client.json',
|
||||
references: [{ path: '../../api/split/tsconfig.host.json' }],
|
||||
})
|
||||
|
||||
expect(collectProjectReferenceFaceViolations(root)).toEqual([
|
||||
'packages/core/client-consumer/tsconfig.json: Project Reference "../../api/split/tsconfig.host.json" enters split project packages/api/split from a Client config; reference "packages/api/split/tsconfig.client.json" instead',
|
||||
'packages/core/host-consumer/tsconfig.json: Project Reference "../../api/split/tsconfig.client.json" enters split project packages/api/split from a Host config; reference "packages/api/split/tsconfig.host.json" instead',
|
||||
])
|
||||
})
|
||||
})
|
||||
129
scripts/project-reference-faces.ts
Normal file
129
scripts/project-reference-faces.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/** Validate compiler-face isolation across workspace Project Reference graphs. */
|
||||
|
||||
import { existsSync, globSync } from 'node:fs'
|
||||
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
|
||||
type ProjectFace = 'host' | 'client'
|
||||
|
||||
interface ProjectReferenceConfig {
|
||||
readonly extends?: unknown
|
||||
readonly references?: ReadonlyArray<{ readonly path?: unknown }>
|
||||
}
|
||||
|
||||
const WORKSPACE_MANIFESTS = [
|
||||
'packages/*/*/package.json',
|
||||
'apps/*/package.json',
|
||||
'vendor/*/package.json',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Find references that enter the wrong leaf of a split Host/Client project.
|
||||
*
|
||||
* A single-config project is neutral and may participate in either graph. Once
|
||||
* a package declares both face configs, every reachable reference must name
|
||||
* the leaf matching the aggregate from which traversal began.
|
||||
*
|
||||
* @param root - Repository root containing both aggregate tsconfigs.
|
||||
* @returns Repo-relative diagnostics for every mismatched reference edge.
|
||||
*/
|
||||
export function collectProjectReferenceFaceViolations(root: string): string[] {
|
||||
const splitRoots = splitProjectRoots(root)
|
||||
const violations: string[] = []
|
||||
const pending = [resolve(root, 'tsconfig.host.json'), resolve(root, 'tsconfig.client.json')]
|
||||
const visited = new Set<string>()
|
||||
for (let configPath = pending.pop(); configPath !== undefined; configPath = pending.pop()) {
|
||||
if (visited.has(configPath) || !existsSync(configPath)) continue
|
||||
visited.add(configPath)
|
||||
const config = projectConfig(root, configPath)
|
||||
const face = projectFace(root, configPath, config)
|
||||
for (const reference of projectReferences(config)) {
|
||||
const targetConfig = referenceConfigPath(configPath, reference)
|
||||
const splitRoot = containingSplitRoot(splitRoots, targetConfig)
|
||||
if (splitRoot !== undefined) {
|
||||
if (face === undefined) {
|
||||
violations.push(
|
||||
`${repoPath(root, configPath)}: Project Reference ${JSON.stringify(reference)} enters split project ${repoPath(root, splitRoot)} from a config with no Host/Client face`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
const expected = resolve(splitRoot, `tsconfig.${face}.json`)
|
||||
if (targetConfig !== expected) {
|
||||
violations.push(
|
||||
`${repoPath(root, configPath)}: Project Reference ${JSON.stringify(reference)} enters split project ${repoPath(root, splitRoot)} from a ${faceLabel(face)} config; reference ${JSON.stringify(repoPath(root, expected))} instead`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
pending.push(targetConfig)
|
||||
}
|
||||
}
|
||||
|
||||
return violations.sort()
|
||||
}
|
||||
|
||||
function splitProjectRoots(root: string): string[] {
|
||||
return globSync(WORKSPACE_MANIFESTS, { cwd: root })
|
||||
.map(manifest => resolve(root, dirname(manifest)))
|
||||
.filter(dir => existsSync(resolve(dir, 'tsconfig.host.json'))
|
||||
&& existsSync(resolve(dir, 'tsconfig.client.json')))
|
||||
.sort((left, right) => right.length - left.length)
|
||||
}
|
||||
|
||||
function projectConfig(root: string, configPath: string): ProjectReferenceConfig {
|
||||
const read = ts.readConfigFile(configPath, path => ts.sys.readFile(path))
|
||||
if (read.error !== undefined) {
|
||||
const message = ts.flattenDiagnosticMessageText(read.error.messageText, '\n')
|
||||
throw new Error(`${repoPath(root, configPath)}: ${message}`)
|
||||
}
|
||||
return read.config as ProjectReferenceConfig
|
||||
}
|
||||
|
||||
function projectReferences(config: ProjectReferenceConfig): string[] {
|
||||
return (config.references ?? [])
|
||||
.map(reference => reference.path)
|
||||
.filter((path): path is string => typeof path === 'string')
|
||||
}
|
||||
|
||||
function projectFace(
|
||||
root: string,
|
||||
configPath: string,
|
||||
config: ProjectReferenceConfig,
|
||||
seen = new Set<string>(),
|
||||
): ProjectFace | undefined {
|
||||
if (basename(configPath) === 'tsconfig.host.json') return 'host'
|
||||
if (basename(configPath) === 'tsconfig.client.json') return 'client'
|
||||
if (configPath === resolve(root, 'tsconfig.base.json')) return 'host'
|
||||
if (configPath === resolve(root, 'tsconfig.base.client.json')) return 'client'
|
||||
if (seen.has(configPath)) return undefined
|
||||
seen.add(configPath)
|
||||
const parent = localExtendsConfig(configPath, config.extends)
|
||||
if (parent === undefined || !existsSync(parent)) return undefined
|
||||
return projectFace(root, parent, projectConfig(root, parent), seen)
|
||||
}
|
||||
|
||||
function localExtendsConfig(configPath: string, value: unknown): string | undefined {
|
||||
if (typeof value !== 'string' || !value.startsWith('.')) return undefined
|
||||
const target = resolve(dirname(configPath), value)
|
||||
return target.endsWith('.json') ? target : `${target}.json`
|
||||
}
|
||||
|
||||
function referenceConfigPath(sourceConfig: string, reference: string): string {
|
||||
const target = resolve(dirname(sourceConfig), reference)
|
||||
return target.endsWith('.json') ? target : resolve(target, 'tsconfig.json')
|
||||
}
|
||||
|
||||
function containingSplitRoot(splitRoots: readonly string[], targetConfig: string): string | undefined {
|
||||
return splitRoots.find((root) => {
|
||||
const path = relative(root, targetConfig)
|
||||
return path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path)
|
||||
})
|
||||
}
|
||||
|
||||
function repoPath(root: string, path: string): string {
|
||||
return relative(root, path).split(sep).join('/')
|
||||
}
|
||||
|
||||
function faceLabel(face: ProjectFace): string {
|
||||
return face === 'host' ? 'Host' : 'Client'
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isForbiddenPublicationFile, validateTarballPayload } from './publication-payload.ts'
|
||||
import {
|
||||
hasTypeRTRemoteNavigation,
|
||||
isForbiddenPublicationFile,
|
||||
validateTarballPayload,
|
||||
} from './publication-payload.ts'
|
||||
|
||||
function validateFixtureTarball(files: readonly string[]): () => void {
|
||||
return () => {
|
||||
@@ -51,4 +55,29 @@ describe('publication payload policy', () => {
|
||||
'package/lib/styles/base.css',
|
||||
])).not.toThrow()
|
||||
})
|
||||
|
||||
it('allows only the TypeRT declaration map and its navigable source tree when requested', () => {
|
||||
const policy = { typeRTRemoteNavigation: true }
|
||||
expect(isForbiddenPublicationFile('src/index.ts', policy)).toBe(false)
|
||||
expect(isForbiddenPublicationFile('lib/typert.remote-client.d.ts.map', policy)).toBe(false)
|
||||
expect(isForbiddenPublicationFile('lib/types/index.d.ts.map', policy)).toBe(true)
|
||||
expect(() => {
|
||||
validateTarballPayload([
|
||||
'package/lib/typert.remote-client.d.ts.map',
|
||||
'package/src/index.ts',
|
||||
], 'fixture.tgz', policy)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('recognizes only the canonical Host-for-Client export pair', () => {
|
||||
expect(hasTypeRTRemoteNavigation({
|
||||
exports: {
|
||||
'./remote': {
|
||||
types: './lib/typert.remote-client.d.ts',
|
||||
default: './lib/typert.remote-client.js',
|
||||
},
|
||||
},
|
||||
})).toBe(true)
|
||||
expect(hasTypeRTRemoteNavigation({ exports: { './remote': './lib/remote.js' } })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
/** Publication payload policy shared by static manifests and packed tarballs. */
|
||||
|
||||
/** Publication exceptions required for TypeRT declaration-map navigation. */
|
||||
export interface PublicationPayloadPolicy {
|
||||
readonly typeRTRemoteNavigation?: boolean
|
||||
}
|
||||
|
||||
/** Whether a package manifest exports generated Host-for-Client metadata with source navigation. */
|
||||
export function hasTypeRTRemoteNavigation(manifest: unknown): boolean {
|
||||
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) return false
|
||||
const exportsField = (manifest as Record<string, unknown>).exports
|
||||
if (exportsField === null || typeof exportsField !== 'object' || Array.isArray(exportsField)) return false
|
||||
const remote = (exportsField as Record<string, unknown>)['./remote']
|
||||
if (remote === null || typeof remote !== 'object' || Array.isArray(remote)) return false
|
||||
const entry = remote as Record<string, unknown>
|
||||
return entry.types === './lib/typert.remote-client.d.ts'
|
||||
&& entry.default === './lib/typert.remote-client.js'
|
||||
}
|
||||
|
||||
/** Normalize a package manifest path or npm tarball member to its payload-relative path. */
|
||||
function payloadPath(file: string): string {
|
||||
const normalized = file.replaceAll('\\', '/').replace(/^\.\/+/, '').replace(/\/+$/, '')
|
||||
@@ -7,17 +24,30 @@ function payloadPath(file: string): string {
|
||||
}
|
||||
|
||||
/** Whether a package payload path exposes source or declaration-map intermediates. */
|
||||
export function isForbiddenPublicationFile(file: string): boolean {
|
||||
export function isForbiddenPublicationFile(
|
||||
file: string,
|
||||
policy: PublicationPayloadPolicy = {},
|
||||
): boolean {
|
||||
const normalized = payloadPath(file)
|
||||
if (policy.typeRTRemoteNavigation === true
|
||||
&& (normalized === 'src'
|
||||
|| normalized.startsWith('src/')
|
||||
|| normalized === 'lib/typert.remote-client.d.ts.map')) {
|
||||
return false
|
||||
}
|
||||
return normalized === 'src'
|
||||
|| normalized.startsWith('src/')
|
||||
|| normalized.endsWith('.d.ts.map')
|
||||
}
|
||||
|
||||
/** Reject source and declaration-map members in a packed npm tarball. */
|
||||
export function validateTarballPayload(files: readonly string[], context: string): void {
|
||||
export function validateTarballPayload(
|
||||
files: readonly string[],
|
||||
context: string,
|
||||
policy: PublicationPayloadPolicy = {},
|
||||
): void {
|
||||
for (const file of files) {
|
||||
if (!isForbiddenPublicationFile(file)) continue
|
||||
if (!isForbiddenPublicationFile(file, policy)) continue
|
||||
const normalized = payloadPath(file)
|
||||
if (normalized === 'src' || normalized.startsWith('src/')) {
|
||||
throw new Error(`${context} publishes source file ${file}`)
|
||||
|
||||
@@ -18,7 +18,7 @@ import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep
|
||||
import { createInterface } from 'node:readline/promises'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { validateTarballPayload } from './publication-payload.ts'
|
||||
import { hasTypeRTRemoteNavigation, validateTarballPayload } from './publication-payload.ts'
|
||||
|
||||
const DEFAULT_REGISTRY = 'https://registry.npm.harnessment.com'
|
||||
const DEFAULT_OUTPUT_DIRECTORY = '.artifacts/npm-baseline'
|
||||
@@ -320,7 +320,11 @@ class ReleaseBundle {
|
||||
if (expected === undefined || !missingNames.delete(artifact.name)) {
|
||||
throw new Error(`unexpected or duplicate packed package: ${artifact.name}`)
|
||||
}
|
||||
if (expected.origin === 'harness') validateTarballPayload(artifact.files, tarball)
|
||||
if (expected.origin === 'harness') {
|
||||
validateTarballPayload(artifact.files, tarball, {
|
||||
typeRTRemoteNavigation: hasTypeRTRemoteNavigation(artifact.manifest),
|
||||
})
|
||||
}
|
||||
if (artifact.version !== version) {
|
||||
throw new Error(`${tarball} has version ${artifact.version}; expected ${version}`)
|
||||
}
|
||||
@@ -394,7 +398,11 @@ class ReleaseBundle {
|
||||
throw new Error(`tarball checksum mismatch: ${pkg.tarball}`)
|
||||
}
|
||||
const artifact = inspectTarball(path, runner)
|
||||
if (pkg.origin === 'harness') validateTarballPayload(artifact.files, pkg.tarball)
|
||||
if (pkg.origin === 'harness') {
|
||||
validateTarballPayload(artifact.files, pkg.tarball, {
|
||||
typeRTRemoteNavigation: hasTypeRTRemoteNavigation(artifact.manifest),
|
||||
})
|
||||
}
|
||||
if (artifact.name !== pkg.name || artifact.version !== this.manifest.version) {
|
||||
throw new Error(`tarball identity mismatch: ${pkg.tarball}`)
|
||||
}
|
||||
@@ -463,13 +471,6 @@ class InstalledBundleSmoke {
|
||||
+ `expected ${this.bundle.manifest.version}`,
|
||||
)
|
||||
}
|
||||
const config = this.runner.capture(
|
||||
process.execPath,
|
||||
[bin, '--dump-default-config'],
|
||||
consumerRoot,
|
||||
environment,
|
||||
)
|
||||
if (config === '') throw new Error('installed dsh --dump-default-config returned no output')
|
||||
this.probeWeb(bin, consumerRoot, environment)
|
||||
console.log('publish-npm-baseline: installed dsh entry and Web startup probes passed')
|
||||
} finally {
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('gate graph validation', () => {
|
||||
'ci-primary',
|
||||
'ci-linux-primary',
|
||||
'ci-static',
|
||||
'ci-lint',
|
||||
'ci-lint-contracts-ready',
|
||||
'ci-coverage',
|
||||
'ci-snapshot',
|
||||
'ci-artifacts',
|
||||
@@ -77,6 +77,12 @@ describe('gate graph validation', () => {
|
||||
await expect(runGates(subject, subject.length, execute)).resolves.toHaveLength(subject.length)
|
||||
})
|
||||
|
||||
it('keeps the public repository link policy in the documentation gate', () => {
|
||||
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
|
||||
|
||||
expect(ids).toContain('public-repository-links')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['empty', [], /gate graph has no gates/],
|
||||
['duplicate ids', [gate('same'), gate('same')], /duplicate gate id "same"/],
|
||||
@@ -112,29 +118,77 @@ describe('gate graph validation', () => {
|
||||
describe('Oxlint gate', () => {
|
||||
it('uses the package script when no worker bound is configured', () => {
|
||||
const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-lint')[0]))
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready')[0]))
|
||||
|
||||
expect(subject).toMatchObject({
|
||||
id: 'lint',
|
||||
displayCommand: 'pnpm run lint',
|
||||
displayCommand: 'pnpm run lint:contracts-ready',
|
||||
command: process.execPath,
|
||||
args: ['/private/pnpm.cjs', 'run', 'lint'],
|
||||
args: ['/private/pnpm.cjs', 'run', 'lint:contracts-ready'],
|
||||
})
|
||||
})
|
||||
|
||||
it('surfaces the configured worker bound on the shared package script', () => {
|
||||
const subject = withEnv('DSH_OXLINT_THREADS', '4', () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-lint')[0]))
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-lint-contracts-ready')[0]))
|
||||
|
||||
expect(subject).toMatchObject({
|
||||
id: 'lint',
|
||||
displayCommand: 'DSH_OXLINT_THREADS=4 pnpm run lint',
|
||||
displayCommand: 'DSH_OXLINT_THREADS=4 pnpm run lint:contracts-ready',
|
||||
command: process.execPath,
|
||||
args: ['/private/pnpm.cjs', 'run', 'lint'],
|
||||
args: ['/private/pnpm.cjs', 'run', 'lint:contracts-ready'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('TypeRT contract preparation', () => {
|
||||
it('prepares primary source consumers once before they run', () => {
|
||||
const subject = withEnv('DSH_OXLINT_THREADS', undefined, () =>
|
||||
withPnpmEntrypoint(() => gatesForMode('ci-primary')))
|
||||
|
||||
expect(subject.find(item => item.id === 'typert-contracts')).toMatchObject({
|
||||
displayCommand: 'pnpm run build:lib:host',
|
||||
args: ['/private/pnpm.cjs', 'run', 'build:lib:host'],
|
||||
})
|
||||
for (const [id, script] of [
|
||||
['typecheck', 'typecheck:contracts-ready'],
|
||||
['lint', 'lint:contracts-ready'],
|
||||
['doc-typecheck', 'doc-typecheck:contracts-ready'],
|
||||
] as const) {
|
||||
expect(subject.find(item => item.id === id)).toMatchObject({
|
||||
displayCommand: `pnpm run ${script}`,
|
||||
args: ['/private/pnpm.cjs', 'run', script],
|
||||
needs: ['typert-contracts'],
|
||||
})
|
||||
}
|
||||
expect(subject.find(item => item.id === 'build')?.needs).toEqual([
|
||||
'typecheck',
|
||||
'lint',
|
||||
'doc-typecheck',
|
||||
])
|
||||
})
|
||||
|
||||
it('reuses contracts from the validated consumer build', () => {
|
||||
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
|
||||
|
||||
expect(subject.find(item => item.id === 'lint-and-duplication')).toMatchObject({
|
||||
displayCommand: 'pnpm run check:ci:lint:contracts-ready',
|
||||
args: ['/private/pnpm.cjs', 'run', 'check:ci:lint:contracts-ready'],
|
||||
})
|
||||
expect(subject.find(item => item.id === 'doc-typecheck')).toMatchObject({
|
||||
displayCommand: 'pnpm run doc-typecheck:contracts-ready',
|
||||
args: ['/private/pnpm.cjs', 'run', 'doc-typecheck:contracts-ready'],
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps standalone doc sync responsible for preparation', () => {
|
||||
const docTypecheck = withPnpmEntrypoint(() =>
|
||||
gatesForMode('doc-sync').find(item => item.id === 'doc-typecheck'))
|
||||
|
||||
expect(docTypecheck?.displayCommand).toBe('pnpm run doc-typecheck')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Node compatibility graph', () => {
|
||||
it('runs the jsdom environment smoke on every advertised Node line', () => {
|
||||
const subject = withPnpmEntrypoint(() => gatesForMode('node-compat'))
|
||||
@@ -189,6 +243,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' },
|
||||
|
||||
@@ -16,7 +16,7 @@ export type Mode =
|
||||
| 'ci-primary'
|
||||
| 'ci-linux-primary'
|
||||
| 'ci-static'
|
||||
| 'ci-lint'
|
||||
| 'ci-lint-contracts-ready'
|
||||
| 'ci-coverage'
|
||||
| 'ci-snapshot'
|
||||
| 'ci-artifacts'
|
||||
@@ -101,7 +101,7 @@ function parseMode(raw: string | undefined): Mode {
|
||||
case 'ci-primary':
|
||||
case 'ci-linux-primary':
|
||||
case 'ci-static':
|
||||
case 'ci-lint':
|
||||
case 'ci-lint-contracts-ready':
|
||||
case 'ci-coverage':
|
||||
case 'ci-snapshot':
|
||||
case 'ci-artifacts':
|
||||
@@ -115,7 +115,7 @@ function parseMode(raw: string | undefined): Mode {
|
||||
return raw
|
||||
default:
|
||||
throw new Error(
|
||||
`run-gates: expected mode ci-primary | ci-linux-primary | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | ci-consumers | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | check-all | doc-sync, got ${JSON.stringify(raw)}.`,
|
||||
`run-gates: expected mode ci-primary | ci-linux-primary | ci-static | ci-lint-contracts-ready | ci-coverage | ci-snapshot | ci-artifacts | ci-consumers | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | check-all | doc-sync, got ${JSON.stringify(raw)}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export function gatesForMode(selected: Mode): Gate[] {
|
||||
return [...ciPrimaryGates(), webSnapshotGate(['built-package-invariants'])]
|
||||
case 'ci-static':
|
||||
return ciStaticGates({ ownsBuild: false })
|
||||
case 'ci-lint':
|
||||
case 'ci-lint-contracts-ready':
|
||||
return [
|
||||
lintGate(),
|
||||
pnpmScript('duplication', 'duplication'),
|
||||
@@ -224,6 +224,7 @@ export function gatesForMode(selected: Mode): Gate[] {
|
||||
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
|
||||
pnpmScript('client-domain-graph', 'verify-client-domain-graph', { label: 'client domain graph' }),
|
||||
pnpmScript('test', 'test'),
|
||||
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
|
||||
pnpmScript('duplication', 'duplication'),
|
||||
snapshotGate(),
|
||||
pnpmScript('build', 'build'),
|
||||
@@ -232,6 +233,7 @@ export function gatesForMode(selected: Mode): Gate[] {
|
||||
...docSyncLeafGates({
|
||||
docTypecheckNeeds: ['build'],
|
||||
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
|
||||
docTypecheckScript: 'doc-typecheck:contracts-ready',
|
||||
}),
|
||||
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
|
||||
]
|
||||
@@ -240,25 +242,36 @@ export function gatesForMode(selected: Mode): Gate[] {
|
||||
}
|
||||
}
|
||||
|
||||
function ciPrimaryGates(): Gate[] {
|
||||
function ciSharedStaticGates(): Gate[] {
|
||||
return [
|
||||
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
||||
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
|
||||
pnpmScript('typecheck', 'typecheck'),
|
||||
lintGate(),
|
||||
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
|
||||
]
|
||||
}
|
||||
|
||||
function ciPrimaryGates(): Gate[] {
|
||||
return [
|
||||
...ciSharedStaticGates(),
|
||||
typertContractsGate(),
|
||||
pnpmScript('typecheck', 'typecheck:contracts-ready', { needs: ['typert-contracts'] }),
|
||||
lintGate({ needs: ['typert-contracts'] }),
|
||||
pnpmScript('duplication', 'duplication'),
|
||||
...coverageGates(),
|
||||
...nodeCompatSmokeGates(),
|
||||
snapshotGate(),
|
||||
...docSyncLeafGates(),
|
||||
...docSyncLeafGates({
|
||||
docTypecheckNeeds: ['typert-contracts'],
|
||||
docTypecheckScript: 'doc-typecheck:contracts-ready',
|
||||
}),
|
||||
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
|
||||
pnpmScript('knip', 'knip'),
|
||||
// typecheck and build now drive the same root solution graph; without the
|
||||
// dependency two concurrent `tsc -b` runs race the same tsbuildinfo files.
|
||||
// The tsc step is an incremental no-op after typecheck.
|
||||
pnpmScript('build', 'build', { needs: ['typecheck'] }),
|
||||
// The prepared typecheck and build both drive Client tsc, while build also
|
||||
// repeats the Host contract pass. Wait for all three consumers so build
|
||||
// neither races tsbuildinfo nor replaces declarations while they are read.
|
||||
pnpmScript('build', 'build', { needs: ['typecheck', 'lint', 'doc-typecheck'] }),
|
||||
pnpmScript('publint', 'publint', { needs: ['build'] }),
|
||||
pnpmScript('node-next-types', 'verify-node-next-types', {
|
||||
label: 'node-next types',
|
||||
@@ -339,10 +352,7 @@ function runningNodeMajor(): number {
|
||||
|
||||
function ciStaticGates(options: { ownsBuild: boolean }): Gate[] {
|
||||
return [
|
||||
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
||||
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
|
||||
...ciSharedStaticGates(),
|
||||
...options.ownsBuild ? [pnpmScript('build', 'build')] : [],
|
||||
...docSyncLeafGates({
|
||||
includeDocTypecheck: options.ownsBuild,
|
||||
@@ -350,6 +360,7 @@ function ciStaticGates(options: { ownsBuild: boolean }): Gate[] {
|
||||
? {
|
||||
docTypecheckNeeds: ['build'],
|
||||
docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
|
||||
docTypecheckScript: 'doc-typecheck:contracts-ready',
|
||||
}
|
||||
: {},
|
||||
docsBuildScript: 'docs:build:mpa',
|
||||
@@ -380,13 +391,13 @@ function ciConsumerGates(): Gate[] {
|
||||
pnpmScript('node-compat', 'check:node-compat', { label: 'Node compatibility' }),
|
||||
pnpmScript('publint', 'publint', { needs: builtTree }),
|
||||
builtPackageInvariantsGate(['publint']),
|
||||
pnpmScript('lint-and-duplication', 'check:ci:lint', {
|
||||
pnpmScript('lint-and-duplication', 'check:ci:lint:contracts-ready', {
|
||||
label: 'lint and duplication',
|
||||
needs: validatedBuild,
|
||||
}),
|
||||
snapshotGate(validatedBuild),
|
||||
webSnapshotGate(validatedBuild),
|
||||
pnpmScript('doc-typecheck', 'doc-typecheck', {
|
||||
pnpmScript('doc-typecheck', 'doc-typecheck:contracts-ready', {
|
||||
needs: validatedBuild,
|
||||
env: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' },
|
||||
}),
|
||||
@@ -442,11 +453,19 @@ function ciWindowsObservationalGates(): Gate[] {
|
||||
]
|
||||
}
|
||||
|
||||
function lintGate(): Gate {
|
||||
function typertContractsGate(): Gate {
|
||||
return pnpmScript('typert-contracts', 'build:lib:host', { label: 'TypeRT contracts' })
|
||||
}
|
||||
|
||||
function lintGate(options: { needs?: string[] } = {}): Gate {
|
||||
const raw = process.env.DSH_OXLINT_THREADS
|
||||
return pnpmScript('lint', 'lint', raw === undefined || raw === ''
|
||||
? {}
|
||||
: { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` })
|
||||
const script = 'lint:contracts-ready'
|
||||
return pnpmScript('lint', script, {
|
||||
...raw === undefined || raw === ''
|
||||
? {}
|
||||
: { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run ${script}` },
|
||||
...options.needs === undefined ? {} : { needs: options.needs },
|
||||
})
|
||||
}
|
||||
|
||||
// The heavy suites run uninstrumented beside the thresholded gate: their
|
||||
@@ -549,6 +568,7 @@ function docSyncLeafGates(options: {
|
||||
includeDocTypecheck?: boolean
|
||||
docTypecheckNeeds?: string[]
|
||||
docTypecheckEnv?: Record<string, string | undefined>
|
||||
docTypecheckScript?: 'doc-typecheck' | 'doc-typecheck:contracts-ready'
|
||||
docsBuildScript?: 'docs:build' | 'docs:build:mpa'
|
||||
} = {}): Gate[] {
|
||||
const docTypecheckOptions: Partial<Gate> = {}
|
||||
@@ -557,7 +577,7 @@ function docSyncLeafGates(options: {
|
||||
return [
|
||||
...options.includeDocTypecheck === false
|
||||
? []
|
||||
: [pnpmScript('doc-typecheck', 'doc-typecheck', docTypecheckOptions)],
|
||||
: [pnpmScript('doc-typecheck', options.docTypecheckScript ?? 'doc-typecheck', docTypecheckOptions)],
|
||||
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),
|
||||
pnpmScript('export-jsdoc', 'verify-export-jsdoc', { label: 'export jsdoc' }),
|
||||
pnpmScript('tool-catalog', 'verify-tool-catalog', { label: 'tool catalog' }),
|
||||
@@ -567,8 +587,10 @@ function docSyncLeafGates(options: {
|
||||
pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }),
|
||||
pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }),
|
||||
pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }),
|
||||
pnpmScript('public-repository-links', 'verify-public-repository-links', { label: 'public repository links' }),
|
||||
pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }),
|
||||
pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }),
|
||||
pnpmScript('config-source-ownership', 'verify-config-source-ownership', { label: 'config source ownership' }),
|
||||
pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience' }),
|
||||
pnpmScript('mermaid', 'verify-mermaid'),
|
||||
pnpmScript('agent-note-classification', 'verify-agent-note-classification', { label: 'agent note classification' }),
|
||||
@@ -595,10 +617,12 @@ function builtBinSmokeGate(needs: string[] = ['build']): Gate {
|
||||
'vitest.e2e.config.ts',
|
||||
'examples/headless-agent/tests/keyless-smoke.e2e.ts',
|
||||
'apps/cli/tests/built-bin.e2e.ts',
|
||||
'packages/examples/cli-demo/tests/built-bin.e2e.ts',
|
||||
'packages/examples/acp-demo/tests/built-bin.e2e.ts',
|
||||
'packages/host/directory-picker-native/tests/built-worker.e2e.ts',
|
||||
'packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts',
|
||||
'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
@@ -61,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",
|
||||
@@ -131,11 +141,6 @@
|
||||
"symbol": "Agent",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "PreStepContext",
|
||||
"source": "packages/core/agent/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "PreStepDecision",
|
||||
@@ -194,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",
|
||||
@@ -214,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",
|
||||
@@ -1489,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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
30
scripts/verify-config-source-ownership.spec.ts
Normal file
30
scripts/verify-config-source-ownership.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { collectConfigSourceOwnershipViolations } from './verify-config-source-ownership.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('configuration source ownership gate', () => {
|
||||
it('rejects inline endpoints in shipped bundle patches', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-config-source-ownership-'))
|
||||
roots.push(root)
|
||||
const directory = join(root, 'packages/bundle/base')
|
||||
mkdirSync(directory, { recursive: true })
|
||||
writeFileSync(
|
||||
join(directory, 'cordis.patch.yml'),
|
||||
'config:\n baseURL: !!js process.env.DEEPSEEK_SEARCH_BASE_URL\n',
|
||||
)
|
||||
|
||||
expect(collectConfigSourceOwnershipViolations(root)).toEqual([
|
||||
'packages/bundle/base/cordis.patch.yml:2: inlines a credential or endpoint from the environment.'
|
||||
+ ' The adapter resolves apiKeyEnv through ctx.credentials and the endpoint through the'
|
||||
+ ' environment snapshot; inlining here bypasses both ladders.',
|
||||
])
|
||||
})
|
||||
})
|
||||
56
scripts/verify-config-source-ownership.ts
Normal file
56
scripts/verify-config-source-ownership.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Gate for forbidden credential or endpoint environment inlines in shipped
|
||||
* Cordis configuration.
|
||||
* @module scripts/verify-config-source-ownership
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
|
||||
const ROOT = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** Shipped Cordis configuration these rules apply to. */
|
||||
const SHIPPED_CONFIG_GLOBS = [
|
||||
'apps/*/config/*.yml',
|
||||
'examples/*/*.cordis.yml',
|
||||
'examples/*/cordis.yml',
|
||||
'packages/bundle/*/cordis.patch.yml',
|
||||
// The Python runtime ships its own default composition inside the wheel.
|
||||
'python/*/src/**/cordis.yml',
|
||||
]
|
||||
|
||||
/** Ordinary single-line forms this narrow source-shape check rejects; not full YAML analysis. */
|
||||
const INLINE_DENY = /^\s*(apiKey|baseURL|apiKeyEnv|authToken|headers)\s*:\s*!!js\b/
|
||||
|
||||
/** Return every forbidden inline environment form in shipped configuration. */
|
||||
export function collectConfigSourceOwnershipViolations(root: string): string[] {
|
||||
const failures: string[] = []
|
||||
for (const glob of SHIPPED_CONFIG_GLOBS) {
|
||||
for (const file of globSync(glob, { cwd: root })) {
|
||||
const rel = file.split(sep).join('/')
|
||||
readFileSync(resolve(root, rel), 'utf8').split('\n').forEach((line, index) => {
|
||||
if (!INLINE_DENY.test(line)) return
|
||||
failures.push(
|
||||
`${rel}:${String(index + 1)}: inlines a credential or endpoint from the environment.`
|
||||
+ ' The adapter resolves apiKeyEnv through ctx.credentials and the endpoint through the'
|
||||
+ ' environment snapshot; inlining here bypasses both ladders.',
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
return failures
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
|
||||
const failures = collectConfigSourceOwnershipViolations(ROOT)
|
||||
if (failures.length > 0) {
|
||||
process.stderr.write('verify-config-source-ownership: configuration source ownership violated:\n')
|
||||
for (const failure of failures) process.stderr.write(` ${failure}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
'verify-config-source-ownership: no credential or endpoint uses the ordinary inline environment form'
|
||||
+ ' in shipped configuration.\n',
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,6 +33,7 @@ const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
|
||||
'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.',
|
||||
'packages/util/brand': 'The package is a type-only primitive erased at compile time.',
|
||||
'packages/util/paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.',
|
||||
'packages/util/environment': 'The package only resolves host environment values; model-facing consumers own any rendered use.',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,10 +58,12 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/client/web-react': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/schema-form': { kind: 'none', reason: 'Browser-side form-rendering library; registers no model surface.' },
|
||||
'packages/client/connection': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/api/remotes': { kind: 'none', reason: 'The Remote BFF selects business methods and identity policy; selected services own any model-visible effect.' },
|
||||
'packages/client/runtime': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-deliverables': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-command': { kind: 'indirect', reason: 'The dispatch paths trigger the host command.execute RPC; each command handler\'s host package owns any model-visible effect.' },
|
||||
'packages/client/ui-model': { kind: 'indirect', reason: 'Selection routes session.selectModel; the host snapshots the target at the next prompt-assembly boundary and owns the model-visible effect.' },
|
||||
@@ -86,6 +89,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.' },
|
||||
@@ -118,9 +124,11 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/support/acp-snapshot': { kind: 'none', reason: 'The test harness observes and normalizes transcripts without changing live requests.' },
|
||||
'packages/support/agent-loop-testkit': { kind: 'none', reason: 'The test helper mounts services but neither drives nor modifies model requests.' },
|
||||
'packages/support/invariants': { kind: 'none', reason: 'The observer validates requests but never rewrites their context.' },
|
||||
'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness observes child-process streams without changing live requests.' },
|
||||
'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness submits an ordinary user task but delegates prompt and tool composition to the loaded tree.' },
|
||||
'packages/support/llm-mock-server': { kind: 'none', reason: 'The test server substitutes provider wire behavior without invoking a real model.' },
|
||||
'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
|
||||
'packages/api/gateway': { kind: 'none', reason: 'Remote dispatch infrastructure; invoked business methods own any model-visible effect.' },
|
||||
'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.' },
|
||||
|
||||
32
scripts/verify-public-repository-links.spec.ts
Normal file
32
scripts/verify-public-repository-links.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { findInternalRepositoryReferences } from './verify-public-repository-links.ts'
|
||||
|
||||
describe('public repository link policy', () => {
|
||||
it('rejects encoded and case-varied internal identities without blocking public repositories', () => {
|
||||
const internalOwner = ['deepseek', 'harness'].join('-')
|
||||
const internalRepository = [internalOwner, internalOwner].join('/')
|
||||
const encodedRepository = internalRepository.replaceAll('-', '%2D').replace('/', '%2F')
|
||||
const htmlEncodedRepository = internalRepository.replace('/', '/')
|
||||
const jsonEscapedRepository = internalRepository.replace('/', '\\/')
|
||||
const unicodeEscapedRepository = internalRepository.replace('/', String.raw`\u002f`)
|
||||
const source = [
|
||||
'https://github.com/deepseek-ai/deepseek-harness-sdk',
|
||||
`https://github.com/${internalOwner}/cordis`,
|
||||
`https://github.com/${internalRepository.toUpperCase()}/issues/1`,
|
||||
`https://github.com/${encodedRepository}/issues/2`,
|
||||
`https://github.com/${htmlEncodedRepository}/issues/3`,
|
||||
`"https:\\/\\/github.com\\/${jsonEscapedRepository}\\/issues\\/4"`,
|
||||
`"https:\\/\\/github.com\\/${unicodeEscapedRepository}\\/issues\\/5"`,
|
||||
`${internalOwner.toUpperCase()}#6`,
|
||||
].join('\n')
|
||||
|
||||
expect(findInternalRepositoryReferences('subject.md', source)).toEqual([
|
||||
{ file: 'subject.md', line: 3 },
|
||||
{ file: 'subject.md', line: 4 },
|
||||
{ file: 'subject.md', line: 5 },
|
||||
{ file: 'subject.md', line: 6 },
|
||||
{ file: 'subject.md', line: 7 },
|
||||
{ file: 'subject.md', line: 8 },
|
||||
])
|
||||
})
|
||||
})
|
||||
90
scripts/verify-public-repository-links.ts
Normal file
90
scripts/verify-public-repository-links.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/** Reject tracked files that expose the internal repository identity. */
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const internalOwner = ['deepseek', 'harness'].join('-')
|
||||
const internalRepository = [internalOwner, internalOwner].join('/')
|
||||
const internalIssueShorthand = `${internalOwner}#`
|
||||
|
||||
const namedReferenceCharacters: Readonly<Record<string, string>> = {
|
||||
hyphen: '-',
|
||||
num: '#',
|
||||
sol: '/',
|
||||
}
|
||||
|
||||
/** Normalize source spellings that render or decode to repository separators. */
|
||||
function canonicalReferenceText(source: string): string {
|
||||
return source
|
||||
.replaceAll('\\/', '/')
|
||||
.replace(/\\u(0023|002d|002f)/gi, (_match, code: string) => String.fromCodePoint(Number.parseInt(code, 16)))
|
||||
.replace(/%(23|2d|2f)/gi, (_match, code: string) => String.fromCodePoint(Number.parseInt(code, 16)))
|
||||
.replace(/&#(?:(\d+)|x([\da-f]+));/gi, (entity, decimal: string | undefined, hexadecimal: string | undefined) => {
|
||||
const code = Number.parseInt(decimal ?? hexadecimal ?? '', decimal === undefined ? 16 : 10)
|
||||
return code === 35 || code === 45 || code === 47 ? String.fromCodePoint(code) : entity
|
||||
})
|
||||
.replace(/&(hyphen|num|sol);/gi, (entity, name: string) => namedReferenceCharacters[name.toLowerCase()] ?? entity)
|
||||
.normalize('NFKC')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
/** One tracked reference to the internal repository. */
|
||||
export interface InternalRepositoryReference {
|
||||
/** Repository-relative file path. */
|
||||
file: string
|
||||
/** One-based source line. */
|
||||
line: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate internal-repository references in one text file.
|
||||
* @param file - Repository-relative path used in diagnostics.
|
||||
* @param source - Text to inspect.
|
||||
* @returns every matching source line.
|
||||
*/
|
||||
export function findInternalRepositoryReferences(file: string, source: string): InternalRepositoryReference[] {
|
||||
const references: InternalRepositoryReference[] = []
|
||||
for (const [index, line] of source.split('\n').entries()) {
|
||||
const canonicalLine = canonicalReferenceText(line)
|
||||
if (canonicalLine.includes(internalRepository) || canonicalLine.includes(internalIssueShorthand)) {
|
||||
references.push({ file, line: index + 1 })
|
||||
}
|
||||
}
|
||||
return references
|
||||
}
|
||||
|
||||
function trackedFiles(repoRoot: string): string[] {
|
||||
return execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8' })
|
||||
.split('\0')
|
||||
.filter(file => file !== '')
|
||||
}
|
||||
|
||||
function scanRepository(repoRoot: string): InternalRepositoryReference[] {
|
||||
const references: InternalRepositoryReference[] = []
|
||||
for (const file of trackedFiles(repoRoot)) {
|
||||
const path = resolve(repoRoot, file)
|
||||
if (!existsSync(path)) continue
|
||||
const stat = lstatSync(path)
|
||||
if (!stat.isFile() && !stat.isSymbolicLink()) continue
|
||||
const source = stat.isSymbolicLink() ? readlinkSync(path) : readFileSync(path, 'utf8')
|
||||
if (source.includes('\0')) continue
|
||||
references.push(...findInternalRepositoryReferences(file, source))
|
||||
}
|
||||
return references
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1]
|
||||
const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href
|
||||
if (isMain) {
|
||||
const references = scanRepository(root)
|
||||
if (references.length === 0) {
|
||||
console.log('verify-public-repository-links: tracked files expose no internal repository identity.')
|
||||
} else {
|
||||
console.error('verify-public-repository-links: internal repository references found:')
|
||||
for (const reference of references) console.error(` ${reference.file}:${String(reference.line)}`)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -204,12 +204,14 @@ cat "$scratch/logs/smoke.log"
|
||||
grep -q '^smoke: win32 x64' "$scratch/logs/smoke.log" || { echo 'wine-windows-gates: Windows Node smoke did not report win32 x64' >&2; exit 1; }
|
||||
|
||||
# ---- the two blocking surfaces, concurrently ------------------------------
|
||||
# The same shape run-gates gives ci-windows-blocking on native Windows:
|
||||
# `build` = tsc -b then tsdown, `production site` = the VitePress build. Both
|
||||
# statuses are captured so one failure cannot hide the other's result.
|
||||
# The build preserves the face order from package.json: compile and bundle the
|
||||
# Host face before compiling and bundling the Client face.
|
||||
# Both statuses are captured so one failure cannot hide the other's result.
|
||||
build_gate() {
|
||||
wine_node "$scratch/logs/tsc.log" "$tsc_js" -b --pretty false || return $?
|
||||
wine_node "$scratch/logs/tsdown.log" "$tsdown_js"
|
||||
wine_node "$scratch/logs/host-tsc.log" "$tsc_js" -b tsconfig.host.json --pretty false || return $?
|
||||
wine_node "$scratch/logs/host-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE host || return $?
|
||||
wine_node "$scratch/logs/client-tsc.log" "$tsc_js" -b tsconfig.client.json --pretty false || return $?
|
||||
wine_node "$scratch/logs/client-tsdown.log" "$tsdown_js" --env.DSH_BUILD_FACE client
|
||||
}
|
||||
site_gate() {
|
||||
cd website
|
||||
@@ -235,7 +237,11 @@ report() {
|
||||
for log in "$@"; do tail -n 200 "$log" >&2 || true; done
|
||||
fi
|
||||
}
|
||||
report 'build (tsc -b, tsdown)' "$build_status" "$scratch/logs/tsc.log" "$scratch/logs/tsdown.log"
|
||||
report 'build (Host tsc/tsdown, Client tsc/tsdown)' "$build_status" \
|
||||
"$scratch/logs/host-tsc.log" \
|
||||
"$scratch/logs/host-tsdown.log" \
|
||||
"$scratch/logs/client-tsc.log" \
|
||||
"$scratch/logs/client-tsdown.log"
|
||||
report 'production site (vitepress build)' "$site_status" "$scratch/logs/site.log"
|
||||
if (( build_status != 0 )); then exit "$build_status"; fi
|
||||
exit "$site_status"
|
||||
|
||||
Reference in New Issue
Block a user