open-standards/wai/live-conformance/takedown_verify.py
Transaction Science 793ed1f2dc
All checks were successful
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 7s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 10s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 12s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 14s
WAI operation catalogue / does this change touch it (pull_request) Successful in 13s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 13s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 14s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 7s
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Successful in 6s
wai-fleet service / does this change touch it (pull_request) Successful in 14s
WAI neural sink gates / does this change touch it (pull_request) Successful in 13s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 12s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 9s
wai-ledger service / does this change touch it (pull_request) Successful in 14s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 14s
wai-key-proxy / does this change touch it (pull_request) Successful in 16s
WAI conformance / does this change touch it (pull_request) Successful in 31s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 32s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
WAI energy receipts / does this change touch it (pull_request) Successful in 33s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 8s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 7s
WAI Studio / does this change touch it (pull_request) Successful in 8s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 9s
wai-web wasm32 / does this change touch it (pull_request) Successful in 25s
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Successful in 5s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 55s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 1m11s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m58s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m22s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 1m51s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m32s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 2m31s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m51s
WAI neural sink gates / the exact JPEG-AI decoder reports its path (pinned inputs, required) (pull_request) Successful in 2m2s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m36s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 4m32s
wai-fleet service / service · linux (pull_request) Successful in 2m42s
wai-key-proxy / proxy · linux (pull_request) Successful in 57s
wai-pack / wai-pack · linux (pull_request) Successful in 3m19s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m30s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 27s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 5m59s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 4m11s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 5m56s
wai-ledger service / service · linux (pull_request) Successful in 1m50s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 7m18s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 1m7s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m48s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 7m43s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 3m41s
wai-fleet service / service · macos (pull_request) Successful in 48s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m39s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 4m54s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 3m2s
wai-pack / wai-pack · macos (pull_request) Successful in 3m27s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 5m55s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 13m52s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m28s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 16m34s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m38s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 27s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m7s
wai-ledger service / service · macos (pull_request) Successful in 1m6s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m35s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 10m56s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 17m20s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 18m48s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 24m50s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 18m38s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m31s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 7m3s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m46s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 8m4s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m47s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 17m16s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 7m47s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 34m23s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 21s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 16s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 7m5s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 18s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 37m20s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 10m52s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 18m17s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 11m18s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 39m59s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 15s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 40m12s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 50s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m42s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 3m47s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 17m30s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 18m34s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 14m28s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 14m15s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 30m4s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h18m5s
wai/one-way-delivery, converted to the layout of the fix: no shared append point
The change of wai/one-way-delivery (f1258784d..987625394, 28 commits), on the fix branch: its
changelog entries as files of wai/changes/, its wai-conformance.yml edits as step files,
corpus files and test tags, and every other file as the branch has it.

- 25240c4f0 wai: timeliness — when each object of a group is of use, and whether each holder held it in time
- c84992362 wai: timeliness_verify.py — the timeliness corpus's second implementation
- 44036fcdc wai: timeliness in the registry, SPEC §5, the changelog and CI; http_receipts.py checks its claims
- 27ce6754c wai: claim_verify.py lists not-held-by-deadline, and the claim corpus holds a session that switches for it
- 73d8d8eb9 wai: timeliness corpus — each holder's own line in key order, a holder's earliest fallback, and §7.2–§7.3's unreached plan rules
- 64c735f0c wai: the files timeliness changes are clippy-clean — a let chain, and the fetch callback's type allowed
- b72dc36bf wai: rung price — what delivering a rung costs, by bearer and direction, and which option a comparer takes
- 4e905e213 wai: rung_price_verify.py — the rung price corpus's second implementation
- 711fa6126 wai: rung price in the registry, SPEC §5, the changelog and CI
- 8ab70654e wai: rung price corpus — a refusal for every form rule 1 and §6.1 set, and the neighbour order a plan gives
- 1bc465959 wai: rung price ranks energy by checked evidence — a class counts only on the seller's own per-hop records, current and under a confirmed key
- 9e97997d9 wai: rung_price — the evidence text bound at its exact sizes, and a key confirmed for another class confirming nothing
- 8f1168683 wai: rung price corpus — a key confirmed for another class, the uncertainty product, each edge and order of the evidence verdict, and the evidence a plan read
- 7330e26ff wai: rung price — §6.1 links jwp-receipts at its first mention, and §10 says a record's age is its room's word
- 12c099457 wai: cache authority — who served what a node holds, and on whose authority
- f8f5d3e09 wai: cache_authority_verify.py — the cache authority corpus's second implementation
- 354146619 wai: cache authority in the registry, SPEC §5, the changelog and CI; its corpus holds what §12 states
- 55c7f3031 wai: timeliness §6 reads holdings records alone; cache-authority §9 derives the lines that read every kind
- b8fc6f02b wai: cache authority's lines read records as timeliness does, its deadline claim meets the envelopes held, and its corpus fixes each order and instant
- 91c5291cf wai: the files cache authority changes are clippy-clean — a boxed envelope, the base's names named once, slices borrowed
- 8e479daff wai: a held envelope boxes both its forms, so neither outweighs the other
- 3ec8da807 wai: impression aggregates — ad presentations counted over a bearer with no return path
- 0433a35c3 wai: impression_aggregate_verify.py — the impression aggregates corpus's second implementation
- 7d8e150e4 wai: impression aggregates in the registry, SPEC §5, the changelog and CI; its corpus states each receipt whole and a control for every malformed file
- d5e8ace02 wai: impression aggregates' size bounds are checked at their edge, and §4.3's decides no outcome
- 4bc8e13bd wai: impression aggregates' table totals are exact, and a receipt given twice is checked when either occurrence was
- 3c9f20524 wai: key_log's two result enums keep their public form, and say so to clippy
- 987625394 wai: Item::with_authority and Count::instance_received build only with the feature that calls each

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 01:45:32 -05:00

884 lines
38 KiB
Python

#!/usr/bin/env python3
"""takedown_verify.py — the second implementation of extensions/takedown.md (wai.media.takedown).
Written from the extension's text, sharing no code with wai-rs. It reads every record of §5
under §2's encodings, computes the derivation closure of §6 from the derivation records (read
with the deriving-edge corpus's second implementation, edge-conformance/edge_verify.py), the
origin claims (read with the synthesis corpus's, live-conformance/synthesis_verify.py), the
delivery receipts of the groups it reaches (read with edge_verify.py too), the
acknowledgements that show who served or received an object (read with the settlement corpus's,
live-conformance/settlement_verify.py) and the cache-transmission claims that show what a node sent
from what it held (read through jwp-receipts §3.1 steps 1 to 4 with the claim corpus's,
claim-conformance/claim_verify.py), holds the takedown's
stated set to it, finds the holders and the purges that cover them (§7), computes the time to
removal against the clock the registry gives (§4, §7.3), and recomputes the counts (§9). Every
signature is checked with WAI's strict Ed25519 rule (claim-conformance/_ed25519.py).
Then it requires every mutated copy of the corpus to fail, and, with each of its own checks
dropped in turn, requires the corpus to catch the weakened verifier.
python3 -I takedown_verify.py <live-conformance/takedown>
The standard library and the `cryptography` package, which the claim verifier imports. Exit
status 0 when everything holds, 1 otherwise.
"""
import calendar
import copy
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
sys.path.insert(0, os.path.join(HERE, "..", "edge-conformance"))
import _ed25519 as ED # noqa: E402
from _blake3 import blake3 # noqa: E402
import edge_verify as EV # noqa: E402
import synthesis_verify as SYV # noqa: E402
import settlement_verify as SEV # noqa: E402
import claim_verify as CV # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "takedown.md")
PUBLISHED = os.path.join(HERE, "..", "registry", "takedown-clocks.json")
SIGN = b"wai:takedown\x01" # §2
LINK = b"wai:takedown-link\x01" # §2
MAX_INT = (1 << 53) - 1
ROLES = ("origin", "store", "relay", "cache", "edge")
CHECKS = (
"registry-shape", "signature", "registry-link", "policy-clocks", "takedown-policy", "takedown-signer",
"notice-held", "notice-targets", "clock-in-policy", "clock-consistent", "records", "claims", "acknowledgements",
"cache-transmissions",
"purge-takedown", "rule-group", "rule-derivation", "rule-claim", "group-expanded",
"exact-supported", "match-not-exact", "exact-complete", "match-held", "match-anchored",
"store-holders", "deriver-holders", "claimant-holders", "publisher-holders", "served-holders", "sent-holders",
"holder-only", "group-coverage", "deadline", "purges-complete", "window",
"counts-as-of", "counts-rows",
)
DROPPED = set()
def on(check):
assert check in CHECKS, check
return check not in DROPPED
class Refuse(Exception):
def __init__(self, reason, at):
super().__init__(reason)
self.reason, self.at = reason, at
class Bad(Exception):
"""A member that breaks its rule: the record is record_malformed."""
def need(cond):
if not cond:
raise Bad()
# ---------------------------------------------------------------- §2 encodings
def is_hex(v, n):
return isinstance(v, str) and re.fullmatch("[0-9a-f]{%d}" % (2 * n), v) is not None
def is_int(v):
return isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= MAX_INT
def text(v, lo, hi):
need(isinstance(v, str) and lo <= len(v) <= hi)
return v
def days_in(y, m):
if m == 2:
return 29 if y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) else 28
return 30 if m in (4, 6, 9, 11) else 31
def date(s):
m = isinstance(s, str) and re.fullmatch(r"([0-9]{4})-([0-9]{2})-([0-9]{2})", s)
need(bool(m))
y, mo, d = (int(m.group(i)) for i in (1, 2, 3))
need(1 <= mo <= 12 and 1 <= d <= days_in(y, mo))
return y, mo, d
def time_value(s):
"""§2: `YYYY-MM-DDTHH:MM:SSZ`, a year of 1970 or later, a second of at most 59: its value."""
m = isinstance(s, str) and re.fullmatch(r"([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z", s)
need(bool(m))
y, mo, d = date(m.group(1))
hh, mi, ss = int(m.group(2)), int(m.group(3)), int(m.group(4))
need(y >= 1970 and hh <= 23 and mi <= 59 and ss <= 59)
return calendar.timegm((y, mo, d, hh, mi, ss, 0, 0, 0))
def clock_id(s):
need(isinstance(s, str) and 1 <= len(s.encode()) <= 64 and re.fullmatch(r"[a-z0-9]+(-[a-z0-9]+)*", s) is not None)
return s
_SIGS = {}
def ed_verify(pk, msg, sig):
"""WAI's strict Ed25519 rule (_ed25519.verify), remembered: the mutations and the dropped
checks verify the corpus many times over, and a verdict is a function of its three inputs."""
k = (pk, msg, sig)
if k not in _SIGS:
_SIGS[k] = ED.verify(pk, msg, sig)
return _SIGS[k]
def link_of(v):
return blake3(LINK + EV.jcs(v).encode("utf-8"))
def members(v, names):
need(isinstance(v, dict) and set(v) == set(names))
return v
# ---------------------------------------------------------------- §3 subjects
def subject(v):
need(isinstance(v, dict))
t = v.get("type")
if t == "object":
members(v, ["type", "content_hash"])
need(is_hex(v["content_hash"], 32))
elif t == "group":
members(v, ["type", "receipt"])
need(is_hex(v["receipt"], 32))
elif t == "track":
members(v, ["type", "track", "from_group"])
need(is_hex(v["track"], 32) and is_int(v["from_group"]))
elif t == "take":
members(v, ["type", "take"])
need(is_hex(v["take"], 32))
else:
raise Bad()
return v
def key(s):
"""A subject's JCS form: lists of subjects are in its byte order (§3)."""
return EV.jcs(s)
def subject_list(v, lo, hi):
need(isinstance(v, list) and lo <= len(v) <= hi)
out = [subject(x) for x in v]
ks = [key(x).encode() for x in out]
need(all(a < b for a, b in zip(ks, ks[1:])))
return out
def obj(h):
return {"type": "object", "content_hash": h}
# ---------------------------------------------------------------- records
def strict(text_):
v = EV.strict(text_)
need(EV.in_subset(v))
return v
def read_registry(t):
try:
v = strict(t)
members(v, ["kind", "clocks"])
need(v["kind"] == "wai-takedown-clocks")
cs = v["clocks"]
need(isinstance(cs, list) and 1 <= len(cs) <= 64)
clocks = {}
ids = []
for c in cs:
members(c, ["id", "type", "duration_s", "starts", "instrument", "section", "applies_from"])
clock_id(c["id"])
need(c["type"] in ("deadline", "reply_window", "unfixed"))
d = c["duration_s"]
need(d is None or (is_int(d) and d >= 1))
need((c["type"] == "unfixed") == (d is None))
need(c["starts"] in ("receipt", "statement"))
text(c["instrument"], 1, 500)
text(c["section"], 1, 200)
date(c["applies_from"])
clocks[c["id"]] = c
ids.append(c["id"].encode())
need(all(a < b for a, b in zip(ids, ids[1:])))
except (Bad, ValueError, KeyError, TypeError, AttributeError):
if on("registry-shape"):
raise Refuse("record_malformed", ["registry"])
raise Refuse("record_malformed", ["registry", "unread"]) from None
return {"link": link_of(v), "clocks": clocks}
def read_signed(t, kind, names, fields, at):
"""§2: the shape, the kind's own members, then the signature."""
try:
v = strict(t)
members(v, names)
need(v["kind"] == kind and is_hex(v["signer_pubkey"], 32) and is_hex(v["sig"], 64))
r = fields(v)
except (Bad, ValueError, KeyError, TypeError, AttributeError):
raise Refuse("record_malformed", at)
bad = ed_verify(bytes.fromhex(v["signer_pubkey"]), SIGN + EV.jcs({k: x for k, x in v.items() if k != "sig"}).encode("utf-8"),
bytes.fromhex(v["sig"]))
if bad and on("signature"):
raise Refuse(bad, at)
r.update(link=link_of(v), signer=v["signer_pubkey"])
return r
def policy_fields(v):
u = v["use"]
need(isinstance(u, list) and len(u) <= 64)
for x in u:
clock_id(x)
need(all(a.encode() < b.encode() for a, b in zip(u, u[1:])))
st = v["stores"]
need(isinstance(st, list) and 1 <= len(st) <= 256)
keys = []
for s in st:
members(s, ["key", "role"])
need(is_hex(s["key"], 32) and s["role"] in ROLES)
keys.append(s["key"])
need(all(a < b for a, b in zip(keys, keys[1:])))
need(is_hex(v["clocks"], 32))
time_value(v["issued_at"])
return {"clocks": v["clocks"], "use": list(u), "stores": keys}
def notice_fields(v):
need(v["type"] in ("request", "order") and v["ground"] in ("illegal", "terms"))
text(v["ground_ref"], 0, 500)
text(v["statement"], 1, 5000)
text(v["contact"], 0, 500)
return {"type": v["type"], "targets": subject_list(v["targets"], 1, 16), "issued_at": time_value(v["issued_at"])}
def takedown_fields(v):
m = v["matched"]
need(isinstance(m, list) and len(m) <= 256)
for e in m:
members(e, ["match", "candidate"])
need(is_hex(e["match"], 32) and is_hex(e["candidate"], 32))
need(all(a["match"] < b["match"] for a, b in zip(m, m[1:])))
need(v["verdict"] in ("remove", "disable"))
a = members(v["automated"], ["detection", "decision"])
need(a["detection"] in ("yes", "no") and a["decision"] in ("not", "partially", "fully"))
text(v["explanation"], 1, 2000)
text(v["facts"], 1, 5000)
need(is_hex(v["policy"], 32) and (v["notice"] is None or is_hex(v["notice"], 32)))
if v["clock"] is not None:
clock_id(v["clock"])
cs = None if v["clock_start"] is None else time_value(v["clock_start"])
ra = None if v["reply_at"] is None else time_value(v["reply_at"])
need(v["ground"] in ("illegal", "terms") and is_int(v["category"]) and 1 <= v["category"] <= 16)
return {"policy": v["policy"], "notice": v["notice"], "clock": v["clock"], "clock_start": cs, "reply_at": ra,
"targets": subject_list(v["targets"], 1, 16), "exact": subject_list(v["exact"], 1, 4096), "matched": m,
"ground": v["ground"], "category": v["category"], "automated": a["decision"] != "not",
"decided_at": time_value(v["decided_at"])}
def match_fields(v):
need(is_hex(v["source"], 32) and is_hex(v["candidate"], 32) and v["source"] != v["candidate"])
SYV.term(v["method"])
need(is_int(v["distance"]) and is_int(v["threshold"]) and v["distance"] <= v["threshold"])
time_value(v["issued_at"])
return {"source": v["source"], "candidate": v["candidate"]}
def purge_fields(v):
need(is_hex(v["takedown"], 32))
return {"takedown": v["takedown"], "subjects": subject_list(v["subjects"], 1, 4096), "purged_at": time_value(v["purged_at"])}
ROW = ["clock", "ground", "category", "source", "takedowns", "met", "missed", "pending", "unfixed", "automated", "median_s"]
def row_order(r):
return (r["clock"] is not None, (r["clock"] or "").encode(), r["ground"], r["category"], r["source"])
def counts_fields(v):
td = v["takedowns"]
need(isinstance(td, list) and len(td) <= 4096 and all(is_hex(x, 32) for x in td))
need(all(a < b for a, b in zip(td, td[1:])))
rows = v["rows"]
need(isinstance(rows, list))
for r in rows:
members(r, ROW)
if r["clock"] is not None:
clock_id(r["clock"])
need(r["ground"] in ("illegal", "terms") and is_int(r["category"]) and 1 <= r["category"] <= 16)
need(r["source"] in ("order", "request", "own"))
need(all(is_int(r[k]) for k in ROW[4:10]) and (r["median_s"] is None or is_int(r["median_s"])))
need(r["takedowns"] >= 1 and r["met"] + r["missed"] + r["pending"] + r["unfixed"] == r["takedowns"])
need(r["automated"] <= r["takedowns"])
need(all(row_order(a) < row_order(b) for a, b in zip(rows, rows[1:])))
f, t = time_value(v["from"]), time_value(v["to"])
need(f < t and is_hex(v["policy"], 32))
time_value(v["issued_at"])
return {"policy": v["policy"], "from": f, "to": t, "as_of": time_value(v["as_of"]), "takedowns": td, "rows": rows}
# ---------------------------------------------------------------- §6 group expansion
def expand(receipts, g):
"""§6.3: (publisher, objects) or None."""
gr = None
for r in receipts:
if isinstance(r, dict) and r.get("kind") == "group":
x = EV.read_group(r)
if x is not None and EV.link(x).hex() == g:
gr = x
break
if gr is None or not EV.group_verifies(gr) or gr["leaf_count"] > 4096:
return None
objs = [EV.read_object(r) for r in receipts if isinstance(r, dict) and r.get("kind") == "object"]
out = []
for p in range(gr["leaf_count"]):
found = next((o for o in objs if o is not None and o["track_alias"] == gr["track_alias"] and o["group_id"] == gr["group_id"]
and o["object_id"] == p and EV.included(o, gr)), None)
if found is None:
return None
out.append(found["content_hash"])
return gr["signer_pubkey"], out
# ---------------------------------------------------------------- §8 one case
def evaluate(case, as_of):
reg = read_registry(case["registry"])
pol = read_signed(case["policy"], "wai-takedown-policy", ["kind", "clocks", "use", "stores", "issued_at", "signer_pubkey", "sig"],
policy_fields, ["policy"])
if pol["clocks"] != reg["link"].hex() and on("registry-link"):
raise Refuse("registry_mismatch", ["policy"])
if any(u not in reg["clocks"] for u in pol["use"]) and on("policy-clocks"):
raise Refuse("clock_unknown", ["policy"])
notices = [read_signed(t, "wai-takedown-notice", ["kind", "type", "targets", "ground", "ground_ref", "statement", "contact",
"issued_at", "signer_pubkey", "sig"], notice_fields, ["notice", i])
for i, t in enumerate(case.get("notices", []))]
at = ["takedown"]
td = read_signed(case["takedown"], "wai-takedown",
["kind", "policy", "notice", "clock", "clock_start", "reply_at", "targets", "verdict", "exact", "matched", "ground",
"category", "automated", "explanation", "facts", "decided_at", "signer_pubkey", "sig"], takedown_fields, at)
if td["policy"] != pol["link"].hex() and on("takedown-policy"):
raise Refuse("policy_mismatch", at)
if td["signer"] != pol["signer"] and on("takedown-signer"):
raise Refuse("not_authority", at)
notice = None
if td["notice"] is not None:
notice = next((n for n in notices if n["link"].hex() == td["notice"]), None)
if notice is None and on("notice-held"):
raise Refuse("notice_unknown", at)
if notice is not None:
nk = {key(x) for x in notice["targets"]}
if (not all(key(x) in nk for x in td["targets"]) or notice["issued_at"] > td["decided_at"]) and on("notice-targets"):
raise Refuse("notice_mismatch", at)
clock = None
if td["clock"] is not None:
if td["clock"] not in pol["use"] and on("clock-in-policy"):
raise Refuse("clock_unknown", at)
clock = reg["clocks"].get(td["clock"])
cs, ra = td["clock_start"], td["reply_at"]
ok = (td["clock"] is None) == (cs is None)
ok = ok and (cs is None or cs <= td["decided_at"])
if ok and clock is not None and cs is not None and clock["starts"] == "receipt":
ok = notice is not None and notice["issued_at"] <= cs
if ok and ra is not None:
ok = clock is not None and clock["type"] == "reply_window" and cs is not None and ra >= cs
if not ok and on("clock-consistent"):
raise Refuse("clock_invalid", at)
records = []
for i, t in enumerate(case.get("records", [])):
try:
r = EV.read_record(t, i)
good = ed_verify(bytes.fromhex(r["signer_pubkey"]), EV.signing_input(r["raw"]), bytes.fromhex(r["sig"])) is None
except EV.Refuse:
r, good = None, False
if not good and on("records"):
raise Refuse("evidence_invalid", ["record", i])
if r is not None:
records.append(r)
claims = []
for i, t in enumerate(case.get("claims", [])):
try:
c = SYV.read(t, "wai-origin", SYV.ORIGIN, SYV.origin_fields)
except SYV.Refuse:
if on("claims"):
raise Refuse("evidence_invalid", ["claim", i])
continue
claims.append(c)
acks = []
for i, a in enumerate(case.get("acknowledgements", [])):
try:
rec = SEV.read_ack(bytes.fromhex(a["record"]))
lists = [bytes.fromhex(x) for x in a["items"]]
need(len(lists) == len(rec["entries"]))
hashes = []
for e, raw in zip(rec["entries"], lists):
items = SEV.read_items(raw)
need(SEV.recomputes(e, items, raw))
hashes += [h.hex() for h, _, _ in items]
hop = rec["downstream"].hex() if rec["mode"] == SEV.ACK and rec["role"] == SEV.HOP else None
acks.append((rec["upstream"].hex(), hop, hashes))
except (SEV.Refuse, Bad, ValueError, KeyError, TypeError):
if on("acknowledgements"):
raise Refuse("evidence_invalid", ["acknowledgement", i])
# The cache-transmission claims (cache-authority §5): each of that class and through
# jwp-receipts §3.1 steps 1 to 4; what each shows, (signer, group, positions sent).
sent = []
for i, t in enumerate(case.get("cache_transmissions", [])):
try:
c = CV.verify_claim(t)
need(c["class"] == "cache-transmission")
except (CV.Reject, Bad):
if on("cache-transmissions"):
raise Refuse("evidence_invalid", ["cache_transmission", i])
continue
bits = bytes.fromhex(c["body"]["sent"])
sent.append((c["signer_pubkey"], c["subject"], [p for p in range(8 * len(bits)) if bits[p // 8] & (0x80 >> (p % 8))]))
matches = [read_signed(t, "wai-takedown-match", ["kind", "source", "candidate", "method", "distance", "threshold", "issued_at",
"signer_pubkey", "sig"], match_fields, ["match", i])
for i, t in enumerate(case.get("matches", []))]
purges = []
for i, t in enumerate(case.get("purges", [])):
p = read_signed(t, "wai-takedown-purge", ["kind", "takedown", "subjects", "purged_at", "signer_pubkey", "sig"], purge_fields,
["purge", i])
if (p["takedown"] != td["link"].hex() or p["purged_at"] > as_of) and on("purge-takedown"):
raise Refuse("purge_mismatch", ["purge", i])
purges.append(p)
# §6.1: the closure, by its rules until none adds a subject.
closure = {key(x): x for x in td["targets"]}
groups = {}
via_records, via_claims = set(), set()
def claim_subject(c):
# Samples a party presents and does not publish (synthesis §3.1) are no item a holder
# serves or keeps for others: such a claim adds nothing (§4.2 rule 3).
s = c["value"]["subject"]
return None if s.get("type") == "samples" else {k: s[k] for k in s}
while True:
before = len(closure)
for s in list(closure.values()):
if s["type"] == "group" and s["receipt"] not in groups:
e = expand(case.get("receipts", []), s["receipt"])
groups[s["receipt"]] = e
if e is not None and on("rule-group"):
for h in e[1]:
closure[key(obj(h))] = obj(h)
objects = {s["content_hash"] for s in closure.values() if s["type"] == "object"}
for i, r in enumerate(records):
if any(x["content_hash"] in objects for x in r["inputs"]) and on("rule-derivation"):
via_records.add(i)
closure[key(obj(r["output"]["content_hash"]))] = obj(r["output"]["content_hash"])
for i, c in enumerate(claims):
if any(x["content_hash"].hex() in objects for x in c["inputs"]) and on("rule-claim"):
via_claims.add(i)
s = claim_subject(c)
if s is not None:
closure[key(s)] = s
unexpanded = any(s["type"] == "group" and s["receipt"] not in groups for s in closure.values())
if len(closure) == before and not unexpanded:
break
for g in sorted(groups):
if groups[g] is None and on("group-expanded"):
raise Refuse("group_unexpanded", ["group", g])
objects = {s["content_hash"] for s in closure.values() if s["type"] == "object"}
# §6.4, §6.5: the stated set and the matches.
candidates = {m["candidate"] for m in matches}
for k, s in enumerate(td["exact"]):
if key(s) not in closure:
if s["type"] == "object" and s["content_hash"] in candidates and on("match-not-exact"):
raise Refuse("match_as_exact", ["exact", k])
if on("exact-supported"):
raise Refuse("closure_unsupported", ["exact", k])
stated = {key(s) for s in td["exact"]}
for k, ks in enumerate(sorted(closure, key=lambda x: x.encode())):
if ks not in stated and on("exact-complete"):
raise Refuse("closure_incomplete", ["closure", k])
for k, e in enumerate(td["matched"]):
m = next((m for m in matches if m["link"].hex() == e["match"] and m["candidate"] == e["candidate"]), None)
if m is None:
if on("match-held"):
raise Refuse("match_unknown", ["matched", k])
continue
if m["source"] not in objects and on("match-anchored"):
raise Refuse("match_unanchored", ["matched", k])
# §7.1: the required pairs.
required, roles = {}, {}
def needs(k, s, role):
roles.setdefault(k, set()).add(role)
required[(k, key(s))] = s
if on("store-holders"):
for k in pol["stores"]:
for s in closure.values():
needs(k, s, "store")
if on("deriver-holders"):
for i in sorted(via_records):
r = records[i]
for x in r["inputs"]:
if x["content_hash"] in objects:
needs(r["signer_pubkey"], obj(x["content_hash"]), "deriver")
needs(r["signer_pubkey"], obj(r["output"]["content_hash"]), "deriver")
if on("claimant-holders"):
for i in sorted(via_claims):
c = claims[i]
for x in c["inputs"]:
if x["content_hash"].hex() in objects:
needs(c["signer"].hex(), obj(x["content_hash"].hex()), "claimant")
if claim_subject(c) is not None:
needs(c["signer"].hex(), claim_subject(c), "claimant")
if on("publisher-holders"):
for g, e in groups.items():
if key({"type": "group", "receipt": g}) in closure:
needs(e[0], {"type": "group", "receipt": g}, "publisher")
if on("served-holders"):
for up, hop, hashes in acks:
for h in hashes:
if h in objects:
needs(up, obj(h), "served")
if hop is not None:
needs(hop, obj(h), "received")
if on("sent-holders"):
# A node's own word that it sent objects of a group, read by position through the group's
# object receipts (§6.3); a group not among the receipts names no object.
for signer, g, pos in sent:
e = expand(case.get("receipts", []), g)
if e is None:
continue
for p in pos:
if p < len(e[1]) and e[1][p] in objects:
needs(signer, obj(e[1][p]), "served")
matched_required = {}
cands = {e["candidate"] for e in td["matched"]}
for k in pol["stores"]:
for c in cands:
matched_required[(k, key(obj(c)))] = obj(c)
for up, hop, hashes in acks:
for h in hashes:
if h in cands:
for k in [up] + ([hop] if hop is not None else []):
matched_required[(k, key(obj(h)))] = obj(h)
# §7.2: what a purge covers.
def covers(p, s):
named = {key(x) for x in p["subjects"]}
if key(s) in named:
return True
if not on("group-coverage"):
return False
if s["type"] == "object":
return any(x["type"] == "group" and groups.get(x["receipt"]) and s["content_hash"] in groups[x["receipt"]][1]
for x in p["subjects"])
if s["type"] == "group" and groups.get(s["receipt"]):
return all(key(obj(h)) in named for h in groups[s["receipt"]][1])
return False
def pair_time(k, s):
ts = [p["purged_at"] for p in purges if (p["signer"] == k or not on("holder-only")) and covers(p, s)]
return min(ts) if ts else None
missing, times = [], []
for (k, ks) in sorted(required, key=lambda x: (x[0], x[1].encode())):
tt = pair_time(k, required[(k, ks)])
if tt is None:
missing.append({"holder": k, "subject": required[(k, ks)]})
else:
times.append(tt)
m_missing, m_times = [], []
for (k, ks) in sorted(matched_required, key=lambda x: (x[0], x[1].encode())):
tt = pair_time(k, matched_required[(k, ks)])
if tt is None:
m_missing.append({"holder": k, "subject": matched_required[(k, ks)]})
else:
m_times.append(tt)
uncounted = []
for i, p in enumerate(purges):
for s in p["subjects"]:
if (p["signer"], key(s)) not in required and (p["signer"], key(s)) not in matched_required:
uncounted.append({"purge": i, "subject": s})
# §7.3: the time to removal and the clock.
complete = not missing
first = min(times) if times else None
last = max(times) if times else None
start = cs if cs is not None else td["decided_at"]
ttr = max(0, last - start) if complete and last is not None else None
deadline = window_end = None
reason = None
if clock is not None and clock["type"] == "deadline":
deadline = start + clock["duration_s"]
if complete and last is not None and (last <= deadline or not on("deadline")):
verdict = "met"
elif complete:
verdict, reason = "missed", "deadline_missed"
elif as_of > deadline and on("purges-complete"):
verdict, reason = "missed", "purge_missing"
else:
verdict = "pending"
elif clock is not None and clock["type"] == "reply_window":
window_end = start + clock["duration_s"] if ra is None else min(ra, start + clock["duration_s"])
if first is not None and first < window_end and on("window"):
verdict, reason = "missed", "window_not_observed"
else:
verdict = "met" if complete else "pending"
else:
verdict = "unfixed" if complete else "pending"
facts = {
"closure": [closure[k] for k in sorted(closure, key=lambda x: x.encode())],
"holders": [{"key": k, "roles": sorted(roles[k]), "required": sum(1 for (kk, _) in required if kk == k)} for k in sorted(roles)],
"missing": missing,
"uncounted": uncounted,
"clock": td["clock"],
"start": start,
"deadline": deadline,
"window_end": window_end,
"first_purge": first,
"last_purge": last,
"time_to_removal_s": ttr,
"verdict": verdict,
"matched": {"candidates": sorted({e["candidate"] for e in td["matched"]}), "missing": m_missing,
"last_purge": max(m_times) if m_times else None},
}
if reason is not None:
out = {"outcome": "missed", "reason": reason, "facts": facts}
elif verdict == "pending":
out = {"outcome": "pending", "facts": facts}
else:
out = {"outcome": "accept", "facts": facts}
source = "own" if notice is None else notice["type"]
return {"policy": pol, "takedown": td, "source": source, "outcome": out}
def verify_case(case):
try:
return evaluate(case, case["as_of"])["outcome"]
except Refuse as e:
return {"outcome": "refuse", "reason": e.reason, "at": e.at}
# ---------------------------------------------------------------- §9 counts
def rows_of(done):
by = {}
for e in done:
t = e["takedown"]
k = (t["clock"] is not None, (t["clock"] or "").encode(), t["ground"], t["category"], e["source"])
r, ttr = by.setdefault(k, ({"clock": t["clock"], "ground": t["ground"], "category": t["category"], "source": e["source"],
"takedowns": 0, "met": 0, "missed": 0, "pending": 0, "unfixed": 0, "automated": 0,
"median_s": None}, []))
r["takedowns"] += 1
r["automated"] += 1 if t["automated"] else 0
f = e["outcome"]["facts"]
r[f["verdict"]] += 1
if f["time_to_removal_s"] is not None:
ttr.append(f["time_to_removal_s"])
out = []
for k in sorted(by):
r, ttr = by[k]
ttr.sort()
r["median_s"] = ttr[(len(ttr) - 1) // 2] if ttr else None
out.append(r)
return out
def verify_counts(counts_text, cases):
c = read_signed(counts_text, "wai-takedown-counts", ["kind", "policy", "from", "to", "as_of", "takedowns", "rows", "issued_at",
"signer_pubkey", "sig"], counts_fields, ["counts"])
done = []
for i, case in enumerate(cases):
try:
done.append(evaluate(case, c["as_of"] if on("counts-as-of") else case["as_of"]))
except Refuse as e:
raise Refuse(e.reason, ["case", i] + e.at)
for e in done:
if e["policy"]["link"].hex() != c["policy"]:
raise Refuse("policy_mismatch", ["counts"])
if e["policy"]["signer"] != c["signer"]:
raise Refuse("not_authority", ["counts"])
links = [e["takedown"]["link"].hex() for e in done]
if set(links) != set(c["takedowns"]) or len(set(links)) != len(links) \
or any(not c["from"] <= e["takedown"]["decided_at"] < c["to"] for e in done):
raise Refuse("counts_mismatch", ["counts"])
rows = rows_of(done)
if rows != c["rows"] and on("counts-rows"):
raise Refuse("counts_mismatch", ["counts"])
return rows
# ---------------------------------------------------------------- the corpus
def case_of(world, over):
c = dict(world)
c.update(over)
c["as_of"] = time_value(c["as_of"])
return c
def section(text_, head):
at = text_.index("\n" + head)
nxt = re.search(r"\n#{2} ", text_[at + 1:])
return text_[at: at + 1 + nxt.start()] if nxt else text_[at:]
with open(EXTENSION, encoding="utf-8") as _f:
_TEXT = _f.read()
REASONS = [line.split("`")[1] for line in section(_TEXT, "## 11. Reasons").splitlines() if line.startswith("| `")]
def check(doc, quiet=True):
fails = []
world = doc["world"]
by = {}
for v in doc["cases"]:
try:
case = case_of(world, v["case"])
by[v["name"]] = case
got = verify_case(case)
except Exception as e: # noqa: BLE001 — a crash on a mutated or weakened run is a failure
got = {"outcome": "crash: %r" % (e,)}
if got != v["expect"]:
fails.append("%s: got %s" % (v["name"], json.dumps(got)[:240]))
if not quiet:
print("FAIL case %s: got %s, the corpus states %s" % (v["name"], json.dumps(got)[:300], json.dumps(v["expect"])[:300]))
elif not quiet:
print("PASS case %s: %s" % (v["name"], got["outcome"] + (" %s" % got["reason"] if "reason" in got else "")
+ (" at %s" % got["at"] if "at" in got else "")))
for v in doc["counts"]:
try:
rows = verify_counts(v["counts"], [by[n] for n in v["cases"]])
got = {"outcome": "accept", "rows": rows}
except Refuse as e:
got = {"outcome": "refuse", "reason": e.reason, "at": e.at}
except Exception as e: # noqa: BLE001
got = {"outcome": "crash: %r" % (e,)}
if got != v["expect"]:
fails.append("%s: got %s" % (v["name"], json.dumps(got)[:240]))
if not quiet:
print("FAIL counts %s: got %s" % (v["name"], json.dumps(got)[:300]))
elif not quiet:
print("PASS counts %s: %s" % (v["name"], got["outcome"] + (" %s" % got["reason"] if "reason" in got else "")))
return fails
def flip_hex_digit(s, at):
return s[:at] + ("1" if s[at] != "1" else "2") + s[at + 1:]
def mutations(doc):
"""Copies of the corpus, each with one defect a conforming verifier must catch."""
by = {v["name"]: i for i, v in enumerate(doc["cases"])}
def mutate(label, f):
d = copy.deepcopy(doc)
f(d)
return label, d
def case(d, name):
return d["cases"][by[name]]
def retext(texts, i, f):
v = json.loads(texts[i])
f(v)
texts[i] = EV.jcs(v)
def facts(d, name):
return case(d, name)["expect"]["facts"]
return [
mutate("a digit of the met request's takedown signature",
lambda d: case(d, "request-48h-met")["case"].update(takedown=EV.jcs(dict(json.loads(case(d, "request-48h-met")["case"]["takedown"]), sig=flip_hex_digit(json.loads(case(d, "request-48h-met")["case"]["takedown"])["sig"], 7))))),
mutate("the met request's last purge withheld", lambda d: case(d, "request-48h-met")["case"]["purges"].pop()),
mutate("the late purge of the missed order moved an hour earlier",
lambda d: retext(case(d, "order-3h-missed")["case"]["purges"], 5, lambda v: v.update(purged_at="2026-10-21T10:57:40Z"))),
mutate("the met request's time to removal stated otherwise", lambda d: facts(d, "request-48h-met").update(time_to_removal_s=42899)),
mutate("the met request's closure stated without its live track", lambda d: facts(d, "request-48h-met")["closure"].pop()),
mutate("the stranger's purge stated as counted", lambda d: facts(d, "stranger-purge-not-counted").update(uncounted=[])),
mutate("the matched candidate stated as purged by nobody", lambda d: facts(d, "request-48h-met")["matched"].update(last_purge=None)),
mutate("a registry duration changed", lambda d: d["world"].update(registry=d["world"]["registry"].replace("172800", "172799"))),
mutate("the generated group's receipts withheld from the world",
lambda d: d["world"].update(receipts=[r for r in d["world"]["receipts"] if r.get("group_id") != 5])),
mutate("the thumbnail's derivation record withheld", lambda d: d["world"]["records"].pop(1)),
mutate("the upscale's claim withheld", lambda d: d["world"]["claims"].pop(1)),
mutate("the cache's acknowledgement of the thumbnail withheld", lambda d: d["world"]["acknowledgements"].pop(0)),
mutate("a byte of an acknowledgement's signature",
lambda d: d["world"]["acknowledgements"][1].update(record=flip_hex_digit(d["world"]["acknowledgements"][1]["record"], len(d["world"]["acknowledgements"][1]["record"]) - 9))),
mutate("the cache's claim that it sent the source withheld",
lambda d: case(d, "cache-transmitted-not-purged")["case"]["cache_transmissions"].pop()),
mutate("the unfixed request stated met", lambda d: facts(d, "request-unfixed").update(verdict="met")),
mutate("the reply window's end moved", lambda d: facts(d, "own-reply-early").update(window_end=facts(d, "own-reply-early")["window_end"] + 1)),
mutate("a refused case's place moved", lambda d: case(d, "derivative-left-out")["expect"].update(at=["closure", 5])),
mutate("a counts row's median stated otherwise", lambda d: d["counts"][0]["expect"]["rows"][0].update(median_s=1199)),
mutate("the counts verified without the pending order", lambda d: d["counts"][0]["cases"].remove("order-3h-pending")),
]
def main(corpus_dir):
fails = []
st = ED._self_test()
print(("PASS" if not st else "FAIL") + " Ed25519 strict rule self-test " + ",".join(st))
fails += ["ed25519 self-test"] if st else []
ok = len(REASONS) == 22 and len(set(REASONS)) == len(REASONS)
print(("PASS" if ok else "FAIL") + " §11 reads from the extension: %d reasons" % len(REASONS))
fails += [] if ok else ["the extension's §11 does not read"]
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
with open(PUBLISHED, encoding="utf-8") as f:
published = f.read()
same = doc["world"]["registry"] == published
print(("PASS" if same else "FAIL") + " the world's registry is registry/takedown-clocks.json as published")
fails += [] if same else ["the world's registry is not the published one"]
fails += check(doc, quiet=False)
extra = sorted(set(os.listdir(corpus_dir)) - {"vectors.json", "README.md", "__pycache__"})
print(("PASS" if not extra else "FAIL") + " no file but vectors.json and README.md")
fails += ["an extra file: %s" % n for n in extra]
seen = {v["expect"].get("reason") for v in doc["cases"]} | {v["expect"].get("reason") for v in doc["counts"]}
missing = [r for r in REASONS if r not in seen]
print(("PASS" if not missing else "FAIL") + " every reason of §11 appears in the corpus")
fails += ["no case gives %s" % r for r in missing]
for label, d in mutations(doc):
caught = check(d)
print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:90] if caught else ""))
if not caught:
fails.append("a mutated copy verifies: %s" % label)
for name in CHECKS:
DROPPED.clear()
DROPPED.add(name)
caught = check(doc)
DROPPED.clear()
print(("PASS" if caught else "FAIL") + " the corpus catches the verifier without its check %s%s"
% (name, " (%s)" % caught[0][:90] if caught else ""))
if not caught:
fails.append("the check %s can be dropped unnoticed" % name)
for f in fails:
print("FAIL " + f)
print(("PASS" if not fails else "FAIL") + " takedown corpus: %d cases, %d counts" % (len(doc["cases"]), len(doc["counts"])))
return 1 if fails else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: takedown_verify.py <live-conformance/takedown>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))