---
title: █████ ███ ██████ ███████ █████ — Stage 3 (extend the ts-check tier)
type: task
status: completed
description: >-
  Extend the incremental //@ts-check tier past Stage 2's 13.5% seam coverage, working the measured
  list of highest-seam uncovered producers and the 1,144 diagnostics remaining in the fourth-shape
  files
base_uri: user:task/league/raise-league-testing-floor-stage-3.md
created_at: '2026-08-19T18:21:49.219Z'
entity_id: c3b86098-4d93-4f23-8f00-a9d9e15b3e7a
finished_at: '2026-08-19T21:07:52.784Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - succeeds [[██████████████████████-██████-███████-█████-█████-████]]
started_at: '2026-08-19T18:23:16.843Z'
tags:
  - user:tag/league-xo-football.md
  - ███████████████-██████-███████████
updated_at: '2026-08-19T21:07:52.784Z'
visibility_analyzed_at: '2026-11-05T23:49:51.931Z'
---

## Why

Stage 2 (`██████████████████████████████████`, Completed) stood the incremental `//@ts-check` tier up and proved it catches the census classes: **157 of 1,167 seams (13.5%) now sit under a type-checked producer**, from 14 files out of 328. It also fixed four real defects the checker surfaced on the first two files alone — a mixed-unit `trade_deadline_at`, a partly-visible pause sibling set, a vocabulary living in three hand-agreed copies, and an undeclared computed method name.

Everything here is **measured, not estimated**. Stage 2 ran the numbers and deliberately took only the cheap tail so the remainder would be sized rather than guessed.

Re-measure before and after with `yarn measure:seams` (`db/tools/measure-cross-module-seams.mjs --coverage`), which reports coverage against `db/gates/ts-check-adoption.json`. Read CLAUDE.md § "The `//@ts-check` tier is OPT-IN per file" first — three of its four properties fail in the direction that reads as a clean tree.

## Plan

- [x] Adopt the top uncovered producers by seam count, highest first. All twelve landed across `1676d0e21`, `e65d3b3c5`, `4bdbb749f`. Coverage went 158/1167 seams (13.5%) to 423/1174 (36%); adopted files 14 to 26.
- [x] Settle the yargs `.argv` shape ONCE, then adopt the three scripts held back for it. Settled as `parseSync()`, applied per-file at adoption rather than swept across all 218 `.argv` sites; three scripts adopted at `3d6e32a44`. Tier at 29 files.
- [x] **Generate the knex table-type map** (added at operator direction, ahead of the remaining items). `db/tools/generate-schema-types.mjs` now emits `db/knex-tables.d.ts` alongside the row types, so `db('player').select(...)` types as a `Pick` of `PlayerRow` in every checked file with no annotation of its own — replacing a hand-copied `@returns` at each of 1,498 `db('<table>')` call sites across 163 tables. Landed `240502ea3`. `db/gates/check-knex-table-types.mjs` holds it as a red/green pair PLUS a separate assertion that `tsconfig.json` still lists the `.d.ts` in `include`, which the pair cannot see.
- [x] **`strictNullChecks` is ON**, landed `deb443b5a`. The premise inverted twice: the plan assumed a large surface, the predecessor measured 24, and the knex map took it to 39 across 12 files. 14 of those were two producers. Three live findings: a null `extension_deadline_at` that would have reset a league's protections a season early, a `division_null_name` key that missed and fell through to a fallback by accident, and `require_auth` returning a bare boolean so no caller was ever narrowed.
- [x] **Ban bare container JSDoc**, landed `32f05d60f`. `@param {Object}` is the TypeScript `Object` interface, so such a tag guarantees an error at every property read while reading as documentation — the dominant blocker across all twelve Stage-3 producers. `eslint-rules/no-bare-container-jsdoc.mjs` is a RATCHET against the 1,179 that already existed: a per-file allowance in a generated baseline, with `yarn check:jsdoc-baseline` failing on any entry that allows more than the file holds.
- [x] Resume adoption by seam count. `b61d9a421` took the tier to 34 files and 38.9% coverage. **Shape-4 is dropped as the organizing principle** — the scanner reports those counts by root rather than by file, so Stage 2's 32-file list is not reproducible, and 40 of the 99 sites sit in `app/core` which is outside the tsconfig `include` entirely. Seam count is what coverage measures, so that is what adoption follows.
- [x] Persist the practices that earned it. Three of five landed, in existing homes rather than a new guideline: the missing-generator principle (with fix-at-the-producer folded in) in [[user:guideline/design-toward-clean-end-state.md]], always-on negative controls for SOURCE gates in [[user:guideline/software/verify-that-a-check-can-fail.md]], and measurement provenance in [[user:guideline/software/simplify-software-plan.md]]. "A wrong annotation is worse than a missing one" was skipped — it is now enforced by machinery (the lint rule) rather than prose, and its general form is the missing-generator corollary.

## Remaining, not blocking

- The 11-seam tail of uncovered producers runs 12 to 23 diagnostics per file (`get-roster` 15, `betting-market-cache` 15, `calculate-points` 23, `identities` 12, `optimize-standings-lineup` 9, `load-simulation-data` done). Cost was probed per file; none is a cheap win.
- 1,160 bare-container tags remain across 189 files, held flat by the ratchet and reducible file-by-file.

## Decisions taken

- **yargs**: `yargs(hideBin(process.argv)).parseSync()`, never `.argv`. `.argv` is typed as a union with a `Promise` because yargs cannot know statically whether async middleware is registered. `parseSync()` returns the same object at runtime without one, and throws rather than returning a promise if one is added later. Both flag paths were smoke-tested against an isolated database, not just type-checked.
- **Default league**: stays optional-typed. See the corrected comment in `libs-server/get-league.mjs`.

## What the tier actually catches

Two classes, and reading them apart is the whole skill:

- **Real defects.** A CLI passing `process.argv[3]` as an integer `lid`; a knex `count()` compared with `>=`; a string `'1'` into dayjs `.add`; `JSON: 1` in a `URLSearchParams` body; a dead parameter ten call sites carefully supplied.
- **Coverage holes that read as documentation.** `@param {Object}` is the TypeScript `Object` interface, and `{Array}`/`{Map}` are generics missing their arguments — such a tag covers nothing while looking like it does. This, not missing annotation, was the dominant blocker across the twelve producers.
- **Inference artifacts that are NOT defects.** An unannotated destructured parameter infers a type excluding its optional properties, so every correct call site reads as an error. Fix at the producer; never with a per-call-site cast.

## Key locations

- Repo: `repository/active/league`
- Predecessor: [[██████████████████████-██████-███████-█████-█████-████]]
- Orchestrator: [[user:orchestrator/league-schema-redesign/league-schema-redesign.md]]
- Tier config: `tsconfig.json`, `db/gates/ts-check-adoption.json`
- Generator: `db/tools/generate-schema-types.mjs` (regenerate after any DDL change)
- Gates: `yarn check:types`, `db/gates/check-ts-check-ratchet.mjs` (both in CI)
- Measurement: `yarn measure:seams`
