关于 write-discoverable-code
write-discoverable-code 是一项编码 Agent 技能,提供通过搜索定位和理解代码的规范。内容涵盖命名、类型、文档、文件组织以及提交前检查。
主要功能
- 01便于搜索的符号命名
- 02品牌化 ID 与明确状态类型
- 03可搜索的导出项文档
- 04按领域组织文件
- 05测试与代码并置及弃用标记
适用场景
- 01为新导出函数命名
- 02按代码搜索需求组织模块
- 03提交前检查代码
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ modem-dev/ skills --skill write-discoverable-code安装后在 Claude Code 中输入/write-discoverable-code确认已加载。
详细介绍
编写易于检索的代码
编码 Agent 通过搜索字符串并阅读命中项附近的一小段代码来发现代码。它们没有悬停文本、无法跳转到定义,也不会记住不同会话之间的信息。这些规则让代码只需一次搜索即可定位,而不是五次。
1. 名称就是搜索查询
- 导出的符号名称应包含 2–4 个词,且至少有一个领域词。
diffUserObjects,而不是diff。queueEventForDispatch,而不是queue。 在一个约 70 万行的单体仓库中进行测量:单词名称全局唯一的概率为 61%;3 词名称为 96%;4 个以上词的名称为 98%。曲线的拐点在 3 个词。 使用能通过 grep 唯一定位的最短名称;其余信息写在文档注释中。 - 给通用动词补上其对象。 使用
sanitizeEmailHtml,而不是sanitize; 使用validateSmtpConfig,而不是validateConfig。只在唯一性需要时添加限定词,然后就此为止。 - 每个符号只保留一个定义位置。 切勿在不同文件间复制函数;应将其移动,并在同一次更改中删除原定义。共享辅助函数应放在一个名称体现其概念的位置,并从其他所有地方导入。
- 不要依赖模块路径来区分通用名称。 用于区分
users/diff.ts和orders/diff.ts的导入语句位于文件顶部,而搜索命中项却在第 300 行。应将上下文放在符号名称中(formatDurationMs),而不是文件夹名中。例外:含义由路径明确承载的严格约定(例如,每个 contract 文件都导出Input/Output)。 - 一个概念只用一种拼写。 选择
organizationId或orgId,并在所有地方保持一致;每个同义词都会让未来的搜索结果减半。编辑代码库时,沿用其中已有的词汇,而不要引入近义词。 - 行为或受众发生变化时,应在同一个提交中重命名。 过时的名称就是误导信息,打开率高达 100%——这也包括可见性标记:现在被其他模块导入的
_private辅助函数需要改成公共名称。 - 文件名也是名称——切勿只用表示角色的文件名。 在搜索结果中,
config.ts、types.ts、utils.ts、helpers.ts、handlers.ts毫无信息量,还会与仓库中其他模块的 config/types/utils 冲突。添加领域前缀: 使用billing-plan-config.ts,而不是config.ts。(index.ts仅在作为精简的重新导出入口时可以接受。)
2. 类型是 Agent 无法跳过的文档
- 为基本类型 ID 添加品牌。 使用
z.string().brand<'UserId'>()(TS)或 newtype(Rust)。transferOwnership(userId: string, orgId: string)这样的签名会让参数对调无法察觉;品牌类型则会让它成为编译错误,并指出涉及的概念。 - 对特权操作使用能力令牌参数类型(例如,要求传入
OrgScopedDb,而不是原始连接)。注释只是请求;必需的类型则是硬性约束。 - 使用判别联合建模状态,而不是使用带有隐式规则的一组可空字段。
- 类型名称应经得起被直接引用——它们会在 Agent 用来自动纠错的编译器错误中被引用。
OrgScopedDb一目了然;Ctx2则不然。避免使用any:每个any都是编译器保持沉默、Agent 只能重新猜测的地方。
3. 在搜索结果落点处说清楚
- 为每个导出项添加一行文档注释,说明代码本身无法体现的最关键约束(单位、时区、“源时间,而非插入时间”、所有权)。 名称搜索会落在定义处;那一行就是你要传达的全部信息。
- 在文档注释中写出通俗易懂的短语。 搜索通常使用自然语言(“rate limit”“retry delay”),而 camelCase 标识符无法匹配短语搜索——搜索“rate limit”时,
RateLimiter不会出现在结果中。每个导出项上方的文档注释都应包含人们会搜索的短语,并使用普通的空格分隔单词:SessionExpiryChecker应写成 /*_ Checks whether the user session has expired. _/,这样搜索“session expired”或“session has expired”时就会找到这里。 - 即使不看导入项,一个模块也应该讲得通。 每个导入的名称及其文档行都应提供足够的信息,让读者无需打开源模块。如果读者确实需要打开,问题出在导入项的名称上,而不是读者身上。
- 保持字符串完整。 绝不要用模板插值拼接事件名称、标志或错误代码(
`github.${entity}.${action}`会导致搜索github.pr.merged时找不到结果)。即使循环写法更简洁,也要写出完整的字面量。 - 错误消息以唯一的字面量前缀开头,这样日志中看到的消息就能直接通过 grep 找到抛出错误的位置。使用
throw new Error(`Webhook signature mismatch for ${id}`),绝不要使用throw new Error(`${prefix}: mismatch`)。 - 每个文件只包含一个可搜索的概念,并让编排代码保持精简。 回答“X 在哪里实现?”的代码应放在以 X 命名的模块中——名称应对应读者会询问的事物,而不是内部机制——不要内联到协调器、管道或服务类中。编排代码应呈现为一系列对命名清晰模块的调用;如果读者通过搜索找到它,每一行都应能引导他们再跳转一步,找到真正的实现。把多个概念的实现埋在一个大文件里,会让所有针对这些概念的搜索都落在同一堵代码墙上。不断拆分,直到每个问题规模的概念都有一个命名明确的归属,然后停止:只在一个概念内部有意义的辅助函数应内联,而每个小函数单独放一个文件,会让一个答案分散在多次阅读中。可用以下方式检验:如果一个模块能回答许多互不相关的问题,那它就包含了不止一个概念。
- 将测试与代码放在一起(
foo.test.ts紧挨着foo.ts),这样一次搜索就能同时找到行为及其规范。 - 标明死路。 在旧路径上添加
@deprecated,并指向新路径。
提交前快速检查清单
- 对每个新增的导出名称进行一次搜索,是否足以找到其实现?
- 交换新函数的两个参数,是否会导致构建失败?
- 调用者必须了解、但签名无法表达的那一件事(单位、时区、所有权、顺序),是否写在定义处?
- 所有日志和错误字符串是否都以原样存在于源代码中?
- 是否有任何内容改变了行为,却没有改变名称?
- 代码移动后,原位置是否已将其删除?
评论
支持 Markdown