Skip to content

05 · 类型与代码

严格类型、棘轮基线、显式默认、删除优先 ​

关键文件tsconfig.base.json.oxlintrc.json.jscpd.jsonscripts/verify-no-unknown-casts.ts.agents/skills/dsh-find-simplifications/vitest.config.ts

类型:不允许悄悄绕过 ​

  • 全仓 strict: true 加 noImplicitAny。每个保留的 any 都要解释为什么无法收窄。
  • oxlint 把 typescript/no-explicit-any 设为 error,配置注释是"每个有意的 any 都需要带理由的局部抑制"。ban-ts-comment 要求 @ts- 注释的说明至少 10 个字符。
  • 禁止新增 as unknown 或 <unknown>。断言到 unknown 会去掉原值与目标类型之间的兼容检查,生产代码可能因此藏住错误接口,测试可能让残缺的 fixture 冒充服务。const value: unknown = JSON.parse(text) 这种声明仍然允许。
  • 跨边界的不透明 id 用 Branded<B>,不用裸 string。
  • 封闭联合的 switch 以 assertNever 结尾;可扩展联合落到有文档说明的 default 分支。
改动后的源码含 tests、scripts、benchmarks扫描verify-no-unknown-casts逐处比对已记录的存量存量断言清单读取多出一处 → 拒绝与清单一致 → 通过少了一处 → 清单缩减写回清单
棘轮基线:存量只减不增。仓库没有为了引入规则一次性改完所有旧代码,那样会把强制决定和许多无关的类型重设计混在一个 PR 里。门禁把现有位置记在 scripts/no-unknown-casts.baseline.json:新增一处就失败,删掉一处就从清单移除,清单只会变短。这个文件 2026-09-19 引入时有 2,779 行,现在是 2,127 行。这个办法适用于任何"想禁止但存量很多"的模式。

在哪里校验:信任静态类型 ​

AI 常见的另一种 slop 是到处加防御性检查。仓库规则是:在同进程、有类型的接口之间信任 TypeScript,不为静态接口已经保证的值添加运行时校验、回退逻辑或恶意输入测试。校验只放在这些位置:解析器与配置、队列、模型或工具的 JSON、持久化文件、worker、子进程、网络协议。

默认值写在明处 ​

  • 显式优于隐式:默认值由所属实现的 resolve(request): Spec 步骤给出,不藏在 run() 里的 ?? default。dsh-shell 的 request/spec 拆分是范本。
  • 插件里不硬编码可调参数:随部署变化的选项做成可从 cordis.yml 修改、经过校验的 Config 字段。一个 DEFAULT_* 常量或测试钩子不算可配置。协议常量、外部规范和安全不变量保持固定。
  • 配置错误尽早报错:能在加载时发现就在加载时失败,永远不静默跳过缺失的引用。

docs/defensive-patterns/general.md 里有一条直接针对模型爱写的 ?. 链:

正确:两种行为都写在契约里
interface ResolveInput { options?: readonly string[] }
type Resolution =
| { kind: 'unknown' }
| { kind: 'value'; value: string }
function resolve(input: ResolveInput): Resolution {
if (input.options === undefined) return { kind: 'unknown' }
return { kind: 'value', value: input.options.join(',') }
}
错误:四层回退藏住了 d 该不该存在
const value = a?.b?.c?.d ?? fallback

契约保持模糊,错误被吞掉,下一个调用方会在上面再加一层回退。

同页的"一个值只表达一个事实"要求区分"未知"、"空"和"不存在",用判别联合 { kind: 'unknown' } | { kind: 'empty' } | { kind: 'value'; value: T },不用一个 null 同时表示三种情况。

重复与死代码 ​

  • oxlint 加载 eslint-plugin-sonarjs,对源码启用 no-identical-expressions、no-identical-functions、no-duplicated-branches。
  • .jscpd.json 做跨文件克隆检测:6 行、60 token 以上即报错,测试目录除外,可用 jscpd:ignore-start/end 局部豁免。命令是 pnpm run duplication。
  • 覆盖率门禁是 packages/*/*/src 下每个文件 100%。docs/testing.md 的说法是:未覆盖的行往往是该删的死代码,不是该补的测试。
  • verify-product-use 要求每个维护中的包都有实际的产品用途,或者有明确登记的非默认角色。

删除优先:dsh-find-simplifications ​

这个 skill 寻找"能移除维护义务"的改动:API、表示形式、生命周期状态、配置路径、依赖、测试或文档。它用几个问题引导搜索:

  • 声明的功能有完整的生效路径吗?一个字段可能被到处拷贝,却没有任何地方设置或读取它。
  • 哪些区分会改变消费者的行为?只诱导出无用选择的公共区分可以合并。
  • 更小的显式行为能否替代整个子系统?要写出放弃的能力和删除的机制。
  • 消费者需要时能直接读权威值吗?派生可能同时删掉订阅、缓存和过期处理。

每个候选都要记录当前 owner、生产者到消费者的路径、会消失什么、保留什么,以及保留它的最强理由。只是把复杂度挪了个地方,或者删减量不明显,都要拒绝。配套规则是"优先使用维护中的依赖,前提是它确实删掉了自有代码和测试"。

其他小规则 ​

  • 空 catch 要写明吞掉的错误和原因,try 只包一条语句。
  • TODO 标记按紧急程度用 FIXME / TODO / XXX。
  • 文件以恰好一个换行结尾;.editorconfig 和 .gitattributes 统一 LF,使字节级比较在所有平台一致。
  • 行宽上限 140 字符,由 @stylistic/max-len 检查。

依据 deepseek-harness 仓库 master 分支及其 git 历史整理