Files
deepseek-harness/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.zh.md
T
ZiyaZhang 8ea5cdd894 docs(i18n): re-translate RFC batch with the prompt-v4 pipeline
146 篇 RFC 译文按 v4 基线(#348)重出:v4 模板+术语表、金标
few-shot、三段协议、切换行后处理;全量机械核对零异常(一处
task id 术语违规已修)。三篇超长 RFC(code-mode 已入,web-seam/
agent-scope/sandbox/cds-core 仍在长文档通道产出)随后补。
2026-07-22 03:07:36 -07:00

6.1 KiB
Raw Blame History

RFC:将包重组为模块化层级结构

English | 中文

Status: implemented

问题

packages/ 原先是扁平的:18 个包(package)全部位于 packages/<name>/,从路径上完全看不出一个包属于核心产品 API、可替换的能力 seam、提供方适配器、产品集成,还是示例/测试支撑。包的 README 带着 FIXME(package-hierarchy),scripts/publint-all.ts 带着 TODO(package-inventory),标记的正是这个问题。核心包、提供方集成、能力 seam、示例 UI 支撑和仅用于快照的回放支撑看起来同样基础。

这不仅仅是外观问题。由于每个顶层包看起来都属于同一个公开接口,未来移除更加困难,而 publish/lint/doc 脚本不得不通过注释或手工维护的静态列表来编码意图,而不是从布局中直接读取。

决策

按模块角色将包分组,统一放在 packages/<group>/<pkg>/ 深度。分组目录是纯容器(没有 package.json);每个包保留其 @deepseek-ai/dsh-<pkg> 名称——这是仓库结构与维护策略的调整,不是包的重命名。

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/

放置决策

  • 能力族使用同名嵌套。 一个族的接口包位于 packages/<group>/<group>/(llm/llm、bash/bash、session-persistence/session-persistence),实现和消费方作为扁平兄弟并列。不设额外的 adapters//impls/ 子层——每个包恰好在深度 2,这使 workspace glob 保持简洁的 packages/*/*,并让一条 @deepseek-ai/dsh-* tsconfig 通配符即可解析所有包(唯一的目录名使 first-on-disk-wins 无歧义)。
  • session 留在 core/;持久化独立成族。 会话日志是核心产品 API。其存储后端构成一个平行的能力族(session-persistence/),与 llm/ 和 bash/ 对称,而非嵌套在 core/session/ 下。
  • agent-loop 在 core/ 中。 它是 agent seam 唯一的具体实现,但作为 harness 的默认产品循环交付,因此与核心主干同处。插件仍然依赖 agent 的词汇,从不依赖 agent-loop,所以循环仍可替换。
  • invariants 和 ui-stdio 属于 support/,不是产品。 invariants 是开发模式的契约检查。ui-stdio 从示例中提取出来以便复用和满足覆盖率门禁——它与示例耦合,因此与 llm-replay(快照测试的回放适配器)一起放在 support/ 中。acp 是 ui/ 的唯一成员,因为它是真正的产品接口(编辑器驱动的 ACP 桥接),与 readline 演示辅助工具在结构上截然不同。

去重包列表

包列表此前在五个地方重复枚举。统一的深度 2 布局使大部分可以被推导:

  • tsconfig.base.json 通过一条 @deepseek-ai/dsh-* paths 通配符(每个分组列一个候选)映射所有包,取代了逐包条目。根 tsconfig.json 复用该源映射,并携带显式 project references 以保持 package/vendor 类型检查边界完整。(这里引入了一个细节:路径候选中包含 /*/,朴素的正则注释剥离器会将其误认为块注释——scripts/doc-typecheck.ts 正是因此通过 TypeScript 解析器读取 JSONC 配置,而非手动剥离注释。)
  • scripts/publint-all.ts 通过读取层级结构(packages/<group>/<pkg>)推导列表,解决了 TODO(package-inventory)。
  • tsconfig.build.json 的 project references 仍为显式列表——TypeScript project references 没有通配符形式。从 manifest(元数据清单)生成这些引用留作后续工作(见 discover package inventories)。

新增的护栏

两道 doc-sync/hygiene 门禁确保结构及其引用保持正确,使本次重组所需的手动检查无需日后重复:

  • scripts/verify-package-paths.ts 标记 Markdown 或 .ts 注释/字符串中的 packages/<path> 引用,如果该引用无法解析且某个路径段命名了一个真实存在的包,即指向已移动包的陈旧路径。如果路径命名的包在任何地方都不存在(前瞻性提案),则不予标记,因此该门禁在 proposed/implemented/rejected 中统一适用。
  • scripts/check-workspace-constraints.ts 断言 packages/<group>/<pkg> 形状:分组目录不带 package.json,且没有包扁平地位于根层或嵌套更深。分组名称保持开放——添加新分组无需修改门禁;只有深度 2 的形状是固定的。

曾考虑的替代方案

  • 第三层(每个族下设 adapters//impls/):否决。统一深度 2 使 workspace glob 保持简洁的 packages/*/*,并让一条 @deepseek-ai/dsh-* tsconfig 通配符即可解析所有包。
  • 将持久化嵌套在 core/session/ 下:否决。存储后端构成一个平行的能力族,与 llm/ 和 bash/ 对称,而会话日志本身属于核心产品 API。
  • ui-stdio 放在 ui/ 下:否决。它是与示例耦合的开发支撑,不是产品接口;acp 是 ui/ 的唯一成员,因为编辑器实际驱动它。

后果

本次重组在一次协调的变更中搅动了 import、workspace glob、文档链接、构建引用和包路径。这种变动在发布前是可接受的(依据 AGENTS.md 中「基础优先于爆炸半径」的立场),因为它阻止了扁平布局将支撑包固化为产品契约,且这是一次性成本:通配符 paths、glob 推导的 publint 列表和形状门禁意味着新增一个包无需额外的结构性编辑。