# Research: Quality Gates & Code Hygiene **Feature**: 031-quality-gates-hygiene **Date**: 2026-03-11 ## R1: Vitest v8 Coverage Configuration **Decision**: Use `@vitest/coverage-v8` with config-based enablement under `test.coverage`. **Rationale**: Vitest 3.x (project uses 3.2.4) supports v8 coverage natively but requires the `@vitest/coverage-v8` peer package installed separately. Config-based `coverage.enabled: true` ensures coverage runs automatically with `vitest run`, integrating into the existing `pnpm check` script without CLI changes. **Configuration**: ```typescript test: { coverage: { provider: "v8", enabled: true, thresholds: { lines: 70, branches: 60, autoUpdate: true, }, }, } ``` **Key details**: - `thresholds.autoUpdate: true` modifies the config file in-place when coverage exceeds thresholds — contributors must commit the updated file. - No change to `pnpm check` script needed — `vitest run` already runs tests and will now include coverage. - `coverage` directory should be added to `.gitignore`. **Alternatives considered**: - CLI flag `--coverage`: Rejected — requires changing the `check` script and doesn't support autoUpdate persistence. - Istanbul provider: Rejected — v8 is the default and faster for Node.js workloads. ## R2: Biome Cognitive Complexity Rule **Decision**: Enable `noExcessiveCognitiveComplexity` with default threshold 15 and refactor 5 existing violations. **Rationale**: The rule uses SonarSource's Cognitive Complexity methodology. Threshold 15 is the industry standard default. The rule is not included in `recommended: true` — it must be explicitly configured. **Configuration**: ```json "complexity": { "noExcessiveCognitiveComplexity": { "level": "error", "options": { "maxAllowedComplexity": 15 } } } ``` **Existing violations** (5 functions): | File | Function | Score | | ---- | -------- | ----- | | `apps/web/src/adapters/bestiary-adapter.ts:257` | `renderEntries` | 23 | | `apps/web/src/persistence/encounter-storage.ts:21` | `loadEncounter` | 17 | | `apps/web/src/persistence/encounter-storage.ts:55` | rehydration callback | 22 | | `scripts/check-layer-boundaries.mjs:51` | `checkLayerBoundaries` | 22 | | `scripts/generate-bestiary-index.mjs:30` | `buildSourceMap` | 19 | All must be refactored before the rule can be enforced. ## R3: Biome A11y Rules Review **Decision**: Enable `noNoninteractiveElementInteractions` explicitly. No nursery-stage a11y rules exist in Biome 2.0. **Rationale**: Biome 2.0 ships 35 a11y rules, 33 of which are `recommended: true`. The two non-recommended stable rules are: - `noNoninteractiveElementInteractions` — directly relevant to this React UI (flags interactive handlers on non-interactive elements) - `noRestrictedElements` — not applicable (requires a config of restricted elements, used for design system enforcement) There are no nursery-stage a11y rules available. The spec's FR-011 (enable nursery rules) resolves to enabling the one relevant non-recommended stable rule instead. ## R4: pnpm audit **Decision**: Add `pnpm audit --audit-level=high` to the `pnpm check` script. **Rationale**: Current dependency tree passes cleanly (0 advisories). The command runs quickly and requires no additional dependencies. Placing it first in the chain provides early failure on CVEs before slower checks run. **Offline consideration**: `pnpm audit` requires network access. If the registry is unreachable, the command fails. This is an acceptable trade-off — pre-commit enforcement is the primary goal, and offline commits are rare. ## R5: Biome-Ignore Audit **Decision**: Fix 1 blanket ignore, reduce 8 a11y ignores in combatant-row.tsx. **Current state** (10 total ignores): | File | Count | Type | | ---- | ----- | ---- | | `packages/domain/src/set-initiative.ts:65` | 1 | Blanket `biome-ignore lint:` | | `apps/web/src/components/combatant-row.tsx` | 8 | Rule-specific (4 pairs of `useKeyWithClickEvents` + `noStaticElementInteractions`) | | `apps/web/src/adapters/bestiary-adapter.ts:375` | 1 | Rule-specific (`noExplicitAny`) | **Fix strategies**: - **set-initiative.ts**: Replace `biome-ignore lint:` with proper type narrowing — the guard on line 64 checks `aHas && bHas`, so TypeScript can narrow if the comparison is restructured (e.g., early return for undefined cases, or explicit `as number` cast with rule-specific ignore). - **combatant-row.tsx**: The 3 inner divs (lines 444-446, 481-486, 491-496) exist solely to call `e.stopPropagation()`. These can be replaced with a small `StopPropagation` wrapper component that uses `onClickCapture` or similar pattern, or the outer row's click handler can check `event.target` to skip when a child interactive element was clicked. The outer row div (lines 405-407) could be converted to a `
` to satisfy both rules, or the click handler can be moved to a semantic element. - **bestiary-adapter.ts**: Keep as-is — rule-specific ignore for `noExplicitAny` on raw JSON is justified. ## R6: Constitution Early Enforcement Principle **Decision**: PATCH amendment to constitution Development Workflow section (version 2.2.0 → 2.2.1). **Rationale**: This is a clarification of the existing rule "No change may be merged unless all automated checks pass." The new language makes explicit that gates must run at the earliest feasible enforcement point (pre-commit), not just in CI. **Versioning**: PATCH — non-semantic clarification per the constitution's versioning policy.