规则检测、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,让团队看到复杂度趋势。
导入链与契约
这一页拆 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,让团队看到复杂度趋势。
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};
核心实现,逐段拆
▸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 去重后按置信度排序。
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 展示趋势。
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 };
关键流程
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 去重失效。
边界 / 失败模式
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,这符合“看项目真实提交面”的定位。