ADR 0009: Tighten the lint/type gates where free; decline isolatedDeclarations
Status: Accepted (2026-07-20) · Phase 4 · extends the toolchain of ADR 0029 · the posture below stands; the tool enforcing it changed (2026-08-25) with ADR 0036
Context
An audit of the lint/type configuration asked a narrow question: are there checks we could enable that would add real safety without paying for a backlog of pre-existing violations? The gates were already green at the strict bar CLAUDE.md §2 mandates (strict plus the seven strict-adjacent tsc flags, Biome recommended). The audit measured each candidate by actually running it against the tree rather than reasoning about it, and sorted candidates by current violation count. A rule that fires zero times today locks in a guarantee at zero code cost; one that fires hundreds of times is a refactor backlog wearing a config toggle.
Decision
Adopt three checks that pass clean today (commit baf90294)
Each was measured at zero current violations across the relevant scope, so enabling it changes no existing code. It only prevents a future regression:
tscnoUncheckedSideEffectImports: truecatches a broken or typo'd side-effect import (import './x') instead of letting it resolve to nothing.- Biome
nursery/noFloatingPromises: errorenforces that every promise is awaited. This is the single highest-value correctness rule for this library: the reader and writer paths are async-heavy, and a droppedawaitis exactly the class of bug that survives unit tests but corrupts streaming output. Caveat noted: it is anursery(still-stabilizing) rule; if a false positive ever appears, the fallback is a scopedbiome-ignore, not disabling it globally. - Biome
suspicious/noConsole: errorinsrckeeps stray debug logging out of the shipped library. Overridden off forscripts/**/*.mjsandtest/**/*.mjs, whereconsoleis the legitimate idiom (the sameoverridesmechanism already used fornoNonNullAssertionin ADR 0029).
Reject isolatedDeclarations
Considered and declined. Enabling it (on the build config, which already emits .d.ts) produces 26 findings, none of which touch the public API barrel (src/index.ts; its 19 exports are clean). The findings cluster in four internal io/xlsx/ modules, and 20 of the 26 fall on one hotspot: the REL table in relationships.ts, which derives OPC relationship-type URIs from the NS table via template literals (`${NS.docRels}/worksheet`). isolatedDeclarations cannot infer those, and every way to satisfy it there is a downgrade:
- annotating
REL: Record<…, string>loses the exact-URI literal types theas constcurrently guarantees; - giving an explicit literal type per key duplicates all 18 URI strings verbatim;
- inlining the full URIs instead of deriving from
NSdefeats the deliberate DRY derivation those tables exist for.
The value isolatedDeclarations offers, a guaranteed-correct, fast-to-emit published .d.ts, is already met here by other means: the public API produces zero findings and is independently pinned by the Expect<Equal> type-tests in src/type-tests/ and the docs-from-barrel generator (ADR 0006). The performance argument (parallel declaration emit) is nil for a 97-file library that builds instantly, and today's inference-based emit already works. So the cost lands on internal plumbing where the benefit doesn't, and contorts the DRY constant tables to satisfy a check whose payoff is already covered.
This is the inverse of the three checks we did adopt: those lock in a guarantee at zero code cost. isolatedDeclarations would make the code worse to satisfy a redundant check, the opposite of CLAUDE.md §5's "harder, better thing", since here the better thing is not contorting relationships.ts.
Consequences
- Positive: three regression classes (broken side-effect imports, floating promises, shipped
consolecalls) are now impossible to reintroduce without a failing gate, with no existing code touched. - Neutral:
biome.jsonremains comment-free JSON (ADR 0029's load-bearing gotcha, where comments silently drop theoverridesarray); rationale lives here. - Revisit
isolatedDeclarationswhen: the project starts shipping hand-authored.d.ts, or the public API grows large enough that inference-based declaration emit becomes a correctness risk. Neither holds today. Do not re-litigate absent one of those triggers.