ratchetRatchet
§ · Nested detail page

规则检测、fingerprint 与 ledger 深度解析

这一页拆 detector 与 baseline。sources/ratchet/hooks/lib/detect.js:279 是组合入口,sources/ratchet/hooks/lib/detect.js:286 到 sources/ratchet/hooks/lib/detect.js:290 串起五类检查,sources/ratchet/hooks/lib/detect.js:275 生成稳定 fingerprint。baseline 在 sources/ratchet/hooks/lib/baseline.js:21 写入 accepted 集合,sources/ratchet/hooks/lib/baseline.js:33 过滤旧项;state 在 sources/ratchet/hooks/lib/state.js:132 写 ledger,让团队看到复杂度趋势。

§ 00 · IMPORT CHAIN

导入链与契约

这一页拆 detector 与 baseline。sources/ratchet/hooks/lib/detect.js:279 是组合入口,sources/ratchet/hooks/lib/detect.js:286 到 sources/ratchet/hooks/lib/detect.js:290 串起五类检查,sources/ratchet/hooks/lib/detect.js:275 生成稳定 fingerprint。baseline 在 sources/ratchet/hooks/lib/baseline.js:21 写入 accepted 集合,sources/ratchet/hooks/lib/baseline.js:33 过滤旧项;state 在 sources/ratchet/hooks/lib/state.js:132 写 ledger,让团队看到复杂度趋势。

src/ratchet/hooks/lib/detect.js · L279-343JavaScript
279function inspect(change, context = {}) {
280  const added = addedLines(change.before, change.after);
281  const removed = addedLines(change.after, change.before);
282  if (!added.length && !isManifest(change.path)) return { added: [], removed, findings: [] };
283
284  const suppressed = suppressedLines(change.after);
285  const findings = [
286    ...scanManifest(change),
287    ...scanPatterns(change, added),
288    ...scanWrappers(change, added),
289    ...scanAbstractions(change, added, context),
290    ...scanDuplicates(change, added, context),
291  ].filter((finding) => !suppressed.has(finding.line));
292
293  const unique = new Map();
294  for (const finding of findings) {
295    const key = finding.tag + '|' + finding.what;
296    const previous = unique.get(key);
297    if (!previous || RANK[finding.confidence] < RANK[previous.confidence]) unique.set(key, finding);
298  }
299
300  const ordered = [...unique.values()].sort(
301    (a, b) => RANK[a.confidence] - RANK[b.confidence] || a.line - b.line
302  );
303  return { added, removed, findings: ordered };
304}
305
306function budgetFindings(session, budget) {
307  if (!budget) return [];
308  const findings = [];
309  const net = session.addedLines - (session.removedLines || 0);
310
311  if (session.newFiles.length > budget.newFiles) {
312    findings.push({
313      tag: 'budget', confidence: CERTAIN, path: '', line: 0,
314      what: session.newFiles.length + ' new files, budget is ' + budget.newFiles,
315      fix: 'fold the extras into files that already exist',
316    });
317  }
318  if (session.deps.length > budget.newDeps) {
319    findings.push({
320      tag: 'budget', confidence: CERTAIN, path: '', line: 0,
321      what: session.deps.length + ' new dependencies, budget is ' + budget.newDeps,
322      fix: 'drop ' + session.deps.slice(budget.newDeps).join(', '),
323    });
324  }
325  if (net > budget.addedLines) {
326    findings.push({
327      tag: 'budget', confidence: CERTAIN, path: '', line: 0,
328      what: net + ' net added lines, budget is ' + budget.addedLines,
329      fix: 'stop adding and start deleting, or raise the budget explicitly',
330    });
331  }
332  return findings;
333}
334
335function format(finding) {
336  const where = finding.path ? finding.path + ':' + finding.line + ' ' : '';
337  return where + finding.tag + ': ' + finding.what + '. ' + finding.fix + '.';
338}
339
340module.exports = {
341  CERTAIN, HEURISTIC, LIKELY, PATTERNS, RANK,
342  budgetFindings, fingerprint, format, inspect, splitLines, suppressedLines,
343};
§ 01 · CORE IMPL

核心实现,逐段拆

▸Detector pipeline

inspect 先用 sources/ratchet/hooks/lib/changes.js:9 算新增行,sources/ratchet/hooks/lib/detect.js:284 识别 ratchet-ignore,随后 sources/ratchet/hooks/lib/detect.js:286 扫依赖、sources/ratchet/hooks/lib/detect.js:287 扫标准库/native 模式、sources/ratchet/hooks/lib/detect.js:288 扫 wrapper、sources/ratchet/hooks/lib/detect.js:289 扫 YAGNI、sources/ratchet/hooks/lib/detect.js:290 扫重复符号。sources/ratchet/hooks/lib/detect.js:293 去重后按置信度排序。

src/ratchet/hooks/lib/detect.js · L279-343JavaScript
279function inspect(change, context = {}) {
280  const added = addedLines(change.before, change.after);
281  const removed = addedLines(change.after, change.before);
282  if (!added.length && !isManifest(change.path)) return { added: [], removed, findings: [] };
283
284  const suppressed = suppressedLines(change.after);
285  const findings = [
286    ...scanManifest(change),
287    ...scanPatterns(change, added),
288    ...scanWrappers(change, added),
289    ...scanAbstractions(change, added, context),
290    ...scanDuplicates(change, added, context),
291  ].filter((finding) => !suppressed.has(finding.line));
292
293  const unique = new Map();
294  for (const finding of findings) {
295    const key = finding.tag + '|' + finding.what;
296    const previous = unique.get(key);
297    if (!previous || RANK[finding.confidence] < RANK[previous.confidence]) unique.set(key, finding);
298  }
299
300  const ordered = [...unique.values()].sort(
301    (a, b) => RANK[a.confidence] - RANK[b.confidence] || a.line - b.line
302  );
303  return { added, removed, findings: ordered };
304}
305
306function budgetFindings(session, budget) {
307  if (!budget) return [];
308  const findings = [];
309  const net = session.addedLines - (session.removedLines || 0);
310
311  if (session.newFiles.length > budget.newFiles) {
312    findings.push({
313      tag: 'budget', confidence: CERTAIN, path: '', line: 0,
314      what: session.newFiles.length + ' new files, budget is ' + budget.newFiles,
315      fix: 'fold the extras into files that already exist',
316    });
317  }
318  if (session.deps.length > budget.newDeps) {
319    findings.push({
320      tag: 'budget', confidence: CERTAIN, path: '', line: 0,
321      what: session.deps.length + ' new dependencies, budget is ' + budget.newDeps,
322      fix: 'drop ' + session.deps.slice(budget.newDeps).join(', '),
323    });
324  }
325  if (net > budget.addedLines) {
326    findings.push({
327      tag: 'budget', confidence: CERTAIN, path: '', line: 0,
328      what: net + ' net added lines, budget is ' + budget.addedLines,
329      fix: 'stop adding and start deleting, or raise the budget explicitly',
330    });
331  }
332  return findings;
333}
334
335function format(finding) {
336  const where = finding.path ? finding.path + ':' + finding.line + ' ' : '';
337  return where + finding.tag + ': ' + finding.what + '. ' + finding.fix + '.';
338}
339
340module.exports = {
341  CERTAIN, HEURISTIC, LIKELY, PATTERNS, RANK,
342  budgetFindings, fingerprint, format, inspect, splitLines, suppressedLines,
343};

▸Baseline 与 mark/ledger

baseline 文件由 sources/ratchet/hooks/lib/baseline.js:21 写入,accepted fingerprints 会排序去重;过滤函数在 sources/ratchet/hooks/lib/baseline.js:33 只保留新问题。mark/ledger 则由 sources/ratchet/hooks/lib/state.js:118 读取、sources/ratchet/hooks/lib/state.js:126 写入、sources/ratchet/hooks/lib/state.js:132 追加,CLI report 在 sources/ratchet/bin/install.js:504 到 sources/ratchet/bin/install.js:541 展示趋势。

src/ratchet/hooks/lib/baseline.js · L21-38JavaScript
 21function write(root, fingerprints, note) {
 22  const file = baselinePath(root);
 23  fs.mkdirSync(path.dirname(file), { recursive: true });
 24  const payload = {
 25    recordedAt: new Date().toISOString(),
 26    note: note || 'findings that predate the ratchet',
 27    accepted: [...new Set(fingerprints)].sort(),
 28  };
 29  fs.writeFileSync(file, JSON.stringify(payload, null, 2) + '\n', 'utf8');
 30  return payload;
 31}
 32
 33function filter(findings, accepted, relativeFor) {
 34  if (!accepted || accepted.size === 0) return findings;
 35  return findings.filter((finding) => !accepted.has(fingerprint(finding, relativeFor(finding))));
 36}
 37
 38module.exports = { filter, read, write };
§ 02 · KEY FLOW

关键流程

addedLines + detectors + fingerprint + baseline filter + ledger trend

Ratchet 的噪音控制来自三处:sources/ratchet/hooks/lib/detect.js:177 支持本地 suppress,sources/ratchet/hooks/lib/baseline.js:33 支持 repo 级 accepted 集合,sources/ratchet/hooks/ratchet-guard.js:125 支持 session 内 reported 去重。三层合起来让工具能长期打开,而不是每次都重复提醒历史问题。

▸规则层的扩展入口

如果要扩展 Ratchet,应该先看 sources/ratchet/hooks/lib/detect.js:200 的 pattern scanner 和 sources/ratchet/hooks/lib/detect.js:209 的 wrapper scanner,再看 sources/ratchet/hooks/lib/detect.js:231 的 abstraction scanner。它们共享 sources/ratchet/hooks/lib/detect.js:194 的 finding 构造函数和 sources/ratchet/hooks/lib/detect.js:275 的 fingerprint 规则;新增检测若不能稳定 fingerprint,就会让 baseline 与 session 去重失效。

§ 03 · EDGE CASES

边界 / 失败模式

  • sources/ratchet/hooks/lib/changes.js:35 到 sources/ratchet/hooks/lib/changes.js:41 解析 JSON 失败会返回空依赖,manifest 语法坏掉时不会给误报。
  • sources/ratchet/hooks/lib/detect.js:231 的 YAGNI 依赖 countImplementations,语言不匹配时可能漏报,适合做建议而非硬阻断。
  • sources/ratchet/hooks/lib/state.js:75 依赖 git ls-files;未跟踪的大量生成文件不会进入 snapshot,这符合“看项目真实提交面”的定位。
Sub-page · 2026-08-05 Source · 0xwilliamortiz/ratchet ↑ ratchet 主页面