/** * cli:audit-ba — render/report-json.ts * * Assembles the AuditBaReport (pure) and derives the exit class: * 0 conforme · 1 warn-only · 2 ≥1 err · 3 parse-suspect (prioritaire). * The report JSON is ALSO written to `/_audit/audit-ba.json` by * index.ts — the machine-readable record the toolchain (and the freshness * checks) read back. */ import { countBySeverity } from '../engine.js' import type { CorpusModel } from '../corpus/model.js' import type { AuditBaReport, Dimension, Finding, JudgmentItem, ParseControlReport, } from '../types.js' import { ucCaseMinorityModules } from '../corpus/conventions.js' import { sourceDocFor } from '../corpus/doc-paths.js' import { resolveRemedy } from '../../../../../lib/remediation.js' export const RULESET_VERSION = '1.1.0' /** * Fill the remediation half of every finding, generically — WITHOUT touching * the 124 rule evaluators. * * `file` derives from (dimension, scope) and `remedy` from (ruleId, dimension), * so a rule only has to opt IN when it can say something the pair cannot: an * `anchor`, a `line`, or a remedy the registry does not cover. Anything a rule * already set is preserved — this never overwrites a deliberate value. * * `remedy` is total: `resolveRemedy` falls back to the dimension's authoring * skill and, failing that, to `manual`. No finding leaves the report without a * named next step. */ export function enrichFindings(findings: Finding[]): Finding[] { return findings.map((f) => { const file = f.file ?? sourceDocFor(f.dimension, f.scope) ?? undefined return { ...f, ...(file !== undefined ? { file } : {}), remedy: f.remedy ?? resolveRemedy(f.ruleId, f.dimension), } }) } /** CONV-001 / CONV-002 — requirement 8: ONE project-scoped warn per detected * corpus convention diverging from the canonical doc form, never an error * per occurrence. `--strict` suppresses the tolerance (the per-rule checks * then flag each occurrence). */ export function conventionFindings(model: CorpusModel, strict: boolean): Finding[] { if (strict) return [] const out: Finding[] = [] const c = model.conventions if (c.ucSectionCase !== 'upper' && model.totals.ucs > 0) { const minority = ucCaseMinorityModules(model.modules, c.ucSectionCase === 'lower' ? 'lower' : 'upper') out.push({ ruleId: 'CONV-001', dimension: 'use-cases', severity: 'warn', scope: {}, message: c.ucSectionCase === 'lower' ? `Convention projet : segment de section des codes UC en minuscules (${Math.round(c.ucSectionLowerShare * 100)} % du corpus) — divergence assumée vs la note doc-templates (UPPERCASE). Le parsing est insensible à la casse ; aucun err par occurrence.` : `Casse MIXTE des segments de section des codes UC (${Math.round(c.ucSectionLowerShare * 100)} % en minuscules) — harmoniser un jour, le parsing tolère les deux.`, ...(minority.length > 0 ? { evidence: [`modules minoritaires : ${minority.join(', ')}`] } : {}), }) } if (c.errorCodeShape !== 'dotted' && model.totals.errorCodes > 0) { out.push({ ruleId: 'CONV-002', dimension: 'rules', severity: 'warn', scope: {}, message: c.errorCodeShape === 'kebab' ? `Convention projet : codes d'erreur en kebab plat (${Math.round(c.errorCodeKebabShare * 100)} % des ${model.totals.errorCodes} codes) là où BR-011 exige \`{module}.{entité}.{cas}\`. Ces chaînes sont assertées VERBATIM par les critères d'acceptation (contrat d'erreur public de l'API) — un renommage impose de réécrire règles et critères d'un même geste. UN warn projet, jamais une erreur par occurrence.` : `Forme MIXTE des codes d'erreur (${Math.round(c.errorCodeKebabShare * 100)} % kebab plat) — le contrat d'API mélange deux grammaires.`, }) } return out } export function buildReport(args: { model: CorpusModel findings: Finding[] judgments: JudgmentItem[] parseControl: ParseControlReport skippedDimensions: { dimension: Dimension; reason: string }[] warnings: string[] }): AuditBaReport { const { model, judgments, parseControl, skippedDimensions, warnings } = args const findings = enrichFindings(args.findings) const sev = countBySeverity(findings) const exitClass: AuditBaReport['exitClass'] = parseControl.status === 'fatal' ? 'parse-suspect' : sev.err > 0 ? 'err' : sev.warn > 0 ? 'warn' : 'ok' return { baRoot: model.baRoot, rulesetVersion: RULESET_VERSION, sourcesHash: model.sourcesHash, totals: model.totals, perModule: model.perModule, conventions: model.conventions, parseControl, findings, judgmentNeeded: judgments, skippedDimensions, exitClass, counts: { ...sev, judgment: judgments.length }, warnings, } } export function exitCodeOf(exitClass: AuditBaReport['exitClass']): number { switch (exitClass) { case 'ok': return 0 case 'warn': return 1 case 'err': return 2 case 'parse-suspect': return 3 } }