// `audit` — run the static engine over the inputs and aggregate findings into an // AuditResult: a preliminary, engine-only verdict per criterion (C/NC/NA for the // static criteria it can decide; "manual" for everything needing rendering or // judgment, surfaced as residual risks). `report` renders this; Claude completes // the manual criteria. import { createHash } from "node:crypto"; import type { AuditResult, CriterionResult, DynamicEngine, Finding, PageCoverage, RenderSignals, ResidualRisk, Severity, Status, GuidelineTally, } from "./types.js"; import { VERSION, SCHEMA_VERSION } from "./types.js"; import { allSC, allGuidelines } from "./wcag.js"; import { parseSource } from "./parse/source.js"; import { attachSignals, isSnapshotDom, PROBES_VERSION, snapshotPageId, WALK_DEPENDENT_SCS } from "./snapshot.js"; import { attr, dynamicSpreadMayProvide, elementsByTag, type Doc, type CaptureProvenance } from "./parse/html.js"; import { CAPTURES_DIR, computeCaptureCoverage, enrichCaptureOrigins, isUnderDir, readCaptureDir, capturesForSources } from "./capture.js"; import { isFullDocument } from "./rules/rule.js"; import { renderedRulesFor, renderedRulesRan, renderedTestedScs } from "./rules/rendered.js"; import { renderedProvesOn } from "./coverage.js"; import { AXE_DECIDES, PROBE_SEVERITY, PROBE_WCAG, isAxeAdvisory, scForAxe, severityFromImpact } from "./axe-map.js"; import { subjectsAbsent, subjectsForSc, subjectsPresentIn } from "./adjudicate-subjects.js"; import { runRules } from "./rules/registry.js"; import { runCrossRules } from "./rules/cross-registry.js"; import { listPacks } from "./standards/registry.js"; import { docRuleRan, runPackRules } from "./standards/pack-rules.js"; import { buildGraphAndDocs } from "./graph/build.js"; import type { DepGraph } from "./graph/graph.js"; import { discover } from "./discover.js"; import { GRAPH_ONLY_EXT } from "./glob.js"; import { readText, today } from "./util.js"; import { INAPPLICABLE_STATUS } from "./types.js"; export type DedupMode = "exact" | "normalized" | "off"; export interface AuditInput { inputs: string[]; stdin?: string; forceJsx?: boolean; include?: string[]; exclude?: string[]; ext?: string[]; // scale controls changed?: boolean; // audit only git-changed files since?: string; // git ref to diff against (implies changed) staged?: boolean; // audit exactly the staged index snapshot (strict pre-commit scope) dedup?: DedupMode; // collapse identical files to one canonical audit (default exact) maxFiles?: number; // hard cap on canonical files audited (logged truncation) graph?: boolean; // also run cross-file rules over a dependency graph (--graph) captureCoverage?: boolean; // compute scope.captureCoverage (implies a graph pass) captureDir?: string; // dir scanned for the repo-wide capture set (coverage); default .ultra11y/captures // In --changed/--since/--staged mode, also ingest the captures under `captureDir` // whose provenance sourceFile matches one of the diffed files (capturesForSources) — // a capture is rarely itself part of the diff, so the audit would otherwise stay // blind to the real rendered DOM for a touched component. No-op outside diff mode // (a full scan's capture ingestion is the CLI appending captureDir as a top-level // input instead — see cmdAudit's `useCaptures`). captureDiff?: boolean; noDefaultExcludes?: boolean; // also audit test/spec/story/__tests__ markup onWarn?: (msg: string) => void; } const has = (d: Doc, ...tags: string[]): boolean => d.elements.some((e) => tags.includes(e.tag)); // Applicability predicate per STATIC success criterion (the only SCs the engine // reports Conforming when clean): is there any relevant element to check? If not, // the SC is NA rather than a hollow "C". WCAG SCs are coarser than the rules, so the // static set is deliberately tiny (see scripts/build-standards.mjs); every other // mapped SC raises only DEFINITE non-conformities and stays "manual" otherwise. const APPLICABLE: Record boolean> = { "1.4.2": (d) => has(d, "audio", "video"), // Audio Control — autoplay-media (audio branch) "2.4.2": (d) => isFullDocument(d), // Page Titled — title-missing-empty "3.1.1": (d) => isFullDocument(d), // Language of Page — html-lang-missing / lang-invalid }; // ---- SUBJECT MATTER: what makes a NON-static criterion applicable at all ----------------- // // A judgment or rendering criterion used to have exactly two outcomes: `NC` when a rule fired, // `manual` otherwise. So a repository containing no audio and no video still reported all five // time-based-media criteria as « to assess » — and under RGAA, whose theme 4 projects from // them, that is 13 criteria a reader has to work through to discover there was never anything // to look at. Measured on a real 300-file audit: 96 of 106 criteria « to assess », whole themes // among them applicable to nothing in scope. // // « Not applicable » is a real, normative verdict, and the engine can prove it for a criterion // whose SUBJECT MATTER is a thing you can look for in the source. So: no media element in the // whole scope ⇒ the media criteria are NA, with a justification naming the observation. // // Three rules keep this honest, because a wrong NA is far worse than an honest « to assess » — // it is a non-conformity hidden inside a report someone signs: // // 1. UNCERTAINTY RESOLVES TOWARDS "APPLICABLE". A predicate answers "could this criterion // possibly apply here?", so it says YES whenever it genuinely cannot tell — an `` // with no declared `type`, for instance. That is not the same as saying yes to everything: // a predicate that treats every embed as a video is not cautious, it is wrong, and it // costs a human the work of re-deriving "nothing here". Measured: one analytics `