implemented(除 4 篇超长文档随后补)、proposed、rejected 全树配对; 同一流水线 + 二遍校验(paraphrase-back + 仓库上下文一致性)产出。 docs/rfc/implemented/AGENTS.md 与其 CLAUDE.md 符号链接列入排除 (agent 指令文件,与根 AGENTS.md 同策略)。
6.3 KiB
RFC: Reorganize packages into a modular hierarchy
English | 中文
Status: implemented
Problem
packages/ was flat: 18 packages all sat at packages/<name>/, so a package's location said nothing about whether it was core product API, a swappable capability seam, a provider adapter, a product integration, or example/test support. The package README carried a FIXME(package-hierarchy) and scripts/publint-all.ts a TODO(package-inventory) flagging exactly this. Core packages, provider integrations, capability seams, example UI support, and snapshot-only replay support all looked equally foundational.
This was not just cosmetic. Because every top-level package looked like part of the same public surface, future removal was harder, and publish/lint/doc scripts had to encode intent through comments or hand-maintained static lists rather than reading it off the layout.
Decision
Packages are grouped by modular role at a uniform packages/<group>/<pkg>/ depth. Group directories are pure containers (no package.json); every package keeps its @deepseek-ai/dsh-<pkg> name — this is repo structure and maintenance policy, not package renaming.
packages/
core/ (product API spine)
session/
system-prompt/
tools/
agent/
agent-loop/
llm/ (product — capability family)
llm/
llm-deepseek/
llm-pi-ai/
bash/ (product — capability family)
bash/
bash-local/
tool-bash/
session-persistence/ (product — capability family)
session-persistence/
session-persistence-jsonl/
session-persistence-sqlite/
ui/ (product integration)
acp/
support/ (dev/test/example infrastructure)
invariants/
ui-stdio/
llm-replay/
Placement decisions
- Same-name nesting for capability families. A family's interface package sits at
packages/<group>/<group>/(llm/llm,bash/bash,session-persistence/session-persistence), with implementations and consumers as flat siblings. There is no extraadapters//impls/sub-tier — every package is exactly depth 2, which keeps the workspace glob a cleanpackages/*/*and lets one@deepseek-ai/dsh-*tsconfig wildcard resolve every package (unique dir names make first-on-disk-wins unambiguous). sessionstays incore/; persistence is its own family. The session log is core product API. Its storage backends form a parallel capability family (session-persistence/) mirroringllm/andbash/, rather than nesting undercore/session/.agent-loopis incore/. It is the one concrete implementation of theagentseam, but it ships as the harness's default product loop, so it lives with the core spine. Plugins still depend on theagentvocabulary, never onagent-loop, so the loop stays swappable.invariantsandui-stdioaresupport/, not product.invariantsis dev-mode contract checking.ui-stdiowas extracted from the examples for reuse and the coverage gate — it is example-coupled, so it sits insupport/alongsidellm-replay(the snapshot-test replay adapter).acpis the onlyui/member because it is a real product surface (the ACP bridge an editor drives), structurally distinct from the readline demo helper.
Deduplicating the package lists
The package list had been enumerated in five places. The uniform depth-2 layout lets most of them be derived instead:
tsconfig.base.jsonmaps every package through a single@deepseek-ai/dsh-*pathswildcard listing one candidate per group, in place of per-package entries. Roottsconfig.jsonreuses 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.tsreads the JSONC config through TypeScript's parser rather than stripping comments by hand for exactly this reason.)scripts/publint-all.tsderives its list by reading the hierarchy (packages/<group>/<pkg>), resolving theTODO(package-inventory).tsconfig.build.json's projectreferencesstay 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).
Guardrails added
Two doc-sync/hygiene gates keep the structure and its references honest, so the manual checks this restructure required do not have to be repeated by hand:
scripts/verify-package-paths.tsflags apackages/<path>reference (in Markdown or a.tscomment/string) that does not resolve and names a real package in a segment — i.e. a stale path to a moved package. A path naming a package that exists nowhere (a forward-looking proposal) is left alone, so the gate applies uniformly across proposed/implemented/rejected.scripts/check-workspace-constraints.tsasserts thepackages/<group>/<pkg>shape: group dirs carry nopackage.json, and no package sits flat at the root or nests deeper. Group names stay open — a new group may be added without editing the gate; only the depth-2 shape is fixed.
Alternatives considered
- A third tier (
adapters//impls/under each family) — rejected: uniform depth 2 keeps the workspace glob a cleanpackages/*/*and lets one@deepseek-ai/dsh-*tsconfig wildcard resolve every package. - Nesting persistence under
core/session/— rejected: the storage backends form a parallel capability family mirroringllm/andbash/, while the session log itself stays core product API. ui-stdiounderui/— rejected: it is example-coupled dev support, not a product surface;acpis the onlyui/member because an editor actually drives it.
Consequences
The restructure churned imports, workspace globs, doc links, build references, and package paths in one coordinated move. That churn is acceptable pre-release (per the AGENTS.md foundation-over-blast-radius stance) because it stops the flat layout from fossilizing support packages as product contracts, and it is a one-time cost: the wildcard paths, the glob-derived publint list, and the shape gate mean a new package needs no further structural edits.