[{"data":1,"prerenderedAt":4163},["ShallowReactive",2],{"navigation":3,"\u002Fpractice\u002Ftesting-quality":189,"\u002Fpractice\u002Ftesting-quality-surround":4158},[4,35,57,75,101,123,149,171],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":34},"第 1 章：认识 Claude Code","i-lucide-rocket","\u002Fintro","1.intro",[10,14,18,22,26,30],{"title":11,"path":12,"stem":13},"什么是 Claude Code","\u002Fintro\u002Fwhat-is-claude-code","1.intro\u002F1.what-is-claude-code",{"title":15,"path":16,"stem":17},"Claude Code 与 Copilot、Cursor、Windsurf 的本质区别","\u002Fintro\u002Fvs-competitors","1.intro\u002F2.vs-competitors",{"title":19,"path":20,"stem":21},"AI 编程助手生态全景与选型指南","\u002Fintro\u002Fecosystem-guide","1.intro\u002F3.ecosystem-guide",{"title":23,"path":24,"stem":25},"LLM 的概率本质","\u002Fintro\u002Fllm-probability","1.intro\u002F4.llm-probability",{"title":27,"path":28,"stem":29},"从聊天机器人到 Agent","\u002Fintro\u002Ffrom-chatbot-to-agent","1.intro\u002F5.from-chatbot-to-agent",{"title":31,"path":32,"stem":33},"Claude Code 的 Agentic Loop 全拆解","\u002Fintro\u002Fagentic-loop","1.intro\u002F6.agentic-loop",false,{"title":36,"icon":37,"path":38,"stem":39,"children":40,"page":34},"第 2 章：安装与配置","i-lucide-settings","\u002Fsetup","2.setup",[41,45,49,53],{"title":42,"path":43,"stem":44},"系统要求与安装方式","\u002Fsetup\u002Fsystem-requirements","2.setup\u002F1.system-requirements",{"title":46,"path":47,"stem":48},"认证、登录与多账户管理","\u002Fsetup\u002Fauthentication","2.setup\u002F2.authentication",{"title":50,"path":51,"stem":52},"选择你的界面","\u002Fsetup\u002Fchoose-interface","2.setup\u002F3.choose-interface",{"title":54,"path":55,"stem":56},"Coding Plan","\u002Fsetup\u002Fcoding-plan","2.setup\u002F4.coding-plan",{"title":58,"icon":59,"path":60,"stem":61,"children":62,"page":34},"第 3 章：快速上手","i-lucide-hand","\u002Fquickstart","3.quickstart",[63,67,71],{"title":64,"path":65,"stem":66},"启动、交互模式与基本命令","\u002Fquickstart\u002Fstartup","3.quickstart\u002F1.startup",{"title":68,"path":69,"stem":70},"让 Claude 理解你的项目","\u002Fquickstart\u002Fcodebase-understanding","3.quickstart\u002F2.codebase-understanding",{"title":72,"path":73,"stem":74},"第一次代码变更","\u002Fquickstart\u002Ffirst-change","3.quickstart\u002F3.first-change",{"title":76,"icon":77,"path":78,"stem":79,"children":80,"page":34},"第 4 章：核心功能","i-lucide-laptop","\u002Fcore-features","4.core-features",[81,85,89,93,97],{"title":82,"path":83,"stem":84},"代码库全景扫描与模块关系分析","\u002Fcore-features\u002Fcodebase-scan","4.core-features\u002F1.codebase-scan",{"title":86,"path":87,"stem":88},"代码编辑与生成","\u002Fcore-features\u002Fedit-generate","4.core-features\u002F2.edit-generate",{"title":90,"path":91,"stem":92},"测试与调试","\u002Fcore-features\u002Ftest-debug","4.core-features\u002F3.test-debug",{"title":94,"path":95,"stem":96},"Git 工作流","\u002Fcore-features\u002Fgit-workflow","4.core-features\u002F4.git-workflow",{"title":98,"path":99,"stem":100},"工具链执行","\u002Fcore-features\u002Ftoolchain","4.core-features\u002F5.toolchain",{"title":102,"icon":103,"path":104,"stem":105,"children":106,"page":34},"第 5 章：进阶配置","i-lucide-wrench","\u002Fadvanced","5.advanced",[107,111,115,119],{"title":108,"path":109,"stem":110},"CLAUDE.md","\u002Fadvanced\u002Fclaude-md","5.advanced\u002F1.claude-md",{"title":112,"path":113,"stem":114},"Skills","\u002Fadvanced\u002Fskills","5.advanced\u002F2.skills",{"title":116,"path":117,"stem":118},"MCP","\u002Fadvanced\u002Fmcp","5.advanced\u002F3.mcp",{"title":120,"path":121,"stem":122},"Hooks 与 Plan 模式","\u002Fadvanced\u002Fhooks-plan","5.advanced\u002F4.hooks-plan",{"title":124,"icon":125,"path":126,"stem":127,"children":128,"page":34},"第 6 章：实战开发","i-lucide-hammer","\u002Fpractice","6.practice",[129,133,137,141,145],{"title":130,"path":131,"stem":132},"需求分析与架构设计","\u002Fpractice\u002Frequirements-architecture","6.practice\u002F1.requirements-architecture",{"title":134,"path":135,"stem":136},"项目脚手架搭建与技术选型","\u002Fpractice\u002Fscaffolding","6.practice\u002F2.scaffolding",{"title":138,"path":139,"stem":140},"核心功能实现","\u002Fpractice\u002Fcore-features","6.practice\u002F3.core-features",{"title":142,"path":143,"stem":144},"测试覆盖、代码审查与质量调优","\u002Fpractice\u002Ftesting-quality","6.practice\u002F4.testing-quality",{"title":146,"path":147,"stem":148},"部署上线与成果分享","\u002Fpractice\u002Fdeployment","6.practice\u002F5.deployment",{"title":150,"icon":151,"path":152,"stem":153,"children":154,"page":34},"第 7 章：心法层","i-lucide-brain","\u002Fmindset","7.mindset",[155,159,163,167],{"title":156,"path":157,"stem":158},"提示词设计原则","\u002Fmindset\u002Fprompt-design","7.mindset\u002F1.prompt-design",{"title":160,"path":161,"stem":162},"上下文管理策略","\u002Fmindset\u002Fcontext-management","7.mindset\u002F2.context-management",{"title":164,"path":165,"stem":166},"安全与权限控制","\u002Fmindset\u002Fsecurity","7.mindset\u002F3.security",{"title":168,"path":169,"stem":170},"Boris Cherny 的 9 条实战心法与团队推广经验","\u002Fmindset\u002Fboris-cherny-tips","7.mindset\u002F4.boris-cherny-tips",{"title":172,"icon":173,"path":174,"stem":175,"children":176,"page":34},"附录","i-lucide-paperclip","\u002Fappendix","8.appendix",[177,181,185],{"title":178,"path":179,"stem":180},"常用命令速查表","\u002Fappendix\u002Fa.command-cheatsheet","8.appendix\u002Fa.command-cheatsheet",{"title":182,"path":183,"stem":184},"AI 核心术语汇编","\u002Fappendix\u002Fb.ai-terminology","8.appendix\u002Fb.ai-terminology",{"title":186,"path":187,"stem":188},"资源链接与延伸阅读","\u002Fappendix\u002Fc.resources","8.appendix\u002Fc.resources",{"id":190,"title":142,"body":191,"description":197,"extension":4152,"links":4153,"meta":4154,"navigation":422,"path":143,"seo":4156,"stem":144,"__hash__":4157},"docs\u002F6.practice\u002F4.testing-quality.md",{"type":192,"value":193,"toc":4105},"minimark",[194,198,201,204,207,210,215,220,226,229,232,251,254,436,440,448,451,454,458,463,466,469,472,475,477,481,485,490,497,505,508,1231,1234,1238,1241,1244,1247,1250,1257,1264,1268,1271,1274,1932,1947,1951,1958,2434,2441,2447,2451,2454,2460,2463,2465,2469,2473,2478,2484,2490,2510,2513,2517,2520,2523,2527,2530,2536,2539,2543,2546,2790,2797,2804,2807,2809,2813,2817,2822,2833,2836,2839,2842,2846,2853,3077,3080,3084,3087,3098,3213,3217,3220,3384,3388,3391,3397,3400,3403,3547,3554,3556,3560,3564,3569,3584,3590,3593,3597,3600,3603,3607,3610,3620,3624,3627,3630,3640,3643,3647,3650,3653,3655,3659,3663,3669,3807,3810,3813,3820,3824,3827,3838,3842,3849,3921,3924,3927,3930,3932,3936,3941,3945,3948,3952,3955,3959,3966,3969,3973,3976,3979,3981,3984,3987,3990,4101],[195,196,197],"p",{},"经过 6.3 节的多轮迭代，我们已经把一个空的脚手架推向了功能可用的最小可行产品（Minimum Viable Product, MVP）。表面上看，主流程跑通了、UI 截图也拿得出手，但任何一位真正把代码送上过生产环境的工程师都知道：从「Demo 跑通」到「敢交付」之间，还隔着一整条质量保障链路——测试、审查、调优、门禁。这条链路的每个环节，过去由人主导、AI 辅助；从 Claude Code 普及之后，许多团队开始反过来：让 AI 主导执行，让人主导决策。",[195,199,200],{},"本节面向两类读者。对产品经理而言，这一章试图回答：为什么 AI 写出来的代码看似能跑，但在压力测试和真实用户场景下会塌方？团队应当怎样的「质量节奏」才能让 AI 不是隐患而是助力？对工程师而言，这一章给出可立即落地的对话脚本、Vitest\u002FPlaywright 配置片段、Hooks 与 Plan 模式的协作范式，以及把「质量」从口号变成 CI 阶段的具体做法。",[195,202,203],{},"需要预先建立的一个心智模型是：在 Claude Code 工作流里，「质量」不再是某一个阶段的工作，而是贯穿整个对话的「持续约束」。约束既来自外部（CI、Hooks、CODEOWNERS），也来自内部（CLAUDE.md、Skills、PR 模板）。本节会从最基础的「测试观重塑」一路讲到「质量门禁工程化」，每一节都包含三类内容：理论澄清、对话脚本、可直接复制的配置代码。读者可以按章节顺序阅读，也可以把本节当作「日常工作的速查手册」——遇到具体问题时跳到对应小节。",[195,205,206],{},"最后请注意：本节不追求「面面俱到」，而追求「每一个建议都能在你下一次对话里立即应用」。如果某个工具或做法暂时与你的项目不匹配，跳过即可；但希望你能在阅读后立刻打开当前项目的 CLAUDE.md，对照本节增补至少一条新规则——这就是 AI 协作时代「学习即落地」的最朴素方式，也是把本节内容真正内化为团队竞争力的唯一捷径。",[208,209],"hr",{},[211,212,214],"h2",{"id":213},"一为什么-ai-协作时代的测试观要重写","一、为什么 AI 协作时代的测试观要重写",[216,217,219],"h3",{"id":218},"_11-llm-生成代码的看似正确陷阱","1.1 LLM 生成代码的「看似正确」陷阱",[221,222,223],"blockquote",{},[195,224,225],{},"给产品经理：你可以把大语言模型（Large Language Model, LLM）想象成一位读了几十亿行代码的实习生。他写代码的速度极快、风格干净，但他并不真的「运行」过自己写的东西。他靠的是「这看起来很像正确的代码」。这就是「看似正确」陷阱。",[195,227,228],{},"在传统编程中，「编译通过」「类型检查通过」「单元测试通过」这三道关卡能拦截绝大多数低级错误。Claude Code 工作流里，前两道由 LLM 自行处理，几乎不会失败；但「单元测试通过」这一道，如果测试本身也是 LLM 写的，就会形成一个值得警惕的闭环：模型既写实现又写测试，二者可能同时朝错误的方向倾斜，对外呈现「绿色 CI」。",[195,230,231],{},"Anthropic 工程师 Boris Cherny 在公开分享中反复强调：「Claude Code 不会替你思考边界条件，它只会非常自信地把你描述过的边界条件落实成代码。」这句话是理解本节所有内容的钥匙——AI 的强项是「把已说清楚的事情做出来」，弱项是「主动质疑没说清楚的部分」。",[195,233,234,235,239,240,243,244,243,247,250],{},"举一个常见的反例：让 Claude 实现一个分页函数，它通常会正确处理 ",[236,237,238],"code",{},"page=1, pageSize=10"," 这种主流场景，但极少主动测试 ",[236,241,242],{},"pageSize=0","、",[236,245,246],{},"page=负数",[236,248,249],{},"pageSize > totalCount"," 这类边界。除非你问它：「列出所有可能的边界情况」，它才会真的把这些情况枚举出来。",[195,252,253],{},"更深层的原因在于：LLM 的训练语料里，「正常路径」的代码远多于「异常路径」。互联网上的开源仓库往往展示「这个功能怎么用」，而不展示「这个功能怎么坏」。结果就是模型对正常路径的概率分布极其密集、对异常路径的概率分布稀疏。当你不显式提示异常路径时，模型会按训练分布来生成代码——这是统计意义上的必然，不是「模型不够聪明」。",[255,256,261],"pre",{"className":257,"code":258,"language":259,"meta":260,"style":260},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F 看似正确的实现：Claude 第一稿生成\nexport function paginate\u003CT>(items: T[], page: number, pageSize: number): T[] {\n  const start = (page - 1) * pageSize\n  return items.slice(start, start + pageSize)\n}\n\n\u002F\u002F 隐藏问题：page=0 \u002F page=-1 \u002F pageSize=0 \u002F 浮点数 \u002F NaN 全部静默通过\n\u002F\u002F 单元测试如果只覆盖 page=1, pageSize=10，CI 会显示绿色，但生产会出事\n","ts","",[236,262,263,272,343,378,411,417,424,430],{"__ignoreMap":260},[264,265,268],"span",{"class":266,"line":267},"line",1,[264,269,271],{"class":270},"sHwdD","\u002F\u002F 看似正确的实现：Claude 第一稿生成\n",[264,273,275,279,283,287,291,295,298,302,305,308,312,315,318,320,323,325,328,330,332,335,337,340],{"class":266,"line":274},2,[264,276,278],{"class":277},"s7zQu","export",[264,280,282],{"class":281},"spNyl"," function",[264,284,286],{"class":285},"s2Zo4"," paginate",[264,288,290],{"class":289},"sMK4o","\u003C",[264,292,294],{"class":293},"sBMFI","T",[264,296,297],{"class":289},">(",[264,299,301],{"class":300},"sHdIc","items",[264,303,304],{"class":289},":",[264,306,307],{"class":293}," T",[264,309,311],{"class":310},"sTEyZ","[]",[264,313,314],{"class":289},",",[264,316,317],{"class":300}," page",[264,319,304],{"class":289},[264,321,322],{"class":293}," number",[264,324,314],{"class":289},[264,326,327],{"class":300}," pageSize",[264,329,304],{"class":289},[264,331,322],{"class":293},[264,333,334],{"class":289},"):",[264,336,307],{"class":293},[264,338,339],{"class":310},"[] ",[264,341,342],{"class":289},"{\n",[264,344,346,349,352,355,359,362,365,369,372,375],{"class":266,"line":345},3,[264,347,348],{"class":281},"  const",[264,350,351],{"class":310}," start",[264,353,354],{"class":289}," =",[264,356,358],{"class":357},"swJcz"," (",[264,360,361],{"class":310},"page",[264,363,364],{"class":289}," -",[264,366,368],{"class":367},"sbssI"," 1",[264,370,371],{"class":357},") ",[264,373,374],{"class":289},"*",[264,376,377],{"class":310}," pageSize\n",[264,379,381,384,387,390,393,396,399,401,403,406,408],{"class":266,"line":380},4,[264,382,383],{"class":277},"  return",[264,385,386],{"class":310}," items",[264,388,389],{"class":289},".",[264,391,392],{"class":285},"slice",[264,394,395],{"class":357},"(",[264,397,398],{"class":310},"start",[264,400,314],{"class":289},[264,402,351],{"class":310},[264,404,405],{"class":289}," +",[264,407,327],{"class":310},[264,409,410],{"class":357},")\n",[264,412,414],{"class":266,"line":413},5,[264,415,416],{"class":289},"}\n",[264,418,420],{"class":266,"line":419},6,[264,421,423],{"emptyLinePlaceholder":422},true,"\n",[264,425,427],{"class":266,"line":426},7,[264,428,429],{"class":270},"\u002F\u002F 隐藏问题：page=0 \u002F page=-1 \u002F pageSize=0 \u002F 浮点数 \u002F NaN 全部静默通过\n",[264,431,433],{"class":266,"line":432},8,[264,434,435],{"class":270},"\u002F\u002F 单元测试如果只覆盖 page=1, pageSize=10，CI 会显示绿色，但生产会出事\n",[216,437,439],{"id":438},"_12-测试金字塔在-claude-code-工作流中的新形态","1.2 测试金字塔在 Claude Code 工作流中的新形态",[195,441,442,443,447],{},"经典测试金字塔（Test Pyramid，Mike Cohn 提出）由下至上分别是：单元测试、集成测试、端到端测试，比例大致为 70%\u002F20%\u002F10%。这个模型在 AI 协作时代依然成立，但",[444,445,446],"strong",{},"比例与重心","发生了微妙变化。",[195,449,450],{},"常见做法是把比例调整为「单元 50% + 契约\u002F集成 30% + 端到端 15% + 探索性 5%」。原因有三：第一，AI 生成单元测试的成本极低，但覆盖率边际收益递减；第二，AI 在生成「跨模块契约测试」时表现出色，可以快速捕捉接口漂移；第三，端到端（End-to-End, E2E）测试从过去的「奢侈品」变成「必需品」，因为它是少数几种能验证「整体行为是否符合用户意图」的手段，而 LLM 最容易在「整体语义」上偏移。",[195,452,453],{},"值得强调的是新增的「探索性」一档。探索性测试（Exploratory Testing）指人工或半自动地以「破坏者视角」尝试打破系统，例如随机点击、并发操作、模糊输入（fuzz testing）。这类测试无法被 LLM 完全替代，但 LLM 可以协助生成探索脚本、整理探索结果、分析发现的异常。一种经典做法是：让 Claude 阅读应用的功能列表，自动生成「攻击者会怎么玩」的探索路径，然后由工程师按照路径手工或脚本执行。",[216,455,457],{"id":456},"_13-把测试当作对话契约而非验证最后一道关卡","1.3 把测试当作「对话契约」而非验证最后一道关卡",[221,459,460],{},[195,461,462],{},"给产品经理：在传统流程里，测试是开发的最后一步——「写完代码再补测试」。在 Claude Code 工作流里，测试是开发的「第一步」——它既是给 AI 的需求说明，也是给 AI 的验收标准。",[195,464,465],{},"这种范式叫做「测试驱动对话」（Test-Driven Conversation, TDC）。它脱胎于 Kent Beck 的测试驱动开发（Test-Driven Development, TDD），但形式更松散：你不必先写出可运行的测试，只需要把「期望的输入输出对」「期望的边界行为」用文字 + 伪代码的形式甩给 Claude，让它先生成测试，再生成实现。",[195,467,468],{},"这样做有三重好处。第一，测试用例本身成为人类与 AI 共享的「契约文档」，避免「我以为我说清楚了」的灾难。第二，AI 会根据测试主动审视自己的实现，减少幻觉。第三，未来重构时，测试套件成为防止 AI 把功能改坏的安全网。",[195,470,471],{},"需要特别强调的一点是：测试用例不是越多越好。常见做法是把「测试用例」分为三档——契约测试（Contract Test）、行为测试（Behavior Test）、回归测试（Regression Test）。契约测试只测「函数签名与不变量」、行为测试覆盖主流业务路径、回归测试针对历史 bug 增量补充。三档测试共同构成可持续维护的测试套件，而不是一锅粥。",[195,473,474],{},"在 PM 视角下，测试驱动对话的最大价值不在于「代码质量」而在于「需求质量」。当你尝试把一个需求写成测试用例时，你会被迫思考所有「之前没想清楚」的细节：用户输入超长字符怎么办？操作中断怎么办？两个用户同时点击同一个按钮怎么办？这些问题在传统的 PRD 评审中往往被「等开发反馈再说」搪塞过去，而测试驱动对话把它们前置到了需求拆解阶段。这是 AI 协作给产品流程带来的最隐秘也最深远的改变。",[208,476],{},[211,478,480],{"id":479},"二与-claude-协作生成测试","二、与 Claude 协作生成测试",[216,482,484],{"id":483},"_21-单元测试让-claude-同时写实现与测试","2.1 单元测试：让 Claude 同时写实现与测试",[221,486,487],{},[195,488,489],{},"给产品经理：单元测试是测试一个函数（或一个小模块）在特定输入下是否返回预期输出。它是测试金字塔的底座，也是 AI 最擅长批量生成的部分。",[195,491,492,493,496],{},"工程实践中，最稳健的方式是「同步生成」：要求 Claude 在同一轮对话里产出实现和测试，并通过 ",[236,494,495],{},"\u002Fthink"," 或 Plan 模式让它先列出测试场景，再写代码。下面是一个推荐的对话模板：",[255,498,503],{"className":499,"code":501,"language":502},[500],"language-text","请为以下需求生成 Vitest 测试和 TypeScript 实现：\n功能：解析用户输入的时长字符串（如 \"2h30m\"、\"45s\"、\"1d6h\"）为毫秒数。\n\n要求：\n1. 先在 Plan 模式下列出至少 12 条测试用例（含正常、边界、非法输入）\n2. 用例审查通过后再生成实现\n3. 实现通过 pnpm vitest run --reporter=verbose 即可验证\n4. 禁止吞掉非法输入，必须 throw new Error\n","text",[236,504,501],{"__ignoreMap":260},[195,506,507],{},"Claude 通常会先回应类似下面的用例清单，再开始写实现：",[255,509,511],{"className":257,"code":510,"language":259,"meta":260,"style":260},"\u002F\u002F tests\u002FparseDuration.spec.ts\nimport { describe, it, expect } from 'vitest'\nimport { parseDuration } from '..\u002Fsrc\u002FparseDuration'\n\ndescribe('parseDuration', () => {\n  \u002F\u002F 正常场景\n  it('parses \"2h30m\" to 9_000_000 ms', () => {\n    expect(parseDuration('2h30m')).toBe(2 * 3600_000 + 30 * 60_000)\n  })\n  it('parses \"45s\" to 45_000 ms', () => {\n    expect(parseDuration('45s')).toBe(45_000)\n  })\n  it('parses \"1d6h\" to 108_000_000 ms', () => {\n    expect(parseDuration('1d6h')).toBe(24 * 3600_000 + 6 * 3600_000)\n  })\n\n  \u002F\u002F 边界场景\n  it('parses \"0s\" to 0 ms', () => {\n    expect(parseDuration('0s')).toBe(0)\n  })\n  it('parses uppercase \"2H30M\"', () => {\n    expect(parseDuration('2H30M')).toBe(2 * 3600_000 + 30 * 60_000)\n  })\n\n  \u002F\u002F 非法场景\n  it('throws on empty string', () => {\n    expect(() => parseDuration('')).toThrow()\n  })\n  it('throws on \"abc\"', () => {\n    expect(() => parseDuration('abc')).toThrow()\n  })\n  it('throws on negative \"-2h\"', () => {\n    expect(() => parseDuration('-2h')).toThrow()\n  })\n  it('throws on overflow \"9999d\"', () => {\n    expect(() => parseDuration('9999d')).toThrow(\u002Foverflow\u002Fi)\n  })\n})\n",[236,512,513,518,555,575,579,605,610,632,681,689,711,742,749,771,815,822,827,833,855,886,893,915,957,964,969,975,997,1026,1033,1055,1085,1092,1114,1144,1151,1173,1216,1223],{"__ignoreMap":260},[264,514,515],{"class":266,"line":267},[264,516,517],{"class":270},"\u002F\u002F tests\u002FparseDuration.spec.ts\n",[264,519,520,523,526,529,531,534,536,539,542,545,548,552],{"class":266,"line":274},[264,521,522],{"class":277},"import",[264,524,525],{"class":289}," {",[264,527,528],{"class":310}," describe",[264,530,314],{"class":289},[264,532,533],{"class":310}," it",[264,535,314],{"class":289},[264,537,538],{"class":310}," expect",[264,540,541],{"class":289}," }",[264,543,544],{"class":277}," from",[264,546,547],{"class":289}," '",[264,549,551],{"class":550},"sfazB","vitest",[264,553,554],{"class":289},"'\n",[264,556,557,559,561,564,566,568,570,573],{"class":266,"line":345},[264,558,522],{"class":277},[264,560,525],{"class":289},[264,562,563],{"class":310}," parseDuration",[264,565,541],{"class":289},[264,567,544],{"class":277},[264,569,547],{"class":289},[264,571,572],{"class":550},"..\u002Fsrc\u002FparseDuration",[264,574,554],{"class":289},[264,576,577],{"class":266,"line":380},[264,578,423],{"emptyLinePlaceholder":422},[264,580,581,584,586,589,592,594,596,599,602],{"class":266,"line":413},[264,582,583],{"class":285},"describe",[264,585,395],{"class":310},[264,587,588],{"class":289},"'",[264,590,591],{"class":550},"parseDuration",[264,593,588],{"class":289},[264,595,314],{"class":289},[264,597,598],{"class":289}," ()",[264,600,601],{"class":281}," =>",[264,603,604],{"class":289}," {\n",[264,606,607],{"class":266,"line":419},[264,608,609],{"class":270},"  \u002F\u002F 正常场景\n",[264,611,612,615,617,619,622,624,626,628,630],{"class":266,"line":426},[264,613,614],{"class":285},"  it",[264,616,395],{"class":357},[264,618,588],{"class":289},[264,620,621],{"class":550},"parses \"2h30m\" to 9_000_000 ms",[264,623,588],{"class":289},[264,625,314],{"class":289},[264,627,598],{"class":289},[264,629,601],{"class":281},[264,631,604],{"class":289},[264,633,634,637,639,641,643,645,648,650,653,655,658,660,663,666,669,671,674,676,679],{"class":266,"line":432},[264,635,636],{"class":285},"    expect",[264,638,395],{"class":357},[264,640,591],{"class":285},[264,642,395],{"class":357},[264,644,588],{"class":289},[264,646,647],{"class":550},"2h30m",[264,649,588],{"class":289},[264,651,652],{"class":357},"))",[264,654,389],{"class":289},[264,656,657],{"class":285},"toBe",[264,659,395],{"class":357},[264,661,662],{"class":367},"2",[264,664,665],{"class":289}," *",[264,667,668],{"class":367}," 3600_000",[264,670,405],{"class":289},[264,672,673],{"class":367}," 30",[264,675,665],{"class":289},[264,677,678],{"class":367}," 60_000",[264,680,410],{"class":357},[264,682,684,687],{"class":266,"line":683},9,[264,685,686],{"class":289},"  }",[264,688,410],{"class":357},[264,690,692,694,696,698,701,703,705,707,709],{"class":266,"line":691},10,[264,693,614],{"class":285},[264,695,395],{"class":357},[264,697,588],{"class":289},[264,699,700],{"class":550},"parses \"45s\" to 45_000 ms",[264,702,588],{"class":289},[264,704,314],{"class":289},[264,706,598],{"class":289},[264,708,601],{"class":281},[264,710,604],{"class":289},[264,712,714,716,718,720,722,724,727,729,731,733,735,737,740],{"class":266,"line":713},11,[264,715,636],{"class":285},[264,717,395],{"class":357},[264,719,591],{"class":285},[264,721,395],{"class":357},[264,723,588],{"class":289},[264,725,726],{"class":550},"45s",[264,728,588],{"class":289},[264,730,652],{"class":357},[264,732,389],{"class":289},[264,734,657],{"class":285},[264,736,395],{"class":357},[264,738,739],{"class":367},"45_000",[264,741,410],{"class":357},[264,743,745,747],{"class":266,"line":744},12,[264,746,686],{"class":289},[264,748,410],{"class":357},[264,750,752,754,756,758,761,763,765,767,769],{"class":266,"line":751},13,[264,753,614],{"class":285},[264,755,395],{"class":357},[264,757,588],{"class":289},[264,759,760],{"class":550},"parses \"1d6h\" to 108_000_000 ms",[264,762,588],{"class":289},[264,764,314],{"class":289},[264,766,598],{"class":289},[264,768,601],{"class":281},[264,770,604],{"class":289},[264,772,774,776,778,780,782,784,787,789,791,793,795,797,800,802,804,806,809,811,813],{"class":266,"line":773},14,[264,775,636],{"class":285},[264,777,395],{"class":357},[264,779,591],{"class":285},[264,781,395],{"class":357},[264,783,588],{"class":289},[264,785,786],{"class":550},"1d6h",[264,788,588],{"class":289},[264,790,652],{"class":357},[264,792,389],{"class":289},[264,794,657],{"class":285},[264,796,395],{"class":357},[264,798,799],{"class":367},"24",[264,801,665],{"class":289},[264,803,668],{"class":367},[264,805,405],{"class":289},[264,807,808],{"class":367}," 6",[264,810,665],{"class":289},[264,812,668],{"class":367},[264,814,410],{"class":357},[264,816,818,820],{"class":266,"line":817},15,[264,819,686],{"class":289},[264,821,410],{"class":357},[264,823,825],{"class":266,"line":824},16,[264,826,423],{"emptyLinePlaceholder":422},[264,828,830],{"class":266,"line":829},17,[264,831,832],{"class":270},"  \u002F\u002F 边界场景\n",[264,834,836,838,840,842,845,847,849,851,853],{"class":266,"line":835},18,[264,837,614],{"class":285},[264,839,395],{"class":357},[264,841,588],{"class":289},[264,843,844],{"class":550},"parses \"0s\" to 0 ms",[264,846,588],{"class":289},[264,848,314],{"class":289},[264,850,598],{"class":289},[264,852,601],{"class":281},[264,854,604],{"class":289},[264,856,858,860,862,864,866,868,871,873,875,877,879,881,884],{"class":266,"line":857},19,[264,859,636],{"class":285},[264,861,395],{"class":357},[264,863,591],{"class":285},[264,865,395],{"class":357},[264,867,588],{"class":289},[264,869,870],{"class":550},"0s",[264,872,588],{"class":289},[264,874,652],{"class":357},[264,876,389],{"class":289},[264,878,657],{"class":285},[264,880,395],{"class":357},[264,882,883],{"class":367},"0",[264,885,410],{"class":357},[264,887,889,891],{"class":266,"line":888},20,[264,890,686],{"class":289},[264,892,410],{"class":357},[264,894,896,898,900,902,905,907,909,911,913],{"class":266,"line":895},21,[264,897,614],{"class":285},[264,899,395],{"class":357},[264,901,588],{"class":289},[264,903,904],{"class":550},"parses uppercase \"2H30M\"",[264,906,588],{"class":289},[264,908,314],{"class":289},[264,910,598],{"class":289},[264,912,601],{"class":281},[264,914,604],{"class":289},[264,916,918,920,922,924,926,928,931,933,935,937,939,941,943,945,947,949,951,953,955],{"class":266,"line":917},22,[264,919,636],{"class":285},[264,921,395],{"class":357},[264,923,591],{"class":285},[264,925,395],{"class":357},[264,927,588],{"class":289},[264,929,930],{"class":550},"2H30M",[264,932,588],{"class":289},[264,934,652],{"class":357},[264,936,389],{"class":289},[264,938,657],{"class":285},[264,940,395],{"class":357},[264,942,662],{"class":367},[264,944,665],{"class":289},[264,946,668],{"class":367},[264,948,405],{"class":289},[264,950,673],{"class":367},[264,952,665],{"class":289},[264,954,678],{"class":367},[264,956,410],{"class":357},[264,958,960,962],{"class":266,"line":959},23,[264,961,686],{"class":289},[264,963,410],{"class":357},[264,965,967],{"class":266,"line":966},24,[264,968,423],{"emptyLinePlaceholder":422},[264,970,972],{"class":266,"line":971},25,[264,973,974],{"class":270},"  \u002F\u002F 非法场景\n",[264,976,978,980,982,984,987,989,991,993,995],{"class":266,"line":977},26,[264,979,614],{"class":285},[264,981,395],{"class":357},[264,983,588],{"class":289},[264,985,986],{"class":550},"throws on empty string",[264,988,588],{"class":289},[264,990,314],{"class":289},[264,992,598],{"class":289},[264,994,601],{"class":281},[264,996,604],{"class":289},[264,998,1000,1002,1004,1007,1009,1011,1013,1016,1018,1020,1023],{"class":266,"line":999},27,[264,1001,636],{"class":285},[264,1003,395],{"class":357},[264,1005,1006],{"class":289},"()",[264,1008,601],{"class":281},[264,1010,563],{"class":285},[264,1012,395],{"class":357},[264,1014,1015],{"class":289},"''",[264,1017,652],{"class":357},[264,1019,389],{"class":289},[264,1021,1022],{"class":285},"toThrow",[264,1024,1025],{"class":357},"()\n",[264,1027,1029,1031],{"class":266,"line":1028},28,[264,1030,686],{"class":289},[264,1032,410],{"class":357},[264,1034,1036,1038,1040,1042,1045,1047,1049,1051,1053],{"class":266,"line":1035},29,[264,1037,614],{"class":285},[264,1039,395],{"class":357},[264,1041,588],{"class":289},[264,1043,1044],{"class":550},"throws on \"abc\"",[264,1046,588],{"class":289},[264,1048,314],{"class":289},[264,1050,598],{"class":289},[264,1052,601],{"class":281},[264,1054,604],{"class":289},[264,1056,1058,1060,1062,1064,1066,1068,1070,1072,1075,1077,1079,1081,1083],{"class":266,"line":1057},30,[264,1059,636],{"class":285},[264,1061,395],{"class":357},[264,1063,1006],{"class":289},[264,1065,601],{"class":281},[264,1067,563],{"class":285},[264,1069,395],{"class":357},[264,1071,588],{"class":289},[264,1073,1074],{"class":550},"abc",[264,1076,588],{"class":289},[264,1078,652],{"class":357},[264,1080,389],{"class":289},[264,1082,1022],{"class":285},[264,1084,1025],{"class":357},[264,1086,1088,1090],{"class":266,"line":1087},31,[264,1089,686],{"class":289},[264,1091,410],{"class":357},[264,1093,1095,1097,1099,1101,1104,1106,1108,1110,1112],{"class":266,"line":1094},32,[264,1096,614],{"class":285},[264,1098,395],{"class":357},[264,1100,588],{"class":289},[264,1102,1103],{"class":550},"throws on negative \"-2h\"",[264,1105,588],{"class":289},[264,1107,314],{"class":289},[264,1109,598],{"class":289},[264,1111,601],{"class":281},[264,1113,604],{"class":289},[264,1115,1117,1119,1121,1123,1125,1127,1129,1131,1134,1136,1138,1140,1142],{"class":266,"line":1116},33,[264,1118,636],{"class":285},[264,1120,395],{"class":357},[264,1122,1006],{"class":289},[264,1124,601],{"class":281},[264,1126,563],{"class":285},[264,1128,395],{"class":357},[264,1130,588],{"class":289},[264,1132,1133],{"class":550},"-2h",[264,1135,588],{"class":289},[264,1137,652],{"class":357},[264,1139,389],{"class":289},[264,1141,1022],{"class":285},[264,1143,1025],{"class":357},[264,1145,1147,1149],{"class":266,"line":1146},34,[264,1148,686],{"class":289},[264,1150,410],{"class":357},[264,1152,1154,1156,1158,1160,1163,1165,1167,1169,1171],{"class":266,"line":1153},35,[264,1155,614],{"class":285},[264,1157,395],{"class":357},[264,1159,588],{"class":289},[264,1161,1162],{"class":550},"throws on overflow \"9999d\"",[264,1164,588],{"class":289},[264,1166,314],{"class":289},[264,1168,598],{"class":289},[264,1170,601],{"class":281},[264,1172,604],{"class":289},[264,1174,1176,1178,1180,1182,1184,1186,1188,1190,1193,1195,1197,1199,1201,1203,1206,1209,1211,1214],{"class":266,"line":1175},36,[264,1177,636],{"class":285},[264,1179,395],{"class":357},[264,1181,1006],{"class":289},[264,1183,601],{"class":281},[264,1185,563],{"class":285},[264,1187,395],{"class":357},[264,1189,588],{"class":289},[264,1191,1192],{"class":550},"9999d",[264,1194,588],{"class":289},[264,1196,652],{"class":357},[264,1198,389],{"class":289},[264,1200,1022],{"class":285},[264,1202,395],{"class":357},[264,1204,1205],{"class":289},"\u002F",[264,1207,1208],{"class":550},"overflow",[264,1210,1205],{"class":289},[264,1212,1213],{"class":367},"i",[264,1215,410],{"class":357},[264,1217,1219,1221],{"class":266,"line":1218},37,[264,1220,686],{"class":289},[264,1222,410],{"class":357},[264,1224,1226,1229],{"class":266,"line":1225},38,[264,1227,1228],{"class":289},"}",[264,1230,410],{"class":310},[195,1232,1233],{},"这个模板的精髓在于「禁止吞掉非法输入」这一条。LLM 默认倾向于「容错」——遇到非法输入返回 0、null 或空字符串，而不是抛出异常。但真实生产环境中，「静默失败」比「抛异常」更危险，因为它会让上游错误悄悄漫延。把这个原则写进 CLAUDE.md，可以让所有后续生成都保留这一行为约束。",[216,1235,1237],{"id":1236},"_22-边界条件测试用对话探索遗漏的场景","2.2 边界条件测试：用对话探索遗漏的场景",[195,1239,1240],{},"边界条件（Boundary Conditions）是「正常输入与非法输入的交界处」，例如数组的第一个\u002F最后一个元素、整数的最大值\u002F最小值、空集合、null 与 undefined 的差异等。Claude 在第一轮通常会遗漏 30%~50% 的边界，但在被显式追问后会快速补齐。",[195,1242,1243],{},"推荐的对话节奏是：",[195,1245,1246],{},"第一轮：「请列出这个函数的所有可能边界条件，按风险等级排序」。\n第二轮：「针对你列出的高风险边界，逐个生成测试用例」。\n第三轮：「假设你是一个恶意用户，你会用什么输入打破这个函数？」",[195,1248,1249],{},"第三轮是最有价值的，因为它把 Claude 从「合作模式」切换到「对抗模式」。在对抗模式下，模型会主动提出诸如「输入超长字符串导致正则回溯」「Unicode 控制字符」「JSON 注入」等更深层的边界。",[195,1251,1252,1253,1256],{},"工程师可以把这个三轮节奏沉淀为一个 Skill，命名为 ",[236,1254,1255],{},"boundary-explore","，每次新增函数时主动调用。这样可以避免「记得问对抗问题」这件事本身依赖人脑的记忆。把流程变成可被工具调用的 Skill，是 Claude Code 工作流相比传统 IDE 的核心优势之一。",[195,1258,1259,1260,1263],{},"值得提醒的是：边界条件的「探索深度」取决于你描述上下文的深度。如果你只说「这是一个分页函数」，Claude 顶多能想到 ",[236,1261,1262],{},"page=0"," 这种通用边界；但如果你说「这是一个面向千万级数据库表的分页函数，被前端无限滚动列表调用」，Claude 会主动考虑「深翻页性能」「offset 漂移」「并发写入下的数据重复或丢失」等深层边界。上下文密度直接决定了边界覆盖密度。",[216,1265,1267],{"id":1266},"_23-集成测试与端到端plan-模式审视外部依赖","2.3 集成测试与端到端：Plan 模式审视外部依赖",[195,1269,1270],{},"集成测试（Integration Test）验证多个模块协作的正确性，端到端测试（E2E Test）验证从用户视角的完整流程。这两类测试都涉及外部依赖：数据库、HTTP 接口、文件系统、第三方 SDK。Claude Code 在生成此类测试时，最容易犯的错误是「假设依赖永远可用」——例如直接连接生产数据库、对真实 API 发请求、不清理测试数据。",[195,1272,1273],{},"进入 Plan 模式（按 Shift+Tab 切换）后，要求 Claude 在动手前先输出「依赖矩阵」：每个外部依赖、是否需要 Mock、Mock 的边界、清理策略。下面是一个集成测试的 Vitest + MSW（Mock Service Worker）骨架：",[255,1275,1277],{"className":257,"code":1276,"language":259,"meta":260,"style":260},"\u002F\u002F tests\u002Fintegration\u002ForderApi.spec.ts\nimport { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'\nimport { setupServer } from 'msw\u002Fnode'\nimport { http, HttpResponse } from 'msw'\nimport { createOrder } from '..\u002F..\u002Fsrc\u002Fapi\u002Forder'\n\n\u002F\u002F 把外部 HTTP 调用 mock 在测试边界内，CI 不需要真实网络\nconst server = setupServer(\n  http.post('https:\u002F\u002Fapi.example.com\u002Forders', async ({ request }) => {\n    const body = await request.json() as { sku: string, qty: number }\n    if (body.qty \u003C= 0) {\n      return HttpResponse.json({ error: 'INVALID_QTY' }, { status: 400 })\n    }\n    return HttpResponse.json({ id: 'ord_123', status: 'created' })\n  })\n)\n\nbeforeAll(() => server.listen({ onUnhandledRequest: 'error' }))\nafterEach(() => server.resetHandlers())\nafterAll(() => server.close())\n\ndescribe('createOrder integration', () => {\n  it('creates order on valid input', async () => {\n    const r = await createOrder({ sku: 'SKU-1', qty: 2 })\n    expect(r.id).toBe('ord_123')\n  })\n  it('throws on qty=0', async () => {\n    await expect(createOrder({ sku: 'SKU-1', qty: 0 })).rejects.toThrow('INVALID_QTY')\n  })\n})\n",[236,1278,1279,1284,1325,1345,1370,1390,1394,1399,1415,1452,1500,1525,1570,1575,1619,1625,1629,1633,1673,1694,1714,1718,1739,1762,1803,1834,1840,1863,1920,1926],{"__ignoreMap":260},[264,1280,1281],{"class":266,"line":267},[264,1282,1283],{"class":270},"\u002F\u002F tests\u002Fintegration\u002ForderApi.spec.ts\n",[264,1285,1286,1288,1290,1292,1294,1296,1298,1300,1302,1305,1307,1310,1312,1315,1317,1319,1321,1323],{"class":266,"line":274},[264,1287,522],{"class":277},[264,1289,525],{"class":289},[264,1291,528],{"class":310},[264,1293,314],{"class":289},[264,1295,533],{"class":310},[264,1297,314],{"class":289},[264,1299,538],{"class":310},[264,1301,314],{"class":289},[264,1303,1304],{"class":310}," beforeAll",[264,1306,314],{"class":289},[264,1308,1309],{"class":310}," afterAll",[264,1311,314],{"class":289},[264,1313,1314],{"class":310}," afterEach",[264,1316,541],{"class":289},[264,1318,544],{"class":277},[264,1320,547],{"class":289},[264,1322,551],{"class":550},[264,1324,554],{"class":289},[264,1326,1327,1329,1331,1334,1336,1338,1340,1343],{"class":266,"line":345},[264,1328,522],{"class":277},[264,1330,525],{"class":289},[264,1332,1333],{"class":310}," setupServer",[264,1335,541],{"class":289},[264,1337,544],{"class":277},[264,1339,547],{"class":289},[264,1341,1342],{"class":550},"msw\u002Fnode",[264,1344,554],{"class":289},[264,1346,1347,1349,1351,1354,1356,1359,1361,1363,1365,1368],{"class":266,"line":380},[264,1348,522],{"class":277},[264,1350,525],{"class":289},[264,1352,1353],{"class":310}," http",[264,1355,314],{"class":289},[264,1357,1358],{"class":310}," HttpResponse",[264,1360,541],{"class":289},[264,1362,544],{"class":277},[264,1364,547],{"class":289},[264,1366,1367],{"class":550},"msw",[264,1369,554],{"class":289},[264,1371,1372,1374,1376,1379,1381,1383,1385,1388],{"class":266,"line":413},[264,1373,522],{"class":277},[264,1375,525],{"class":289},[264,1377,1378],{"class":310}," createOrder",[264,1380,541],{"class":289},[264,1382,544],{"class":277},[264,1384,547],{"class":289},[264,1386,1387],{"class":550},"..\u002F..\u002Fsrc\u002Fapi\u002Forder",[264,1389,554],{"class":289},[264,1391,1392],{"class":266,"line":419},[264,1393,423],{"emptyLinePlaceholder":422},[264,1395,1396],{"class":266,"line":426},[264,1397,1398],{"class":270},"\u002F\u002F 把外部 HTTP 调用 mock 在测试边界内，CI 不需要真实网络\n",[264,1400,1401,1404,1407,1410,1412],{"class":266,"line":432},[264,1402,1403],{"class":281},"const",[264,1405,1406],{"class":310}," server ",[264,1408,1409],{"class":289},"=",[264,1411,1333],{"class":285},[264,1413,1414],{"class":310},"(\n",[264,1416,1417,1420,1422,1425,1427,1429,1432,1434,1436,1439,1442,1445,1448,1450],{"class":266,"line":683},[264,1418,1419],{"class":310},"  http",[264,1421,389],{"class":289},[264,1423,1424],{"class":285},"post",[264,1426,395],{"class":310},[264,1428,588],{"class":289},[264,1430,1431],{"class":550},"https:\u002F\u002Fapi.example.com\u002Forders",[264,1433,588],{"class":289},[264,1435,314],{"class":289},[264,1437,1438],{"class":281}," async",[264,1440,1441],{"class":289}," ({",[264,1443,1444],{"class":300}," request",[264,1446,1447],{"class":289}," })",[264,1449,601],{"class":281},[264,1451,604],{"class":289},[264,1453,1454,1457,1460,1462,1465,1467,1469,1472,1475,1478,1480,1483,1485,1488,1490,1493,1495,1497],{"class":266,"line":691},[264,1455,1456],{"class":281},"    const",[264,1458,1459],{"class":310}," body",[264,1461,354],{"class":289},[264,1463,1464],{"class":277}," await",[264,1466,1444],{"class":310},[264,1468,389],{"class":289},[264,1470,1471],{"class":285},"json",[264,1473,1474],{"class":357},"() ",[264,1476,1477],{"class":277},"as",[264,1479,525],{"class":289},[264,1481,1482],{"class":357}," sku",[264,1484,304],{"class":289},[264,1486,1487],{"class":293}," string",[264,1489,314],{"class":289},[264,1491,1492],{"class":357}," qty",[264,1494,304],{"class":289},[264,1496,322],{"class":293},[264,1498,1499],{"class":289}," }\n",[264,1501,1502,1505,1507,1510,1512,1515,1518,1521,1523],{"class":266,"line":713},[264,1503,1504],{"class":277},"    if",[264,1506,358],{"class":357},[264,1508,1509],{"class":310},"body",[264,1511,389],{"class":289},[264,1513,1514],{"class":310},"qty",[264,1516,1517],{"class":289}," \u003C=",[264,1519,1520],{"class":367}," 0",[264,1522,371],{"class":357},[264,1524,342],{"class":289},[264,1526,1527,1530,1532,1534,1536,1538,1541,1544,1546,1548,1551,1553,1556,1558,1561,1563,1566,1568],{"class":266,"line":744},[264,1528,1529],{"class":277},"      return",[264,1531,1358],{"class":310},[264,1533,389],{"class":289},[264,1535,1471],{"class":285},[264,1537,395],{"class":357},[264,1539,1540],{"class":289},"{",[264,1542,1543],{"class":357}," error",[264,1545,304],{"class":289},[264,1547,547],{"class":289},[264,1549,1550],{"class":550},"INVALID_QTY",[264,1552,588],{"class":289},[264,1554,1555],{"class":289}," },",[264,1557,525],{"class":289},[264,1559,1560],{"class":357}," status",[264,1562,304],{"class":289},[264,1564,1565],{"class":367}," 400",[264,1567,541],{"class":289},[264,1569,410],{"class":357},[264,1571,1572],{"class":266,"line":751},[264,1573,1574],{"class":289},"    }\n",[264,1576,1577,1580,1582,1584,1586,1588,1590,1593,1595,1597,1600,1602,1604,1606,1608,1610,1613,1615,1617],{"class":266,"line":773},[264,1578,1579],{"class":277},"    return",[264,1581,1358],{"class":310},[264,1583,389],{"class":289},[264,1585,1471],{"class":285},[264,1587,395],{"class":357},[264,1589,1540],{"class":289},[264,1591,1592],{"class":357}," id",[264,1594,304],{"class":289},[264,1596,547],{"class":289},[264,1598,1599],{"class":550},"ord_123",[264,1601,588],{"class":289},[264,1603,314],{"class":289},[264,1605,1560],{"class":357},[264,1607,304],{"class":289},[264,1609,547],{"class":289},[264,1611,1612],{"class":550},"created",[264,1614,588],{"class":289},[264,1616,541],{"class":289},[264,1618,410],{"class":357},[264,1620,1621,1623],{"class":266,"line":817},[264,1622,686],{"class":289},[264,1624,410],{"class":310},[264,1626,1627],{"class":266,"line":824},[264,1628,410],{"class":310},[264,1630,1631],{"class":266,"line":829},[264,1632,423],{"emptyLinePlaceholder":422},[264,1634,1635,1638,1640,1642,1644,1647,1649,1652,1654,1656,1659,1661,1663,1666,1668,1670],{"class":266,"line":835},[264,1636,1637],{"class":285},"beforeAll",[264,1639,395],{"class":310},[264,1641,1006],{"class":289},[264,1643,601],{"class":281},[264,1645,1646],{"class":310}," server",[264,1648,389],{"class":289},[264,1650,1651],{"class":285},"listen",[264,1653,395],{"class":310},[264,1655,1540],{"class":289},[264,1657,1658],{"class":357}," onUnhandledRequest",[264,1660,304],{"class":289},[264,1662,547],{"class":289},[264,1664,1665],{"class":550},"error",[264,1667,588],{"class":289},[264,1669,541],{"class":289},[264,1671,1672],{"class":310},"))\n",[264,1674,1675,1678,1680,1682,1684,1686,1688,1691],{"class":266,"line":857},[264,1676,1677],{"class":285},"afterEach",[264,1679,395],{"class":310},[264,1681,1006],{"class":289},[264,1683,601],{"class":281},[264,1685,1646],{"class":310},[264,1687,389],{"class":289},[264,1689,1690],{"class":285},"resetHandlers",[264,1692,1693],{"class":310},"())\n",[264,1695,1696,1699,1701,1703,1705,1707,1709,1712],{"class":266,"line":888},[264,1697,1698],{"class":285},"afterAll",[264,1700,395],{"class":310},[264,1702,1006],{"class":289},[264,1704,601],{"class":281},[264,1706,1646],{"class":310},[264,1708,389],{"class":289},[264,1710,1711],{"class":285},"close",[264,1713,1693],{"class":310},[264,1715,1716],{"class":266,"line":895},[264,1717,423],{"emptyLinePlaceholder":422},[264,1719,1720,1722,1724,1726,1729,1731,1733,1735,1737],{"class":266,"line":917},[264,1721,583],{"class":285},[264,1723,395],{"class":310},[264,1725,588],{"class":289},[264,1727,1728],{"class":550},"createOrder integration",[264,1730,588],{"class":289},[264,1732,314],{"class":289},[264,1734,598],{"class":289},[264,1736,601],{"class":281},[264,1738,604],{"class":289},[264,1740,1741,1743,1745,1747,1750,1752,1754,1756,1758,1760],{"class":266,"line":959},[264,1742,614],{"class":285},[264,1744,395],{"class":357},[264,1746,588],{"class":289},[264,1748,1749],{"class":550},"creates order on valid input",[264,1751,588],{"class":289},[264,1753,314],{"class":289},[264,1755,1438],{"class":281},[264,1757,598],{"class":289},[264,1759,601],{"class":281},[264,1761,604],{"class":289},[264,1763,1764,1766,1769,1771,1773,1775,1777,1779,1781,1783,1785,1788,1790,1792,1794,1796,1799,1801],{"class":266,"line":966},[264,1765,1456],{"class":281},[264,1767,1768],{"class":310}," r",[264,1770,354],{"class":289},[264,1772,1464],{"class":277},[264,1774,1378],{"class":285},[264,1776,395],{"class":357},[264,1778,1540],{"class":289},[264,1780,1482],{"class":357},[264,1782,304],{"class":289},[264,1784,547],{"class":289},[264,1786,1787],{"class":550},"SKU-1",[264,1789,588],{"class":289},[264,1791,314],{"class":289},[264,1793,1492],{"class":357},[264,1795,304],{"class":289},[264,1797,1798],{"class":367}," 2",[264,1800,541],{"class":289},[264,1802,410],{"class":357},[264,1804,1805,1807,1809,1812,1814,1817,1820,1822,1824,1826,1828,1830,1832],{"class":266,"line":971},[264,1806,636],{"class":285},[264,1808,395],{"class":357},[264,1810,1811],{"class":310},"r",[264,1813,389],{"class":289},[264,1815,1816],{"class":310},"id",[264,1818,1819],{"class":357},")",[264,1821,389],{"class":289},[264,1823,657],{"class":285},[264,1825,395],{"class":357},[264,1827,588],{"class":289},[264,1829,1599],{"class":550},[264,1831,588],{"class":289},[264,1833,410],{"class":357},[264,1835,1836,1838],{"class":266,"line":977},[264,1837,686],{"class":289},[264,1839,410],{"class":357},[264,1841,1842,1844,1846,1848,1851,1853,1855,1857,1859,1861],{"class":266,"line":999},[264,1843,614],{"class":285},[264,1845,395],{"class":357},[264,1847,588],{"class":289},[264,1849,1850],{"class":550},"throws on qty=0",[264,1852,588],{"class":289},[264,1854,314],{"class":289},[264,1856,1438],{"class":281},[264,1858,598],{"class":289},[264,1860,601],{"class":281},[264,1862,604],{"class":289},[264,1864,1865,1868,1870,1872,1875,1877,1879,1881,1883,1885,1887,1889,1891,1893,1895,1897,1899,1901,1903,1906,1908,1910,1912,1914,1916,1918],{"class":266,"line":1028},[264,1866,1867],{"class":277},"    await",[264,1869,538],{"class":285},[264,1871,395],{"class":357},[264,1873,1874],{"class":285},"createOrder",[264,1876,395],{"class":357},[264,1878,1540],{"class":289},[264,1880,1482],{"class":357},[264,1882,304],{"class":289},[264,1884,547],{"class":289},[264,1886,1787],{"class":550},[264,1888,588],{"class":289},[264,1890,314],{"class":289},[264,1892,1492],{"class":357},[264,1894,304],{"class":289},[264,1896,1520],{"class":367},[264,1898,541],{"class":289},[264,1900,652],{"class":357},[264,1902,389],{"class":289},[264,1904,1905],{"class":310},"rejects",[264,1907,389],{"class":289},[264,1909,1022],{"class":285},[264,1911,395],{"class":357},[264,1913,588],{"class":289},[264,1915,1550],{"class":550},[264,1917,588],{"class":289},[264,1919,410],{"class":357},[264,1921,1922,1924],{"class":266,"line":1035},[264,1923,686],{"class":289},[264,1925,410],{"class":357},[264,1927,1928,1930],{"class":266,"line":1057},[264,1929,1228],{"class":289},[264,1931,410],{"class":310},[195,1933,1934,1935,1938,1939,1942,1943,1946],{},"E2E 层面，常见做法是用 Playwright（Microsoft 出品的开源浏览器自动化框架）。让 Claude 阅读 ",[236,1936,1937],{},"playwright.config.ts"," 与既有用例后，生成新流程的 E2E 用例时，可以指定「严格基线」——不允许使用 ",[236,1940,1941],{},"await page.waitForTimeout(任意数字)","，必须用 ",[236,1944,1945],{},"expect(...).toBeVisible()"," 这类条件等待，从根上避免 flaky test（间歇失败的测试）。",[216,1948,1950],{"id":1949},"_24-测试夹具fixture与工厂方法的自动生成","2.4 测试夹具（Fixture）与工厂方法的自动生成",[195,1952,1953,1954,1957],{},"Fixture 指测试运行前准备好的「样板数据」，工厂方法（Factory）指根据少量参数批量构造对象的小工具。手写 Fixture 是繁琐的，但 LLM 生成 Fixture 几乎零成本。建议在项目根放一个 ",[236,1955,1956],{},"tests\u002Ffixtures\u002Findex.ts"," 桶文件，把所有 Factory 集中导出：",[255,1959,1961],{"className":257,"code":1960,"language":259,"meta":260,"style":260},"\u002F\u002F tests\u002Ffixtures\u002Findex.ts\nimport type { User, Order, Product } from '..\u002F..\u002Fsrc\u002Ftypes'\n\nlet userIdCounter = 1\nexport function makeUser(overrides: Partial\u003CUser> = {}): User {\n  return {\n    id: `usr_${userIdCounter++}`,\n    name: 'Test User',\n    email: `test${userIdCounter}@example.com`,\n    createdAt: new Date('2026-01-01T00:00:00Z'),\n    ...overrides,\n  }\n}\n\nlet orderIdCounter = 1\nexport function makeOrder(overrides: Partial\u003COrder> = {}): Order {\n  return {\n    id: `ord_${orderIdCounter++}`,\n    userId: 'usr_1',\n    sku: 'SKU-1',\n    qty: 1,\n    status: 'created',\n    ...overrides,\n  }\n}\n\nexport function makeProduct(overrides: Partial\u003CProduct> = {}): Product {\n  return {\n    sku: 'SKU-1',\n    title: 'Test Product',\n    priceCents: 999,\n    stock: 10,\n    ...overrides,\n  }\n}\n",[236,1962,1963,1968,2001,2005,2018,2054,2060,2088,2104,2130,2156,2165,2170,2174,2178,2189,2221,2227,2249,2265,2280,2291,2306,2314,2318,2322,2326,2358,2364,2378,2394,2406,2418,2426,2430],{"__ignoreMap":260},[264,1964,1965],{"class":266,"line":267},[264,1966,1967],{"class":270},"\u002F\u002F tests\u002Ffixtures\u002Findex.ts\n",[264,1969,1970,1972,1975,1977,1980,1982,1985,1987,1990,1992,1994,1996,1999],{"class":266,"line":274},[264,1971,522],{"class":277},[264,1973,1974],{"class":277}," type",[264,1976,525],{"class":289},[264,1978,1979],{"class":310}," User",[264,1981,314],{"class":289},[264,1983,1984],{"class":310}," Order",[264,1986,314],{"class":289},[264,1988,1989],{"class":310}," Product",[264,1991,541],{"class":289},[264,1993,544],{"class":277},[264,1995,547],{"class":289},[264,1997,1998],{"class":550},"..\u002F..\u002Fsrc\u002Ftypes",[264,2000,554],{"class":289},[264,2002,2003],{"class":266,"line":345},[264,2004,423],{"emptyLinePlaceholder":422},[264,2006,2007,2010,2013,2015],{"class":266,"line":380},[264,2008,2009],{"class":281},"let",[264,2011,2012],{"class":310}," userIdCounter ",[264,2014,1409],{"class":289},[264,2016,2017],{"class":367}," 1\n",[264,2019,2020,2022,2024,2027,2029,2032,2034,2037,2039,2042,2045,2047,2050,2052],{"class":266,"line":413},[264,2021,278],{"class":277},[264,2023,282],{"class":281},[264,2025,2026],{"class":285}," makeUser",[264,2028,395],{"class":289},[264,2030,2031],{"class":300},"overrides",[264,2033,304],{"class":289},[264,2035,2036],{"class":293}," Partial",[264,2038,290],{"class":289},[264,2040,2041],{"class":293},"User",[264,2043,2044],{"class":289},">",[264,2046,354],{"class":289},[264,2048,2049],{"class":289}," {}):",[264,2051,1979],{"class":293},[264,2053,604],{"class":289},[264,2055,2056,2058],{"class":266,"line":419},[264,2057,383],{"class":277},[264,2059,604],{"class":289},[264,2061,2062,2065,2067,2070,2073,2076,2079,2082,2085],{"class":266,"line":426},[264,2063,2064],{"class":357},"    id",[264,2066,304],{"class":289},[264,2068,2069],{"class":289}," `",[264,2071,2072],{"class":550},"usr_",[264,2074,2075],{"class":289},"${",[264,2077,2078],{"class":310},"userIdCounter",[264,2080,2081],{"class":289},"++",[264,2083,2084],{"class":289},"}`",[264,2086,2087],{"class":289},",\n",[264,2089,2090,2093,2095,2097,2100,2102],{"class":266,"line":432},[264,2091,2092],{"class":357},"    name",[264,2094,304],{"class":289},[264,2096,547],{"class":289},[264,2098,2099],{"class":550},"Test User",[264,2101,588],{"class":289},[264,2103,2087],{"class":289},[264,2105,2106,2109,2111,2113,2116,2118,2120,2122,2125,2128],{"class":266,"line":683},[264,2107,2108],{"class":357},"    email",[264,2110,304],{"class":289},[264,2112,2069],{"class":289},[264,2114,2115],{"class":550},"test",[264,2117,2075],{"class":289},[264,2119,2078],{"class":310},[264,2121,1228],{"class":289},[264,2123,2124],{"class":550},"@example.com",[264,2126,2127],{"class":289},"`",[264,2129,2087],{"class":289},[264,2131,2132,2135,2137,2140,2143,2145,2147,2150,2152,2154],{"class":266,"line":691},[264,2133,2134],{"class":357},"    createdAt",[264,2136,304],{"class":289},[264,2138,2139],{"class":289}," new",[264,2141,2142],{"class":285}," Date",[264,2144,395],{"class":357},[264,2146,588],{"class":289},[264,2148,2149],{"class":550},"2026-01-01T00:00:00Z",[264,2151,588],{"class":289},[264,2153,1819],{"class":357},[264,2155,2087],{"class":289},[264,2157,2158,2161,2163],{"class":266,"line":713},[264,2159,2160],{"class":289},"    ...",[264,2162,2031],{"class":310},[264,2164,2087],{"class":289},[264,2166,2167],{"class":266,"line":744},[264,2168,2169],{"class":289},"  }\n",[264,2171,2172],{"class":266,"line":751},[264,2173,416],{"class":289},[264,2175,2176],{"class":266,"line":773},[264,2177,423],{"emptyLinePlaceholder":422},[264,2179,2180,2182,2185,2187],{"class":266,"line":817},[264,2181,2009],{"class":281},[264,2183,2184],{"class":310}," orderIdCounter ",[264,2186,1409],{"class":289},[264,2188,2017],{"class":367},[264,2190,2191,2193,2195,2198,2200,2202,2204,2206,2208,2211,2213,2215,2217,2219],{"class":266,"line":824},[264,2192,278],{"class":277},[264,2194,282],{"class":281},[264,2196,2197],{"class":285}," makeOrder",[264,2199,395],{"class":289},[264,2201,2031],{"class":300},[264,2203,304],{"class":289},[264,2205,2036],{"class":293},[264,2207,290],{"class":289},[264,2209,2210],{"class":293},"Order",[264,2212,2044],{"class":289},[264,2214,354],{"class":289},[264,2216,2049],{"class":289},[264,2218,1984],{"class":293},[264,2220,604],{"class":289},[264,2222,2223,2225],{"class":266,"line":829},[264,2224,383],{"class":277},[264,2226,604],{"class":289},[264,2228,2229,2231,2233,2235,2238,2240,2243,2245,2247],{"class":266,"line":835},[264,2230,2064],{"class":357},[264,2232,304],{"class":289},[264,2234,2069],{"class":289},[264,2236,2237],{"class":550},"ord_",[264,2239,2075],{"class":289},[264,2241,2242],{"class":310},"orderIdCounter",[264,2244,2081],{"class":289},[264,2246,2084],{"class":289},[264,2248,2087],{"class":289},[264,2250,2251,2254,2256,2258,2261,2263],{"class":266,"line":857},[264,2252,2253],{"class":357},"    userId",[264,2255,304],{"class":289},[264,2257,547],{"class":289},[264,2259,2260],{"class":550},"usr_1",[264,2262,588],{"class":289},[264,2264,2087],{"class":289},[264,2266,2267,2270,2272,2274,2276,2278],{"class":266,"line":888},[264,2268,2269],{"class":357},"    sku",[264,2271,304],{"class":289},[264,2273,547],{"class":289},[264,2275,1787],{"class":550},[264,2277,588],{"class":289},[264,2279,2087],{"class":289},[264,2281,2282,2285,2287,2289],{"class":266,"line":895},[264,2283,2284],{"class":357},"    qty",[264,2286,304],{"class":289},[264,2288,368],{"class":367},[264,2290,2087],{"class":289},[264,2292,2293,2296,2298,2300,2302,2304],{"class":266,"line":917},[264,2294,2295],{"class":357},"    status",[264,2297,304],{"class":289},[264,2299,547],{"class":289},[264,2301,1612],{"class":550},[264,2303,588],{"class":289},[264,2305,2087],{"class":289},[264,2307,2308,2310,2312],{"class":266,"line":959},[264,2309,2160],{"class":289},[264,2311,2031],{"class":310},[264,2313,2087],{"class":289},[264,2315,2316],{"class":266,"line":966},[264,2317,2169],{"class":289},[264,2319,2320],{"class":266,"line":971},[264,2321,416],{"class":289},[264,2323,2324],{"class":266,"line":977},[264,2325,423],{"emptyLinePlaceholder":422},[264,2327,2328,2330,2332,2335,2337,2339,2341,2343,2345,2348,2350,2352,2354,2356],{"class":266,"line":999},[264,2329,278],{"class":277},[264,2331,282],{"class":281},[264,2333,2334],{"class":285}," makeProduct",[264,2336,395],{"class":289},[264,2338,2031],{"class":300},[264,2340,304],{"class":289},[264,2342,2036],{"class":293},[264,2344,290],{"class":289},[264,2346,2347],{"class":293},"Product",[264,2349,2044],{"class":289},[264,2351,354],{"class":289},[264,2353,2049],{"class":289},[264,2355,1989],{"class":293},[264,2357,604],{"class":289},[264,2359,2360,2362],{"class":266,"line":1028},[264,2361,383],{"class":277},[264,2363,604],{"class":289},[264,2365,2366,2368,2370,2372,2374,2376],{"class":266,"line":1035},[264,2367,2269],{"class":357},[264,2369,304],{"class":289},[264,2371,547],{"class":289},[264,2373,1787],{"class":550},[264,2375,588],{"class":289},[264,2377,2087],{"class":289},[264,2379,2380,2383,2385,2387,2390,2392],{"class":266,"line":1057},[264,2381,2382],{"class":357},"    title",[264,2384,304],{"class":289},[264,2386,547],{"class":289},[264,2388,2389],{"class":550},"Test Product",[264,2391,588],{"class":289},[264,2393,2087],{"class":289},[264,2395,2396,2399,2401,2404],{"class":266,"line":1087},[264,2397,2398],{"class":357},"    priceCents",[264,2400,304],{"class":289},[264,2402,2403],{"class":367}," 999",[264,2405,2087],{"class":289},[264,2407,2408,2411,2413,2416],{"class":266,"line":1094},[264,2409,2410],{"class":357},"    stock",[264,2412,304],{"class":289},[264,2414,2415],{"class":367}," 10",[264,2417,2087],{"class":289},[264,2419,2420,2422,2424],{"class":266,"line":1116},[264,2421,2160],{"class":289},[264,2423,2031],{"class":310},[264,2425,2087],{"class":289},[264,2427,2428],{"class":266,"line":1146},[264,2429,2169],{"class":289},[264,2431,2432],{"class":266,"line":1153},[264,2433,416],{"class":289},[195,2435,2436,2437,2440],{},"让 Claude 在新增类型时同步更新 ",[236,2438,2439],{},"fixtures\u002Findex.ts","，可以避免「测试数据散落各处」的反模式。配合 CLAUDE.md 中的提示「新增类型必须同步生成 Factory」，工程化效果立竿见影。",[195,2442,2443,2444,2446],{},"进阶模式是引入「随机化 Fixture」——基于 faker.js（开源数据生成库）让 Factory 默认生成随机但合法的数据，只在测试需要确定性时用 ",[236,2445,2031],{}," 覆盖。这样能在测试运行间隙意外发现「依赖固定数据顺序」「依赖某个特定字段长度」的隐藏 bug。需要注意的是：随机化必须配合「种子（seed）记录」机制——一旦某次测试失败，能精确复现当次的随机数据，否则会变成一场调试噩梦。",[216,2448,2450],{"id":2449},"_25-让-claude-阅读现有测试套件生成补全用例","2.5 让 Claude 阅读现有测试套件，生成补全用例",[195,2452,2453],{},"当项目已经存在一定规模的测试套件后，最有价值的协作方式是「补全」——让 Claude 扫描现有用例，识别遗漏的覆盖角落。推荐对话脚本：",[255,2455,2458],{"className":2456,"code":2457,"language":502},[500],"请扫描 tests\u002F 目录下所有 .spec.ts 文件，并对照 src\u002F 下的实现：\n1. 列出测试覆盖率报告中行覆盖低于 80% 的文件\n2. 对每个文件，提出 3~5 条新增测试用例的建议\n3. 优先级排序：业务关键 > 边界条件 > 异常路径\n4. 不要立即写代码，先输出报告\n",[236,2459,2457],{"__ignoreMap":260},[195,2461,2462],{},"这一步通常能在十几秒内识别出几十处「漏测点」，远比工程师人工逐文件 review 高效。报告确认后，再分批让 Claude 生成实际用例，每批不超过 10 个，便于人工审核。",[208,2464],{},[211,2466,2468],{"id":2467},"三覆盖率指标的正确用法","三、覆盖率指标的正确用法",[216,2470,2472],{"id":2471},"_31-行覆盖分支覆盖变异测试的区别","3.1 行覆盖、分支覆盖、变异测试的区别",[221,2474,2475],{},[195,2476,2477],{},"给产品经理：覆盖率（Coverage）是回答「你的测试到底测了代码的多少」这个问题的指标。它有三种粒度，从粗到细：行覆盖、分支覆盖、变异测试。",[195,2479,2480,2483],{},[444,2481,2482],{},"行覆盖（Line Coverage）","：被测试执行过的代码行数占总行数的比例。这是最常见的指标，但也最容易误导——一行代码被「执行」不代表它的行为被「断言」过。",[195,2485,2486,2489],{},[444,2487,2488],{},"分支覆盖（Branch Coverage）","：每个 if\u002Felse、switch、三元表达式的「真分支」和「假分支」是否都被覆盖。比行覆盖更严格，但仍然只能保证「分支被走过」，不能保证「分支的行为正确」。",[195,2491,2492,2495,2496,2498,2499,2502,2503,2498,2506,2509],{},[444,2493,2494],{},"变异测试（Mutation Testing）","：自动修改源代码（例如把 ",[236,2497,2044],{}," 改成 ",[236,2500,2501],{},">=","、把 ",[236,2504,2505],{},"&&",[236,2507,2508],{},"||","），然后跑测试套件，看是否有测试因此失败。如果修改了代码但所有测试仍然通过，说明这部分代码「没有真正被测试约束」。常用工具是 Stryker（开源、支持 TS\u002FJS\u002FVue）。",[195,2511,2512],{},"变异测试是当前最接近「真测试覆盖率」的指标，但运行成本也最高，常见做法是只在每周一次的夜间 CI 上跑全量、平时只跑被改动文件的变异。",[216,2514,2516],{"id":2515},"_32-覆盖率达到-80-之后剩下-20-怎么取舍","3.2 覆盖率达到 80% 之后，剩下 20% 怎么取舍",[195,2518,2519],{},"业界长期存在一个共识：行覆盖 80% 是合理目标，90%+ 是奢侈，100% 是迷信。原因是最后 20% 通常包含三类代码：错误处理分支、防御性代码、平台兼容性补丁。这些代码的「期望行为」是「永远不被触发」，强行覆盖会导致测试本身脆弱。",[195,2521,2522],{},"更好的做法是：让 Claude 对未覆盖代码做「风险标注」——逐行解释「如果这行代码出 bug，最坏后果是什么、触发概率多大」。然后人工决定哪些必须补测试、哪些可以忽略、哪些应该删掉（YAGNI 原则——You Aren't Gonna Need It）。",[216,2524,2526],{"id":2525},"_33-让-claude-解释每条未覆盖代码的风险等级","3.3 让 Claude 解释每条未覆盖代码的风险等级",[195,2528,2529],{},"对话脚本示例：",[255,2531,2534],{"className":2532,"code":2533,"language":502},[500],"请对照 coverage\u002Flcov-report 的结果，扫描 src\u002Fpayment\u002F 下所有未覆盖的代码行：\n1. 按文件分组列出未覆盖的代码块\n2. 对每个代码块，标注风险等级（高\u002F中\u002F低）：\n   - 高：涉及金额、用户身份、外部 API 调用\n   - 中：涉及内部状态变更\n   - 低：日志、调试、字符串拼接\n3. 对高风险代码块，立即补测试用例\n4. 对中风险代码块，列入 TODO\n5. 对低风险代码块，建议是否可以删除\n",[236,2535,2533],{"__ignoreMap":260},[195,2537,2538],{},"这个流程可以把「机械的覆盖率指标」转换为「业务驱动的测试投入」，避免团队陷入「为覆盖率而覆盖率」的内耗。",[216,2540,2542],{"id":2541},"_34-把覆盖率纳入-ci-守门但不要做硬性卡死","3.4 把覆盖率纳入 CI 守门，但不要做「硬性卡死」",[195,2544,2545],{},"常见做法是设置一个「下限」而不是「目标」——例如「整体行覆盖不低于 75%、新增代码行覆盖不低于 90%」。这样既保证基线，又允许局部低覆盖（例如基础设施代码）存在。Vitest 配置示例：",[255,2547,2549],{"className":257,"code":2548,"language":259,"meta":260,"style":260},"\u002F\u002F vitest.config.ts\nimport { defineConfig } from 'vitest\u002Fconfig'\n\nexport default defineConfig({\n  test: {\n    coverage: {\n      provider: 'v8',\n      reporter: ['text', 'lcov', 'html'],\n      thresholds: {\n        lines: 75,\n        functions: 75,\n        branches: 70,\n        statements: 75,\n        \u002F\u002F 对新增代码（与 main 对比的 diff）要求更严格\n        \u002F\u002F diff 模式需要 vitest 3.x +\n      },\n      exclude: ['**\u002F*.config.ts', '**\u002Ftypes\u002F**', '**\u002Ffixtures\u002F**'],\n    },\n  },\n})\n",[236,2550,2551,2556,2576,2580,2593,2602,2611,2627,2666,2675,2687,2698,2710,2721,2726,2731,2736,2774,2779,2784],{"__ignoreMap":260},[264,2552,2553],{"class":266,"line":267},[264,2554,2555],{"class":270},"\u002F\u002F vitest.config.ts\n",[264,2557,2558,2560,2562,2565,2567,2569,2571,2574],{"class":266,"line":274},[264,2559,522],{"class":277},[264,2561,525],{"class":289},[264,2563,2564],{"class":310}," defineConfig",[264,2566,541],{"class":289},[264,2568,544],{"class":277},[264,2570,547],{"class":289},[264,2572,2573],{"class":550},"vitest\u002Fconfig",[264,2575,554],{"class":289},[264,2577,2578],{"class":266,"line":345},[264,2579,423],{"emptyLinePlaceholder":422},[264,2581,2582,2584,2587,2589,2591],{"class":266,"line":380},[264,2583,278],{"class":277},[264,2585,2586],{"class":277}," default",[264,2588,2564],{"class":285},[264,2590,395],{"class":310},[264,2592,342],{"class":289},[264,2594,2595,2598,2600],{"class":266,"line":413},[264,2596,2597],{"class":357},"  test",[264,2599,304],{"class":289},[264,2601,604],{"class":289},[264,2603,2604,2607,2609],{"class":266,"line":419},[264,2605,2606],{"class":357},"    coverage",[264,2608,304],{"class":289},[264,2610,604],{"class":289},[264,2612,2613,2616,2618,2620,2623,2625],{"class":266,"line":426},[264,2614,2615],{"class":357},"      provider",[264,2617,304],{"class":289},[264,2619,547],{"class":289},[264,2621,2622],{"class":550},"v8",[264,2624,588],{"class":289},[264,2626,2087],{"class":289},[264,2628,2629,2632,2634,2637,2639,2641,2643,2645,2647,2650,2652,2654,2656,2659,2661,2664],{"class":266,"line":432},[264,2630,2631],{"class":357},"      reporter",[264,2633,304],{"class":289},[264,2635,2636],{"class":310}," [",[264,2638,588],{"class":289},[264,2640,502],{"class":550},[264,2642,588],{"class":289},[264,2644,314],{"class":289},[264,2646,547],{"class":289},[264,2648,2649],{"class":550},"lcov",[264,2651,588],{"class":289},[264,2653,314],{"class":289},[264,2655,547],{"class":289},[264,2657,2658],{"class":550},"html",[264,2660,588],{"class":289},[264,2662,2663],{"class":310},"]",[264,2665,2087],{"class":289},[264,2667,2668,2671,2673],{"class":266,"line":683},[264,2669,2670],{"class":357},"      thresholds",[264,2672,304],{"class":289},[264,2674,604],{"class":289},[264,2676,2677,2680,2682,2685],{"class":266,"line":691},[264,2678,2679],{"class":357},"        lines",[264,2681,304],{"class":289},[264,2683,2684],{"class":367}," 75",[264,2686,2087],{"class":289},[264,2688,2689,2692,2694,2696],{"class":266,"line":713},[264,2690,2691],{"class":357},"        functions",[264,2693,304],{"class":289},[264,2695,2684],{"class":367},[264,2697,2087],{"class":289},[264,2699,2700,2703,2705,2708],{"class":266,"line":744},[264,2701,2702],{"class":357},"        branches",[264,2704,304],{"class":289},[264,2706,2707],{"class":367}," 70",[264,2709,2087],{"class":289},[264,2711,2712,2715,2717,2719],{"class":266,"line":751},[264,2713,2714],{"class":357},"        statements",[264,2716,304],{"class":289},[264,2718,2684],{"class":367},[264,2720,2087],{"class":289},[264,2722,2723],{"class":266,"line":773},[264,2724,2725],{"class":270},"        \u002F\u002F 对新增代码（与 main 对比的 diff）要求更严格\n",[264,2727,2728],{"class":266,"line":817},[264,2729,2730],{"class":270},"        \u002F\u002F diff 模式需要 vitest 3.x +\n",[264,2732,2733],{"class":266,"line":824},[264,2734,2735],{"class":289},"      },\n",[264,2737,2738,2741,2743,2745,2747,2750,2752,2754,2756,2759,2761,2763,2765,2768,2770,2772],{"class":266,"line":829},[264,2739,2740],{"class":357},"      exclude",[264,2742,304],{"class":289},[264,2744,2636],{"class":310},[264,2746,588],{"class":289},[264,2748,2749],{"class":550},"**\u002F*.config.ts",[264,2751,588],{"class":289},[264,2753,314],{"class":289},[264,2755,547],{"class":289},[264,2757,2758],{"class":550},"**\u002Ftypes\u002F**",[264,2760,588],{"class":289},[264,2762,314],{"class":289},[264,2764,547],{"class":289},[264,2766,2767],{"class":550},"**\u002Ffixtures\u002F**",[264,2769,588],{"class":289},[264,2771,2663],{"class":310},[264,2773,2087],{"class":289},[264,2775,2776],{"class":266,"line":835},[264,2777,2778],{"class":289},"    },\n",[264,2780,2781],{"class":266,"line":857},[264,2782,2783],{"class":289},"  },\n",[264,2785,2786,2788],{"class":266,"line":888},[264,2787,1228],{"class":289},[264,2789,410],{"class":310},[195,2791,2792,2793,2796],{},"「硬性卡死」的反模式是把阈值设到 95% 以上——这会让团队转而绕过测试（写无意义的 ",[236,2794,2795],{},"expect(true).toBe(true)","），反而损害质量文化。",[195,2798,2799,2800,2803],{},"另一个值得警惕的反模式是「覆盖率作为绩效指标」。一旦覆盖率与考核挂钩，工程师会立即学会如何「刷覆盖率」：把没断言的代码塞进测试函数体、对返回值不做任何检查、用 ",[236,2801,2802],{},"expect.anything()"," 取代具体断言。这些手法都能让覆盖率数字上升而不真正提升质量。健康的团队会把覆盖率作为「健康度参考」而非「考核目标」——它告诉你哪里值得关注，但不告诉你「该不该奖励某个工程师」。",[195,2805,2806],{},"补充一个常被忽略的视角：覆盖率不仅要看「广度」也要看「时效」。一段三年没改过的代码，即使覆盖率 100%，也可能因为依赖升级而隐式失效；一段昨天刚写的代码，即使覆盖率 60%，因为最近被多次执行，反而更可信。常见做法是引入「测试新鲜度」指标——上次测试通过到现在的时间间隔，把超过 30 天没跑过的测试视为「需要重新验证」。",[208,2808],{},[211,2810,2812],{"id":2811},"四代码审查ai-与人各司其职","四、代码审查——AI 与人各司其职",[216,2814,2816],{"id":2815},"_41-让-claude-做第一遍审查工程师做决策审查","4.1 让 Claude 做「第一遍审查」，工程师做「决策审查」",[221,2818,2819],{},[195,2820,2821],{},"给产品经理：代码审查（Code Review）是同事在合并代码前互相检查的过程，类似「论文同行评议」。它的目的不仅是抓 bug，更是知识传播、风格统一、责任分担。",[195,2823,2824,2825,2828,2829,2832],{},"AI 协作时代，代码审查可以拆成两层。",[444,2826,2827],{},"第一遍由 Claude 做","：检查命名、风格、明显的 bug、安全漏洞、与既有代码的一致性。",[444,2830,2831],{},"第二遍由工程师做","：判断架构选择、业务正确性、可维护性、是否值得合并。",[195,2834,2835],{},"这样分工的理由是：第一遍是「机械活」，AI 比人快 10 倍且不会疲劳；第二遍是「判断活」，AI 没有上下文与责任，无法替代人。Anthropic 在 Claude Code 文档中明确建议：「Never let AI be the final approver of code merging into main.」（永远不要让 AI 做合并到主干的最终批准者。）",[195,2837,2838],{},"实践细节上，可以让 Claude 的「第一遍审查」自动生成一份评审报告（review report），格式包含：每条改动的风险等级、需要工程师重点关注的代码块、AI 自身的不确定性区域（uncertainty zones）。这份报告作为 PR 评论自动发到 GitHub\u002FGitLab，工程师在收到通知后只需要把注意力放在 AI 标注的「不确定区域」上，而不是从头读完整 diff。这种「注意力路由」是 AI 协作给代码审查带来的最实际的提速。",[195,2840,2841],{},"此外，团队应当建立一个共识：如果 Claude 的第一遍审查标注了「PASS」但工程师后续发现了问题，应当把这个 case 反馈到 review-skill 的 Checklist 中，让下次审查能识别此类问题。换句话说，Checklist 是活的——每一次 Claude 漏掉的问题，都应该补一条规则进去。这是「Compound Engineering」（复利式工程）的核心思想：每一次教训都让系统变得更聪明，而不是让单个工程师变得更累。",[216,2843,2845],{"id":2844},"_42-审查清单checklist的工程化沉淀","4.2 审查清单（Checklist）的工程化沉淀",[195,2847,2848,2849,2852],{},"把团队的 review 经验沉淀为 Checklist，存放在 ",[236,2850,2851],{},".claude\u002Fskills\u002Fcode-review\u002F"," 下，然后让 Claude 每次审查时自动加载。一个示例 Checklist：",[255,2854,2858],{"className":2855,"code":2856,"language":2857,"meta":260,"style":260},"language-markdown shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Code Review Checklist\n\n## 命名与风格\n- [ ] 函数名是动词短语，变量名是名词短语\n- [ ] 没有缩写（除非全队公认，如 `id`、`url`）\n- [ ] 文件长度 \u003C 300 行（超过则建议拆分）\n\n## 类型与安全\n- [ ] 没有 `any`（除非有注释说明原因）\n- [ ] 外部输入有 zod\u002Fvalibot 校验\n- [ ] 异常被显式抛出，没有静默吞掉\n\n## 业务正确性\n- [ ] 金额、库存、积分等数值字段使用整数（分\u002F克），不用浮点\n- [ ] 时间字段使用 ISO 8601 字符串或 Date 对象，不用本地时间字符串\n- [ ] 数据库写入有事务边界\n\n## 测试\n- [ ] 新增\u002F修改的逻辑有对应测试\n- [ ] 测试覆盖了至少一条非法输入路径\n- [ ] 没有 `it.skip` 或 `it.only` 残留\n\n## 安全\n- [ ] 用户输入没有直接拼接到 SQL\u002FShell\u002FHTML\n- [ ] 没有把密钥、Token 写进代码\n- [ ] 日志没有打印 PII（个人身份信息）\n","markdown",[236,2859,2860,2868,2872,2880,2888,2913,2920,2924,2931,2948,2955,2962,2966,2973,2980,2987,2994,2998,3005,3012,3019,3045,3049,3056,3063,3070],{"__ignoreMap":260},[264,2861,2862,2865],{"class":266,"line":267},[264,2863,2864],{"class":289},"# ",[264,2866,2867],{"class":293},"Code Review Checklist\n",[264,2869,2870],{"class":266,"line":274},[264,2871,423],{"emptyLinePlaceholder":422},[264,2873,2874,2877],{"class":266,"line":345},[264,2875,2876],{"class":289},"## ",[264,2878,2879],{"class":293},"命名与风格\n",[264,2881,2882,2885],{"class":266,"line":380},[264,2883,2884],{"class":289},"-",[264,2886,2887],{"class":310}," [ ] 函数名是动词短语，变量名是名词短语\n",[264,2889,2890,2892,2895,2897,2899,2901,2903,2905,2908,2910],{"class":266,"line":413},[264,2891,2884],{"class":289},[264,2893,2894],{"class":310}," [ ] 没有缩写（除非全队公认，如 ",[264,2896,2127],{"class":289},[264,2898,1816],{"class":550},[264,2900,2127],{"class":289},[264,2902,243],{"class":310},[264,2904,2127],{"class":289},[264,2906,2907],{"class":550},"url",[264,2909,2127],{"class":289},[264,2911,2912],{"class":310},"）\n",[264,2914,2915,2917],{"class":266,"line":419},[264,2916,2884],{"class":289},[264,2918,2919],{"class":310}," [ ] 文件长度 \u003C 300 行（超过则建议拆分）\n",[264,2921,2922],{"class":266,"line":426},[264,2923,423],{"emptyLinePlaceholder":422},[264,2925,2926,2928],{"class":266,"line":432},[264,2927,2876],{"class":289},[264,2929,2930],{"class":293},"类型与安全\n",[264,2932,2933,2935,2938,2940,2943,2945],{"class":266,"line":683},[264,2934,2884],{"class":289},[264,2936,2937],{"class":310}," [ ] 没有 ",[264,2939,2127],{"class":289},[264,2941,2942],{"class":550},"any",[264,2944,2127],{"class":289},[264,2946,2947],{"class":310},"（除非有注释说明原因）\n",[264,2949,2950,2952],{"class":266,"line":691},[264,2951,2884],{"class":289},[264,2953,2954],{"class":310}," [ ] 外部输入有 zod\u002Fvalibot 校验\n",[264,2956,2957,2959],{"class":266,"line":713},[264,2958,2884],{"class":289},[264,2960,2961],{"class":310}," [ ] 异常被显式抛出，没有静默吞掉\n",[264,2963,2964],{"class":266,"line":744},[264,2965,423],{"emptyLinePlaceholder":422},[264,2967,2968,2970],{"class":266,"line":751},[264,2969,2876],{"class":289},[264,2971,2972],{"class":293},"业务正确性\n",[264,2974,2975,2977],{"class":266,"line":773},[264,2976,2884],{"class":289},[264,2978,2979],{"class":310}," [ ] 金额、库存、积分等数值字段使用整数（分\u002F克），不用浮点\n",[264,2981,2982,2984],{"class":266,"line":817},[264,2983,2884],{"class":289},[264,2985,2986],{"class":310}," [ ] 时间字段使用 ISO 8601 字符串或 Date 对象，不用本地时间字符串\n",[264,2988,2989,2991],{"class":266,"line":824},[264,2990,2884],{"class":289},[264,2992,2993],{"class":310}," [ ] 数据库写入有事务边界\n",[264,2995,2996],{"class":266,"line":829},[264,2997,423],{"emptyLinePlaceholder":422},[264,2999,3000,3002],{"class":266,"line":835},[264,3001,2876],{"class":289},[264,3003,3004],{"class":293},"测试\n",[264,3006,3007,3009],{"class":266,"line":857},[264,3008,2884],{"class":289},[264,3010,3011],{"class":310}," [ ] 新增\u002F修改的逻辑有对应测试\n",[264,3013,3014,3016],{"class":266,"line":888},[264,3015,2884],{"class":289},[264,3017,3018],{"class":310}," [ ] 测试覆盖了至少一条非法输入路径\n",[264,3020,3021,3023,3025,3027,3030,3032,3035,3037,3040,3042],{"class":266,"line":895},[264,3022,2884],{"class":289},[264,3024,2937],{"class":310},[264,3026,2127],{"class":289},[264,3028,3029],{"class":550},"it.skip",[264,3031,2127],{"class":289},[264,3033,3034],{"class":310}," 或 ",[264,3036,2127],{"class":289},[264,3038,3039],{"class":550},"it.only",[264,3041,2127],{"class":289},[264,3043,3044],{"class":310}," 残留\n",[264,3046,3047],{"class":266,"line":917},[264,3048,423],{"emptyLinePlaceholder":422},[264,3050,3051,3053],{"class":266,"line":959},[264,3052,2876],{"class":289},[264,3054,3055],{"class":293},"安全\n",[264,3057,3058,3060],{"class":266,"line":966},[264,3059,2884],{"class":289},[264,3061,3062],{"class":310}," [ ] 用户输入没有直接拼接到 SQL\u002FShell\u002FHTML\n",[264,3064,3065,3067],{"class":266,"line":971},[264,3066,2884],{"class":289},[264,3068,3069],{"class":310}," [ ] 没有把密钥、Token 写进代码\n",[264,3071,3072,3074],{"class":266,"line":977},[264,3073,2884],{"class":289},[264,3075,3076],{"class":310}," [ ] 日志没有打印 PII（个人身份信息）\n",[195,3078,3079],{},"让 Claude 在每次 review 时输出「逐条勾选 + 不通过原因」，可以极大降低人工 review 的认知负担。",[216,3081,3083],{"id":3082},"_43-安全敏感代码永远不让-claude-拍板","4.3 安全敏感代码：永远不让 Claude 拍板",[195,3085,3086],{},"涉及密码学、认证、授权、支付、个人信息（Personally Identifiable Information, PII）的代码，必须人工最终审查。原因不是 AI 写不好，而是责任无法转移——一旦出事，「Claude 给的方案」不能成为团队的免责理由。",[195,3088,3089,3090,3093,3094,3097],{},"实践上，可以在仓库根的 ",[236,3091,3092],{},"CODEOWNERS"," 文件中标注「敏感目录」，并在 ",[236,3095,3096],{},".claude\u002Fsettings.json"," 的 Hooks 中加一条 PreToolUse 规则：当 Claude 试图修改这些目录时，先打印警告并要求显式确认。",[255,3099,3102],{"className":3100,"code":3101,"language":1471,"meta":260,"style":260},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"Edit|Write\",\n        \"command\": \"if echo \\\"$CLAUDE_TOOL_PATH\\\" | grep -qE '(auth|crypto|payment|pii)\u002F'; then echo 'WARNING: editing security-sensitive code, require human review before commit' >&2; fi\"\n      }\n    ]\n  }\n}\n",[236,3103,3104,3108,3123,3138,3143,3165,3195,3200,3205,3209],{"__ignoreMap":260},[264,3105,3106],{"class":266,"line":267},[264,3107,342],{"class":289},[264,3109,3110,3113,3116,3119,3121],{"class":266,"line":274},[264,3111,3112],{"class":289},"  \"",[264,3114,3115],{"class":281},"hooks",[264,3117,3118],{"class":289},"\"",[264,3120,304],{"class":289},[264,3122,604],{"class":289},[264,3124,3125,3128,3131,3133,3135],{"class":266,"line":345},[264,3126,3127],{"class":289},"    \"",[264,3129,3130],{"class":293},"PreToolUse",[264,3132,3118],{"class":289},[264,3134,304],{"class":289},[264,3136,3137],{"class":289}," [\n",[264,3139,3140],{"class":266,"line":380},[264,3141,3142],{"class":289},"      {\n",[264,3144,3145,3148,3151,3153,3155,3158,3161,3163],{"class":266,"line":413},[264,3146,3147],{"class":289},"        \"",[264,3149,3150],{"class":367},"matcher",[264,3152,3118],{"class":289},[264,3154,304],{"class":289},[264,3156,3157],{"class":289}," \"",[264,3159,3160],{"class":550},"Edit|Write",[264,3162,3118],{"class":289},[264,3164,2087],{"class":289},[264,3166,3167,3169,3172,3174,3176,3178,3181,3184,3187,3189,3192],{"class":266,"line":419},[264,3168,3147],{"class":289},[264,3170,3171],{"class":367},"command",[264,3173,3118],{"class":289},[264,3175,304],{"class":289},[264,3177,3157],{"class":289},[264,3179,3180],{"class":550},"if echo ",[264,3182,3183],{"class":310},"\\\"",[264,3185,3186],{"class":550},"$CLAUDE_TOOL_PATH",[264,3188,3183],{"class":310},[264,3190,3191],{"class":550}," | grep -qE '(auth|crypto|payment|pii)\u002F'; then echo 'WARNING: editing security-sensitive code, require human review before commit' >&2; fi",[264,3193,3194],{"class":289},"\"\n",[264,3196,3197],{"class":266,"line":426},[264,3198,3199],{"class":289},"      }\n",[264,3201,3202],{"class":266,"line":432},[264,3203,3204],{"class":289},"    ]\n",[264,3206,3207],{"class":266,"line":683},[264,3208,2169],{"class":289},[264,3210,3211],{"class":266,"line":691},[264,3212,416],{"class":289},[216,3214,3216],{"id":3215},"_44-用-skills-把团队-code-review-规范固化","4.4 用 Skills 把团队 Code Review 规范固化",[195,3218,3219],{},"Skills（参考 5.2 节）是 Claude Code 的「可复用工作流模块」。一个 review-skill 通常包含：触发关键词、执行步骤、输出模板、依赖的命令。下面是一个最小化的 review-skill 描述：",[255,3221,3223],{"className":2855,"code":3222,"language":2857,"meta":260,"style":260},"---\nname: review-pr\ndescription: 对当前分支与 main 的 diff 做完整代码审查\n---\n\n## 步骤\n1. 运行 `git diff main...HEAD --stat` 获取改动文件清单\n2. 对每个改动文件：\n   a. 读取改动内容\n   b. 对照 `.claude\u002Fskills\u002Fcode-review\u002Fchecklist.md`\n   c. 逐条标注 PASS \u002F FAIL \u002F N\u002FA\n3. 汇总输出：\n   - 总改动行数\n   - FAIL 项汇总（必须修复）\n   - 建议项汇总（推荐修复）\n   - 风险评估（合并到 main 的风险等级：低\u002F中\u002F高）\n4. 不自动修改文件，只输出报告\n\n## 禁止\n- 禁止假装通过（必须有具体证据）\n- 禁止跳过 FAIL 项（即使工程师认为不重要）\n",[236,3224,3225,3230,3240,3250,3254,3258,3265,3283,3291,3296,3309,3314,3322,3330,3337,3344,3351,3359,3363,3370,3377],{"__ignoreMap":260},[264,3226,3227],{"class":266,"line":267},[264,3228,3229],{"class":289},"---\n",[264,3231,3232,3235,3237],{"class":266,"line":274},[264,3233,3234],{"class":357},"name",[264,3236,304],{"class":289},[264,3238,3239],{"class":550}," review-pr\n",[264,3241,3242,3245,3247],{"class":266,"line":345},[264,3243,3244],{"class":357},"description",[264,3246,304],{"class":289},[264,3248,3249],{"class":550}," 对当前分支与 main 的 diff 做完整代码审查\n",[264,3251,3252],{"class":266,"line":380},[264,3253,3229],{"class":289},[264,3255,3256],{"class":266,"line":413},[264,3257,423],{"emptyLinePlaceholder":422},[264,3259,3260,3262],{"class":266,"line":419},[264,3261,2876],{"class":289},[264,3263,3264],{"class":293},"步骤\n",[264,3266,3267,3270,3273,3275,3278,3280],{"class":266,"line":426},[264,3268,3269],{"class":289},"1.",[264,3271,3272],{"class":310}," 运行 ",[264,3274,2127],{"class":289},[264,3276,3277],{"class":550},"git diff main...HEAD --stat",[264,3279,2127],{"class":289},[264,3281,3282],{"class":310}," 获取改动文件清单\n",[264,3284,3285,3288],{"class":266,"line":432},[264,3286,3287],{"class":289},"2.",[264,3289,3290],{"class":310}," 对每个改动文件：\n",[264,3292,3293],{"class":266,"line":683},[264,3294,3295],{"class":310},"   a. 读取改动内容\n",[264,3297,3298,3301,3303,3306],{"class":266,"line":691},[264,3299,3300],{"class":310},"   b. 对照 ",[264,3302,2127],{"class":289},[264,3304,3305],{"class":550},".claude\u002Fskills\u002Fcode-review\u002Fchecklist.md",[264,3307,3308],{"class":289},"`\n",[264,3310,3311],{"class":266,"line":713},[264,3312,3313],{"class":310},"   c. 逐条标注 PASS \u002F FAIL \u002F N\u002FA\n",[264,3315,3316,3319],{"class":266,"line":744},[264,3317,3318],{"class":289},"3.",[264,3320,3321],{"class":310}," 汇总输出：\n",[264,3323,3324,3327],{"class":266,"line":751},[264,3325,3326],{"class":289},"   -",[264,3328,3329],{"class":310}," 总改动行数\n",[264,3331,3332,3334],{"class":266,"line":773},[264,3333,3326],{"class":289},[264,3335,3336],{"class":310}," FAIL 项汇总（必须修复）\n",[264,3338,3339,3341],{"class":266,"line":817},[264,3340,3326],{"class":289},[264,3342,3343],{"class":310}," 建议项汇总（推荐修复）\n",[264,3345,3346,3348],{"class":266,"line":824},[264,3347,3326],{"class":289},[264,3349,3350],{"class":310}," 风险评估（合并到 main 的风险等级：低\u002F中\u002F高）\n",[264,3352,3353,3356],{"class":266,"line":829},[264,3354,3355],{"class":289},"4.",[264,3357,3358],{"class":310}," 不自动修改文件，只输出报告\n",[264,3360,3361],{"class":266,"line":835},[264,3362,423],{"emptyLinePlaceholder":422},[264,3364,3365,3367],{"class":266,"line":857},[264,3366,2876],{"class":289},[264,3368,3369],{"class":293},"禁止\n",[264,3371,3372,3374],{"class":266,"line":888},[264,3373,2884],{"class":289},[264,3375,3376],{"class":310}," 禁止假装通过（必须有具体证据）\n",[264,3378,3379,3381],{"class":266,"line":895},[264,3380,2884],{"class":289},[264,3382,3383],{"class":310}," 禁止跳过 FAIL 项（即使工程师认为不重要）\n",[216,3385,3387],{"id":3386},"_45-pr-描述与-commit-message-的-ai-协作","4.5 PR 描述与 Commit Message 的 AI 协作",[195,3389,3390],{},"PR 描述与 Commit Message 是「未来的自己」最常依赖的文档。让 Claude 在每次 commit 前自动生成草稿，工程师只做微调，可以让仓库历史的可读性大幅提升。一个推荐的 commit message 模板（中文）：",[255,3392,3395],{"className":3393,"code":3394,"language":502},[500],"\u003C类型>(\u003C范围>): \u003C简短描述>\n\n\u003C详细说明：为什么改、改了什么、影响什么>\n\n\u003C关联：Issue \u002F 需求文档 \u002F 设计稿>\n",[236,3396,3394],{"__ignoreMap":260},[195,3398,3399],{},"类型遵循 Conventional Commits 规范（feat\u002Ffix\u002Fdocs\u002Frefactor\u002Ftest\u002Fchore\u002Fperf）。注意把这条规则写进 CLAUDE.md，否则 Claude 默认会用英文。",[195,3401,3402],{},"PR 描述则更进一步——它不仅描述「改了什么」，还要描述「为什么改」「如何验证」「回滚方案」。这四要素是工程团队应对生产事故时最依赖的信息。让 Claude 在生成 PR 描述时强制包含这四个章节，可以让仓库的「事故响应能力」整体上一个台阶。一个推荐的 PR 描述模板：",[255,3404,3406],{"className":2855,"code":3405,"language":2857,"meta":260,"style":260},"## 改了什么\n- 新增 `parseDuration` 函数，支持 d\u002Fh\u002Fm\u002Fs 后缀\n- 修改 `OrderService.create` 接收新的 timeout 参数\n\n## 为什么改\n- 关联需求：PROD-1234 订单超时配置化\n- 当前硬编码 30 分钟，运营无法按品类调整\n\n## 如何验证\n- 单元测试：`pnpm vitest run tests\u002FparseDuration.spec.ts`\n- 集成测试：在 staging 环境创建一笔订单，确认 timeout 生效\n- 手工验证：管理后台「订单配置」页面调整后立即生效\n\n## 回滚方案\n- 改动是向后兼容的：旧调用方不传 timeout 时使用默认 30 分钟\n- 出现问题时直接 revert commit `\u003Chash>`，无需数据迁移\n",[236,3407,3408,3415,3431,3448,3452,3459,3466,3473,3477,3484,3498,3505,3512,3516,3523,3530],{"__ignoreMap":260},[264,3409,3410,3412],{"class":266,"line":267},[264,3411,2876],{"class":289},[264,3413,3414],{"class":293},"改了什么\n",[264,3416,3417,3419,3422,3424,3426,3428],{"class":266,"line":274},[264,3418,2884],{"class":289},[264,3420,3421],{"class":310}," 新增 ",[264,3423,2127],{"class":289},[264,3425,591],{"class":550},[264,3427,2127],{"class":289},[264,3429,3430],{"class":310}," 函数，支持 d\u002Fh\u002Fm\u002Fs 后缀\n",[264,3432,3433,3435,3438,3440,3443,3445],{"class":266,"line":345},[264,3434,2884],{"class":289},[264,3436,3437],{"class":310}," 修改 ",[264,3439,2127],{"class":289},[264,3441,3442],{"class":550},"OrderService.create",[264,3444,2127],{"class":289},[264,3446,3447],{"class":310}," 接收新的 timeout 参数\n",[264,3449,3450],{"class":266,"line":380},[264,3451,423],{"emptyLinePlaceholder":422},[264,3453,3454,3456],{"class":266,"line":413},[264,3455,2876],{"class":289},[264,3457,3458],{"class":293},"为什么改\n",[264,3460,3461,3463],{"class":266,"line":419},[264,3462,2884],{"class":289},[264,3464,3465],{"class":310}," 关联需求：PROD-1234 订单超时配置化\n",[264,3467,3468,3470],{"class":266,"line":426},[264,3469,2884],{"class":289},[264,3471,3472],{"class":310}," 当前硬编码 30 分钟，运营无法按品类调整\n",[264,3474,3475],{"class":266,"line":432},[264,3476,423],{"emptyLinePlaceholder":422},[264,3478,3479,3481],{"class":266,"line":683},[264,3480,2876],{"class":289},[264,3482,3483],{"class":293},"如何验证\n",[264,3485,3486,3488,3491,3493,3496],{"class":266,"line":691},[264,3487,2884],{"class":289},[264,3489,3490],{"class":310}," 单元测试：",[264,3492,2127],{"class":289},[264,3494,3495],{"class":550},"pnpm vitest run tests\u002FparseDuration.spec.ts",[264,3497,3308],{"class":289},[264,3499,3500,3502],{"class":266,"line":713},[264,3501,2884],{"class":289},[264,3503,3504],{"class":310}," 集成测试：在 staging 环境创建一笔订单，确认 timeout 生效\n",[264,3506,3507,3509],{"class":266,"line":744},[264,3508,2884],{"class":289},[264,3510,3511],{"class":310}," 手工验证：管理后台「订单配置」页面调整后立即生效\n",[264,3513,3514],{"class":266,"line":751},[264,3515,423],{"emptyLinePlaceholder":422},[264,3517,3518,3520],{"class":266,"line":773},[264,3519,2876],{"class":289},[264,3521,3522],{"class":293},"回滚方案\n",[264,3524,3525,3527],{"class":266,"line":817},[264,3526,2884],{"class":289},[264,3528,3529],{"class":310}," 改动是向后兼容的：旧调用方不传 timeout 时使用默认 30 分钟\n",[264,3531,3532,3534,3537,3539,3542,3544],{"class":266,"line":824},[264,3533,2884],{"class":289},[264,3535,3536],{"class":310}," 出现问题时直接 revert commit ",[264,3538,2127],{"class":289},[264,3540,3541],{"class":550},"\u003Chash>",[264,3543,2127],{"class":289},[264,3545,3546],{"class":310},"，无需数据迁移\n",[195,3548,3549,3550,3553],{},"这种结构化的 PR 描述让 reviewer 不必反复在 IM 上追问「这个改动到底想解决什么问题」。把模板存放在 ",[236,3551,3552],{},".github\u002Fpull_request_template.md"," 中，结合 Claude 自动生成，工程师只需做最后一道润色。",[208,3555],{},[211,3557,3559],{"id":3558},"五质量调优从能跑到好用","五、质量调优——从「能跑」到「好用」",[216,3561,3563],{"id":3562},"_51-性能调优让-claude-阅读-profiling-数据并提出假设","5.1 性能调优：让 Claude 阅读 profiling 数据并提出假设",[221,3565,3566],{},[195,3567,3568],{},"给产品经理：性能调优是把「能跑」的代码变成「跑得快」「跑得省资源」的代码。它分两步：先测量瓶颈在哪，再针对性优化。永远不要凭直觉优化——「过早优化是万恶之源」（Donald Knuth）。",[195,3570,3571,3572,3575,3576,3579,3580,3583],{},"测量阶段，常用工具有：Chrome DevTools Performance 面板、Node.js 内置的 ",[236,3573,3574],{},"--inspect"," + ",[236,3577,3578],{},"--prof","、Vue DevTools 的 Performance 面板、",[236,3581,3582],{},"node --cpu-prof"," 生成的 V8 火焰图。把 profiling 输出保存为 JSON 或 HTML 后，可以直接喂给 Claude：",[255,3585,3588],{"className":3586,"code":3587,"language":502},[500],"请阅读附件中的 cpu-profile.json（V8 CPU profiler 输出），分析：\n1. 占用 CPU 时间最多的前 5 个函数\n2. 调用栈最深的路径\n3. 提出 3 个优化假设，每个假设：\n   - 假设的瓶颈成因\n   - 验证方法（如何确认假设成立）\n   - 修复方案（如果假设成立）\n4. 不要直接动手改，先列假设\n",[236,3589,3587],{"__ignoreMap":260},[195,3591,3592],{},"这个对话流程的关键是「先假设、后验证、再修复」。LLM 喜欢直接跳到「修复方案」，但跳过假设环节常常会出现「优化了不是瓶颈的地方」。",[216,3594,3596],{"id":3595},"_52-内存与资源泄漏的对话式排查","5.2 内存与资源泄漏的对话式排查",[195,3598,3599],{},"内存泄漏（Memory Leak）在 Node.js \u002F 浏览器场景中常见的根因：未清理的 setInterval、未解绑的 EventListener、闭包持有大对象、缓存无 TTL（Time To Live）。Vue\u002FReact 应用中还有「组件卸载未清理副作用」这种典型问题。",[195,3601,3602],{},"排查时把 Heap Snapshot（堆快照）的对比结果交给 Claude，让它识别「时间点 A 到时间点 B 之间，哪类对象数量增长异常」。一旦定位到嫌疑类，再让它扫描代码寻找该类的创建点，最终找到泄漏根因。",[216,3604,3606],{"id":3605},"_53-错误处理与可观测性logging-tracing","5.3 错误处理与可观测性（Logging \u002F Tracing）",[195,3608,3609],{},"可观测性（Observability）是「事后理解系统行为」的能力，由三大支柱组成：日志（Logs）、指标（Metrics）、追踪（Traces）。",[195,3611,3612,3613,243,3616,3619],{},"让 Claude 帮你统一日志格式（推荐 JSON 结构化日志，便于后续聚合）、规范错误码（按业务模块前缀化，如 ",[236,3614,3615],{},"ORD_NOT_FOUND",[236,3617,3618],{},"PAY_GATEWAY_TIMEOUT","）、添加 OpenTelemetry（CNCF 标准追踪协议）的关键 span。一个常见的反模式是「到处 console.log」，应当让 Claude 在 review 阶段把这些 console.log 替换为结构化 logger 调用。",[216,3621,3623],{"id":3622},"_54-用户体验调优从崩溃率首屏时间到交互流畅度","5.4 用户体验调优：从崩溃率、首屏时间到交互流畅度",[195,3625,3626],{},"前端质量调优的核心指标是 Web Vitals（Google 提出的用户体验三件套）：LCP（Largest Contentful Paint，最大内容绘制时间，目标 \u003C 2.5s）、INP（Interaction to Next Paint，交互响应时间，目标 \u003C 200ms）、CLS（Cumulative Layout Shift，累计布局偏移，目标 \u003C 0.1）。",[195,3628,3629],{},"让 Claude 阅读 Lighthouse 报告或 Real User Monitoring（真实用户监控）数据，按「影响最多用户的问题」排序，逐项给出修复建议。注意：影响 5% 用户的 LCP 抖动比影响 0.1% 用户的崩溃更值得优先修复——这是产品判断，不是技术判断。",[195,3631,3632,3633,3034,3636,3639],{},"在前端调优场景下，Claude 还擅长一个传统工程容易忽视的环节：「资源预算」（Performance Budget）。把首屏 JS 体积上限设为 200KB（gzipped）、字体文件总大小上限设为 100KB、首屏图片懒加载阈值设为视口外 200px——这些数字写进 CI 阶段的 ",[236,3634,3635],{},"bundlesize",[236,3637,3638],{},"size-limit"," 配置后，每一次 PR 都会被自动拦截超标。让 Claude 在每次新增依赖前先估算「这个依赖会让首屏 bundle 增加多少 KB」，可以避免「装一个工具库膨胀 50KB」的常见悲剧。",[195,3641,3642],{},"服务端调优同理——常见做法是设定「每个 HTTP 接口 P99 响应时间不超过 500ms」「每个数据库查询不超过 50ms」「每条消息处理不超过 200ms」。把这些阈值嵌入接口的集成测试或合成监控（Synthetic Monitoring），让 Claude 在新增接口时自动检查并提示风险。",[216,3644,3646],{"id":3645},"_55-渐进式重构让-claude-一次只改一个维度","5.5 渐进式重构：让 Claude 一次只改一个维度",[195,3648,3649],{},"重构（Refactoring）是「不改变外部行为的前提下改善内部结构」（Martin Fowler 定义）。AI 协作时，最危险的反模式是「一次大重构」——让 Claude 同时改架构、改命名、改类型、改测试，最终人工无法 review。",[195,3651,3652],{},"正确做法是「一次只改一个维度」：先单独改命名，跑测试，提交；再单独改类型，跑测试，提交；再单独改架构，跑测试，提交。每一步都让 git 历史可回滚。把这条规则写进 CLAUDE.md：「重构必须分维度、分批提交，单次提交不超过 200 行 diff」。",[208,3654],{},[211,3656,3658],{"id":3657},"六质量门禁的工程化","六、质量门禁的工程化",[216,3660,3662],{"id":3661},"_61-把测试lint类型检查安全扫描接入-hooks","6.1 把测试、Lint、类型检查、安全扫描接入 Hooks",[195,3664,3665,3666,3668],{},"Hooks（参考 5.4 节）是 Claude Code 在工具调用前\u002F后执行命令的扩展点。把质量检查接入 Hooks，可以让「写代码 = 自动跑检查」成为不可绕过的肌肉记忆。一个 ",[236,3667,3096],{}," 的示例：",[255,3670,3672],{"className":3100,"code":3671,"language":1471,"meta":260,"style":260},"{\n  \"hooks\": {\n    \"PostToolUse\": [\n      {\n        \"matcher\": \"Edit|Write\",\n        \"command\": \"pnpm exec tsc --noEmit && pnpm exec eslint --max-warnings=0 \\\"$CLAUDE_TOOL_PATH\\\"\"\n      }\n    ],\n    \"Stop\": [\n      {\n        \"command\": \"pnpm exec vitest run --reporter=dot && echo 'all tests passed'\"\n      }\n    ]\n  }\n}\n",[236,3673,3674,3678,3690,3703,3707,3725,3748,3752,3757,3770,3774,3791,3795,3799,3803],{"__ignoreMap":260},[264,3675,3676],{"class":266,"line":267},[264,3677,342],{"class":289},[264,3679,3680,3682,3684,3686,3688],{"class":266,"line":274},[264,3681,3112],{"class":289},[264,3683,3115],{"class":281},[264,3685,3118],{"class":289},[264,3687,304],{"class":289},[264,3689,604],{"class":289},[264,3691,3692,3694,3697,3699,3701],{"class":266,"line":345},[264,3693,3127],{"class":289},[264,3695,3696],{"class":293},"PostToolUse",[264,3698,3118],{"class":289},[264,3700,304],{"class":289},[264,3702,3137],{"class":289},[264,3704,3705],{"class":266,"line":380},[264,3706,3142],{"class":289},[264,3708,3709,3711,3713,3715,3717,3719,3721,3723],{"class":266,"line":413},[264,3710,3147],{"class":289},[264,3712,3150],{"class":367},[264,3714,3118],{"class":289},[264,3716,304],{"class":289},[264,3718,3157],{"class":289},[264,3720,3160],{"class":550},[264,3722,3118],{"class":289},[264,3724,2087],{"class":289},[264,3726,3727,3729,3731,3733,3735,3737,3740,3742,3744,3746],{"class":266,"line":419},[264,3728,3147],{"class":289},[264,3730,3171],{"class":367},[264,3732,3118],{"class":289},[264,3734,304],{"class":289},[264,3736,3157],{"class":289},[264,3738,3739],{"class":550},"pnpm exec tsc --noEmit && pnpm exec eslint --max-warnings=0 ",[264,3741,3183],{"class":310},[264,3743,3186],{"class":550},[264,3745,3183],{"class":310},[264,3747,3194],{"class":289},[264,3749,3750],{"class":266,"line":426},[264,3751,3199],{"class":289},[264,3753,3754],{"class":266,"line":432},[264,3755,3756],{"class":289},"    ],\n",[264,3758,3759,3761,3764,3766,3768],{"class":266,"line":683},[264,3760,3127],{"class":289},[264,3762,3763],{"class":293},"Stop",[264,3765,3118],{"class":289},[264,3767,304],{"class":289},[264,3769,3137],{"class":289},[264,3771,3772],{"class":266,"line":691},[264,3773,3142],{"class":289},[264,3775,3776,3778,3780,3782,3784,3786,3789],{"class":266,"line":713},[264,3777,3147],{"class":289},[264,3779,3171],{"class":367},[264,3781,3118],{"class":289},[264,3783,304],{"class":289},[264,3785,3157],{"class":289},[264,3787,3788],{"class":550},"pnpm exec vitest run --reporter=dot && echo 'all tests passed'",[264,3790,3194],{"class":289},[264,3792,3793],{"class":266,"line":744},[264,3794,3199],{"class":289},[264,3796,3797],{"class":266,"line":751},[264,3798,3204],{"class":289},[264,3800,3801],{"class":266,"line":773},[264,3802,2169],{"class":289},[264,3804,3805],{"class":266,"line":817},[264,3806,416],{"class":289},[195,3808,3809],{},"PostToolUse 在每次文件修改后跑类型检查与 Lint，Stop 在 Claude 即将停止时跑全量测试。这两层 Hook 配合，可以把 95% 的低级错误拦在本机。",[195,3811,3812],{},"工程实践中还有第三层值得加上——「PrePush Hook」，在 git push 前跑一次完整的本地 CI。这一层的价值在于：如果远程 CI 跑全量需要 10 分钟，本地 PrePush 跑相同检查只需要 2 分钟（因为可以跳过镜像构建、跨平台兼容性等步骤），把失败拦截在 push 之前可以省下大量「等 CI 失败再来一次」的循环时间。Husky（Git Hooks 管理工具）+ lint-staged（只对暂存文件跑检查）是最常见的本地实现组合。",[195,3814,3815,3816,3819],{},"需要警惕的是「Hook 层级过多导致的开发体验下降」。如果每次保存文件都要等 30 秒检查通过，工程师会本能地寻找绕过方式（例如 ",[236,3817,3818],{},"--no-verify","）。健康的 Hook 设计是「分层快速失败」——最快的检查放最前面（如 Lint \u003C 1s），最慢的检查放最后面（如全量测试），允许工程师在中间任何一层「短路」继续工作，但绝不允许跳过最终的 push 前检查。",[216,3821,3823],{"id":3822},"_62-plan-模式作为高风险变更的最后一道闸门","6.2 Plan 模式作为「高风险变更的最后一道闸门」",[195,3825,3826],{},"Plan 模式（Shift+Tab 切换）让 Claude 先输出执行计划、等待人工确认后再动手。它特别适合三类场景：数据库 schema 变更、生产配置文件修改、影响超过 10 个文件的批量重构。",[195,3828,3829,3830,3833,3834,3837],{},"实操经验是把这三类场景列入 CLAUDE.md 的硬规则：「修改 ",[236,3831,3832],{},"prisma\u002Fschema.prisma"," 必须先进入 Plan 模式」「修改 ",[236,3835,3836],{},".github\u002Fworkflows\u002F"," 必须先进入 Plan 模式」。这样 Claude 在动手前会自动切换，避免遗忘。",[216,3839,3841],{"id":3840},"_63-失败回滚git-二分-claude-协助定位","6.3 失败回滚：git 二分 + Claude 协助定位",[195,3843,3844,3845,3848],{},"一旦 CI 失败，最高效的定位手段是 ",[236,3846,3847],{},"git bisect","（二分查找首次引入 bug 的提交）。Claude 可以协助你写 bisect 脚本：",[255,3850,3854],{"className":3851,"code":3852,"language":3853,"meta":260,"style":260},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# 给 Claude：请生成一个 bisect 脚本，自动判断当前 commit 是好是坏\n# 判断标准：pnpm vitest run tests\u002Fregression\u002Forder.spec.ts 通过即为好\ngit bisect start\ngit bisect bad HEAD\ngit bisect good v1.0.0\ngit bisect run pnpm vitest run tests\u002Fregression\u002Forder.spec.ts\n","bash",[236,3855,3856,3861,3866,3877,3889,3901],{"__ignoreMap":260},[264,3857,3858],{"class":266,"line":267},[264,3859,3860],{"class":270},"# 给 Claude：请生成一个 bisect 脚本，自动判断当前 commit 是好是坏\n",[264,3862,3863],{"class":266,"line":274},[264,3864,3865],{"class":270},"# 判断标准：pnpm vitest run tests\u002Fregression\u002Forder.spec.ts 通过即为好\n",[264,3867,3868,3871,3874],{"class":266,"line":345},[264,3869,3870],{"class":293},"git",[264,3872,3873],{"class":550}," bisect",[264,3875,3876],{"class":550}," start\n",[264,3878,3879,3881,3883,3886],{"class":266,"line":380},[264,3880,3870],{"class":293},[264,3882,3873],{"class":550},[264,3884,3885],{"class":550}," bad",[264,3887,3888],{"class":550}," HEAD\n",[264,3890,3891,3893,3895,3898],{"class":266,"line":413},[264,3892,3870],{"class":293},[264,3894,3873],{"class":550},[264,3896,3897],{"class":550}," good",[264,3899,3900],{"class":550}," v1.0.0\n",[264,3902,3903,3905,3907,3910,3913,3916,3918],{"class":266,"line":419},[264,3904,3870],{"class":293},[264,3906,3873],{"class":550},[264,3908,3909],{"class":550}," run",[264,3911,3912],{"class":550}," pnpm",[264,3914,3915],{"class":550}," vitest",[264,3917,3909],{"class":550},[264,3919,3920],{"class":550}," tests\u002Fregression\u002Forder.spec.ts\n",[195,3922,3923],{},"定位到引入 bug 的 commit 后，让 Claude 阅读该 commit 的 diff、解释「这次改动为什么会触发 bug」，并给出修复方案。这套流程通常 5 分钟内能定位绝大多数回归问题。",[195,3925,3926],{},"补充一个进阶技巧：当 bisect 跨越的提交数量很大（例如几百个）时，可以让 Claude 先做「语义二分」——根据 commit message 的关键词预判嫌疑提交，例如优先验证包含「refactor」「rename」「upgrade」字样的提交。这种语义启发式定位常常能把 bisect 步数从 9 步压缩到 3 步。",[195,3928,3929],{},"另一种值得掌握的回滚策略是「特性开关回滚」（Feature Flag Rollback）。把每个新功能藏在一个特性开关后面（例如使用 LaunchDarkly、Unleash 或自建的开关服务），出问题时不需要 git revert + 重新部署，直接在配置面板把开关切回 off 即可。Claude 可以协助你识别哪些代码路径应该加开关、生成开关配置的样板代码、并为每个开关写一份简短的「启用\u002F关闭」运维 Runbook。",[208,3931],{},[211,3933,3935],{"id":3934},"七把质量内化为团队习惯","七、把质量内化为团队习惯",[221,3937,3938],{},[195,3939,3940],{},"给产品经理：所有工具、配置、Skills 加起来都比不上一件事——团队是否真的相信「质量是大家共同的责任」。AI 协作只能放大已有的文化，不能凭空创造文化。",[216,3942,3944],{"id":3943},"_71-周会上谈质量而不是只谈进度","7.1 周会上谈质量，而不是只谈进度",[195,3946,3947],{},"许多团队的周会只谈「这周做了什么、下周做什么」，质量话题只在事故发生后被动出现。健康的团队会在每次周会预留 10 分钟，固定问三个问题：本周覆盖率涨跌？本周线上错误率涨跌？本周有哪些「绿色 CI 但其实有问题」的差点出事？这种主动复盘会让 AI 协作产生的隐性问题被尽早识别。",[216,3949,3951],{"id":3950},"_72-把质量数据变成所有人都能看的仪表盘","7.2 把质量数据变成所有人都能看的仪表盘",[195,3953,3954],{},"测试覆盖率、Lint 警告数、类型 any 数量、PR 平均合并时长、生产错误率——把这五个数字放在团队首页或飞书\u002F钉钉机器人每日推送，会比任何说教都有效。让 Claude 协助生成数据采集脚本与可视化页面（推荐用 Grafana 或自建的简易 Dashboard），一次投入长期受益。",[216,3956,3958],{"id":3957},"_73-新人-onboarding-的质量传承","7.3 新人 Onboarding 的质量传承",[195,3960,3961,3962,3965],{},"每来一位新工程师，让 ta 在前两周做一件事——读完仓库根的 CLAUDE.md、",[236,3963,3964],{},".claude\u002Fskills\u002F"," 下所有 Skill、最近 20 个 PR 的 Review 评论。这个过程比任何文档培训都更高效，因为它把「团队的质量观」具象化为可阅读的对话历史。",[195,3967,3968],{},"更进一步，可以让新人在第一周完成一个「故意失败的练习」——在沙箱仓库里提交一段明知有 bug 的代码，观察团队的 CI、Hooks、AI Review 各自能在哪一层拦下来。如果某一层没拦住，说明那一层的规则需要补强；如果全部拦住了，新人也能直观地理解「我们的安全网到底有多密」。这种「红队演练」式的 Onboarding，会让新人对质量体系建立深刻的肌肉记忆。",[216,3970,3972],{"id":3971},"_74-与-ai-协作的反脆弱心态","7.4 与 AI 协作的「反脆弱」心态",[195,3974,3975],{},"Nassim Taleb 提出的「反脆弱」（Antifragile）概念在 AI 协作时代格外贴切——一个反脆弱的系统不仅能在压力下不崩溃，反而能从压力中变得更强。把这个理念套用到 Claude Code 工作流：每一次 AI 写错的代码、每一次 CI 拦下的问题、每一次线上事故，都应当转化为对 CLAUDE.md、Skills、Hooks 的具体增量改进。三个月之后，你的项目会演化成一个「AI 越用越聪明」的有机体，而不是「AI 越用越乱」的随机森林。",[195,3977,3978],{},"具体落实到对话层面，每周做一次「质量复盘对话」——把本周所有 CI 失败、所有线上告警、所有 review 中发现的问题贴给 Claude，让它分析共性、提炼模式、起草规则更新提案。工程师审查后采纳，规则就此固化进系统。这种「对话即治理」的工作方式，是传统工程团队没有过的全新形态。",[208,3980],{},[211,3982,3983],{"id":3983},"总结",[195,3985,3986],{},"测试、审查、调优、门禁这条链路，过去是工程师的「负担」，现在是 AI 与工程师协作的「主战场」。本节给出的所有对话脚本、配置片段、Skills 模板，目标只有一个——让质量从「事后补救」变成「事前内嵌」。当质量约束被结构化地嵌入对话流、Hook 流、CI 流之后，Claude Code 就从一个「会写代码的助手」升级为「会守护代码的伙伴」。下一节（6.5）我们将带着「质量已达标」的代码进入部署上线环节，看 Claude Code 如何协助处理 CI\u002FCD、灰度发布、生产监控、回滚演练，把整个交付链路做成一个端到端可观测、可回滚、可演进的工程系统。",[211,3988,3989],{"id":3989},"延伸阅读",[3991,3992,3993,4003,4010,4017,4024,4031,4038,4045,4052,4059,4066,4073,4080,4087,4094],"ul",{},[3994,3995,3996],"li",{},[3997,3998,4002],"a",{"href":3999,"rel":4000},"https:\u002F\u002Fdocs.anthropic.com\u002Fclaude\u002Fdocs\u002Fclaude-code",[4001],"nofollow","Anthropic Claude Code 官方文档",[3994,4004,4005],{},[3997,4006,4009],{"href":4007,"rel":4008},"https:\u002F\u002Fwww.youtube.com\u002Fresults?search_query=boris+cherny+claude+code",[4001],"Boris Cherny: Claude Code 工程心法（YouTube 公开演讲）",[3994,4011,4012],{},[3997,4013,4016],{"href":4014,"rel":4015},"https:\u002F\u002Fvitest.dev\u002F",[4001],"Vitest 官方文档",[3994,4018,4019],{},[3997,4020,4023],{"href":4021,"rel":4022},"https:\u002F\u002Fplaywright.dev\u002F",[4001],"Playwright 官方文档",[3994,4025,4026],{},[3997,4027,4030],{"href":4028,"rel":4029},"https:\u002F\u002Fmswjs.io\u002F",[4001],"Mock Service Worker（MSW）官方文档",[3994,4032,4033],{},[3997,4034,4037],{"href":4035,"rel":4036},"https:\u002F\u002Fstryker-mutator.io\u002F",[4001],"Stryker Mutator 变异测试工具",[3994,4039,4040],{},[3997,4041,4044],{"href":4042,"rel":4043},"https:\u002F\u002Fwww.mountaingoatsoftware.com\u002Fblog\u002Fthe-forgotten-layer-of-the-test-automation-pyramid",[4001],"Mike Cohn: The Forgotten Layer of the Test Pyramid",[3994,4046,4047],{},[3997,4048,4051],{"href":4049,"rel":4050},"https:\u002F\u002Fmartinfowler.com\u002Farticles\u002Fpractical-test-pyramid.html",[4001],"Martin Fowler: Test Pyramid",[3994,4053,4054],{},[3997,4055,4058],{"href":4056,"rel":4057},"https:\u002F\u002Fweb.dev\u002Fvitals\u002F",[4001],"Google Web Vitals 官方文档",[3994,4060,4061],{},[3997,4062,4065],{"href":4063,"rel":4064},"https:\u002F\u002Fopentelemetry.io\u002Fdocs\u002F",[4001],"OpenTelemetry 官方文档",[3994,4067,4068],{},[3997,4069,4072],{"href":4070,"rel":4071},"https:\u002F\u002Fwww.conventionalcommits.org\u002Fzh-hans\u002F",[4001],"Conventional Commits 规范",[3994,4074,4075],{},[3997,4076,4079],{"href":4077,"rel":4078},"https:\u002F\u002Fwww.istqb.org\u002Fcertifications\u002Fcertified-tester-foundation-level",[4001],"ISTQB 软件测试基础大纲",[3994,4081,4082],{},[3997,4083,4086],{"href":4084,"rel":4085},"https:\u002F\u002Fwww.oreilly.com\u002Flibrary\u002Fview\u002Ftest-driven-development\u002F0321146530\u002F",[4001],"Kent Beck: Test-Driven Development By Example",[3994,4088,4089],{},[3997,4090,4093],{"href":4091,"rel":4092},"https:\u002F\u002Frefactoring.com\u002F",[4001],"Martin Fowler: Refactoring（重构）官方资源站点",[3994,4095,4096],{},[3997,4097,4100],{"href":4098,"rel":4099},"https:\u002F\u002Fwww.anthropic.com\u002Fnews",[4001],"Anthropic: Claude Code Best Practices Blog",[4102,4103,4104],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":260,"searchDepth":267,"depth":274,"links":4106},[4107,4112,4119,4125,4132,4139,4144,4150,4151],{"id":213,"depth":274,"text":214,"children":4108},[4109,4110,4111],{"id":218,"depth":345,"text":219},{"id":438,"depth":345,"text":439},{"id":456,"depth":345,"text":457},{"id":479,"depth":274,"text":480,"children":4113},[4114,4115,4116,4117,4118],{"id":483,"depth":345,"text":484},{"id":1236,"depth":345,"text":1237},{"id":1266,"depth":345,"text":1267},{"id":1949,"depth":345,"text":1950},{"id":2449,"depth":345,"text":2450},{"id":2467,"depth":274,"text":2468,"children":4120},[4121,4122,4123,4124],{"id":2471,"depth":345,"text":2472},{"id":2515,"depth":345,"text":2516},{"id":2525,"depth":345,"text":2526},{"id":2541,"depth":345,"text":2542},{"id":2811,"depth":274,"text":2812,"children":4126},[4127,4128,4129,4130,4131],{"id":2815,"depth":345,"text":2816},{"id":2844,"depth":345,"text":2845},{"id":3082,"depth":345,"text":3083},{"id":3215,"depth":345,"text":3216},{"id":3386,"depth":345,"text":3387},{"id":3558,"depth":274,"text":3559,"children":4133},[4134,4135,4136,4137,4138],{"id":3562,"depth":345,"text":3563},{"id":3595,"depth":345,"text":3596},{"id":3605,"depth":345,"text":3606},{"id":3622,"depth":345,"text":3623},{"id":3645,"depth":345,"text":3646},{"id":3657,"depth":274,"text":3658,"children":4140},[4141,4142,4143],{"id":3661,"depth":345,"text":3662},{"id":3822,"depth":345,"text":3823},{"id":3840,"depth":345,"text":3841},{"id":3934,"depth":274,"text":3935,"children":4145},[4146,4147,4148,4149],{"id":3943,"depth":345,"text":3944},{"id":3950,"depth":345,"text":3951},{"id":3957,"depth":345,"text":3958},{"id":3971,"depth":345,"text":3972},{"id":3983,"depth":274,"text":3983},{"id":3989,"depth":274,"text":3989},"md",null,{"date":4155},"2026-04-26",{"title":142,"description":197},"mzEiTxfPHzvc-2b8Yggqy6kxBSX1TP-Gbxlzeuqd1wA",[4159,4161],{"title":138,"path":139,"stem":140,"description":4160,"children":-1},"多轮迭代开发",{"title":146,"path":147,"stem":148,"description":4162,"children":-1},"6.4 的尾声留下一句承诺：带着\"质量已达标\"的代码进入部署上线环节。这一节兑现承诺。部署不是开发的下游，而是产品价值真正开始流动的起点——代码静静躺在仓库里时它的价值是零，只有当真实用户在真实场景中调用它，价值才被点亮。",1777395310223]