Files
deepseek-harness/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.md
T
Ziya 2565133af3 docs(i18n): RFC tree batch — 146 bilingual pairs via the committed pipeline
implemented(除 4 篇超长文档随后补)、proposed、rejected 全树配对;
同一流水线 + 二遍校验(paraphrase-back + 仓库上下文一致性)产出。
docs/rfc/implemented/AGENTS.md 与其 CLAUDE.md 符号链接列入排除
(agent 指令文件,与根 AGENTS.md 同策略)。
2026-07-15 23:25:06 -07:00

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 extra adapters//impls/ sub-tier — every package is exactly depth 2, which keeps the workspace glob a clean packages/*/* and lets one @deepseek-ai/dsh-* tsconfig wildcard resolve every package (unique dir names make first-on-disk-wins unambiguous).
  • session stays in core/; persistence is its own family. The session log is core product API. Its storage backends form a parallel capability family (session-persistence/) mirroring llm/ and bash/, rather than nesting under core/session/.
  • agent-loop is in core/. It is the one concrete implementation of the agent seam, but it ships as the harness's default product loop, so it lives with the core spine. Plugins still depend on the agent vocabulary, never on agent-loop, so the loop stays swappable.
  • invariants and ui-stdio are support/, not product. invariants is dev-mode contract checking. ui-stdio was extracted from the examples for reuse and the coverage gate — it is example-coupled, so it sits in support/ alongside llm-replay (the snapshot-test replay adapter). acp is the only ui/ 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.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).

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.ts flags a packages/<path> reference (in Markdown or a .ts comment/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.ts asserts the packages/<group>/<pkg> shape: group dirs carry no package.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 clean packages/*/* 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 mirroring llm/ and bash/, while the session log itself stays core product API.
  • ui-stdio under ui/ — rejected: it is example-coupled dev support, not a product surface; acp is the only ui/ 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.