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.”