toolkit

Migration gate · Playbook

Coverage thresholds — where the ratio and the floor come from

Issue #235: a coverage claim (“312 URLs, all covered”) means nothing without an independent denominator to be a ratio against, and a ratio needs a published bar, not a constant buried in a script — the same argument #70 already made for klink/nosepass/orbeetle. This is that page for the migration lane’s coverage rows.

Provenance. Seeded verbatim from nkp-studio’s packages/capture/src/gates/completeness.ts DEFAULT_COMPLETENESS_CONFIG — a completeness gate built on a denominator, tuned from real client captures rather than from taste. Two are wired into a swanna row today; the rest are published for the same reason the two are — a number anyone can question needs to be a number anyone can read — and are candidates for a future row rather than a silent gap.

Setting Value Gated by Enforced today?
minPageRatio 0.95 coverage-ratio — captured URL count (ariados) ÷ expected total (yamask’s WordPress REST denominator) must be at or above this Yes
thinWordFloor 50 no-thin-pages — a target page’s body word count, absolute, independent of the source Yes
maxThinRatio 0.30 the fraction of pages tolerated below thinWordFloor before the book (not just the one page) is worth flagging Not yet — no-thin-pages grades per page today, not the book-wide fraction
maxBodyTierRatio 0.50 the fraction of pages whose content came from a last-resort extraction tier Not yet — no row reads an extraction-tier signal
minImageResolveRatio 0.90 the fraction of images referenced in body content that resolve in the archive/inventory Not yet
maxDanglingLinkRatio 0.10 the fraction of internal links resolving to a captured slug Not yet
maxDuplicateTitleRatio 0.15 the fraction of pages sharing a duplicate or templated title Not yet

The denominator itself

denominator-established (yamask, capture group) sums X-WP-Total from every viewable, REST-exposed WordPress post type (/wp-json/wp/v2/types, then /wp-json/wp/v2/<rest_base>?per_page=1 per type) — the database’s own count, available only while the source still exists. “Viewable” is WordPress’s own metadata (is_post_type_viewable(), mirrored in the REST response), not a naming guess: it is what makes a WooCommerce product type count toward the total on a store while nav_menu_item/revision/other internal plumbing never does, without per-client configuration.

A section deliberately out of scope for a given migration (--exclude-type "<slug>: <reason>" on yamask) is removed from the denominator entirely and named on the sheet — a capped section is not a coverage failure, and the exclusion is a recorded decision, never a silent shrink of the bar.

No denominator available (REST disabled, the source not WordPress) is reported as SKIPPED with the reason, on both denominator-established and coverage-ratio — never a silent PASS on a number nobody checked.

Reading the ratio/floor together

A page thin on both sides passes the ratio-based ariados/doduo comparisons wholesale (38 words next to a source that was already 40 is a 5% drop, far under any comparison tolerance) — no-thin-pages is the one row that grades a page’s substance in absolute terms, independent of whatever the source looked like. coverage-ratio answers a different question at the URL-set level: not “is each known page still good” but “do we know about every page at all.”