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 分支。
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检查。