mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge branch 'worktree-subagent-seam-pr2.5' into worktree-subagent-seam-pr3
# Conflicts: # docs/module-graph.md # packages/subagent/README.md
This commit is contained in:
@@ -5,29 +5,33 @@ The file-by-file checklist for a new `@deepseek-ai/dsh-<name>` package. (Verifie
|
||||
## 1. Create the package
|
||||
|
||||
```
|
||||
packages/<name>/
|
||||
packages/<group>/<pkg>/
|
||||
package.json # copy from packages/core/tools, adjust name/description/deps
|
||||
tsconfig.json # extends ../../tsconfig.base.json, rootDir src, outDir lib,
|
||||
# references: vendor/cosmokit, vendor/cordis (+ vendor/schemastery
|
||||
# if you use Config, + ../<dep> for each dsh dependency)
|
||||
tsconfig.json # extends ../../../tsconfig.base.json, rootDir src,
|
||||
# outDir lib/types, references: ../../../vendor/cosmokit,
|
||||
# ../../../vendor/cordis (+ ../../../vendor/schemastery if
|
||||
# you use Config, + ../../<group>/<dep> for each dsh dep)
|
||||
src/index.ts # service default export or plugin (name/inject/apply/Config)
|
||||
tests/<x>.spec.ts
|
||||
README.md # service API, events, extension points, design notes
|
||||
```
|
||||
|
||||
package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, `version: 0.0.1`, `type: module`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop.
|
||||
Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it.
|
||||
|
||||
package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, `version: 0.0.1`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list is precise: `lib/index.js`, `lib/types/**/*.d.ts`, `lib/types/**/*.d.ts.map`, and `src`; do not publish `lib/types` JS or JS-map intermediates or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
|
||||
|
||||
In-package relative imports use explicit `.ts` specifiers in source (for example, `export * from './types.ts'`). The compiler rewrites those to `.js` in emitted JS and leaves explicit `.ts` specifiers in declarations, which standard NodeNext/Node16 TypeScript consumers resolve to the sibling `.d.ts` files.
|
||||
|
||||
## 2. Register it in the root configs
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `tsconfig.base.json` | add `"@deepseek-ai/dsh-<name>": ["./packages/<name>/src"]` to `paths` |
|
||||
| `tsconfig.typecheck.json` | same entry (this file overrides the map wholesale) |
|
||||
| `tsconfig.build.json` | add `{ "path": "./packages/<name>" }` to `references` |
|
||||
| `scripts/publint-all.ts` | add `'packages/<name>'` to the array |
|
||||
| `tsconfig.base.json` | no edit for an existing group; for a new group, add a `./packages/<group>/*/src` candidate to the `@deepseek-ai/dsh-*` wildcard |
|
||||
| `tsconfig.json` | add `{ "path": "./packages/<group>/<pkg>" }` to `references` |
|
||||
| `tsconfig.build.json` | add `{ "path": "./packages/<group>/<pkg>" }` to `references` |
|
||||
| `knip.json` | only if the package has non-`*.spec.ts` entries (e.g. `*.e2e.ts` → add a per-workspace override like `packages/llm/llm-deepseek`) |
|
||||
|
||||
Covered automatically by globs — no edits needed: root `package.json` workspaces, `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`.
|
||||
Covered automatically by globs or package-manifest discovery — no edits needed: root `package.json` workspaces, `scripts/publint-all.ts`, `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`, `scripts/check-workspace-constraints.ts`.
|
||||
|
||||
## 3. Decide the package topology
|
||||
|
||||
@@ -39,7 +43,7 @@ For a swappable capability, split interface / implementation / consumer into sep
|
||||
pnpm install # registers the workspace
|
||||
pnpm run constraints && pnpm run typecheck && pnpm run lint
|
||||
pnpm run test:coverage # 100% per-file over src (types.ts exempt)
|
||||
pnpm run build && pnpm run knip && pnpm run publint
|
||||
pnpm run build && pnpm run hygiene
|
||||
```
|
||||
|
||||
Test expectations: every registry/registration needs an HMR-safety test (register from a child fiber, dispose it, assert cleanup). Excessive tests are welcome — see AGENTS.md.
|
||||
|
||||
@@ -12,13 +12,13 @@ vendor/<dir>/
|
||||
README.md LICENSE # if upstream ships them
|
||||
```
|
||||
|
||||
`tsconfig.json` mirrors the other vendored packages — `rootDir: src`, `outDir: lib`, the strictness relaxations upstream code needs, and a `references` entry for every other vendored package it imports:
|
||||
`tsconfig.json` mirrors the other vendored packages — `rootDir: src`, `outDir: lib/types`, the strictness relaxations upstream code needs, and a `references` entry for every other vendored package it imports:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src", "outDir": "lib",
|
||||
"rootDir": "src", "outDir": "lib/types",
|
||||
"noUncheckedIndexedAccess": false, "exactOptionalPropertyTypes": false,
|
||||
"noImplicitOverride": false, "noUnusedLocals": false, "noUnusedParameters": false
|
||||
},
|
||||
@@ -27,19 +27,21 @@ vendor/<dir>/
|
||||
}
|
||||
```
|
||||
|
||||
`package.json` invariants: `"private": true` (vendored packages are never published), keep upstream's `name`/`version`/`exports`/`type`, and list its cordis deps in `peerDependencies` (matching the upstream manifest). Transitive upstream deps must themselves be vendored or already present — vendoring one package often means vendoring its dependency tree (e.g. `@cordisjs/plugin-http` pulls `@cordisjs/fetch-file`).
|
||||
`package.json` invariants: `"private": true` (vendored packages are never published), keep upstream's `name`/`version`/`exports`/`type`, point declaration metadata at `lib/types`, publish `.d.ts` and `.d.ts.map` declaration outputs, and list its cordis deps in `peerDependencies` (matching the upstream manifest). Transitive upstream deps must themselves be vendored or already present — vendoring one package often means vendoring its dependency tree (e.g. `@cordisjs/plugin-http` pulls `@cordisjs/fetch-file`).
|
||||
|
||||
Local relative imports/exports in vendored TypeScript source use explicit `.ts` specifiers after copying. This is a repo-local build-shape divergence from upstream: `rewriteRelativeImportExtensions` emits `.js` runtime imports while declarations keep explicit `.ts` specifiers that NodeNext/Node16 TypeScript consumers can resolve.
|
||||
|
||||
## 2. Register it in the root configs
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `tsconfig.base.json` | add `"<npm-name>": ["./vendor/<dir>/src"]` to `paths` |
|
||||
| `tsconfig.typecheck.json` | add `"<npm-name>": ["./vendor/<dir>/lib"]` — this file points at built declarations, not src. If the package's `types` entry isn't `lib/index.d.ts`, point at that built file instead (e.g. `logger-console` maps to `./vendor/logger-console/lib/shared`, matching its `"types": "lib/shared.d.ts"`). |
|
||||
| `tsconfig.json` | add `{ "path": "./vendor/<dir>" }` to `references` |
|
||||
| `tsconfig.build.json` | add `{ "path": "./vendor/<dir>" }` to `references` (before the `packages/*` entries) |
|
||||
| `vendor/README.md` | add a manifest table row (dir, npm name, version, upstream repo, commit SHA) and log any local modifications |
|
||||
| `scripts/publint-all.ts` | only if the vendored package is itself published from here (vendored deps normally are not — skip) |
|
||||
|
||||
Covered automatically by globs — no edits needed: root `package.json` workspaces (`vendor/*`), `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`. A per-package `vendor/<dir>/tsdown.config.ts` is needed ONLY if the build shape diverges from the root default (dual ESM/CJS or multiple entries — see `vendor/schemastery` and `vendor/logger-console`).
|
||||
Covered automatically by globs — no edits needed: root `package.json` workspaces (`vendor/*`), `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`. A per-package `vendor/<dir>/tsdown.config.ts` is needed ONLY if the build shape diverges from the root default (dual ESM/CJS or multiple entries — see `vendor/schemastery` and `vendor/logger-console`); its entry should read the JS emitted under `lib/types`.
|
||||
|
||||
## 3. Mind the manifest guard
|
||||
|
||||
@@ -49,8 +51,8 @@ Covered automatically by globs — no edits needed: root `package.json` workspac
|
||||
|
||||
```sh
|
||||
pnpm install # registers the workspace
|
||||
pnpm run typecheck # the base→lib path split means: run once after a fresh add
|
||||
pnpm run typecheck
|
||||
pnpm run build && pnpm run test && pnpm run constraints
|
||||
```
|
||||
|
||||
Note the `tsconfig` two-map split (called out in [AGENTS.md](../../AGENTS.md) § Secrets/.env): `lint`'s type-aware rules resolve vendored packages through their built `lib/` declarations, so run `pnpm run typecheck` (which builds them) once after adding the package or lint reports unresolved-type errors.
|
||||
The source `paths` map is shared by build and root typecheck configs. The important isolation boundary is the project-reference graph: vendored source must be referenced through its own `vendor/<dir>/tsconfig.json`, not pulled into a root strict program.
|
||||
|
||||
@@ -31,7 +31,7 @@ Run typecheck once after a fresh clone:
|
||||
pnpm run typecheck
|
||||
```
|
||||
|
||||
That first typecheck builds declaration output used by type-aware linting for vendored packages. Without it, `pnpm run lint` can report unresolved-type `no-unsafe-*` errors even when source code is fine.
|
||||
That first typecheck runs the package/vendor build graph and the root no-emit `tsconfig.json` graph for examples, tests, and scripts. The root graph uses the same source `paths` map but relies on project references so vendored code is checked under its own tsconfig settings.
|
||||
|
||||
If you are preparing to push from a fresh clone or worktree, also build once:
|
||||
|
||||
@@ -39,7 +39,7 @@ If you are preparing to push from a fresh clone or worktree, also build once:
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
`pnpm run hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files. A fresh worktree has no bundled JS until `pnpm run build` runs.
|
||||
`pnpm run hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files, and `verify-node-next-types`, which validates built declarations against a temporary NodeNext consumer. A fresh worktree has no bundled JS or declarations until `pnpm run build` runs.
|
||||
|
||||
## Environment variables
|
||||
|
||||
@@ -76,10 +76,10 @@ The GitHub workflow runs these gates on each pull request:
|
||||
- `pnpm run test:coverage`
|
||||
- `pnpm run test:snapshot`
|
||||
- `pnpm run build`
|
||||
- `pnpm run knip && pnpm run publint`
|
||||
- `pnpm run hygiene`
|
||||
- an echo-agent smoke test that checks the demo's tool call, tool result, and JSONL output
|
||||
|
||||
`pnpm run hygiene` is the local shorthand for `pnpm run knip && pnpm run publint && pnpm run constraints`; CI splits `pnpm run constraints` into its own earlier step, then runs `pnpm run knip && pnpm run publint` after `pnpm run build`.
|
||||
`pnpm run hygiene` is the local shorthand for `pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types`; CI also runs `pnpm run constraints` as an earlier fail-fast step, then runs the full hygiene script after `pnpm run build`.
|
||||
|
||||
## Daily commands
|
||||
|
||||
@@ -89,7 +89,7 @@ Use these from the repo root:
|
||||
pnpm run test # unit tests
|
||||
pnpm run test:coverage # unit tests with per-file coverage gates
|
||||
pnpm run test:e2e # real-API tests; self-skips without DEEPSEEK_API_KEY
|
||||
pnpm run typecheck # build declarations, then typecheck source, tests, and examples
|
||||
pnpm run typecheck # build package/vendor outputs, then typecheck examples, tests, and scripts
|
||||
pnpm run lint # eslint .
|
||||
pnpm run lint:fix # eslint . --fix
|
||||
pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs
|
||||
@@ -100,8 +100,9 @@ pnpm run verify-type-equiv # fail if a ```ts type-equiv doc block drifts from i
|
||||
pnpm run doc-sync # doc-typecheck, cordis-catalog freshness, markdown wrap/link, and type-equiv verification
|
||||
pnpm run gen-module-graph # regenerate docs/module-graph.md from package peerDeps
|
||||
pnpm run verify-module-graph # fail if docs/module-graph.md is stale
|
||||
pnpm run build # build declarations and JS bundles
|
||||
pnpm run hygiene # knip, publint, and workspace constraints
|
||||
pnpm run build # emit lib/types intermediates, then bundle lib/index.* runtime files
|
||||
pnpm run verify-node-next-types # fail if built declarations are not NodeNext-consumable
|
||||
pnpm run hygiene # knip, publint, workspace constraints, and NodeNext declaration check
|
||||
```
|
||||
|
||||
When changing package public behavior, update the relevant README or JSDoc in the same change. `pnpm run doc-sync` catches checked TypeScript snippets, cordis events/services catalog drift, and hard-wrapped markdown prose, but broader prose/API sync still needs review.
|
||||
|
||||
@@ -63,13 +63,13 @@ graph TD
|
||||
subagent-acp --> agent
|
||||
subagent-acp --> llm
|
||||
subagent-acp --> subagent
|
||||
subagent-inprocess --> agent
|
||||
subagent-inprocess --> llm
|
||||
subagent-inprocess --> session
|
||||
subagent-inprocess --> subagent
|
||||
subagent-mock --> agent
|
||||
subagent-mock --> llm
|
||||
subagent-mock --> subagent
|
||||
subagent-spawn --> agent
|
||||
subagent-spawn --> llm
|
||||
subagent-spawn --> session
|
||||
subagent-spawn --> subagent
|
||||
tool-subagent --> agent
|
||||
tool-subagent --> llm
|
||||
tool-subagent --> subagent
|
||||
@@ -85,7 +85,9 @@ graph TD
|
||||
subagent-fork --> agent
|
||||
subagent-fork --> session
|
||||
subagent-fork --> subagent
|
||||
subagent-fork --> subagent-spawn
|
||||
subagent-fork --> subagent-inprocess
|
||||
subagent-spawn --> subagent
|
||||
subagent-spawn --> subagent-inprocess
|
||||
```
|
||||
|
||||
| Package | Depends on |
|
||||
@@ -112,9 +114,10 @@ graph TD
|
||||
| `tool-bash` | `agent`, `bash`, `llm`, `tools` |
|
||||
| `agent-core` | `agent`, `agent-loop`, `invariants`, `llm`, `session`, `system-prompt`, `tool-bash`, `tools` |
|
||||
| `subagent-acp` | `agent`, `llm`, `subagent` |
|
||||
| `subagent-inprocess` | `agent`, `llm`, `session`, `subagent` |
|
||||
| `subagent-mock` | `agent`, `llm`, `subagent` |
|
||||
| `subagent-spawn` | `agent`, `llm`, `session`, `subagent` |
|
||||
| `tool-subagent` | `agent`, `llm`, `subagent`, `tools` |
|
||||
| `acp-agent` | `acp`, `agent-core`, `session-persistence-jsonl` |
|
||||
| `stdio-agent` | `agent`, `agent-core`, `session`, `session-persistence-jsonl`, `ui-stdio` |
|
||||
| `subagent-fork` | `agent`, `session`, `subagent`, `subagent-spawn` |
|
||||
| `subagent-fork` | `agent`, `session`, `subagent`, `subagent-inprocess` |
|
||||
| `subagent-spawn` | `subagent`, `subagent-inprocess` |
|
||||
|
||||
@@ -127,6 +127,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
|
||||
| [tsdown for JS bundling instead of dumble](implemented/process/2026-06-11-tsdown-over-dumble.md) | 2026-06-11 |
|
||||
| [Doc-sync enforcement](implemented/process/2026-06-11-doc-sync-enforcement.md) | 2026-06-11 |
|
||||
| [pnpm as the package manager instead of Yarn 4](implemented/process/2026-06-16-pnpm-over-yarn.md) | 2026-06-16 |
|
||||
| [TSC-first build and one tsconfig](implemented/process/2026-06-17-ts-build-config.md) | 2026-06-17 |
|
||||
| [Markdown cross-link validity linting](implemented/process/2026-06-18-markdown-cross-link-lint.md) | 2026-06-18 |
|
||||
| [Core-data-structures catalog and the `ts type-equiv` drift gate](implemented/process/2026-06-20-core-data-structures-catalog.md) | 2026-06-20 |
|
||||
| [Generated cordis events + services catalog](implemented/process/2026-06-20-generated-cordis-catalog.md) | 2026-06-20 |
|
||||
|
||||
@@ -51,7 +51,7 @@ packages/
|
||||
|
||||
The package list had been enumerated in five places. The uniform depth-2 layout lets most of them be derived instead:
|
||||
|
||||
- `tsconfig.base.json` and `tsconfig.typecheck.json` each map every package through a single `@deepseek-ai/dsh-*` `paths` wildcard listing one candidate per group, in place of 18 per-package entries. (One subtlety this introduced: a path candidate contains `/*/`, which a naive regex comment-stripper mistakes for a block comment — `scripts/doc-typecheck.ts` reads the `paths` map via the TypeScript JSONC API rather than stripping comments by hand for exactly this reason.)
|
||||
- `tsconfig.base.json` maps every package through a single `@deepseek-ai/dsh-*` `paths` wildcard listing one candidate per group, in place of per-package entries. Root `tsconfig.json` reuses that source map and carries the explicit project references that keep package/vendor typecheck boundaries intact. (One subtlety this introduced: a path candidate contains `/*/`, which a naive regex comment-stripper mistakes for a block comment — `scripts/doc-typecheck.ts` reads the JSONC config through TypeScript's parser rather than stripping comments by hand for exactly this reason.)
|
||||
- `scripts/publint-all.ts` derives its list by reading the hierarchy (`packages/<group>/<pkg>`), resolving the `TODO(package-inventory)`.
|
||||
- `tsconfig.build.json`'s project `references` stay an explicit list — TypeScript project references have no wildcard form. Generating these from a manifest is left to a follow-up (see [discover package inventories](../../proposed/process/2026-06-20-discover-package-inventory.md)).
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ AGENTS.md promises that docs and code stay strictly in sync, but the promise was
|
||||
|
||||
Two gates, mirroring the existing `scripts/` style (tsx ESM, one job each):
|
||||
|
||||
1. **`doc-typecheck`** extracts every fenced ` ```ts ` block from `README.md`, `docs/**`, and `packages/*/README.md`, writes them to a temp project, and compiles with `tsc --noEmit`. The temp tsconfig copies only resolution-relevant options and the workspace `paths` map from `tsconfig.typecheck.json` (vendor → built `lib`, harness → `src`) — resolving vendor to `lib` is essential, or tsc type-checks raw vendor source and floods the run. A block that is a deliberate sketch opts out with an explicit ` ```ts ignore-check ` info string; the script reports the opt-out ratio and fails if it exceeds half, so the escape hatch can't quietly become the norm.
|
||||
1. **`doc-typecheck`** extracts every fenced ` ```ts ` block from `README.md`, `docs/**`, and `packages/*/README.md`, writes them to a temp project extending the root `tsconfig.json`, and compiles it with `tsc -b`. The temp project reuses the source `paths` map and the root project references, so documentation examples see source while vendored code remains checked under its own tsconfig settings. A block that is a deliberate sketch opts out with an explicit ` ```ts ignore-check ` info string; the script reports the opt-out ratio and fails if it exceeds half, so the escape hatch can't quietly become the norm.
|
||||
2. **`verify-event-taxonomy`** extracts the event names from the `interface Events` blocks across `packages/*/src` and from the taxonomy table in `docs/architecture.md`, and asserts the two sets match exactly. Verify, don't generate: the table keeps its hand-written Mode/Purpose columns; only the set of names is checked. (Landing this surfaced three events the table had been missing — `tools/change`, `llm/adapter-change`, `system-prompt/change`.) **Superseded** by [the generated cordis catalog](2026-06-20-generated-cordis-catalog.md): this gate and its `architecture.md` table are retired in favor of a fully-generated `docs/cordis-catalog/events-and-services.md` and its `verify-cordis-catalog` freshness gate. The other gates here (`doc-typecheck`, and the `verify-md-wrap` amendment below) are unaffected.
|
||||
|
||||
Both run via a shared `doc-sync` package.json script that the lefthook pre-push hook and CI both invoke ([mechanical quality gates](2026-06-11-quality-gates.md): hooks and CI call the same scripts, so the gate fires locally before a push — not only after it). They run after `pnpm run typecheck` (which emits the vendor `lib/` that doc-typecheck resolves against). API-extractor golden reports ([the deferred API-extractor-reports proposal](../../proposed/process/2026-06-11-api-extractor-reports.md)) were deliberately **deferred** — low value for an internal monorepo where reviewers already see the source diff, and a heavy, finicky dependency.
|
||||
Both run via a shared `doc-sync` package.json script that the lefthook pre-push hook and CI both invoke ([mechanical quality gates](2026-06-11-quality-gates.md): hooks and CI call the same scripts, so the gate fires locally before a push — not only after it). They run after `pnpm run typecheck`, which validates the package/vendor build graph that doc-typecheck references. API-extractor golden reports ([the deferred API-extractor-reports proposal](../../proposed/process/2026-06-11-api-extractor-reports.md)) were deliberately **deferred** — low value for an internal monorepo where reviewers already see the source diff, and a heavy, finicky dependency.
|
||||
|
||||
**Amendment (2026-06-17):** a third gate, **`verify-md-wrap`**, was later folded into `doc-sync`. It parses each in-scope Markdown file (`README.md`, `docs/**`, `packages/*/README.md`, plus `AGENTS.md` / `packages/AGENTS.md`) with `mdast-util-from-markdown` + GFM and fails on any `paragraph` node spanning more than one source line, enforcing the AGENTS.md "Markdown is not hard-wrapped" convention. Same verify-don't-generate principle: it reports hard-wraps and never rewrites, so it adds no formatting churn. `doc-sync` is now three gates.
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ This codebase is developed primarily by coding agents. Agents follow enforced ga
|
||||
|
||||
Every AGENTS.md promise gets a command that exits non-zero, wired into git hooks and CI both calling the same package.json scripts:
|
||||
|
||||
- Max-strict TypeScript (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, …); tests and examples typecheck in CI via `tsconfig.typecheck.json` (vendored packages resolve as built declarations).
|
||||
- Max-strict TypeScript (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, …); examples, tests, and scripts typecheck in CI via the root no-emit `tsconfig.json` while package/vendor code stays behind its own project-reference boundary.
|
||||
- ESLint strict-type-checked + @stylistic (the house style, enforced); vendored code excluded.
|
||||
- Per-file 100% coverage on `packages/*/src` (v8); unreachable defensive guards carry `/* v8 ignore */ ` with stated reasons instead of deletion.
|
||||
- knip (dead code/deps), publint (package correctness), workspace constraints (workspace rules: private, cordis peer+dev, uniform version, ESM).
|
||||
- knip (dead code/deps), publint (package correctness), workspace constraints (workspace rules: private, cordis peer+dev, uniform version, ESM), and a NodeNext consumer typecheck for built package declarations.
|
||||
- lefthook pre-commit (lint staged, typecheck, vendor-manifest guard) and pre-push (tests, hygiene); CI runs the full matrix on node 24/26 plus a demo smoke test driving the echo-agent end to end.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -15,12 +15,12 @@ Build output currently matters only for `pnpm run build` + publint (nothing publ
|
||||
Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week, VoidZero-backed, actively released):
|
||||
|
||||
- Root `tsdown.config.ts` with `workspace: ['vendor/*', 'packages/*']` (explicit globs, not `workspace: true`, which would also pick up `examples/*` — they have package.json files but are not pnpm workspaces).
|
||||
- Shared shape: entry `src/index.ts`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ holds tsc's .d.ts output).
|
||||
- Shared shape: entry `lib/types/index.js`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ also holds TSC's `lib/types` intermediate tree). The entry was originally `src/index.ts`; the [TSC-first build RFC](2026-06-17-ts-build-config.md) later moved tsdown to bundling TSC-emitted JS so TypeScript transform behavior comes from one compiler.
|
||||
- Two per-package overrides in vendor/ (ours, like the regenerated tsconfigs; logged in vendor/README.md): schemastery (dual `.mjs`/`.cjs` via `outExtensions`), logger-console (two single-entry passes so the shared base class is inlined into each entry instead of a hash-named chunk, matching upstream's published shape).
|
||||
- `scripts/build.ts` deleted; `pnpm run build` = `tsc -b && tsdown`.
|
||||
- `scripts/build.ts` deleted; `pnpm run build` = `tsc -b tsconfig.build.json && tsdown`.
|
||||
|
||||
Alternatives considered: **direct esbuild script** (most established engine, zero wrapper risk, but hand-maintains the per-package spec table tsdown's workspace mode gives us); **pkgroll** (closest drop-in philosophically, but 78k dl/wk and Rollup-based — strictly weaker maintenance story than tsdown); **keep dumble** (perfect upstream alignment, unacceptable bus factor).
|
||||
|
||||
## Consequences
|
||||
|
||||
Output file lists are byte-for-byte-list identical to dumble's (verified by snapshot diff at migration time); externals still come from each package's dependencies/peerDependencies. We give up dumble's exports-field inference — new packages with non-default shapes need a per-package `tsdown.config.ts` instead of just package.json fields. Future option: tsdown could also absorb declaration bundling (isolatedDeclarations) if `tsc -b` ever becomes the bottleneck; that would be a new RFC.
|
||||
Runtime bundle outputs still follow the dumble-era public entry shape (`lib/index.js`, plus package-specific variants such as `schemastery`'s `lib/index.mjs`/`lib/index.cjs` and `logger-console`'s `lib/browser.js`); declarations now live under `lib/types` per the [TSC-first build RFC](2026-06-17-ts-build-config.md). Externals still come from each package's dependencies/peerDependencies. We give up dumble's exports-field inference — new packages with non-default shapes need a per-package `tsdown.config.ts` instead of just package.json fields. Future option: tsdown could also absorb declaration bundling (isolatedDeclarations) if `tsc -b` ever becomes the bottleneck; that would be a new RFC.
|
||||
|
||||
73
docs/rfc/implemented/process/2026-06-17-ts-build-config.md
Normal file
73
docs/rfc/implemented/process/2026-06-17-ts-build-config.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# RFC: TSC-first build and one tsconfig
|
||||
|
||||
Status: implemented (accepted 2026-06-20)
|
||||
|
||||
<!-- XXX: legacy ADR/RFC body format, not yet normalized to a unified RFC template. -->
|
||||
|
||||
## Context
|
||||
|
||||
The current TypeScript build and typecheck setup had these issues:
|
||||
|
||||
- `build` used `tsc` to transform `.ts` to `.d.ts` files for packages under `packages/<group>/<pkg>` and `vendor/*`, and then used `tsdown` to transform `.ts` to bundled `.js` files. This made two tools do TypeScript transform.
|
||||
- `typecheck` tended to validate packages, vendor source, examples, tests, and scripts through one root typecheck config.
|
||||
|
||||
The goal is to make build and typecheck use matching tsconfig boundaries and TypeScript resolution/transform behavior. Build should generate `.js`, `.d.ts`, `.js.map`, and `.d.ts.map` through one compiler and config, so publish output and type validation stay consistent.
|
||||
|
||||
Validation found several concrete technical issues and possible routes:
|
||||
|
||||
- `tsdown` uses `oxc` to transform TypeScript, which is not the same behavior as `tsc`.
|
||||
- Bundled `.d.ts` emitted by `tsdown` conflicts with Cordis' internal relative module augmentation shape.
|
||||
- The tsc output is affected by `allowImportingTsExtensions`, so we need to ensure that generated `.js` files do not import `.ts` files and generated `.d.ts` files keep explicit relative specifiers that NodeNext/Node16 accepts. Therefore, in-package relative imports use explicit `.ts` specifiers in TypeScript source and `rewriteRelativeImportExtensions` rewrites those specifiers to `.js` in emitted JS.
|
||||
- Bundled `.js` emitted by `tsdown` is not the same behavior as per-file `.js` emitted by `tsc -b`, such as decorator transform behavior.
|
||||
- `vendor/*/src`, examples, tests, and scripts cannot all be plain-included in one root strict program.
|
||||
- Directly typechecking `vendor/*/src` under the root strict config triggers many type errors outside this project's ownership.
|
||||
- Package dependencies under `packages/*/*` on `vendor` are resolved to the `vendor/*/lib` for different tsconfig strictness.
|
||||
|
||||
|
||||
## Decision
|
||||
|
||||
In-package relative imports use explicit `.ts` specifiers.
|
||||
|
||||
`pnpm run build` is a two-stage build:
|
||||
|
||||
- Stage 1: `tsc -b tsconfig.build.json` emits per-module `.js`, declarations `.d.ts`, JS sourcemaps `.js.map`, and declaration sourcemaps `.d.ts.map` into each package's `lib/types`. This is the authoritative TypeScript compilation result. For publish we keep `.d.ts` / `.d.ts.map` and ignore `.js` / `.js.map`.
|
||||
- The build project uses the project-reference graph that `tsc -b` compiles. For example, root `tsconfig.build.json` references package and vendor tsconfigs. It validates and emits package/vendor build results.
|
||||
- Stage 2: a bundler reads the emitted JS under `lib/types` and writes the bundled runtime entry as `lib/index.js` or `lib/index.mjs` (follow current behavior). This stage is bundling only. It must not read TypeScript source or emit declarations.
|
||||
|
||||
`tsdown` is no longer the owner of TypeScript compilation or declaration output.
|
||||
|
||||
`pnpm run typecheck` runs build mode over the root `tsconfig.json`.
|
||||
- The root `tsconfig.json` is the single development/typecheck project. It typechecks examples, tests, and scripts with `noEmit`, and validates package/vendor source through references.
|
||||
- Referenced package/vendor projects keep the same emit behavior as build, so typecheck can refresh their `lib/types` outputs instead of using a separate no-emit graph. Project-specific strictness changes live in the owning `packages/*/*/tsconfig.json` or `vendor/*/tsconfig.json`.
|
||||
- The root no-emit project disables `rewriteRelativeImportExtensions`; it emits nothing and includes tests that import helpers across project-reference boundaries. Package/vendor emit projects keep the rewrite enabled.
|
||||
|
||||
The command orchestration shape is:
|
||||
|
||||
```sh
|
||||
pnpm run build:
|
||||
tsc -b tsconfig.build.json
|
||||
tsdown
|
||||
|
||||
pnpm run verify-node-next-types:
|
||||
tsx scripts/verify-node-next-types.ts
|
||||
|
||||
pnpm run typecheck:
|
||||
tsc -b tsconfig.json
|
||||
```
|
||||
|
||||
`pnpm run demo:*` still runs `src` directly through tsx and root paths, without a compile step.
|
||||
|
||||
## Consequences
|
||||
|
||||
Build responsibilities are clearer:
|
||||
|
||||
- Each module under `packages/<group>/<pkg>` and `vendor/*` has one local tsconfig for build, typecheck, and tools that run source directly, such as `tsx` and `vitest`.
|
||||
- The `build` command uses `tsconfig.build.json`. `tsc -b` owns the publishable per-module `.js` and `.d.ts` output, and the bundler owns only `lib/index.*`.
|
||||
- `lib/types/*.d.ts` and `.d.ts.map` are the publish declaration output.
|
||||
- `lib/types/*.d.ts` uses explicit `.ts` relative specifiers, which TypeScript's NodeNext/Node16 resolver maps to sibling `.d.ts` files.
|
||||
- `lib/types/*.js` is only a bundler input and must not be used as a runtime entry or public import target.
|
||||
- `lib/index.*` is the publish runtime output and is generated by the bundler, currently `tsdown`.
|
||||
- `pnpm run verify-node-next-types` scans built declarations for relative specifiers without file extensions, then typechecks a temporary external ESM consumer with `moduleResolution: "NodeNext"` against the built `types`/`exports` surface, so declaration specifier regressions fail before publish.
|
||||
- The `typecheck` command uses `tsconfig.json`. Examples, tests, and scripts are checked by the root no-emit project, while packages and vendor modules keep the same emit behavior as `build`. Package and vendor source stays behind project-reference boundaries.
|
||||
|
||||
The Cordis vendor copy now has one more type-structure divergence from upstream. During upstream sync, that divergence must be reapplied or explicitly retired.
|
||||
Reference in New Issue
Block a user