{"rubric_version":5,"governing_rule":"`Blocked or challenged` requires POSITIVE evidence — a vendor-identifying header, a body signature, or a content-type flip. Ambiguity defaults to `Answered, no quote` or `Unreachable` plus a sub-code, never to a blocked claim. Misclassifying a bare 403 as blocked is recoverable; publishing a blocked claim we cannot support is not.","evaluation":"Rules are evaluated in the order below; the first match wins. Every observation records the rule id that decided it.","outcomes":["Quoted","Answered, no quote","Blocked or challenged","Unreachable","Not probed"],"note":"There is no value anywhere in this rubric, or anywhere in the schema, in which `dead` or `down` could be written. That is deliberate.","rules":[{"id":"R0","outcome":"Not probed","says":"Our own vantage failed this cycle — every canary was unreachable. Nothing in the cycle is a claim about anyone's endpoint."},{"id":"R1","outcome":"Unreachable","says":"DNS did not resolve (NXDOMAIN or SERVFAIL)."},{"id":"R2","outcome":"Unreachable","says":"The TCP connection timed out or was refused."},{"id":"R3","outcome":"Unreachable","says":"The connection was reset, or the TLS handshake failed. The CAUSE is undecidable from one vantage and we do not guess at it."},{"id":"R3b","outcome":"Unreachable","says":"The request failed before any response, in a way we could not name. We record it as unnamed rather than guessing at a timeout — MEDIUM confidence, because the outcome is solid and the cause is not."},{"id":"R4","outcome":"Blocked or challenged","says":"Cloudflare's own `cf-mitigated: challenge` header. Vendor-stated, definitive."},{"id":"R5","outcome":"Blocked or challenged","says":"AWS WAF's `x-amzn-waf-action` header said challenge or captcha. Vendor-stated, definitive."},{"id":"R6","outcome":"Blocked or challenged","says":"A DataDome block: its cookie, its script marker, or its captcha host."},{"id":"R7","outcome":"Blocked or challenged","says":"A HUMAN/PerimeterX block: its JSON block payload or its cookie."},{"id":"R8","outcome":"Blocked or challenged","says":"A Cloudflare 1xxx error code from the BLOCK family, read out of the response body — and only when the response ALSO carries an error status and a header Cloudflare's own edge sets. A page that merely discusses the error is prose, not evidence."},{"id":"R9","outcome":"Blocked or challenged","says":"Rate limited — HTTP 429, which is self-describing; or Cloudflare error 1015 in the body, under the same edge-corroboration rule as R8. Any `Retry-After` is recorded verbatim, and its absence is recorded too."},{"id":"R10","outcome":"Blocked or challenged","says":"HTTP 451: unavailable for legal reasons."},{"id":"R11","outcome":"Blocked or challenged","says":"HTTP 406 with no other signal — a WAF shape we grade at MEDIUM confidence, not high. Never applied when a valid payment quote parsed."},{"id":"R12","outcome":"Blocked or challenged","says":"A challenge page served with any status code, including 200, identified by vendor markers plus a content-type flip away from the JSON we asked for. Never applied when a valid payment quote parsed."},{"id":"R13","outcome":"Blocked or challenged","says":"A 403 whose body says in words that we were rate limited. Medium confidence; confirmable from the series. Never applied when a valid payment quote parsed."},{"id":"R14a","outcome":"Quoted","says":"A well-formed x402 v2 challenge in the `PAYMENT-REQUIRED` header — checked regardless of status code."},{"id":"R14b","outcome":"Quoted","says":"A well-formed x402 v1 challenge in the response body."},{"id":"R14c","outcome":"Quoted","says":"Both x402 transports on one response. Recorded once, never double-counted."},{"id":"R14d","outcome":"Quoted","says":"An MPP challenge in `WWW-Authenticate: Payment` carrying at least an amount or a recipient. We test the auth SCHEME token, never the header's presence (see R18b) — and a bare scheme with no price and no recipient is not a quote."},{"id":"R14e","outcome":"Quoted","says":"Both rails on one response: an x402 challenge AND an MPP challenge. Which rails an endpoint serves is its own fact."},{"id":"R15","outcome":"Answered, no quote","says":"HTTP 402 carrying nothing parseable in any transport — including an MPP scheme token with no amount and no recipient. Alive, asking to be paid, not payable as specified. The parse that was attempted is on the record."},{"id":"R16","outcome":"Answered, no quote","says":"A CDN-attributed origin failure. Either a 520-527 status, which is Cloudflare's own and self-describing, or a 5xx carrying a Cloudflare origin error code in the body — and the body form requires a header Cloudflare's edge sets, under the same corroboration rule as R8 and R9. Affirmative evidence the edge is alive and the origin is not; never a block claim."},{"id":"R17","outcome":"Answered, no quote","says":"A domain-parking page. Evidence the operator left, recorded at medium confidence."},{"id":"R18","outcome":"Answered, no quote","says":"A bare 403 with no vendor evidence. DELIBERATELY not a block: it is byte-identical to an origin's own 403, and AWS WAF's default block looks exactly like it."},{"id":"R18b","outcome":"Answered, no quote","says":"A credential wall — 401/407 whose offered schemes contain no `Payment` challenge."},{"id":"R19","outcome":"Answered, no quote","says":"Any other live response. The class is high-confidence; what it MEANS is left to the series."},{"id":"R20","outcome":"Not probed","says":"We did not look, for a reason about us: policy-excluded, not yet live, a scheduler gap, our own fault, a live robots.txt rule, a backoff we owed the operator after being rate-limited or blocked, a request we refuse to send because we hold no body for it or its URL is still a template, or the collection cycle running out of its wall-clock budget before it reached this endpoint."}],"quote_guarded_rules":{"rules":["R11","R12","R13"],"says":"These three rules are medium-confidence and run before the quote rules. None of them can fire when a well-formed payment challenge parsed — an endpoint that is quoting is recorded as quoting, whatever else its response carries."},"inputs":"Classification reads status + headers + content-type + body together. Status alone is insufficient and is the specific mistake that produces a corrupt health dataset.","confidence":["definitive","high","medium"],"changelog_note":"Every rule change bumps the version above and lands in the changelog with what it changed and what it did to records already held. An observation records the version that graded it, so a grade is re-runnable - and a version with no changelog entry would make that promise unkeepable, which is why the two are asserted together in our test suite.","changelog":[{"version":5,"date":"2026-08-21","adr":"ADR-025","changed":"R16 (CDN-attributed origin failure) now requires a header Cloudflare's edge sets before it will read a 1xxx origin code out of a response BODY - the same corroboration R8 and R9 got in v4. The 520-527 status arm is unchanged: those codes are Cloudflare's own. Before this, a 500 page that merely mentioned \"error code 1016\" was published as evidence the edge was alive and the origin was not, at high confidence.","effect_on_existing_records":"NO existing observation changes outcome, and NO existing record was rewritten. Measured before the change was made: R16 had never fired on the live archive. Observations taken before this version still say `rubric_version: 4`, permanently - they record the rules that actually graded them, and the boundary is visible in the data as a series break rather than only in this changelog. CORRECTED 2026-08-21 (ADR-027): this entry first said `dataset/` must be re-derived once for the version stamp to agree. It did not. Regeneration was stamping a module constant instead of reading the version the archive already records on every capture row; re-deriving would have flattened the v4/v5 boundary out of the data."},{"version":4,"date":"2026-08-21","adr":"ADR-013","changed":"A Cloudflare 1xxx code read out of a BODY now needs an error status AND an edge header (R8, R9's 1015 arm). And an MPP challenge needs at least an amount or a recipient: a bare `WWW-Authenticate: Payment` is no longer a quote, and lands on R15 with the parse attempt on the record.","effect_on_existing_records":"None. Made before go-live, against an empty archive - which is why it was free."},{"version":3,"date":"2026-08-20","adr":"(no ADR; recovered from commit 09538fd)","changed":"Two fixes, both found by M6 reading classify.ts against the method page it was writing. (1) THE QUOTE GUARD REACHED ONLY R12. R11 (a 406 read as a suspected WAF block) and R13 were unguarded, so a response carrying a well-formed 402 challenge could still be graded `Blocked or challenged` at medium confidence - blocked-vs-dead getting the wrong answer on an endpoint that was quoting normally, which is the product's core claim. All three medium rules now require `challenge.quote === null`. (2) `transport-unknown` was added as a named sub-code at MEDIUM confidence: a transport failure we could not name was previously reported as one we could, at high confidence.","effect_on_existing_records":"None: the archive did not exist yet. Bumped 2026-08-20T21:12Z, collector-live is 2026-08-21T08:00Z - fifteen hours before the first tick."},{"version":2,"date":"2026-08-20","adr":"(no ADR; the first committed rubric, commit 7614187)","changed":"The first rubric that ever ran: R6's F01-F30 as amended by Batch 0's refutation (F22 split into F22a/b/c/d for v2-header, v1-body, dual-transport and the pulsefeed regression; F29 re-pointed at the auth SCHEME token because MPP challenges also use WWW-Authenticate; F31 added so a valid MPP challenge classifies as Quoted rather than auth-required). R14's body-only definition was already superseded by R14a-R14e at this point - see DATA-MODEL.","effect_on_existing_records":"None: the archive did not exist yet. Bumped 2026-08-20T18:28Z, fourteen hours before the first tick."},{"version":1,"date":"(never shipped)","adr":"(none)","changed":"🔴 THERE IS NO v1 TO RECORD, AND THAT IS A MEASUREMENT RATHER THAN AN ADMISSION. Every commit that has ever contained collector/src/classify.ts already carries `RUBRIC_VERSION = 2` - the constant was bumped during drafting, before the file was first committed - so v1 never graded a response and never existed outside an editor. Checked by walking every commit in the repository for the value of that constant, not by memory.","effect_on_existing_records":"None. No code stamped v1 was ever run."}]}