/** * cli:audit-ba — engine.ts * * Runs the registry over the CorpusModel: project-scoped rules once, * app-scoped rules per app, module-scoped rules per module. Rule evaluators * NEVER touch the filesystem for corpus content — the model is the corpus. */ import { detectRuleContradictions, scopeLabel, type RuleContradiction } from '../../../../lib/rule-contradictions.js' import type { CorpusModel } from './corpus/model.js' import type { Dimension, Finding, JudgmentItem } from './types.js' import type { RuleCtx, RuleDef } from './rules/registry.js' export interface EngineResult { findings: Finding[] judgments: JudgmentItem[] } export function runRules( defs: RuleDef[], model: CorpusModel, opts: { dimensions?: Dimension[]; projectRoot?: string; strict: boolean }, ): EngineResult { const findings: Finding[] = [] const judgments: JudgmentItem[] = [] const wanted = opts.dimensions ? new Set(opts.dimensions) : null const collect = (r: { findings: Finding[]; judgments?: JudgmentItem[] }): void => { findings.push(...r.findings) if (r.judgments) judgments.push(...r.judgments) } for (const def of defs) { if (wanted && !wanted.has(def.dimension)) continue const base: Omit = { model, strict: opts.strict, ...(opts.projectRoot !== undefined ? { projectRoot: opts.projectRoot } : {}), } if (def.scope === 'project') { collect(def.evaluate({ ...base })) } else if (def.scope === 'app') { for (const app of model.apps) collect(def.evaluate({ ...base, app })) } else { for (const module of model.modules) collect(def.evaluate({ ...base, module })) } } return { findings, judgments } } /** * The engine's own contradiction check — the SAME detector /support-report * runs on a captured envelope (lib/rule-contradictions). A `dedupOf` finding * is the same evaluator as its primary rule under a second id, so a mirror in * err while every covering primary is ok is a defect of THIS CLI, on ANY * corpus — the DemoGestionFlotte XD-005 incident ran 9 modules through it * before anyone could say so. Run on the final findings of every audit. */ export function selfContradictions(findings: Finding[]): RuleContradiction[] { return detectRuleContradictions( findings.map((f) => ({ ruleId: f.ruleId, severity: f.severity, scope: f.scope, message: f.message, evidence: f.evidence ?? [], ...(f.dedupOf !== undefined ? { dedupOf: f.dedupOf } : {}), })), ) } /** The envelope warning (empty when the run agrees with itself). */ export function selfContradictionWarnings(findings: Finding[]): string[] { const contradictions = selfContradictions(findings) if (contradictions.length === 0) return [] const pairs = [...new Set(contradictions.map((c) => `${c.mirrorRuleId}→${c.primaryRuleId}`))].sort() const scopes = [...new Set(contradictions.map((c) => scopeLabel(c.scope)))] return [ `audit.rule-contradiction : ${contradictions.length} verdict(s) miroir en désaccord avec leur règle primaire sur la même portée — ${pairs.join(', ')} (${scopes.join(', ')}). ` + 'Un finding `dedupOf` est le MÊME évaluateur que sa règle primaire : le défaut est dans la CLI, jamais dans le corpus. ' + 'Ne pas modifier le document pour faire taire le miroir — transmettre via /support-report (classe `rule-contradiction`, preuve mécanique depuis cet envelope).', ] } export function countBySeverity(findings: Finding[]): { ok: number; warn: number; err: number } { return { ok: findings.filter((f) => f.severity === 'ok').length, warn: findings.filter((f) => f.severity === 'warn').length, err: findings.filter((f) => f.severity === 'err').length, } }