"""The sealed rule for "have the agent discovery files spread since April?".

Sealed before any domain of this study was asked for any of these paths. Before the seal I
read: Cloudflare's post of 17 April 2026 (https://blog.cloudflare.com/agent-readiness/), SEP-2127,
the Server Card discovery.md and schema.json with its example files (repository
modelcontextprotocol/ext-server-card), sections 8.2 and 4.4.1 of the A2A specification 1.0.0, and
search snippets. I asked no site for one of these files in this study. The earlier llms-txt pack's
result (88 valid of the top 1,000, 6 September 2026) concerns llms.txt only. I looked for a current
count: Cloudflare says its Radar dataset updates weekly, but radar.cloudflare.com answered this server
403, and its survey page heimdall.testing.chambers.cfdata.org shows headings with no figures. So I
read no count newer than April.

Claim tested. Cloudflare, April 2026, top 200,000 domains (filtered by category): MCP Server
Cards and API Catalogs "together appear on fewer than 15 sites in the entire dataset". That
is under 0.0075 %. Since then the card's proposal went Final and much has been written telling
sites to publish these files. Nobody I read says "tenfold": the bar is my own reading of "these have
spread since April", the smallest growth that could not be a difference between two lists. The table
of counts is the product; the verdict only keeps me from reading it kindly.

Frame. Tranco list GQ3LK (28 October 2026, re-downloadable). SAMPLE_A: SIZE_A domains at random,
seed SEED, ranks 1 to 200,000. SAMPLE_B, printed only: ranks 1 to 1,000 entire.

1. Per domain, first CONTROL (a random name under /.well-known/ fixed in this file), and nothing
   further if the control gets no HTTP answer; then each path of PATHS. GET over https, certificate
   not checked, at most 5 redirects, TIMEOUT_S a request, body read to MAX_BODY bytes. If the bare
   domain gives no HTTP answer to the control, www.<domain> takes its place. One attempt each.
2. A domain is REACHABLE if the control got any HTTP answer. Unreachable ones are counted and left
   out of every share.
3. A path HOLDS a valid file by classify() below, on status 200, and only if its body differs from
   the control's body (a catch-all that answers everything is not a file).
4. PRIMARY = reachable domains in SAMPLE_A where mcp_card holds, or api_catalog holds, or ai_catalog
   holds with an MCP card entry. The first two are the files Cloudflare counted; the third is the route
   the Final SEP now names, and Cloudflare's count did not include it, so this count is wider than the
   one it is compared with: that favours the claim. Those two files alone are printed beside it, like
   for like. Estimate k/n with a Wilson 95 % interval.
5. Verdict, bar TENFOLD = 0.075 % (ten times Cloudflare's ceiling): killed if the upper bound is below
   the bar; survived if the lower bound is above the bar; inconclusive otherwise.

Void if any of:
V1 Fewer than 60 % of SAMPLE_A reachable.
V2 More than 40 % of the reachable answer the control with 401, 403, 429 or 503: then the scan
   mostly saw walls.
V3 llms.txt holds on fewer than 2 % of the reachable in SAMPLE_B: the same ranks gave 8.8 % in
   September (research/llms-txt), so below that the scanner cannot see a file known to be there.
V4 fixtures.py, served over real HTTP by a local server through the scanner's own fetch code, comes
   back with any wrong class. Its good files include the extension repository's own valid examples.

Printed only, cannot move the verdict: every path's count in A and B; the two files alone; how many
MCP cards carry the Final schema's exact $schema URL; A2A cards old and new; how
many positives share one body or one final host (one vendor's template is one decision, not many);
status-code tally per path; ranks 1-10,000 against the rest of A; each positive read by hand, whether
it is a real file for a real service. A hand reading may be reported beside the verdict and cannot
change it.

Limits, said before the data: Tranco is not Radar; Cloudflare's filter is not mine and removes some
domains I keep, so my rate is if anything lower than theirs for the same world; one server in Europe;
an honest User-Agent, so sites that block bots look like walls; a domain that serves its card only on a
subdomain (mcp.example.com) is not seen, and neither was it in the count I am comparing with. It says
"a file is there", never that any agent has read it.
"""
import json

SEED = 20261104
SIZE_A = 30000
RANK_A = 200000
TRANCO = "GQ3LK"
TIMEOUT_S = 6
MAX_BODY = 262144
CONTROL = "/.well-known/vesper-control-7f3a91c2.json"
PATHS = {
    "mcp_card": "/.well-known/mcp/server-card.json",
    "api_catalog": "/.well-known/api-catalog",
    "ai_catalog": "/.well-known/ai-catalog.json",
    "a2a_card": "/.well-known/agent-card.json",
    "a2a_old": "/.well-known/agent.json",
    "llms": "/llms.txt",
}
TENFOLD = 0.00075
MCP_CARD_TYPE = "application/mcp-server-card+json"
A2A_NEEDS = ("name", "version", "capabilities", "skills")


def _json_object(body):
    try:
        data = json.loads(body.decode("utf-8-sig"))
    except (ValueError, UnicodeDecodeError, RecursionError):
        return None
    return data if isinstance(data, dict) else None


def _is_mcp_card(d):
    """The Final schema's identity fields ($schema not demanded), or an earlier draft's shape: named,
    and says where or how to connect."""
    def text(key):
        return isinstance(d.get(key), str) and bool(d[key])
    if text("name") and text("version") and text("description"):
        return True
    named = text("name") or isinstance(d.get("serverInfo"), dict)
    connects = any(d.get(k) for k in ("remotes", "transport", "endpoints", "protocolVersion",
                                      "supportedProtocolVersions"))
    return bool(named and connects)


def _is_a2a_card(d):
    return (isinstance(d.get("name"), str) and bool(d["name"]) and isinstance(d.get("version"), str)
            and isinstance(d.get("capabilities"), dict) and isinstance(d.get("skills"), list))


def _is_api_catalog(d):
    """RFC 9727: a linkset document with at least one entry."""
    return isinstance(d.get("linkset"), list) and bool(d["linkset"])


def _is_ai_catalog(d):
    return isinstance(d.get("entries"), list) and bool(d["entries"])


def _is_llms(body):
    try:
        text = body.decode("utf-8-sig")
    except UnicodeDecodeError:
        return False
    return text.lstrip().startswith("# ")


def classify(key, status, body, control_body):
    """Does this answer hold a valid file of this kind? Rule step 3."""
    if status != 200 or not body or body == control_body:
        return False
    if key == "llms":
        return _is_llms(body)
    d = _json_object(body)
    if d is None:
        return False
    return {"mcp_card": _is_mcp_card, "api_catalog": _is_api_catalog, "ai_catalog": _is_ai_catalog,
            "a2a_card": _is_a2a_card, "a2a_old": _is_a2a_card}[key](d)


def ai_catalog_has_mcp(body):
    d = _json_object(body) or {}
    entries = d.get("entries")
    return isinstance(entries, list) and any(
        isinstance(e, dict) and e.get("type") == MCP_CARD_TYPE for e in entries)


def wilson(k, n, z=1.959964):
    if n == 0:
        return (0.0, 1.0)
    p = k / n
    den = 1 + z * z / n
    mid = (p + z * z / (2 * n)) / den
    half = z * ((p * (1 - p) / n + z * z / (4 * n * n)) ** 0.5) / den
    return (max(0.0, mid - half), min(1.0, mid + half))


def verdict(k, n):
    lo, hi = wilson(k, n)
    if hi < TENFOLD:
        return "killed"
    if lo > TENFOLD:
        return "survived"
    return "inconclusive"
