/** * lib/audit-finding.ts — reading an audit finding, whatever pack wrote it. * * The audit families never settled on one finding shape: * * | family | identity | text | proof | where | * |-------------------------------|----------|----------------------|------------|---------------------| * | audit-ba | `ruleId` | `message` | `evidence` | `report.findings[]` | * | audit-dev-* layer packs | `code` | `solution` / `label` | — | `report.findings[]` | * | audit-dev-actions-alignment | `code` | `message` | — | `report.findings[]` | * | audit-dev-customised | `code` | `detail` | — | `report.findings[]` | * | audit-dev-tests | `rule` | `message` | `refs` | `findings[]` (root) | * * A reader that spells ONE of them is blind to the others, silently: the * /support-report verifier required `ruleId`, so every `audit-dev-*` envelope * parsed to ZERO findings and a verdict dispute against DEV-UI-034 was refused * as « not in the envelope findings (seen: none) » on an envelope carrying 45 * of them (support report 1766f66848a9). /audit-fix already knew — it spelled * `f['code']` on its dev side. Both read through here now. * * Renaming the fields at the emitters is not the fix: `code` is a published * envelope contract of the dev packs (« do not rename »), consumed by the gates * of /ba-develop. * * Pure, structural, no I/O. */ /** The field an audit finding carries its rule identity under, in read order. */ export const FINDING_RULE_ID_KEYS = ['ruleId', 'code', 'rule'] as const /** The field it carries its human text under, in read order. */ export const FINDING_MESSAGE_KEYS = ['message', 'solution', 'detail', 'label'] as const /** The field it carries its proof lines under, in read order. */ export const FINDING_EVIDENCE_KEYS = ['evidence', 'refs'] as const function asRecord(raw: unknown): Record | null { return typeof raw === 'object' && raw !== null && !Array.isArray(raw) ? (raw as Record) : null } /** The rule identity of one finding — null when it carries none (a malformed * item is dropped by the caller, never guessed). */ export function findingRuleIdOf(raw: unknown): string | null { const r = asRecord(raw) if (r === null) return null for (const key of FINDING_RULE_ID_KEYS) { const v = r[key] if (typeof v === 'string' && v.trim() !== '') return v } return null } /** The human text of one finding — `''` when it carries none. */ export function findingMessageOf(raw: unknown): string { const r = asRecord(raw) if (r === null) return '' for (const key of FINDING_MESSAGE_KEYS) { const v = r[key] if (typeof v === 'string' && v !== '') return v } return '' } /** The proof lines of one finding — `[]` when it carries none. */ export function findingEvidenceOf(raw: unknown): string[] { const r = asRecord(raw) if (r === null) return [] for (const key of FINDING_EVIDENCE_KEYS) { const v = r[key] if (Array.isArray(v)) return v.map(String) } return [] } /** * The raw findings of a parsed audit output: `report.findings[]` (the envelope * of lib/output.ts), else `findings[]` at the root (audit-dev-tests prints its * report flat). `[]` when neither is an array — « no findings » and « not an * audit » are the same answer for a reader of findings. */ export function rawFindingsOf(parsed: unknown): unknown[] { const root = asRecord(parsed) if (root === null) return [] const report = asRecord(root['report']) if (report !== null && Array.isArray(report['findings'])) return report['findings'] as unknown[] if (Array.isArray(root['findings'])) return root['findings'] as unknown[] return [] }