/** * Finding builder — assembles a `Finding` and computes its fingerprint. * * The fingerprint is a content-addressed hash of a small identity tuple chosen * by each rule. It is *not* the source location: two findings at different * lines for the same offending value should have different fingerprints, but * adding a blank line above them must not change either fingerprint. Rules * pick identity inputs that capture "what's wrong" (file, ruleId, componentId, * prop, property, value) without leaning on lineNumber/column. */ import type { GovernanceSeverity } from "../governance.js"; import type { FactEvidence, FactLocation } from "../facts/index.js"; import { canonicalJson, hash64Hex } from "../facts/ids.js"; import { byRuleId } from "../codes/index.js"; import { normalizeFinding, normalizeSeverity } from "../schemas/index.js"; import type { Severity } from "../schemas/index.js"; import type { Finding, FindingFix } from "./types.js"; export interface MakeFindingInput { ruleId: string; ruleVersion: string; severity: GovernanceSeverity | Severity; message: string; location: FactLocation; evidence: FactEvidence[]; fingerprintIdentity: Record; fix?: FindingFix; attributes?: Record; } export function makeFinding(input: MakeFindingInput): Finding { if (input.evidence.length === 0) { throw new Error(`makeFinding(${input.ruleId}): findings must carry at least one evidence fact`); } const fingerprint = hash64Hex( canonicalJson({ ruleId: input.ruleId, ...input.fingerprintIdentity }) ); const code = byRuleId.get(input.ruleId); return normalizeFinding({ ruleId: input.ruleId, ruleVersion: input.ruleVersion, severity: normalizeSeverity(input.severity), code: code?.code, helpUrl: code?.explainUrl, message: input.message, fingerprint, location: input.location, evidence: input.evidence, fix: input.fix, attributes: input.attributes, }); }