首页
/ ECC Quality Gate 实战指南:Hook 驱动的单文件格式质量门禁原理、配置与手动校验

ECC Quality Gate 实战指南:Hook 驱动的单文件格式质量门禁原理、配置与手动校验

2026-09-07 16:29:23作者:昌雅子Ethen

导读

ECC(Everything Claude Code)的质量门禁(Quality Gate)是运行在 PostToolUse 阶段的轻量格式校验器:Agent 每编辑完一个文件,它会读取 hook 输入中携带的 file_path,按文件扩展名自动选择 Biome / Prettier / gofmt / ruff 对该文件做格式化一致性检查。本文以 commands/quality-gate.md 为骨架,结合 quality-gate.js 源码、resolve-formatter.js 解析层与 quality-gate.test.js 测试,讲清它的设计动机、文件类型覆盖矩阵、环境变量开关、手动运行方法与 hook 装配链路。读完你既能手动触发它做单文件格式核查,也能理解它在 Claude Code 等 harness 的 PostToolUse 分发器里究竟如何被调度。

一、命令定位:一条"操作员入口"而非独立 CLI

quality-gate 命令在仓库中是操作员(Operator)入口,对应一个真实存在的 hook 事件:post:quality-gate。它作为 PostToolUse hook 常驻运行,实现位于 quality-gate.js。与多数"命令行工具"不同,它不接收路径形式的 CLI 参数,而是完全由 hook 引擎的 stdin JSON 驱动。也就是说,日常使用中它由 Agent 在编辑文件后自动触发;手动运行只是模拟 hook 的 stdin 输入来复现同一条执行路径。

description: Run the ECC formatter quality gate for a single file and report remediation steps.

这条描述揭示了它的边界:只做格式化质量门禁(formatter quality gate),而且是单文件级别,覆盖的职责是"检查 + 报告整改步骤",而非完整 CI 流水线。

二、核心机制:hook 输入驱动的单文件校验

quality-gate.js 可以看到脚本的入口逻辑:

function run(rawInput) {
  try {
    const input = JSON.parse(rawInput);
    const filePath = String(input.tool_input?.file_path || '');
    maybeRunQualityGate(filePath);
  } catch {
    // Ignore parse errors.
  }
  return rawInput;
}

几个值得注意的设计点:

  • 目标文件来自 tool_input.file_path,而不是 argv。hook 引擎在 Edit/Write/MultiEdit 后投递的 JSON payload 中携带了被编辑文件路径,脚本直接解析即可。
  • run() 是"透传"(pass-through):无论校验结果如何,它始终原样返回 rawInput。即便 JSON 无法解析、tool_input 缺失或为空,也会被 catch 静默吞掉,绝不抛错阻断 Agent 的工具调用。
  • 脚本同时暴露了 module.exports = { run },这让它在 run-with-flags.js 的分发框架里可以通过 require() 直连执行(省掉一次 Node 子进程启动,约 50–100ms),而不是走 legacy 的 spawn 路径。
  • 独立运行时(require.main === module),stdin 读取有 1 MB 上限(MAX_STDIN,超出部分直接截断,防止超大 payload 拖垮进程。

maybeRunQualityGate 的前置条件在 quality-gate.js

if (!filePath || !fs.existsSync(filePath)) {
  return;
}
filePath = path.resolve(filePath);
const ext = path.extname(filePath).toLowerCase();
const fix = String(process.env.ECC_QUALITY_GATE_FIX || '').toLowerCase() === 'true';
const strict = String(process.env.ECC_QUALITY_GATE_STRICT || '').toLowerCase() === 'true';

文件不存在时直接 no-op;路径先 path.resolve 成绝对路径,便于后续项目根目录的向上查找;两个行为开关则完全来自环境变量(见第四节)。

三、文件类型覆盖矩阵:谁来校验哪个扩展名

门禁按扩展名分派到不同工具,完整对应关系如下表:

扩展名 工具与命令 说明
.ts / .tsx / .js / .jsx Biome check Prettier --check(以项目实际配置为准) 当项目使用 Biome 时被整体跳过,见下方解释
.json / .md Biome check 或 Prettier --check 即使用 Biome,这两类文件仍会走质量门禁
.go gofmt(fix 模式 -w,check 模式 -l 依赖系统 PATH 中的 gofmt
.py ruff format(check 模式追加 --check 依赖 ruff 可执行文件
其他扩展名 无操作 直接返回

为什么 Biome 下的 JS/TS 会被跳过

这是最容易困惑的一点。看 quality-gate.js 的分派逻辑:

if (formatter === 'biome') {
  // JS/TS already handled by post-edit-format via `biome check --write`
  if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
    return;
  }
  // .json / .md — still need quality gate
  ...
}

原因是仓库中另一个 hook——post-edit-format——在编辑完成后已经先执行过 biome check --write 做自动修复。如果质量门禁再对 JS/TS 跑一遍 check,属于重复劳动且必然通过,所以代码注释明确写"skip"。真正需要质量门禁兜底的是 Biome 场景下 .json.md(这两类不在 post-edit-format 的自动修复范围内),以及 Prettier 项目、Go、Python 的完整覆盖。因此可以把这条 gate 理解为对 post-edit-format 自动格式化管线的互补层

边界声明:Lint 与类型检查不在此列

原文档特意强调:lint 与类型检查不属于本门禁。当需要 lint / type / test 完整流水线时,应转向仓库的 verification-loop 技能(skills/verification-loop/SKILL.md)或各语言的验证类技能。质量门禁只回答一个问题:"这个文件是否符合项目的格式化标准?"

四、两个环境变量开关:FIX 与 STRICT

行为切换不通过 CLI flags,而是环境变量。源码比较是大小写不敏感的精确 'true' 匹配:

const fix = String(process.env.ECC_QUALITY_GATE_FIX || '').toLowerCase() === 'true';
const strict = String(process.env.ECC_QUALITY_GATE_STRICT || '').toLowerCase() === 'true';
环境变量 取值 作用
ECC_QUALITY_GATE_FIX true / 其他 true 时改为应用格式化修复:Biome 追加 --write、Prettier 用 --write、gofmt 用 -w、ruff 去掉 --check;否则只做只读检查
ECC_QUALITY_GATE_STRICT true / 其他 true 时把 formatter 非零退出码记录为 gate failure(向 stderr 写 [QualityGate] ... failed);默认不置位则校验失败仅"静默观察"

STRICT 模式下各类文件的失败判据(源码细节)

不同工具判断"失败"的方式并不一致,实现里做了差异化处理:

  • Biome / Prettier / ruff:以子进程退出码 result.status !== 0 为准(quality-gate.js)。
  • gofmt check 模式比较特殊(quality-gate.js):gofmt -l 即使发现文件未格式化也可能返回 0,它靠 stdout 列出有差异的文件名来表达结果。因此实现是:-l 非零退出 → 失败;退出码为 0 但 stdout 非空 → 也判失败。这里体现了 gofmt 与 JS 生态 formatter 语义差异的兼容处理。
  • 值得再次强调:日志不等于阻断。脚本始终透传原始输入且不设置非零退出码,strict 模式的作用是把失败"记下来、写进 stderr",供上层 Agent 在响应中报告发现并给出整改步骤——这正是该命令描述中 "report remediation steps" 的落点。

五、手动运行:模拟 hook 的 stdin JSON

要脱离 harness 手动对一个文件跑质量门禁,只需把 hook 风格 JSON 通过管道喂给脚本。默认是 check-only(只读检查):

echo '{"tool_input":{"file_path":"src/example.ts"}}' \
  | node scripts/hooks/quality-gate.js

需要自动修复严格报告时,在命令前设置环境变量:

echo '{"tool_input":{"file_path":"src/example.ts"}}' \
  | ECC_QUALITY_GATE_FIX=true node scripts/hooks/quality-gate.js
echo '{"tool_input":{"file_path":"src/example.ts"}}' \
  | ECC_QUALITY_GATE_STRICT=true node scripts/hooks/quality-gate.js

参数替换规则(原文档 "Arguments" 一节):quality-gate 命令接受一个可选的 [path] 说明参数,但脚本本身不读 argv。当你在命令层传入某个路径时,正确的做法是把该路径替换进上面 stdin JSON 的 tool_input.file_path 字段再执行:

echo '{"tool_input":{"file_path":"src/main.py"}}' \
  | ECC_QUALITY_GATE_STRICT=true node scripts/hooks/quality-gate.js

手动执行后,操作者的任务是报告 formatter 发现并给出具体整改步骤(例如把未格式化处交给 ECC_QUALITY_GATE_FIX=true 重跑,或按工具输出手工修正格式差异)。需要留意:脚本自身所有运行日志都走 process.stderr,stdout 只透传原始 JSON 输入。

六、执行一次门禁的完整调用链

一次门禁从文件路径到 formatter 落地,依次经过以下环节(对应 quality-gate.js 的函数):

  1. 入口 run(rawInput):JSON 解析 → 提取 file_path → 交给 maybeRunQualityGate,全程容错。
  2. 文件检查:路径为空或文件不存在即返回(quality-gate.js)。这一点被测试大量覆盖,属于"缺文件静默放行"的安全设计。
  3. 扩展名分派:JS/TS 家族的 JSON/MD、.go.py 三类分支各走各的工具。
  4. formatter 探测(仅 JS 家族需要):调用 detectFormatter(projectRoot),见第七节。
  5. 执行:统一走内部 exec 封装(quality-gate.js):
function exec(command, args, cwd = process.cwd()) {
  return spawnSync(command, args, {
    cwd,
    encoding: 'utf8',
    env: process.env,
    timeout: 15000
  });
}

每个子进程都被限定 15 秒超时、UTF-8 编码、继承当前环境变量,且在工作目录为项目根的前提下运行——保证像 npx/本地 node_modules/.bin 这类相对解析能正常工作。

  1. 结果判定与日志:strict 模式下非零状态/未格式化输出会以 [QualityGate] <工具> check failed for <path> 形式写 stderr。

七、formatter 探测与解析层:resolve-formatter.js

quality-gate.js 自己不做任何 formatter 发现工作,而是复用共享模块 resolve-formatter.js。该模块被 post-edit-format.jsquality-gate.js 共同依赖,把"找项目根、探测 formatter、解析二进制"三件事统一起来并带进程级缓存。

findProjectRoot:沿目录向上找项目标记

const PROJECT_ROOT_MARKERS = ['package.json', ...BIOME_CONFIGS, ...PRETTIER_CONFIGS];

从目标文件所在目录逐级向上,直到遇到 package.json、Biome 或 Prettier 配置之一即停;刻意在进入用户主目录(os.homedir())前停止,避免把 ~/.prettierrc 之类的全局 dotfile 误判为项目根(resolve-formatter.js)。未找到任何标记时回退到起始目录。

detectFormatter:Biome 优先于 Prettier

探测顺序有明确优先级(resolve-formatter.js):

  1. 项目根存在 biome.json / biome.jsonc → 判定为 biome
  2. package.json 含顶层 prettier 键 → 判定为 prettier(注意它在 Prettier 配置文件之前被检查);
  3. 存在任意 Prettier 配置文件(.prettierrc.json/.js/.cjs/.mjs/.yml/.yaml/.toml 变体,或 prettier.config.js/.cjs/.mjs)→ 判定为 prettier
  4. 都没有 → null,此时质量门禁对 JS 家族文件直接跳过。

resolveFormatterBin:本地安装优先,包管理器兜底

找到 formatter 后还需要确定"用什么命令执行"(resolve-formatter.js):

  • node_modules/.bin/biomenode_modules/.bin/prettier 存在,直接用本地可执行文件(prefix 为空数组),避免包解析开销;
  • 否则回退到包管理器 runner:读取项目 package manager 配置的 execCmd(默认 npx,也支持 pnpm / yarn / bunx),并把包名(@biomejs/biomeprettier)拼到 prefix。例如最终命令形如 npx @biomejs/biome check <file>
  • Windows 平台会把 runner 名映射为 .cmd shim(npx.cmdpnpm.cmd 等);
  • 三组查找都带 Map 缓存(projectRootCache / formatterCache / binCache),测试可用 clearCaches() 复位。

八、Hook 装配链路:从 hooks.json 到 posttooluse-dispatcher

原文档指出 hook 接线经由 hooks/hooks.json 中的异步 PostToolUse 分发器进入,内部注册表保留了 post:quality-gate 标识符与 standard/strict profiles。这一点在 posttooluse-dispatcher.js 中得到印证:

{ id: 'post:quality-gate', matcher: 'Edit|Write|MultiEdit', profiles: 'standard,strict', script: 'scripts/hooks/quality-gate.js', run: runQualityGate },

完整的调度链如下:

  1. 用户在 Claude Code / Codex 等 harness 中执行 Edit / Write / MultiEdit 工具;
  2. hooks.jsonPostToolUse 段注册了 post:dispatcher:sync(同步)与 post:dispatcher:async(后台)两个分发入口,均由 posttooluse-dispatcher.js 统一接管;
  3. dispatcher 内部按注册表匹配事件:Edit|Write|MultiEdit 命中 post:quality-gate,profile 限定为 standard,strict
  4. 由于 quality-gate.js 导出了 run()run-with-flags.js 会走"直接 require() + 调用 run(raw)"的快速路径;同时 run-with-flags.js 还会先用 isHookEnabled() 校验 hook profile 是否启用、用 ECC_HOOK_ID 等环境变量传递执行上下文;
  5. 门禁对刚写入的文件执行第三节的格式化检查。

整套结构可以概括为:hooks.json 只声明事件挂载点,posttooluse-dispatcher 持有注册表与 profile 语义,run-with-flags 负责进程复用与开关判断,quality-gate 专注单文件格式化校验,四层各司其职。

九、测试验证:pass-through 行为是质量基线

tests/hooks/quality-gate.test.js(运行方式:node tests/hooks/quality-gate.test.js)把"永远不破坏 Agent 工具调用"这一安全属性测到了非常细的粒度,核心断言是:run() 返回的字符串必须与输入完全一致。测试分组包括:

  • 合法 JSON:带 file_path、不带 file_path、嵌套结构(额外 other 数组)——都原样返回;
  • 非法 JSONthis is not json at all {{{、截断的 {"tool_input": {、尾部垃圾 {"tool_input": {}}extra——都不崩溃、原样透传;
  • 文件不存在:指向 /tmp 下不存在的 .js / .py / .go 路径——均 no-op 透传;
  • 空输入与空结构:空字符串、纯空白、{}tool_input: nullfile_path: ''——全部优雅处理;
  • 真实文件但未配置 formatter:临时写一个 .js 文件后运行——因找不到 formatter 配置而跳过,仍然透传。

这套测试同时验证了第五节边界:在缺工具、缺文件、坏输入的环境里,门禁宁可什么都不做也不许阻塞 Agent。失败语义(strict 下的 stderr 日志)属于运行期行为,测试覆盖的是它的"无破坏性"契约。

十、典型整改工作流

手动或自动触发门禁得到 [QualityGate] ... failed 记录后,可按工具类型做如下整改:

场景 建议整改步骤
Prettier 项目 check 失败 ECC_QUALITY_GATE_FIX=true 重跑让 Prettier --write 直接落盘,或本地执行 npx prettier --write <file>
Biome 项目的 .json/.md 失败 重跑并让 Biome check --write 修复(Biome 下 JS/TS 由 post-edit-format 覆盖,不需在此处理)
.go 未格式化 重跑(gofmt -w 修复)或手工按 gofmt -l 指出的文件修正缩进/对齐
.py 格式失败 重跑启用 ruff format <file>(等价修复),或本地执行 ruff format <file>
项目本就不含任何 formatter 配置 这是预期行为——门禁跳过 JS/TS,无需整改;如需启用请为项目引入 Biome 或 Prettier 配置

修复后建议用 check-only 模式再跑一次做回归确认;若要进入更严格的 lint/type/test 流水线,则切换到 verification-loop 技能(skills/verification-loop/SKILL.md)完成后续验证。

十一、易错点与边界小结

  • 不要传 CLI 路径参数:脚本不解析 argv,传参无效;一切通过 stdin JSON 的 tool_input.file_path
  • 环境变量需精确为 truefix = 'TRUE' 也会因为先 toLowerCase() 再比较而被正确识别,但空串、1yes 都不生效。
  • JS/TS 在 Biome 项目里不报错是特性不是 bug:它们已被 post-edit-formatbiome check --write 自动处理。
  • 门禁是"报告式"而非"拦截式":透传 stdout、不设失败退出码,strict 只负责写 stderr 失败记录——真正的"拦截"动作由上层 Agent 依据日志决策。
  • .py/.go 依赖全局工具gofmtruff 需在 PATH 中可执行;缺失时无显式报错,属于 fail-open。
  • 语言覆盖有限:只覆盖 JS/TS/JSON/Markdown(经 Biome/Prettier)、Go、Python;其余扩展名一律跳过。Lint、类型检查、测试不在此门禁范围内。

十二、延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388