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 session-surface
Reconciles the session-surface work (surfaceOp/sourceEventSeqs provenance as the sole derivation path) with master's worktree-subagent series (fork-seed boundary + out-of-process subagent backends). Semantic reconciliations beyond the textual auto-merge: - SQLite SCHEMA_VERSION: both sides bumped 2->3. Merged to a single v3 carrying BOTH column families — master's seed_length on `sessions` and surface's source_event_seqs/surface_op on `events`. writeRow + both INSERT sites bind the full set; the schema doc lists all three added columns as the v2->v3 gap. - agent-loop runStep request: master's `sessionId: session.id` and surface's per-append surfaceOp/sourceEventSeqs coexist (different regions). - Fork seed + surface: a fork seeds the child from the parent's LIVE events, which now carry surfaceOp, so the child's surface rebuilds correctly. Verified end-to-end — the subagent-fork replay recalls the inherited "SAFFRON" codeword through the seeded prefix. - Subagent snapshot fixtures (recorded pre-surface) re-enriched via KEYLESS deterministic replay: only surfaceOp/sourceEventSeqs added onto existing recorded lines (matched by seq), no recorded value changed. Not re-recorded against the live API. Gates: typecheck, test (1112), test:snapshot (14), doc-sync, lint, build, hygiene all green.
This commit is contained in:
@@ -33,6 +33,18 @@ interface PackageManifest {
|
||||
version?: string
|
||||
private?: boolean
|
||||
type?: string
|
||||
main?: string
|
||||
types?: string
|
||||
bin?: string | Record<string, string>
|
||||
exports?: Record<
|
||||
string,
|
||||
| {
|
||||
types?: string
|
||||
default?: string
|
||||
}
|
||||
| undefined
|
||||
>
|
||||
files?: string[]
|
||||
peerDependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
}
|
||||
@@ -73,6 +85,29 @@ function workspaceManifests(): WorkspaceManifest[] {
|
||||
return manifests
|
||||
}
|
||||
|
||||
const dshPackageFiles = [
|
||||
'lib/index.js',
|
||||
'lib/types/**/*.d.ts',
|
||||
'lib/types/**/*.d.ts.map',
|
||||
'src',
|
||||
] as const
|
||||
|
||||
const dshBinPackageFiles = [
|
||||
'lib/index.js',
|
||||
'lib/bin.js',
|
||||
'lib/types/**/*.d.ts',
|
||||
'lib/types/**/*.d.ts.map',
|
||||
'src',
|
||||
] as const
|
||||
|
||||
function sameStringList(actual: readonly string[] | undefined, expected: readonly string[]): boolean {
|
||||
return !!actual && actual.length === expected.length && actual.every((value, index) => value === expected[index])
|
||||
}
|
||||
|
||||
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
||||
return manifest.bin ? dshBinPackageFiles : dshPackageFiles
|
||||
}
|
||||
|
||||
function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
const errors: string[] = []
|
||||
const label = manifest.name ?? dir
|
||||
@@ -100,6 +135,22 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {
|
||||
if (manifest.type !== 'module') {
|
||||
errors.push(`${label}: package.json must set "type": "module"`)
|
||||
}
|
||||
if (manifest.main !== 'lib/index.js') {
|
||||
errors.push(`${label}: package.json must set "main": "lib/index.js"`)
|
||||
}
|
||||
if (manifest.types !== 'lib/types/index.d.ts') {
|
||||
errors.push(`${label}: package.json must set "types": "lib/types/index.d.ts"`)
|
||||
}
|
||||
if (manifest.exports?.['.']?.types !== './lib/types/index.d.ts') {
|
||||
errors.push(`${label}: package.json exports["."].types must be "./lib/types/index.d.ts"`)
|
||||
}
|
||||
if (manifest.exports?.['.']?.default !== './lib/index.js') {
|
||||
errors.push(`${label}: package.json exports["."].default must be "./lib/index.js"`)
|
||||
}
|
||||
const expectedFiles = expectedDshPackageFiles(manifest)
|
||||
if (!sameStringList(manifest.files, expectedFiles)) {
|
||||
errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.map(error => `${relative(root, join(root, dir, 'package.json'))}: ${error}`)
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* Markdown so documentation can't drift from the API it documents.
|
||||
*
|
||||
* Every ```ts block in README.md, docs/** and packages/* /README.md is
|
||||
* extracted to a temp file and compiled with `tsc --noEmit` against the
|
||||
* workspace sources (resolved through the same `paths` map vitest uses, so no
|
||||
* build is required first). A block that is a deliberate sketch rather than
|
||||
* compilable code opts out with an explicit ` ```ts ignore-check ` info string
|
||||
* — the opt-out is visible in the source, and this script reports the ratio so
|
||||
* the escape hatch can't quietly become the norm. A third info string,
|
||||
* extracted to a temp typecheck project and compiled against the workspace
|
||||
* sources through the same project-reference boundaries used by repo
|
||||
* typecheck. A block that is a deliberate sketch rather than compilable code
|
||||
* opts out with an explicit ` ```ts ignore-check ` info string — the opt-out
|
||||
* is visible in the source, and this script reports the ratio so the escape
|
||||
* hatch can't quietly become the norm. A third info string,
|
||||
* doc-typecheck.ts recognizes two more fence variants and skips both (each is a
|
||||
* separately-checked category, not an unchecked sketch, so neither counts in the
|
||||
* opt-out ratio): ` ```ts type-equiv ` is a verbatim source-type paste that
|
||||
@@ -88,16 +88,9 @@ function extractBlocks(absPath: string): Block[] {
|
||||
return blocks
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the workspace `paths` map from tsconfig.typecheck.json (JSONC). This map
|
||||
* resolves vendored packages to their BUILT declarations (`lib`) and harness
|
||||
* packages to source (`src`) — the same resolution `pnpm run lint`/`typecheck` use.
|
||||
* Resolving vendor to `lib` (not `src`) is essential: otherwise tsc type-checks
|
||||
* raw vendor source and floods the run with unrelated errors. Requires the
|
||||
* vendor `lib/` to exist (a fresh clone runs `pnpm run build` first; CI does too).
|
||||
*/
|
||||
function workspacePaths(): Record<string, string[]> {
|
||||
const file = join(root, 'tsconfig.typecheck.json')
|
||||
/** Reuse the repo typecheck graph references from a temp project one directory below root. */
|
||||
function workspaceReferences(): { path: string }[] {
|
||||
const file = join(root, 'tsconfig.json')
|
||||
// Parse with TypeScript's own JSONC reader, not a hand-rolled comment strip:
|
||||
// a regex strip mistakes the `/*/` in a wildcard path candidate
|
||||
// (`./packages/core/*/src`) for a block comment and corrupts the map.
|
||||
@@ -106,27 +99,24 @@ function workspacePaths(): Record<string, string[]> {
|
||||
throw new Error(`doc-typecheck: cannot read ${file}: ${ts.flattenDiagnosticMessageText(result.error.messageText, '\n')}`)
|
||||
}
|
||||
// `config` is typed `any` by the TS API; narrow it to the one field we read.
|
||||
const config = result.config as { compilerOptions: { paths: Record<string, string[]> } }
|
||||
return config.compilerOptions.paths
|
||||
const { references } = result.config as { compilerOptions: { paths: Record<string, string[]> }; references: { path: string }[] }
|
||||
return references.map(({ path }) => {
|
||||
const relativeToTemp = path.startsWith('./') ? `../${path.slice(2)}` : `../${path}`
|
||||
return { path: relativeToTemp }
|
||||
})
|
||||
}
|
||||
|
||||
/** The standalone tsconfig for the temp project (copies base resolution, no
|
||||
* composite/declaration settings that would fight `--noEmit`). */
|
||||
/** The standalone tsconfig for the temp typecheck project. */
|
||||
function tempTsconfig(): string {
|
||||
return JSON.stringify({
|
||||
extends: '../tsconfig.json',
|
||||
compilerOptions: {
|
||||
target: 'es2024',
|
||||
module: 'esnext',
|
||||
moduleResolution: 'bundler',
|
||||
allowImportingTsExtensions: true,
|
||||
strict: true,
|
||||
noEmit: true,
|
||||
skipLibCheck: true,
|
||||
types: ['node'],
|
||||
baseUrl: root,
|
||||
ignoreDeprecations: '6.0',
|
||||
paths: workspacePaths(),
|
||||
noUnusedLocals: false,
|
||||
noUnusedParameters: false,
|
||||
tsBuildInfoFile: './tsconfig.tsbuildinfo',
|
||||
},
|
||||
include: ['block-*.ts'],
|
||||
references: workspaceReferences(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -164,11 +154,12 @@ try {
|
||||
})
|
||||
|
||||
try {
|
||||
execFileSync('node_modules/.bin/tsc', ['-p', join(tmp, 'tsconfig.json')], { cwd: root, stdio: 'pipe' })
|
||||
execFileSync('node_modules/.bin/tsc', ['-b', join(tmp, 'tsconfig.json')], { cwd: root, stdio: 'pipe' })
|
||||
} catch (error: unknown) {
|
||||
const out = (error as { stdout?: Buffer }).stdout?.toString() ?? ''
|
||||
const failed = error as { stdout?: Buffer; stderr?: Buffer }
|
||||
const out = `${failed.stdout?.toString() ?? ''}${failed.stderr?.toString() ?? ''}`
|
||||
// Rewrite "block-N.ts(line,col)" to the real "file:fenceLine" for triage.
|
||||
const remapped = out.replace(/block-(\d+)\.ts\((\d+),(\d+)\)/g, (_m, idx: string, ln: string, col: string) => {
|
||||
const remapped = out.replace(/(?:[^\s:()]*[/\\])?block-(\d+)\.ts\((\d+),(\d+)\)/g, (_m, idx: string, ln: string, col: string) => {
|
||||
const block = fileForBlock.get(`block-${idx}.ts`)
|
||||
if (!block) return `block-${idx}.ts(${ln},${col})`
|
||||
return `${block.file} (block at line ${block.line}, +${ln}:${col})`
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashRunResult", "source": "packages/bash/bash/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "CollectedOutput", "source": "packages/bash/bash/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashTask", "source": "packages/bash/bash/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashTaskRead", "source": "packages/bash/bash/src/types.ts" }
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashTaskRead", "source": "packages/bash/bash/src/types.ts" },
|
||||
|
||||
{ "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentCapabilities", "source": "packages/subagent/subagent/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentStartRequest", "source": "packages/subagent/subagent/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentResult", "source": "packages/subagent/subagent/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentStopReasonMap", "source": "packages/subagent/subagent/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentRun", "source": "packages/subagent/subagent/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentProvider", "source": "packages/subagent/subagent/src/types.ts" }
|
||||
]
|
||||
}
|
||||
|
||||
160
scripts/verify-node-next-types.ts
Normal file
160
scripts/verify-node-next-types.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Verify that built package declarations are consumable by a standard external
|
||||
* TypeScript ESM project using NodeNext resolution.
|
||||
*
|
||||
* Run after `pnpm run build` has emitted declaration files under package
|
||||
* `lib/types` directories.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
interface ExportTarget {
|
||||
types?: string
|
||||
}
|
||||
|
||||
interface PackageManifest {
|
||||
name?: string
|
||||
types?: string
|
||||
exports?: Record<string, ExportTarget | string | null>
|
||||
}
|
||||
|
||||
interface WorkspacePackage {
|
||||
dir: string
|
||||
name: string
|
||||
manifest: PackageManifest
|
||||
}
|
||||
|
||||
function readPackage(path: string): WorkspacePackage | null {
|
||||
const manifest = JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
|
||||
if (!manifest.name) return null
|
||||
return { dir: dirname(path), name: manifest.name, manifest }
|
||||
}
|
||||
|
||||
function workspacePackages(): WorkspacePackage[] {
|
||||
return [
|
||||
...globSync('vendor/*/package.json', { cwd: root }),
|
||||
...globSync('packages/*/*/package.json', { cwd: root }),
|
||||
]
|
||||
.map(path => readPackage(resolve(root, path)))
|
||||
.filter(pkg => pkg !== null)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
const declarationSpecifierPattern = /(?:from\s*|import\s*\(\s*|import\s+|declare\s+module\s*)["'](\.{0,2}(?:\/[^"']*)?)["']/g
|
||||
const hasExtension = /\.[^/.]+$/
|
||||
|
||||
function relativeSpecifiersMissingExtensions(): string[] {
|
||||
const errors: string[] = []
|
||||
const files = [
|
||||
...globSync('vendor/*/lib/types/**/*.d.ts', { cwd: root }),
|
||||
...globSync('packages/*/*/lib/types/**/*.d.ts', { cwd: root }),
|
||||
].sort()
|
||||
|
||||
for (const file of files) {
|
||||
const text = readFileSync(resolve(root, file), 'utf8')
|
||||
for (const match of text.matchAll(declarationSpecifierPattern)) {
|
||||
const specifier = match[1]
|
||||
if (!specifier) continue
|
||||
const isRelative = specifier === '.' || specifier.startsWith('./') || specifier.startsWith('../')
|
||||
if (isRelative && !hasExtension.test(specifier)) errors.push(`${file}: ${specifier}`)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
function publicSpecifiers(pkg: WorkspacePackage): string[] {
|
||||
const specifiers = new Set<string>()
|
||||
if (pkg.manifest.types) specifiers.add(pkg.name)
|
||||
|
||||
for (const [key, target] of Object.entries(pkg.manifest.exports ?? {})) {
|
||||
if (key.includes('*') || key === './package.json') continue
|
||||
if (typeof target !== 'object' || target === null || !target.types) continue
|
||||
specifiers.add(key === '.' ? pkg.name : `${pkg.name}/${key.slice(2)}`)
|
||||
}
|
||||
|
||||
return [...specifiers].sort()
|
||||
}
|
||||
|
||||
function linkPackage(pkg: WorkspacePackage, nodeModules: string): void {
|
||||
const parts = pkg.name.split('/')
|
||||
const link = resolve(nodeModules, ...parts)
|
||||
mkdirSync(dirname(link), { recursive: true })
|
||||
symlinkSync(pkg.dir, link, 'dir')
|
||||
}
|
||||
|
||||
const packages = workspacePackages()
|
||||
const badSpecifiers = relativeSpecifiersMissingExtensions()
|
||||
if (badSpecifiers.length > 0) {
|
||||
console.error('verify-node-next-types: declaration files still contain relative specifiers without file extensions.')
|
||||
console.error(badSpecifiers.join('\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const missingOutputs = packages
|
||||
.filter(pkg => pkg.manifest.types && !existsSync(resolve(pkg.dir, pkg.manifest.types)))
|
||||
.map(pkg => `${pkg.name}: missing ${pkg.manifest.types}`)
|
||||
|
||||
if (missingOutputs.length > 0) {
|
||||
console.error('verify-node-next-types: build outputs are missing; run `pnpm run build` first.')
|
||||
console.error(missingOutputs.join('\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const tmp = mkdtempSync(resolve(root, '.node-next-types-'))
|
||||
let failed = false
|
||||
|
||||
try {
|
||||
const nodeModules = resolve(tmp, 'node_modules')
|
||||
mkdirSync(nodeModules, { recursive: true })
|
||||
for (const pkg of packages) linkPackage(pkg, nodeModules)
|
||||
|
||||
const rootTypes = resolve(root, 'node_modules/@types/node')
|
||||
if (existsSync(rootTypes)) {
|
||||
const typesDir = resolve(nodeModules, '@types')
|
||||
mkdirSync(typesDir, { recursive: true })
|
||||
symlinkSync(rootTypes, resolve(typesDir, 'node'), 'dir')
|
||||
}
|
||||
|
||||
writeFileSync(resolve(tmp, 'package.json'), `${JSON.stringify({ type: 'module', private: true }, null, 2)}\n`)
|
||||
writeFileSync(resolve(tmp, 'tsconfig.json'), `${JSON.stringify({
|
||||
compilerOptions: {
|
||||
target: 'es2024',
|
||||
module: 'NodeNext',
|
||||
moduleResolution: 'NodeNext',
|
||||
strict: true,
|
||||
// Third-party SDK declarations can have their own lib-check noise under a
|
||||
// symlinked temp install. The explicit scan above owns our regression:
|
||||
// relative specifiers without file extensions in built declarations.
|
||||
skipLibCheck: true,
|
||||
preserveSymlinks: true,
|
||||
noEmit: true,
|
||||
types: ['node'],
|
||||
},
|
||||
include: ['index.ts'],
|
||||
}, null, 2)}\n`)
|
||||
|
||||
const imports = packages.flatMap(publicSpecifiers)
|
||||
.map((specifier, index) => `import * as mod${index} from ${JSON.stringify(specifier)};\nvoid mod${index};`)
|
||||
.join('\n')
|
||||
writeFileSync(resolve(tmp, 'index.ts'), `${imports}\n`)
|
||||
|
||||
execFileSync(resolve(root, 'node_modules/.bin/tsc'), ['-p', resolve(tmp, 'tsconfig.json'), '--pretty', 'false'], {
|
||||
cwd: root,
|
||||
stdio: 'pipe',
|
||||
})
|
||||
console.log(`verify-node-next-types: ${packages.length} workspace package declaration surface(s) compile under NodeNext.`)
|
||||
} catch (error: unknown) {
|
||||
failed = true
|
||||
const output = error as { stdout?: Buffer; stderr?: Buffer }
|
||||
console.error('verify-node-next-types: NodeNext consumer typecheck failed.\n')
|
||||
console.error(`${output.stdout?.toString() ?? ''}${output.stderr?.toString() ?? ''}`)
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
if (failed) process.exit(1)
|
||||
Reference in New Issue
Block a user