DeepSeek Harness 的 Cordis 对外 API JSDoc 完整性门禁:把「每个导出都有文档」编译为机械化的 CI 契约
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
本篇技术指南围绕 DeepSeek Harness 仓库中一项已落地(implemented)的工程决策展开:如何为 Cordis 对外服务接口与事件契约建立不可豁免的 JSDoc 完整性门禁,让"每个导出都有解释语义的 JSDoc"这类写作规则从"靠评审人肉检查"升级为"目录生成器 + CI 自动拒绝"。读完你将掌握:门禁覆盖的精确范围(哪些事件、哪些服务方法必须写文档)、每条守卫的具体判定规则与免检例外(this接收者、waterfall 的next、void返回值)、它为什么放在生成器而非 ESLint 规则里,以及如何在本地复现verify-cordis-catalog检查。
问题背景:IDE 引导最重要的地方,文档却可以缺席
Cordis 是 DeepSeek Harness 的插件运行时,"Everything is a Plugin" 的架构使得ctx.<key>服务与Events事件成为跨插件协作的公共接口。此前生成的 Cordis 目录只强制了事件分发模式(@mode标签),却未强制要求完整的服务与事件契约:
- 方法可以完全没有描述文字;
- 参数或返回值可以在跨插件 API 接口上不写文档;
- 而这些恰恰是 IDE 悬停提示、Agent 阅读源码时引导最重要的地方。
仓库根目录 AGENTS.md 中的通用写作规则(「每个导出都有解释语义的 JSDoc」)只能靠评审以行文形式检查,而本仓库的既定偏好是把不变式编码为机械门禁——这正是本次决策的出发点。
核心决策:为什么门禁必须放在目录生成器里
「Cordis 服务函数与事件」这一范围有精确的机器定义,而只有目录生成器知道它:
- 事件是
declare module 'cordis'内interface Events的成员; - 服务接口是每个
interface Context键所指向的类的公开方法; - ESLint 规则看不到这层映射——它无法知道哪些
interface Events成员、哪些ctx.<key>类构成 Cordis 对外服务接口; - 生成器在每次运行时恰好计算出这层映射,因此门禁被放置在 scripts/gen-cordis-catalog.ts,它复用同一次遍历和同一套
@mode先例,对所有编目内容强制执行 JSDoc 完整性要求。
从 package.json 可以看到配套的命令接线:
# 生成(默认会先执行全部门禁检查,任何违规都会中止) pnpm run gen-cordis-catalog # 校验模式:验证已提交的生成产物是否新鲜 pnpm run verify-cordis-catalog其中verify-cordis-catalog(即tsx scripts/gen-cordis-catalog.ts --check)被挂入 scripts/run-gates.ts 的doc-sync门禁组,因此相关文档变更和 CI 会执行同一道门禁,无需另行接线。
门禁契约:四条守卫与两类免检
事件:描述文字 + 每个载荷参数的非空@param
每个事件需要:
- 描述性文字(JSDoc 正文,位于块标签之上,解释发生了什么、监听者可以做什么);
- 为每个载荷参数提供非空的
@param。
载荷参数是携带事件数据的签名参数;以下两类免检:
this接收者注解:属于签名机制而非数据载荷;- 尾部的 waterfall
next:它是分发机制,其语义已由@mode waterfall标签(及其结构交叉检查)拥有,逐事件重述只是样板代码。
值得注意的有意不对称:为免检参数写文档是允许的,只有"缺失"才被检查——门禁强制载荷契约,但拒绝要求样板代码。
在 packages/typert/generator/src/cordis-catalog.ts 的collectEvents实现中可以看到完整判定:@mode缺失、描述文字为空、@param缺失都会触发违规;checkParams会以parameter => parameter.receiver || (hasNext && parameter === last)作为免检谓词,恰好对应this与尾参next。
服务类:类级 JSDoc + 每方法的@param/@returns
服务类需要:
- 类级 JSDoc(类名
ctx.<key>指向的类或接口必须有说明文字); - 每个公开方法需要描述性文字;
- 每个参数提供非空的
@param; - 非空的
@returns——除非标注的返回类型是void/Promise<void>,此时@returns可选(resolve 时机有时值得记录),但从不强制要求。
checkReturns(见 cordis-catalog.ts)的实现与此完全一致:它通过渲染器解析返回类型,type === 'void' || type === 'Promise<void>'时直接放行,否则要求@returns存在且描述非空。
陈旧标签报错:@param必须命中真实参数
@param命名了一个不存在的参数即为违规,这与@mode与签名矛盾的检查对称(cordis-catalog.ts)。标签描述必须非空;超出此范围的语义质量(描述是否准确、是否传达意图)由评审负责。
遍历可检查的显式性:纯 AST,不用类型检查器
门禁是纯 AST 遍历(不使用类型检查器),因此带来两条硬性约束:
- 服务方法必须显式标注返回类型——推断的返回类型无法被分类,也就无法判定是否需要
@returns; - 接口参数必须是简单标识符——解构模式没有名称可供
@param匹配。
从源码结构看,这两条约束在采纳时均未构成限制(所有方法已有标注、不存在解构的 seam 参数),但它们现在是承重要求,违反时会被机械检测到(checkParams 会对 binding pattern 参数直接报错)。
聚合错误报告:一次看到完整清单
所有违规被聚合为一条错误信息,列出全部违规项——修复时一次看到完整清单,而不是被"快速失败"逐个打断。此前快速失败的@mode检查也被移入同一份聚合报告,消息文本不变(见 reportViolations,所有collectEvents/collectServices收集到的违规统一在此抛出)。
这种设计对"以绿色状态落地"至关重要:采纳本门禁时发现的约139 处缺口在同一变更中全部补齐,生成器拒绝重新生成、CI 拒绝合并,直到清单清零。
两种视图并存:可扫读的正文 + 完整的源码契约
生成器保留同一源码注释的两种视图(cordis-catalog.ts 的parseJsDoc):
- 正文摘要:
parseJsDoc在第一个块标签(@param/@returns/@mode等)处结束条目正文,因此可读索引区只含描述文字,块标签文本不会泄漏进周围正文; - 签名块:
ts cordis-catalog围栏包含原始 JSDoc,完整保留@param、@returns和@mode。
读者因此既能看到干净的概览正文,又能在签名旁看到确切契约;同时,源码编辑会同时刷新可读索引和签名旁展示的精确契约——文档与源码之间不存在第二个手写副本。
负路径测试:每条守卫都有合成 fixture 验证
门禁的每条判定都有对应的负路径测试(即"违规必须触发"的测试),运行于合成 fixture 之上,覆盖:
- 事件缺
@mode、@mode与尾参next矛盾、@mode waterfall却无next; - 缺
@param、陈旧@param、空描述@param、解构参数; - 服务方法缺
@param、缺@returns、缺类级 JSDoc、推断返回类型; - 免检规则成立:
this接收者与 waterfallnext不需要@param,void方法不需要@returns。
这些测试集中在 packages/typert/generator/tests/cordis-catalog-contract.spec.ts(collectEvents/collectServices的契约级测试),并配合 packages/typert/generator/tests/cordis-catalog.spec.ts 验证真实工作区投影与页面渲染。此外,scripts/gen-cordis-catalog-partition.spec.ts 与 scripts/gen-cordis-catalog-record.spec.ts 分别守护分区映射与记录级产物。
曾考虑的替代方案:为什么是生成器而不是 ESLint
| 方案 | 结论与理由 |
|---|---|
| ESLint 规则 | 否决。ESLint 无法看到该范围的机器定义(哪些interface Events成员、哪些ctx.<key>类构成 Cordis 对外服务接口);目录生成器在每次运行时恰好计算这层映射,因此门禁放在生成器里。 |
| 将每个方法展开为单独的正文小节 | 否决。目录保留一个服务章节和一个签名块以维持可扫读性;附着于每个声明的 JSDoc 则在原处保留完整的方法契约。 |
| 逃逸标签(豁免开关) | 不设。该接口面小且经过策展——采纳时约12 个服务、57 个方法、27 个事件——要点在于检查不可豁免。 |
配套的失败封闭分区:服务与事件不可能静默消失
门禁之外,生成器还维护一套双向失败的封闭分区(walkPartitionProblems):
- 每个被渲染的
ctx.<key>服务、每个事件 scope 都必须映射到恰好一个docs/subsystems/页面(见 SERVICE_PAGE 与 EVENT_SCOPE_PAGE); - 映射表中的条目若不再被发现,报"删除陈旧条目";
- 独立的 AST 扫描读取每一个
declare module '@deepseek-ai/cordis'的 Context/Events merge(任意深度),渲染投影不可见的新键必须显式列入 SERVICE_WALK_EXEMPTIONS 或 EVENT_WALK_EXEMPTIONS 并注明文档所有者(launcher 提供的引导值、client 端浏览器面服务、客户端事件等类别); - 反向自检:被渲染的键/事件若扫描不可见,说明扫描自身退化了(glob、prefilter 或模块块遍历),必须修扫描而非改映射表。
这一层确保"JSDoc 完整性门禁"不是孤岛:一个新服务或新事件要么进目录接受文档检查,要么显式登记豁免,永远不可能静默漏检。
后果与工程权衡
- 新事件或服务方法不能带着未记录的参数或结果落地:生成器拒绝重新生成,
verify-cordis-catalog使doc-sync和 CI 失败; - 服务接口必须显式标注返回类型并使用标识符参数:两项约束在采纳时未构成限制,但现在是承重要求;
- AGENTS.md 通用 JSDoc 规则在此接口上获得更严格的特例:仅当方法无参数且返回
void时,一行摘要才足够(通用规则是"一行能说清就用一行"); - 为
next或this写@param合法但不检查:这是有意的不对称——门禁强制载荷契约,拒绝要求样板代码; - 每个生成的事件或方法片段都携带原始 JSDoc,正文摘要不含标签:源码编辑会同时刷新可读索引与签名旁的确切契约。
在本地复现与运用这套门禁
# 1. 校验已提交的生成产物(包含全部 JSDoc 完整性检查) pnpm run verify-cordis-catalog # 2. 修改了某个服务的公开方法或事件签名后,重新生成目录 pnpm run gen-cordis-catalog # 3. 作为 doc-sync 门禁组的一部分随 CI 执行 pnpm run doc-sync实践建议:当你在 Harness 的某个packages/*/*/src/**下新增ctx.<key>服务方法、事件或interface Context/interface Eventsmerge 时,遵循以下清单即可一次通过:
- 给类/接口写类级 JSDoc,给事件写描述正文;
- 每个参数写
@param name - 描述(描述非空;this与 waterfall 尾参next可免); - 非
void/Promise<void>的方法写@returns; - 使用简单标识符参数,不要用解构模式;显式标注返回类型;
- 新服务/事件若渲染投影不可见,在 gen-cordis-catalog.ts 的豁免表登记并注明文档所有者;
- 运行
pnpm run gen-cordis-catalog重新生成,确认无聚合违规后再提交。
这套门禁的完整实现集中在 scripts/gen-cordis-catalog.ts(驱动器、分区映射、策略常量)与 packages/typert/generator/src/cordis-catalog.ts(投影器、parseJsDoc、各条守卫),测试则见 packages/typert/generator/tests/cordis-catalog-contract.spec.ts。对于想要在自有插件架构中复制"写作规则机械化"模式的团队,本实现提供了一个可参考的范本:把只有特定组件才掌握的精确范围定义,与纯 AST、可测试、聚合报错的门禁逻辑放在同一处,并用双向失败封闭的分区表堵住所有静默逃逸路径。
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考