Reorganize packages into a modular hierarchy

Move the 18 flat packages/<name> packages into role-grouped dirs:
core/, llm/, bash/, session-persistence/, ui/, support/. Group dirs are
pure containers; each package keeps its @deepseek-ai/dsh-* name.

Collapse the per-package tsconfig paths maps (base + typecheck) into one
@deepseek-ai/dsh-* wildcard with a candidate per group, and derive the
publint list from the hierarchy. Update all depth-coupled globs/configs
(workspace, tsdown, vitest, eslint, knip, tsconfig includes/refs,
per-package tsconfigs, generators, doc-script scopes, type-equiv manifest)
and the cross-package/script relative imports in tests.

Fix doc-typecheck's workspacePaths() to parse tsconfig JSONC via the
TypeScript API instead of a regex comment-strip, which corrupted the
new wildcard `/*/` path candidates.

WIP: doc cross-links and package/RFC docs still to update.
This commit is contained in:
Tianyi Cui
2026-06-20 22:55:20 +08:00
parent 906705e353
commit d02e9f1bd6
191 changed files with 822 additions and 624 deletions

View File

@@ -9,7 +9,12 @@ import { readdirSync, readFileSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..')
const workspaceGlobs = ['vendor', 'packages'] as const
// vendor/* is single-level; packages/<group>/<pkg> nests one level deeper
// (the group dirs — core/llm/bash/… — are pure containers with no manifest).
const workspaceGlobs = [
{ dir: 'vendor', depth: 1 },
{ dir: 'packages', depth: 2 },
] as const
const vendoredPackages = new Set([
'cordis',
'cosmokit',
@@ -42,15 +47,25 @@ function readJson(path: string): PackageManifest {
return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
}
/** Repo-relative dirs holding a package.json, walked to the configured depth. */
function packageDirs(base: string, depth: number): string[] {
if (depth === 1) {
return readdirSync(join(root, base), { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => join(base, entry.name))
}
return readdirSync(join(root, base), { withFileTypes: true })
.filter(entry => entry.isDirectory())
.flatMap(group => packageDirs(join(base, group.name), depth - 1))
}
function workspaceManifests(): WorkspaceManifest[] {
const manifests: WorkspaceManifest[] = [
{ dir: '.', manifest: readJson(join(root, 'package.json')) },
]
for (const workspaceDir of workspaceGlobs) {
for (const entry of readdirSync(join(root, workspaceDir), { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const dir = join(workspaceDir, entry.name)
for (const { dir: base, depth } of workspaceGlobs) {
for (const dir of packageDirs(base, depth)) {
manifests.push({ dir, manifest: readJson(join(root, dir, 'package.json')) })
}
}

View File

@@ -24,6 +24,7 @@ import { execFileSync } from 'node:child_process'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
import { glob } from 'node:fs/promises'
import ts from 'typescript'
const root = resolve(import.meta.dirname, '..')
@@ -96,13 +97,17 @@ function extractBlocks(absPath: string): Block[] {
* vendor `lib/` to exist (a fresh clone runs `pnpm run build` first; CI does too).
*/
function workspacePaths(): Record<string, string[]> {
const raw = readFileSync(join(root, 'tsconfig.typecheck.json'), 'utf8')
// Strip // line comments and /* */ block comments so JSON.parse accepts it.
const stripped = raw
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/.*$/gm, '$1')
return (JSON.parse(stripped) as { compilerOptions: { paths: Record<string, string[]> } })
.compilerOptions.paths
const file = join(root, 'tsconfig.typecheck.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.
const result = ts.readConfigFile(file, p => readFileSync(p, 'utf8'))
if (result.error) {
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
}
/** The standalone tsconfig for the temp project (copies base resolution, no
@@ -125,7 +130,7 @@ function tempTsconfig(): string {
})
}
const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md']
const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
const files: string[] = []
for (const pattern of markdownGlobs) {

View File

@@ -208,7 +208,7 @@ function memberSignature(member: ts.TypeElement | ts.ClassElement, sf: ts.Source
* `scanRoot` defaults to the repo root; tests pass a fixture dir. */
export function collectEvents(scanRoot: string = root): EventEntry[] {
const entries: EventEntry[] = []
for (const rel of globSync('packages/*/src/*.ts', { cwd: scanRoot }).sort()) {
for (const rel of globSync('packages/*/*/src/*.ts', { cwd: scanRoot }).sort()) {
const abs = resolve(scanRoot, rel)
const text = readFileSync(abs, 'utf8')
if (!text.includes('interface Events')) continue
@@ -248,7 +248,7 @@ export function collectEvents(scanRoot: string = root): EventEntry[] {
* `scanRoot` defaults to the repo root; tests pass a fixture dir. */
export function collectServices(scanRoot: string = root): ServiceEntry[] {
const entries: ServiceEntry[] = []
for (const rel of globSync('packages/*/src/index.ts', { cwd: scanRoot }).sort()) {
for (const rel of globSync('packages/*/*/src/index.ts', { cwd: scanRoot }).sort()) {
const abs = resolve(scanRoot, rel)
const text = readFileSync(abs, 'utf8')
if (!text.includes('interface Context')) continue

View File

@@ -4,7 +4,7 @@
* The architectural shape of the harness lives implicitly in each package's
* `peerDependencies` — the canonical runtime-dependency signal (devDeps mirror
* these as `workspace:^` plus test-only extras, which would add noise). This
* script reads every `packages/* /package.json`, keeps only the
* script reads every `packages/* /* /package.json`, keeps only the
* `@deepseek-ai/dsh-*` peer edges (dropping the `cordis` peer), and renders a
* GitHub-viewable Mermaid graph plus a dependency table.
*
@@ -34,7 +34,7 @@ interface Pkg {
/** Read every workspace package and its `@deepseek-ai/dsh-*` peer edges. */
function collect(): Pkg[] {
const pkgs: Pkg[] = []
for (const rel of globSync('packages/*/package.json', { cwd: root })) {
for (const rel of globSync('packages/*/*/package.json', { cwd: root })) {
const json = JSON.parse(readFileSync(resolve(root, rel), 'utf8')) as {
name: string
peerDependencies?: Record<string, string>

View File

@@ -1,31 +1,22 @@
import { execFileSync } from 'node:child_process'
import { readdirSync } from 'node:fs'
import { resolve } from 'node:path'
// publint every publishable package (vendor/ is private upstream code and
// examples/ are not packages; both are out of scope).
// TODO(package-inventory): derive this from the deliberate package hierarchy.
const packages = [
'packages/llm',
'packages/session',
'packages/session-persistence',
'packages/session-persistence-jsonl',
'packages/session-persistence-sqlite',
'packages/system-prompt',
'packages/tools',
'packages/agent',
'packages/agent-loop',
'packages/bash',
'packages/llm-deepseek',
'packages/llm-pi-ai',
'packages/bash-local',
'packages/tool-bash',
'packages/invariants',
'packages/acp',
'packages/ui-stdio',
'packages/llm-replay',
]
// publint every harness package. Packages live at packages/<group>/<pkg>
// (the group dirs — core/llm/bash/… — are pure containers); vendor/ is private
// upstream code and examples/ are not packages, both out of scope. Derived
// from the hierarchy so a new package needs no edit here.
const root = resolve(import.meta.dirname, '..')
const packagesRoot = resolve(root, 'packages')
const packages = readdirSync(packagesRoot, { withFileTypes: true })
.filter(group => group.isDirectory())
.flatMap(group =>
readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true })
.filter(pkg => pkg.isDirectory())
.map(pkg => `packages/${group.name}/${pkg.name}`),
)
for (const path of packages) {
execFileSync('node_modules/.bin/publint', [path], { cwd: root, stdio: 'inherit' })
}

View File

@@ -1,41 +1,41 @@
{
"comment": "Maps each ` ```ts type-equiv ` block (by doc + declared symbol) to the source symbol it must match verbatim. verify-type-equiv.ts enforces a 1:1 correspondence: every type-equiv block has exactly one entry here, and every entry resolves to exactly one block. Add an entry when you add a type-equiv block; remove it when you remove the block.",
"entries": [
{ "doc": "docs/core-data-structures/core.md", "symbol": "Branded", "source": "packages/llm/src/brand.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "ContentBlockMap", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "Message", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "MessageSourceMap", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "FinishReasonMap", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "GenerateOptions", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "GenerateResult", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "ToolSchema", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "SessionEvent", "source": "packages/session/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "Agent", "source": "packages/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "Branded", "source": "packages/llm/llm/src/brand.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "ContentBlockMap", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "Message", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "MessageSourceMap", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "FinishReasonMap", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "GenerateOptions", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "GenerateResult", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "ToolSchema", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/core.md", "symbol": "Agent", "source": "packages/core/agent/src/types.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "StreamChunk", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "TokenUsage", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "ContentBlockMap", "source": "packages/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "StreamChunk", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "TokenUsage", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/llm-streaming.md", "symbol": "ContentBlockMap", "source": "packages/llm/llm/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionEventMap", "source": "packages/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionEvent", "source": "packages/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "TurnTriggerMap", "source": "packages/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "TurnEndReasonMap", "source": "packages/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionEventMap", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "TurnTriggerMap", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/session.md", "symbol": "TurnEndReasonMap", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionHeader", "source": "packages/session/src/types.ts" },
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "CreateSessionOptions", "source": "packages/session/src/types.ts" },
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "SessionHeader", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/persistence.md", "symbol": "CreateSessionOptions", "source": "packages/core/session/src/types.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolDefinition", "source": "packages/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "SchemaProp", "source": "packages/tools/src/schema.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "SchemaSpec", "source": "packages/tools/src/schema.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "InferArgs", "source": "packages/tools/src/schema.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolExecution", "source": "packages/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolExecutionResult", "source": "packages/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolDefinition", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "SchemaProp", "source": "packages/core/tools/src/schema.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "SchemaSpec", "source": "packages/core/tools/src/schema.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "InferArgs", "source": "packages/core/tools/src/schema.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolExecution", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/tools.md", "symbol": "ToolExecutionResult", "source": "packages/core/tools/src/index.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashExecRequest", "source": "packages/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashExecSpec", "source": "packages/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashRunResult", "source": "packages/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "CollectedOutput", "source": "packages/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashTask", "source": "packages/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashTaskRead", "source": "packages/bash/src/types.ts" }
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashExecRequest", "source": "packages/bash/bash/src/types.ts" },
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashExecSpec", "source": "packages/bash/bash/src/types.ts" },
{ "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" }
]
}

View File

@@ -49,6 +49,7 @@ const PATTERNS = [
'README.md',
'docs/**/*.md',
'packages/*/*.md',
'packages/*/*/*.md',
'AGENTS.md',
'packages/AGENTS.md',
'.agents/skills/**/*.md',

View File

@@ -36,7 +36,7 @@ import type { Nodes } from 'mdast'
const root = resolve(import.meta.dirname, '..')
/** Files to check: doc-typecheck's scope plus the AGENTS.md pair. */
const PATTERNS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'AGENTS.md', 'packages/AGENTS.md']
const PATTERNS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'AGENTS.md', 'packages/AGENTS.md']
/** A located hard-wrap: a prose paragraph spanning more than one source line. */
interface Violation {

View File

@@ -36,7 +36,7 @@ const root = resolve(import.meta.dirname, '..')
* added to a doc with NO manifest entry is still discovered here and reported as
* an orphan, instead of being silently skipped.
*/
const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md']
const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
/** One manifest entry: a documented type-equiv block and its source symbol. */
interface ManifestEntry {