/** * lib/audit-rules.ts — THE `--rules` and coverage contract every audit-dev-* * pack shares (audit 2026-09-24, C-CRIT-01 / C-WARN-04 / C-WARN-14). * * Two ways a deterministic gate used to answer green on nothing: * * 1. `--rules` never checked membership: a retired (`DEV-API-004`), spec-only * (`DEV-UI-019`), conversational (`DEV-DAT-004`), reserved (`DEV-WIRE-002`) * or plainly unknown id ran nothing, and the envelope echoed it in * `rulesRun` with `valid: true`. * 2. A scan of ZERO files: an empty or mis-pointed `--module-path` / * `--project-path` / `--web-path` made every rule answer `ok {count: 0}`, * `success: true`, exit 0 — and gates.md, which only reads * `findings[].severity`, shipped the module on an audit of nothing. * * `resolveRuleFilter` is the one verdict on a requested rule list: implemented * ids run; retired / spec-only / conversational ids become an EXPLICIT `ok` * stub finding naming their real channel (never a silent echo); reserved and * unknown ids are REFUSED (`success: false`). `assessCoverage` is the one * verdict on an empty scan: `err` when the BA side declares items and the code * side inspected nothing, `warn` when both sides are empty. Each pack emits it * as `DEV--000` and exposes `report.coverage` + `report.rulesRun`. * * The coverage verdict is not optional: a targeted probe that inspected nothing * still fails. Every implemented id ending in `-000` is ALWAYS in `run` — * appended when the `--rules` request omitted it. Until 2026-09-24 (QC D1) the * filter dropped it, so the documented gate invocations (`audit-dev-api --rules * DEV-API-024`, `--rules DEV-API-010`, …) audited nothing on a mis-pointed path * and answered green — the very class the 000 rule exists to make visible. */ /** `external` = the rule HAS a deterministic checker, in another CLI (e.g. DEV-CORE-011 → derive-rbac-grants --mode check). */ export type RuleStubKind = 'retired' | 'spec-only' | 'conversational' | 'external' export interface RuleStub { kind: RuleStubKind /** Where the rule's concern is really enforced (the replacement rule, the scaffolder, the BA audit…). */ note: string } export interface RuleRegistry { /** Rule ids the pack's engine actually executes. */ implemented: readonly string[] /** Ids present in the SKILL that run nothing here but have a named channel — answered as an explicit `ok` stub. */ stubs?: Readonly> /** Ids reserved in the SKILL/schema and NOT implemented — requesting one is refused. */ reserved?: Readonly> } export interface ResolvedRuleStub { code: string kind: RuleStubKind note: string } export interface RuleFilterResult { /** Ids the engine must execute: requested ∩ implemented, or every implemented id when nothing was requested. */ run: string[] /** Requested ids that run nothing but have a named channel — one `ok` stub finding each. */ stubs: ResolvedRuleStub[] /** Refusals (unknown or reserved ids). Non-empty ⇒ the CLI answers `success: false`. */ errors: string[] } export function resolveRuleFilter(requested: readonly string[], registry: RuleRegistry): RuleFilterResult { if (requested.length === 0) return { run: [...registry.implemented], stubs: [], errors: [] } const implemented = new Set(registry.implemented) const run: string[] = [] const stubs: ResolvedRuleStub[] = [] const errors: string[] = [] const seen = new Set() for (const code of requested) { if (seen.has(code)) continue seen.add(code) if (implemented.has(code)) { run.push(code); continue } const stub = registry.stubs?.[code] if (stub) { stubs.push({ code, ...stub }); continue } const reserved = registry.reserved?.[code] if (reserved !== undefined) { errors.push(`rule "${code}" is reserved, not implemented — ${reserved}. Requesting it would run nothing and answer green.`) continue } errors.push(`rule "${code}" is unknown to this pack (implemented: ${registry.implemented.join(', ')})`) } // The coverage verdict is not optional: a targeted probe that inspected nothing still fails. for (const code of registry.implemented) { if (isCoverageRule(code) && !run.includes(code)) run.push(code) } return { run, stubs, errors } } /** `DEV--000` — the pack's coverage verdict (`assessCoverage`): in `run` on every request, `--rules` included. */ export function isCoverageRule(code: string): boolean { return code.endsWith('-000') } /** Finding label of a stub: `DEV_API_004_retired`, `DEV_UI_019_spec_only`, `DEV_DAT_004_conversational`. */ export function stubLabel(code: string, kind: RuleStubKind): string { return `${code.replace(/-/g, '_')}_${kind.replace(/-/g, '_')}` } /** The `params.note` of a stub finding — says out loud that nothing ran here and where the concern lives. */ export function stubNote(stub: ResolvedRuleStub): string { const what = stub.kind === 'retired' ? 'RETIRED' : stub.kind === 'spec-only' ? 'SPEC-ONLY (no deterministic checker)' : stub.kind === 'external' ? 'EXTERNAL (its deterministic checker is ANOTHER CLI)' : 'CONVERSATIONAL (no deterministic checker)' return `${stub.code} is ${what} — nothing ran here: ${stub.note}` } export interface CoverageInput { /** Items the BA side declares (entities, ACs, pagespecs…). */ baItems: number /** Files the code side actually inspected. */ filesInspected: number /** Human label of the BA side, e.g. `BA entities (entité.md)`. */ baLabel: string /** Human label of the code side, e.g. `migration files (src/**\/Migrations/*.cs)`. */ codeLabel: string } export interface CoverageVerdict { severity: 'err' | 'warn' note: string } /** * `err` when the BA side has content and the code side inspected nothing (the * gate audited NOTHING — mis-pointed path or the producing phase never ran); * `warn` when both sides are empty (nothing to audit, said out loud); `null` * when something was inspected. */ export function assessCoverage(c: CoverageInput): CoverageVerdict | null { if (c.filesInspected > 0) return null if (c.baItems > 0) { return { severity: 'err', note: `${c.baItems} ${c.baLabel} declared but 0 ${c.codeLabel} inspected — this run audited NOTHING (mis-pointed path, or the producing phase never ran); every other finding of this run answered on an empty scan.`, } } return { severity: 'warn', note: `0 ${c.baLabel} and 0 ${c.codeLabel} — nothing to audit on either side.` } } /** The one remediation text of a `DEV--000` finding: it names the invocation, never the code. */ export const COVERAGE_SOLUTION = 'Point the CLI at the directories that hold the generated code and the BA module (a gate that inspects nothing cannot pass), or run the producing phase first. Never heal code for this finding — it names the invocation, not the code (auto-healing row `audit.nothing-inspected`).'