Files
initiative/specs/031-quality-gates-hygiene/research.md

5.5 KiB

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:

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:

"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 <div role="button" tabIndex={0} onKeyDown={...}> 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.