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:
@@ -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')) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ const PATTERNS = [
|
||||
'README.md',
|
||||
'docs/**/*.md',
|
||||
'packages/*/*.md',
|
||||
'packages/*/*/*.md',
|
||||
'AGENTS.md',
|
||||
'packages/AGENTS.md',
|
||||
'.agents/skills/**/*.md',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user