/** * The blocking deny-set — the SINGLE source of truth for "which findings may * hard-deny a UI write" across every altitude of the contract: the CLI * pre-write hook (Node) AND the Contract Mode emit gate (a browser-safe * serverless route). Lifted out of `packages/cli/src/hook/check.ts` * (Contract Mode Phase 0) so the two cannot fork — a deny here must imply a CI * failure, and that invariant only holds if both read the same criteria. * * Pure and browser-safe: it reads only a `Finding`'s `ruleId` / `severity` / * `fix` shape — no Node APIs, no config off disk, no clock. The CLI hook * re-exports `isDenyEligible` from here. */ import { severityLevel } from "../severity.js"; import type { Finding } from "./types.js"; /** * The ONLY rules whose findings may hard-deny a write: deterministic, * single-file-decidable, high-confidence classes (architecture Open Q2). A rule * is added here DELIBERATELY — new rules gain no blocking power by default, so a * future heuristic rule can't silently start denying writes. Heuristic/INFERRED * classes (`composition/*`, `tailwind/*arbitrary*`) are absent on purpose: at * most advisory, never a block. */ export const BLOCKING_RULE_ALLOWLIST: ReadonlySet = new Set([ "styles/no-raw-color", // raw hex/rgb/hsl color literal "styles/no-raw-spacing", // off-scale / untokenized spacing literal "tailwind/off-scale-spacing-token", // off-scale Tailwind spacing utility "components/prefer-library", // raw element bypasses a canonical component "components/preferred-component", // imported component bypasses its canonical ]); /** * Mirrors `scanWithFacts`'s own CI exit logic: a finding gates CI when it is * error-severity, or warn-severity under `ci.failOnWarnings`. Gating on exactly * this set is what makes a local/emit deny imply a CI failure. */ export function gatesCi(finding: Finding, failOnWarnings: boolean): boolean { const level = severityLevel(finding.severity); return level === "error" || (failOnWarnings && level === "warn"); } /** A finding may deny iff it is allowlisted, gates CI, and is not a flagged-heuristic fix. */ export function isDenyEligible(finding: Finding, failOnWarnings: boolean): boolean { if (!BLOCKING_RULE_ALLOWLIST.has(finding.ruleId)) return false; // Advisory findings — the low-confidence precision tiers a rule flags with // `attributes.advisory` (prefer-library's role-reimpl / interactive-div / // html-advisory / icon-only tiers, and the reimplements-primitive className // heuristic) — are surfaced but NEVER block a write, even if a project raises // their severity or sets failOnWarnings. Same "only deny when the engine is // confident" principle as the non-deterministic-fix guard below. if (finding.attributes?.advisory === true) return false; if (!gatesCi(finding, failOnWarnings)) return false; // An explicitly non-deterministic fix is the engine's own "low confidence / // ambiguous" signal (e.g. several tokens share a value, or a heuristic swap). // Those stay advisory — only deny when the engine is confident. A finding with // no fix at all (e.g. a raw color with no matching token) still gates CI, so it // remains deny-eligible. if (finding.fix?.deterministic === false) return false; return true; }