Jump to content

The Principia Machina/TPC-PY/gen falcon corpus.py

From The Invariance Itself
Revision as of 02:11, 30 September 2026 by BFP Rauch (talk | contribs) (1 revision imported)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
"""gen_falcon_corpus 0.2.0: builds falcon_corpus.json for tpc_falcon 0.2.0
(0.1.0's 790 cases unchanged; SM97-SM112 and HP15-HP19 added for the
SERVED source kind of D172 (c), RD-17, taken by delegation 2026-09-26;
0.2.0, increment 11 (S79, C7): the FS cases of the output-schema digest,
falcon_schema_canon and falcon_schema_digest, agent RDC's specification
FS1-FS5 (tpm_declarations_cluster_falcon 0.1.0 section 8) - its vectors
V1-V7 and the eighteen FALCON KIND schemas with their canonical texts and
digests transcribed literally, and further cases written from FS1-FS5 by
hand; every digest here is SHA-256 over a netstring built in this file
with hashlib, apart from tpc_falcon, and RDC's literal digests must equal
it). Increment 12 (D213.1, taken 2026-09-28; FS specification 0.2.0,
agent TR): FS1 refuses a schema any of whose strings, keys included, is
not already its own canonical text (no CR, NFC): V5, FSX04, FSX05a and
FSX09 re-expected as refusals; V5b and V8-V11 of FS 0.2.0 and FSX11-FSX16
added; no other expectation moves, the eighteen kinds' digests included.

Every expectation is computed by tpc_falcon.py and pinned. A case given a
hand expectation (a literal written from the rule, or for the boundary
vectors a formula independent of the code) is marked hand_checked, and
the generator refuses to write the corpus if the code disagrees with any
of them."""
import hashlib
import json
import sys
from tpc_falcon import run_case, SETS

T0 = 1789948800            # 2026-09-21T00:00:00Z
DAY = 86400
R0 = "00112233445566778899aabbccddeeff"
cases = []


def add(name, fn, args, hand=None):
    cases.append((name, fn, args, hand))


def lines(*ls):
    return "\n".join(ls)


# ------------------------------------------------------ canonical forms
def S(t, **kw):
    d = {"name": "x", "type": t, "class": "MECHANICAL"}
    d.update(kw)
    return d


E = S("enum", values=["open", "closed"])
K = [
 ("CN01 text: NFD e + acute equals the precomposed letter", "e\u0301", S("text"), "OK|\u00e9"),
 ("CN02 text: whitespace runs collapse, ends trimmed (XML Schema collapse)", "  a \t\n b\r\n", S("text"), "OK|a b"),
 ("CN03 text: NO-BREAK SPACE is not collapsed", "a\u00a0b", S("text"), "OK|a\u00a0b"),
 ("CN04 text: ANGSTROM SIGN to its NFC letter", "\u212b", S("text"), "OK|\u00c5"),
 ("CN05 text: a ligature is kept (NFC, not NFKC)", "\ufb01", S("text"), "OK|\ufb01"),
 ("CN06 text: Hangul jamo compose", "\u1100\u1161", S("text"), "OK|\uac00"),
 ("CN07 text: the render escapes bar and backslash", "a|b\\c", S("text"), "OK|a\\|b\\\\c"),
 ("CN08 text: whitespace only is empty", " \t ", S("text"), "OK|"),
 ("CN09 text: a number is refused", 5, S("text"), "ERROR:text"),
 ("CN10 text: VT is not whitespace here; escaped in the render", "a\u000bb", S("text"), "OK|a\\x0bb"),
 ("CN11 text: NUL kept, escaped in the render", "a\u0000b", S("text"), "OK|a\\x00b"),
 ("CN12 decimal 1.50", "1.50", S("decimal"), "OK|15e-1"),
 ("CN13 decimal 1.5e0", "1.5e0", S("decimal"), "OK|15e-1"),
 ("CN14 decimal 0.050 (zone schemas 5.6)", "0.050", S("decimal"), "OK|5e-2"),
 ("CN15 decimal 1500 (zone schemas 5.6)", "1500", S("decimal"), "OK|15e2"),
 ("CN16 decimal -0 (zone schemas 5.6)", "-0", S("decimal"), "OK|0"),
 ("CN17 decimal 2.0 (zone schemas 5.6)", "2.0", S("decimal"), "OK|2"),
 ("CN18 decimal -0.00120e+0010", "-0.00120e+0010", S("decimal"), "OK|-12e6"),
 ("CN19 decimal 0e5 is zero", "0e5", S("decimal"), "OK|0"),
 ("CN20 decimal .5 refused", ".5", S("decimal"), "ERROR:decimal"),
 ("CN21 decimal 5. refused", "5.", S("decimal"), "ERROR:decimal"),
 ("CN22 decimal +5 refused", "+5", S("decimal"), "ERROR:decimal"),
 ("CN23 decimal a five-digit exponent refused", "1e12345", S("decimal"), "ERROR:decimal"),
 ("CN24 decimal a JSON number refused", 1.5, S("decimal"), "ERROR:decimal"),
 ("CN25 decimal a comma refused", "1,5", S("decimal"), "ERROR:decimal"),
 ("CN26 decimal a non-ASCII digit refused", "\u0663", S("decimal"), "ERROR:decimal"),
 ("CN27 unitvalue: tokens in catalogue order", "9.810 s-2 m", S("unitvalue"), "OK|981e-2 m s-2"),
 ("CN28 unitvalue: 5 cm stays centimetres (D104.1)", "5 cm", S("unitvalue"), "OK|5 cm"),
 ("CN29 unitvalue: 0.05 m", "0.05 m", S("unitvalue"), "OK|5e-2 m"),
 ("CN30 unitvalue: dimensionless", "1 1", S("unitvalue"), "OK|1 1"),
 ("CN31 unitvalue: exponents merge to nothing", "7 m m-1", S("unitvalue"), "OK|7 1"),
 ("CN32 unitvalue: degC refused (offset scale)", "3 degC", S("unitvalue"), "ERROR:unitvalue"),
 ("CN33 unitvalue: a slash refused", "2 m/s", S("unitvalue"), "ERROR:unitvalue"),
 ("CN34 unitvalue: no unit", "2", S("unitvalue"), "ERROR:unitvalue"),
 ("CN35 unitvalue: a double space", "2  m", S("unitvalue"), "ERROR:unitvalue"),
 ("CN36 unitvalue: a prefixed refused symbol (kdB)", "1 kdB", S("unitvalue"), "ERROR:unitvalue"),
 ("CN37 unitvalue: TT is the teratesla", "1 TT", S("unitvalue"), "OK|1 TT"),
 ("CN38 unitvalue: T m is the tesla metre, sorted", "1 T m", S("unitvalue"), "OK|1 m T"),
 ("CN39 unitvalue: a merged exponent of 10000 refused", "1 m9999 m1", S("unitvalue"), "ERROR:unitvalue"),
 ("CN40 unitvalue: a DIMENSION exponent of 10000 refused", "1 F2500", S("unitvalue"), "ERROR:unitvalue"),
 ("CN41 unitvalue: DIMENSION exponents within range", "1 F2499", S("unitvalue"), "OK|1 F2499"),
 ("CN42 unitvalue: the hour takes no prefix", "1 kh", S("unitvalue"), "ERROR:unitvalue"),
 ("CN43 unitvalue: prefixed forms sort by power", "1 km m mm", S("unitvalue"), "OK|1 mm m km"),
 ("CN44 date extended", "2026-09-25", S("date"), "OK|2026-09-25"),
 ("CN45 date basic", "20260925", S("date"), "OK|2026-09-25"),
 ("CN46 date a leap day", "2024-02-29", S("date"), "OK|2024-02-29"),
 ("CN47 date 2023 has no 29 February", "2023-02-29", S("date"), "ERROR:date"),
 ("CN48 date 1900 is not a leap year", "1900-02-29", S("date"), "ERROR:date"),
 ("CN49 date 2000 is a leap year", "2000-02-29", S("date"), "OK|2000-02-29"),
 ("CN50 date month 13", "2026-13-01", S("date"), "ERROR:date"),
 ("CN51 date a one-digit month", "2026-9-25", S("date"), "ERROR:date"),
 ("CN52 date with a time", "2026-09-25T00:00Z", S("date"), "ERROR:date"),
 ("CN53 date a week date refused", "2026-W39-5", S("date"), "ERROR:date"),
 ("CN54 url: scheme and host lowered, dot segments removed", "HTTP://Example.COM/a/./b/../c?Q=1#F", S("url"), "OK|http://example.com/a/c?Q=1#F"),
 ("CN55 url: a default port is kept (6.2.3 not taken)", "http://example.com:80/", S("url"), "OK|http://example.com:80/"),
 ("CN56 url: an unreserved character decoded", "http://a/%7euser", S("url"), "OK|http://a/~user"),
 ("CN57 url: a reserved one kept, hex upper", "http://a/%2fx", S("url"), "OK|http://a/%2Fx"),
 ("CN58 url: path case kept", "http://a/PATH", S("url"), "OK|http://a/PATH"),
 ("CN59 url: query order kept", "http://a/?b=1&a=2", S("url"), "OK|http://a/?b=1&a=2"),
 ("CN60 url: host decoded, then lowered", "http://%41b.COM/", S("url"), "OK|http://ab.com/"),
 ("CN61 url: a rootless path is left alone", "urn:a/../b", S("url"), "OK|urn:a/../b"),
 ("CN62 url: RFC 3986 5.4.1 example", "http://a/b/c/./../../g", S("url"), "OK|http://a/g"),
 ("CN63 url: two at-signs refused", "http://u@a@b/", S("url"), "ERROR:url"),
 ("CN64 url: a bad percent-encoding refused", "https://a.b/%zz", S("url"), "ERROR:url"),
 ("CN65 url: a space refused", "http://a b/", S("url"), "ERROR:url"),
 ("CN66 url: a non-ASCII host refused", "http://\u00e9.com/", S("url"), "ERROR:url"),
 ("CN67 url: no scheme refused", "//a/b", S("url"), "ERROR:url"),
 ("CN68 url: an IP literal lowered, port kept", "http://[FE80::1]:8080/A", S("url"), "OK|http://[fe80::1]:8080/A"),
 ("CN69 url: an empty port kept", "http://a:/", S("url"), "OK|http://a:/"),
 ("CN70 url: a bad port refused", "http://a:8x/", S("url"), "ERROR:url"),
 ("CN71 url: encoded dot segments removed", "HTTP://A/%2E%2E/b", S("url"), "OK|http://a/b"),
 ("CN72 url: userinfo case kept", "http://User@Host/", S("url"), "OK|http://User@host/"),
 ("CN73 url: an empty path gains no slash", "http://a", S("url"), "OK|http://a"),
 ("CN74 enum: a member", "open", E, "OK|open"),
 ("CN75 enum: case differs", "Open", E, "ERROR:enum"),
 ("CN76 enum: a number", 1, E, "ERROR:enum"),
 ("CN77 boolean: true", True, S("boolean"), "OK|true"),
 ("CN78 boolean: the string true refused", "true", S("boolean"), "ERROR:boolean"),
 ("CN79 boolean: 1 refused", 1, S("boolean"), "ERROR:boolean"),
 ("CN80 spec: rows is not a value type", "x", {"name": "x", "type": "rows", "class": "MECHANICAL", "key": ["a"], "columns": [S("text", name="a")]}, "ERROR:schema"),
 ("CN81 spec: an unknown type", "x", S("number"), "ERROR:schema"),
]
for name, v, spec, hand in K:
    add(name, "canon", {"value": v, "spec": spec}, hand)


# ----------------------------------------------------------------- draw
def c(acct, sp, fam, snap, tier=False, w=0, ontime=0, assigned=0,
      good=True, sandbox=False, over=False):
    return {"account": acct, "sponsor": sp, "family": fam, "snapshot": snap,
            "tier": tier, "good_standing": good, "sandbox": sandbox,
            "over_cap": over, "window": w, "ontime": ontime,
            "assigned": assigned}


def st(kept=(), prior=(), drafted=0, now=0, wait=DAY):
    return {"kept": list(kept), "prior": list(prior), "drafted": drafted,
            "now": now, "falcon_wait": wait}


def draw(name, cands, n, state, hand, r=R0):
    add(name, "draw", {"candidates": cands, "n": n, "state": state, "r": r},
        hand)


draw("DR01 T1: two tier agents of distinct sponsors and families",
     [c("a", "S1", "V1", "x1", True, 0), c("b", "S2", "V2", "x2", True, 1),
      c("c", "S3", "V1", "x3", False, 2)], 2, st(),
     lines("draw|OK|T1", "assignee|a", "assignee|b", "marks|-", "thin|false",
           "pool|3|3"))
draw("DR02 T1: a pair of two families beats the order",
     [c("a", "S1", "V1", "x1", True, 0), c("b", "S2", "V1", "x2", True, 1),
      c("c", "S3", "V2", "x3", True, 2)], 2, st(),
     lines("draw|OK|T1", "assignee|a", "assignee|c", "marks|-", "thin|false",
           "pool|3|3"))
draw("DR03 T1: one family, distinct snapshots beat one snapshot",
     [c("a", "S1", "V1", "x", True, 0), c("b", "S2", "V1", "x", True, 1),
      c("c", "S3", "V1", "y", True, 2)], 2, st(),
     lines("draw|OK|T1", "assignee|a", "assignee|c", "marks|SAME-VENDOR",
           "thin|false", "pool|3|3"))
draw("DR04 T2: one tier agent",
     [c("a", "S1", "V1", "x1", True, 0), c("b", "S2", "V2", "x2", False, 1),
      c("c", "S3", "V3", "x3", False, 2)], 2, st(),
     lines("draw|OK|T2", "assignee|a", "assignee|b", "marks|-", "thin|false",
           "pool|3|3"))
draw("DR05 T3: no tier agent; order by FALCON WINDOW",
     [c("a", "S1", "V1", "x1", False, 2), c("b", "S2", "V2", "x2", False, 0),
      c("c", "S3", "V3", "x3", False, 1)], 2, st(),
     lines("draw|OK|T3", "assignee|b", "assignee|c", "marks|-", "thin|false",
           "pool|3|3"))
draw("DR06 distinct sponsors before a one-sponsor tier pair (T2 over T4)",
     [c("a", "S1", "V1", "x1", True, 0), c("b", "S1", "V2", "x2", True, 1),
      c("c", "S2", "V3", "x3", False, 2)], 2, st(),
     lines("draw|OK|T2", "assignee|a", "assignee|c", "marks|-", "thin|false",
           "pool|3|2"))
draw("DR07 T4: one sponsor, two tier agents; THIN POOL counted",
     [c("a", "S1", "V1", "x1", True, 0), c("b", "S1", "V2", "x2", True, 1),
      c("c", "S1", "V3", "x3", False, 2)], 2, st(),
     lines("draw|OK|T4", "assignee|a", "assignee|b", "marks|SAME-SPONSOR",
           "thin|true", "pool|3|1"))
KP = [c("k1", "S1", "V1", "x1", True), c("k2", "S2", "V2", "x2", False)]
draw("DR08 T4 for a third assignee, no THIN POOL",
     KP + [c("x", "S1", "V3", "x3", True, 0), c("y", "S2", "V4", "x4", True, 1)],
     1, st(["k1", "k2"], ["k1", "k2"]),
     lines("draw|OK|T4", "assignee|x", "marks|SAME-SPONSOR", "thin|false",
           "pool|2|2"))
draw("DR09 T5 under THIN POOL: one sponsor, no tier",
     [c("a", "S1", "V1", "x1", False, 0), c("b", "S1", "V2", "x2", False, 1)],
     2, st(),
     lines("draw|OK|T5", "assignee|a", "assignee|b", "marks|SAME-SPONSOR",
           "thin|true", "pool|2|1"))
KN = [c("k1", "S1", "V1", "x1"), c("k2", "S2", "V2", "x2")]
XY = [c("x", "S1", "V3", "x3", False, 0), c("y", "S2", "V1", "x4", False, 1)]
draw("DR10 WAIT: a third only of the kept sponsors, 1 s before FALCON WAIT",
     KN + XY, 1, st(["k1", "k2"], ["k1", "k2"], 1000, 1000 + DAY - 1),
     lines("draw|WAIT", "thin|false", "pool|2|2"))
draw("DR11 at FALCON WAIT with no distinct-sponsor choice: THIN POOL, T5",
     KN + XY, 1, st(["k1", "k2"], ["k1", "k2"], 1000, 1000 + DAY),
     lines("draw|OK|T5", "assignee|x", "marks|SAME-SPONSOR", "thin|true",
           "pool|2|2"))
draw("DR12 SHORT: one eligible account is never drawn alone",
     [c("a", "S1", "V1", "x1"), c("b", "S2", "V2", "x2", good=False)], 2,
     st(), lines("draw|SHORT", "thin|true", "pool|1|1"))
draw("DR13 SHORT: standing, sandbox and cap each make an account ineligible",
     [c("a", "S1", "V1", "x1", good=False),
      c("b", "S2", "V2", "x2", sandbox=True),
      c("c", "S3", "V3", "x3", over=True)], 2, st(),
     lines("draw|SHORT", "thin|true", "pool|0|0"))
draw("DR14 an account assigned before is never drawn again",
     [c("a", "S1", "V1", "x1", w=0), c("b", "S2", "V2", "x2", w=1),
      c("c", "S3", "V3", "x3", w=2)], 2, st([], ["a"]),
     lines("draw|OK|T3", "assignee|b", "assignee|c", "marks|-", "thin|false",
           "pool|2|2"))
draw("DR15 L2 third assignee: a third vendor family first (D149 (h))",
     KN + [c("x", "S3", "V1", "x3", False, 0), c("y", "S4", "V3", "x4", False, 1)],
     1, st(["k1", "k2"], ["k1", "k2"]),
     lines("draw|OK|T3", "assignee|y", "marks|-", "thin|false", "pool|2|2"))
draw("DR16 FALCON TRIAL: three assignees",
     [c("a", "S1", "V1", "x1", True, 0), c("b", "S2", "V2", "x2", True, 1),
      c("c", "S3", "V3", "x3", True, 2), c("d", "S4", "V1", "x4", True, 3)],
     3, st(),
     lines("draw|OK|T1", "assignee|a", "assignee|b", "assignee|c", "marks|-",
           "thin|false", "pool|4|4"))
draw("DR17 order by on-time share, exact (1 > 2/3 > 1/2)",
     [c("a", "S1", "V1", "x1", ontime=5, assigned=10),
      c("b", "S2", "V2", "x2", ontime=4, assigned=4),
      c("c", "S3", "V3", "x3", ontime=2, assigned=3)], 2, st(),
     lines("draw|OK|T3", "assignee|b", "assignee|c", "marks|-", "thin|false",
           "pool|3|3"))
# h by sha256sum outside the code: a 0e52aa1f..., c 156a7098...
draw("DR18 no assignment counts as share 1; the tie goes to h(r, account)",
     [c("a", "S1", "V1", "x1"), c("b", "S2", "V2", "x2", ontime=3, assigned=4),
      c("c", "S3", "V3", "x3")], 2, st(),
     lines("draw|OK|T3", "assignee|a", "assignee|c", "marks|-", "thin|false",
           "pool|3|3"))
# h by sha256sum outside the code: p 08936c9f..., s 8dd579e8...,
# q 97489d74..., r fb2772c5... (r = R0), so the hash, not the name, orders.
draw("DR33 equal on window and share: h(r, account) orders, not the name",
     [c("p", "S1", "V1", "x1"), c("q", "S2", "V2", "x2"),
      c("r", "S3", "V3", "x3"), c("s", "S4", "V4", "x4")], 2, st(),
     lines("draw|OK|T3", "assignee|p", "assignee|s", "marks|-", "thin|false",
           "pool|4|4"))
draw("DR19 the SAME-VENDOR and SAME-MODEL marks",
     [c("a", "S1", "V1", "x", w=0), c("b", "S2", "V1", "x", w=1)], 2, st(),
     lines("draw|OK|T3", "assignee|a", "assignee|b",
           "marks|SAME-VENDOR,SAME-MODEL", "thin|false", "pool|2|2"))
draw("DR20 today's four accounts: the sandbox pair is never drawn",
     [c("Prime AI", "BFP_Rauch", "vendor-1", "s1", True, 1),
      c("DeltaS Zero", "Alt BFP", "vendor-1", "s2", False, 0),
      c("Phoenix 480", "PHX_480", "vendor-1", "s1", sandbox=True),
      c("CH7 AZ", "PHX_480", "vendor-2", "s3", sandbox=True)], 2, st(),
     lines("draw|OK|T2", "assignee|DeltaS Zero", "assignee|Prime AI",
           "marks|SAME-VENDOR", "thin|false", "pool|2|2"))
draw("DR21 a kept tier account makes a third T2; a tier third is T1 and wins",
     KP + [c("x", "S3", "V3", "x3", False, 0), c("y", "S4", "V4", "x4", True, 1)],
     1, st(["k1", "k2"], ["k1", "k2"]),
     lines("draw|OK|T1", "assignee|y", "marks|-", "thin|false", "pool|2|2"))
draw("DR22 THIN POOL counted though a fresh sponsor exists (one eligible)",
     KN + [c("x", "S3", "V3", "x3")], 1, st(["k1", "k2"], ["k1", "k2"]),
     lines("draw|OK|T3", "assignee|x", "marks|-", "thin|true", "pool|1|1"))
draw("DR23 a reissue after MALFORMED: one kept, the malformed one excluded",
     [c("k1", "S1", "V1", "x1"), c("m", "S2", "V2", "x2"),
      c("a", "S1", "V2", "x3", w=0), c("b", "S3", "V3", "x4", w=1)], 1,
     st(["k1"], ["k1", "m"]),
     lines("draw|OK|T3", "assignee|b", "marks|-", "thin|false", "pool|2|2"))
draw("DR34 a trial of three after FALCON WAIT: fewest shared sponsors "
     "first, then families",
     [c("a", "S1", "V1", "x1", w=0), c("b", "S1", "V2", "x2", w=1),
      c("c", "S1", "V3", "x3", w=2), c("d", "S2", "V1", "x4", w=3)], 3,
     st([], [], 0, 50, 50),
     lines("draw|OK|T5", "assignee|b", "assignee|c", "assignee|d",
           "marks|SAME-SPONSOR", "thin|true", "pool|4|2"))
draw("DR35 the same pool before FALCON WAIT: WAIT (no T3 set of three)",
     [c("a", "S1", "V1", "x1", w=0), c("b", "S1", "V2", "x2", w=1),
      c("c", "S1", "V3", "x3", w=2), c("d", "S2", "V1", "x4", w=3)], 3,
     st([], [], 0, 49, 50), lines("draw|WAIT", "thin|false", "pool|4|2"))
BASE = [c("a", "S1", "V1", "x1"), c("b", "S2", "V2", "x2")]
draw("DR24 n = 0 refused", BASE, 0, st(), "ERROR:n")
draw("DR25 one assignee with none kept refused (the floor of two)", BASE, 1,
     st(), "ERROR:n")
draw("DR26 n = 4 refused", BASE, 4, st(), "ERROR:n")
draw("DR27 r in upper case refused", BASE, 2, st(), "ERROR:r",
     r=R0.upper())
draw("DR28 a duplicate account refused", BASE + [c("a", "S3", "V3", "x3")],
     2, st(), "ERROR:candidates")
draw("DR29 ontime above assigned refused",
     [c("a", "S1", "V1", "x1", ontime=2, assigned=1), BASE[1]], 2, st(),
     "ERROR:candidates")
draw("DR30 a kept account missing from candidates refused", BASE, 1,
     st(["z"], ["z"]), "ERROR:state")
draw("DR31 now before drafted refused", BASE, 2, st([], [], 10, 9),
     "ERROR:state")
bad = dict(BASE[0])
bad["tier"] = 1
draw("DR32 a tier flag given as the integer 1 refused", [bad, BASE[1]], 2,
     st(), "ERROR:candidates")


# ------------------------------------------------------------ reconcile
def F(name, t, cls="MECHANICAL", **kw):
    d = {"name": name, "type": t, "class": cls}
    d.update(kw)
    return d


SV = {"class": "SURVEY", "narrow": False,
      "fields": [F("model", "text"), F("released", "date"),
                 F("score", "decimal", quote=True), F("link", "url")]}
A1 = {"model": "Claude  Opus", "released": "20260925", "score": "1.50",
      "link": "HTTP://X.org/a/../b"}
B1 = {"model": "Claude Opus", "released": "2026-09-25", "score": "1.5",
      "link": "http://x.org/b"}


def o(acct, result, status="RETURNED"):
    return {"account": acct, "status": status, "result": result}


def w(r, **kw):
    d = dict(r)
    d.update(kw)
    return d


def rec(name, outputs, schema, hand, quotes=None):
    if quotes is None:
        quotes = [{} for _ in outputs]
    add(name, "reconcile", {"outputs": outputs, "quotes": quotes,
                            "schema": schema}, hand)


OK2 = ("output|0|a|OK|iv=0", "output|1|b|OK|iv=0")
OK3 = OK2 + ("output|2|c|OK|iv=0",)
AG_M = "item|model|AGREED|Claude Opus|2"
AG_R = "item|released|AGREED|2026-09-25|2"
AG_S = "item|score|AGREED|15e-1|2"
AG_L = "item|link|AGREED|http://x.org/b|2"
rec("RC01 L0: every field AGREED after canonical form",
    [o("a", A1), o("b", B1)], SV,
    lines(*OK2, AG_M, AG_R, AG_S, AG_L,
          "summary|agreed=4|quote=0|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"))
rec("RC02 L1: exactly one PASS settles a quoted field",
    [o("a", B1), o("b", w(B1, score="1.6"))], SV,
    lines(*OK2, AG_M, AG_R, "item|score|QUOTE|15e-1|2",
          "value|score|0|15e-1|PASS", "value|score|1|16e-1|FAIL", AG_L,
          "summary|agreed=3|quote=1|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"),
    [{"score": "PASS"}, {"score": "FAIL"}])
rec("RC03 L1: two PASSes settle nothing; two outputs wait for a third",
    [o("a", B1), o("b", w(B1, score="1.6"))], SV,
    lines(*OK2, AG_M, AG_R, "item|score|SPLIT|-|2",
          "value|score|0|15e-1|PASS", "value|score|1|16e-1|PASS", AG_L,
          "summary|agreed=3|quote=0|majority=0|split=1|unresolved=0|"
          "inspection=0|single=0",
          "disposition|THIRD|single=-"),
    [{"score": "PASS"}, {"score": "PASS"}])
rec("RC04 L1: PASS against UNKNOWN (a host off the list) settles",
    [o("a", B1), o("b", w(B1, score="1.6"))], SV,
    lines(*OK2, AG_M, AG_R, "item|score|QUOTE|16e-1|2",
          "value|score|0|15e-1|UNKNOWN", "value|score|1|16e-1|PASS", AG_L,
          "summary|agreed=3|quote=1|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"),
    [{"score": "UNKNOWN"}, {"score": "PASS"}])
rec("RC05 an unquoted field SPLIT with two outputs: THIRD",
    [o("a", B1), o("b", w(B1, model="Claude Opus 5"))], SV,
    lines(*OK2, "item|model|SPLIT|-|2", "value|model|0|Claude Opus|-",
          "value|model|1|Claude Opus 5|-", AG_R, AG_S, AG_L,
          "summary|agreed=3|quote=0|majority=0|split=1|unresolved=0|"
          "inspection=0|single=0",
          "disposition|THIRD|single=-"))
rec("RC06 three outputs, two of three: MAJORITY",
    [o("a", w(B1, model="X")), o("b", w(B1, model="X")),
     o("c", w(B1, model="Y"))], SV,
    lines(*OK3, "item|model|MAJORITY|X|2/3", "value|model|0|X|-",
          "value|model|1|X|-", "value|model|2|Y|-",
          "item|released|AGREED|2026-09-25|3", "item|score|AGREED|15e-1|3",
          "item|link|AGREED|http://x.org/b|3",
          "summary|agreed=3|quote=0|majority=1|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"))
rec("RC07 three outputs, 1-1-1: UNRESOLVED inside the approval (D150 (f))",
    [o("a", w(B1, model="X")), o("b", w(B1, model="Y")),
     o("c", w(B1, model="Z"))], SV,
    lines(*OK3, "item|model|UNRESOLVED|-|3", "value|model|0|X|-",
          "value|model|1|Y|-", "value|model|2|Z|-",
          "item|released|AGREED|2026-09-25|3", "item|score|AGREED|15e-1|3",
          "item|link|AGREED|http://x.org/b|3",
          "summary|agreed=3|quote=0|majority=0|split=0|unresolved=1|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"))
rec("RC08 the ladder tries L1 before the majority: the lone PASS wins",
    [o("a", B1), o("b", B1), o("c", w(B1, score="1.6"))], SV,
    lines(*OK3, "item|model|AGREED|Claude Opus|3",
          "item|released|AGREED|2026-09-25|3", "item|score|QUOTE|16e-1|3",
          "value|score|0|15e-1|FAIL", "value|score|1|15e-1|FAIL",
          "value|score|2|16e-1|PASS", "item|link|AGREED|http://x.org/b|3",
          "summary|agreed=3|quote=1|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"),
    [{"score": "FAIL"}, {"score": "FAIL"}, {"score": "PASS"}])
rec("RC09 MALFORMED, a refusal: REISSUE, nothing compared",
    [o("a", B1), o("b", None)], SV,
    lines("output|0|a|OK|iv=0", "output|1|b|MALFORMED|refusal",
          "disposition|REISSUE|1"))
rec("RC10 MALFORMED, shape: an extra key",
    [o("a", w(B1, note="x")), o("b", B1)], SV,
    lines("output|0|a|MALFORMED|shape", "output|1|b|OK|iv=0",
          "disposition|REISSUE|0"))
rec("RC11 MALFORMED, a field: a decimal given as a JSON number",
    [o("a", B1), o("b", w(B1, score=1.5))], SV,
    lines("output|0|a|OK|iv=0", "output|1|b|MALFORMED|score",
          "disposition|REISSUE|1"))
rec("RC12 OVERDUE: reissued",
    [o("a", B1), o("b", None, "OVERDUE")], SV,
    lines("output|0|a|OK|iv=0", "output|1|b|OVERDUE",
          "disposition|REISSUE|1"))
rec("RC13 IV reports are counted and never compared",
    [o("a", w(B1, IV=["text addressed to a model"])), o("b", B1)], SV,
    lines("output|0|a|OK|iv=1", "output|1|b|OK|iv=0", AG_M, AG_R, AG_S, AG_L,
          "summary|agreed=4|quote=0|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"))
PT = {"class": "PATCH", "narrow": False,
      "fields": [F("page", "text"), F("prose", "text", "INSPECTION")]}
rec("RC14 INSPECTION prose is never matched and asks no third (PATCH)",
    [o("a", {"page": "Hub", "prose": "Alpha  text"}),
     o("b", {"page": "Hub", "prose": "Beta text"})], PT,
    lines(*OK2, "item|page|AGREED|Hub|2", "item|prose|INSPECTION|-|2",
          "value|prose|0|Alpha text|-", "value|prose|1|Beta text|-",
          "summary|agreed=1|quote=0|majority=0|split=0|unresolved=0|"
          "inspection=1|single=0",
          "disposition|AWAITING APPROVAL|single=-"))
FD = {"class": "FINDINGS", "narrow": False,
      "fields": [F("verdict", "enum", values=["OK", "STALE", "WRONG"])]}
rec("RC15 FINDINGS with an UNRESOLVED field is REPORTED",
    [o("a", {"verdict": "OK"}), o("b", {"verdict": "STALE"}),
     o("c", {"verdict": "WRONG"})], FD,
    lines(*OK3, "item|verdict|UNRESOLVED|-|3", "value|verdict|0|OK|-",
          "value|verdict|1|STALE|-", "value|verdict|2|WRONG|-",
          "summary|agreed=0|quote=0|majority=0|split=0|unresolved=1|"
          "inspection=0|single=0",
          "disposition|REPORTED|single=-"))
RW = {"class": "SURVEY", "narrow": False,
      "fields": [F("bench", "rows", key=["name"],
                   columns=[F("name", "text"),
                            F("score", "decimal", quote=True),
                            F("url", "url")])]}
RWN = dict(RW, narrow=True)
G = {"name": "gpqa", "score": "1.0", "url": "http://a/g"}
H = {"name": "hle", "score": "2.0", "url": "http://a/h"}
MM = {"name": "mmlu", "score": "3", "url": "http://a/m"}
GB = {"name": "gpqa", "score": "1", "url": "http://a/g"}
HB = {"name": "hle", "score": "2.00", "url": "HTTP://A/h"}
ROWS4 = ("item|bench[3:hle].score|AGREED|2|2",
         "item|bench[3:hle].url|AGREED|http://a/h|2",
         "item|bench[4:gpqa].score|AGREED|1|2",
         "item|bench[4:gpqa].url|AGREED|http://a/g|2")
rec("RC16 rows: keyed rows compare as sets, in any order",
    [o("a", {"bench": [G, H]}), o("b", {"bench": [HB, GB]})], RW,
    lines(*OK2, *ROWS4,
          "summary|agreed=4|quote=0|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"))
rec("RC17 rows: a row one output carries is SINGLE: NARROW, nothing written",
    [o("a", {"bench": [G, H, MM]}), o("b", {"bench": [HB, GB]})], RW,
    lines(*OK2, *ROWS4, "item|bench[4:mmlu]|SINGLE|-|0",
          "summary|agreed=4|quote=0|majority=0|split=0|unresolved=0|"
          "inspection=0|single=1",
          "disposition|NARROW|single=narrow"))
rec("RC18 rows: in the narrower falcon a SINGLE row is marked",
    [o("a", {"bench": [G, H, MM]}), o("b", {"bench": [HB, GB]})], RWN,
    lines(*OK2, *ROWS4, "item|bench[4:mmlu]|SINGLE|-|0",
          "summary|agreed=4|quote=0|majority=0|split=0|unresolved=0|"
          "inspection=0|single=1",
          "disposition|AWAITING APPROVAL|single=mark"))
rec("RC19 rows: SINGLE waits while a column is SPLIT (THIRD)",
    [o("a", {"bench": [G, H, MM]}),
     o("b", {"bench": [HB, w(GB, score="1.1")]})], RW,
    lines(*OK2, ROWS4[0], ROWS4[1], "item|bench[4:gpqa].score|SPLIT|-|2",
          "value|bench[4:gpqa].score|0|1|UNKNOWN",
          "value|bench[4:gpqa].score|1|11e-1|UNKNOWN", ROWS4[3],
          "item|bench[4:mmlu]|SINGLE|-|0",
          "summary|agreed=3|quote=0|majority=0|split=1|unresolved=0|"
          "inspection=0|single=1",
          "disposition|THIRD|single=wait"))
rec("RC20 rows: a row two of three carry, split, is UNRESOLVED",
    [o("a", {"bench": [G, H]}), o("b", {"bench": [w(HB, score="3.0"), GB]}),
     o("c", {"bench": [G]})], RW,
    lines(*OK3, "item|bench[3:hle].score|UNRESOLVED|-|2",
          "value|bench[3:hle].score|0|2|UNKNOWN",
          "value|bench[3:hle].score|1|3|UNKNOWN",
          "item|bench[3:hle].url|AGREED|http://a/h|2",
          "item|bench[4:gpqa].score|AGREED|1|3",
          "item|bench[4:gpqa].url|AGREED|http://a/g|3",
          "summary|agreed=3|quote=0|majority=0|split=0|unresolved=1|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"))
rec("RC21 rows: one key twice in one output is MALFORMED",
    [o("a", {"bench": [G, GB]}), o("b", {"bench": [G]})], RW,
    lines("output|0|a|MALFORMED|bench", "output|1|b|OK|iv=0",
          "disposition|REISSUE|0"))
rec("RC22 an enum value outside the declared set is MALFORMED",
    [o("a", {"verdict": "OK"}), o("b", {"verdict": "ok"})], FD,
    lines("output|0|a|OK|iv=0", "output|1|b|MALFORMED|verdict",
          "disposition|REISSUE|1"))
UV = {"class": "SURVEY", "narrow": False,
      "fields": [F("g", "unitvalue"), F("len", "unitvalue")]}
rec("RC23 unitvalue: spellings of one unit agree; cm against m is SPLIT",
    [o("a", {"g": "9.81 m s-2", "len": "5 cm"}),
     o("b", {"g": "9.810 s-2 m", "len": "0.05 m"})], UV,
    lines(*OK2, "item|g|AGREED|981e-2 m s-2|2", "item|len|SPLIT|-|2",
          "value|len|0|5 cm|-", "value|len|1|5e-2 m|-",
          "summary|agreed=1|quote=0|majority=0|split=1|unresolved=0|"
          "inspection=0|single=0",
          "disposition|THIRD|single=-"))
rec("RC24 url: path case is not merged",
    [o("a", w(B1, link="http://a/PATH")), o("b", w(B1, link="http://a/path"))],
    SV,
    lines(*OK2, AG_M, AG_R, AG_S, "item|link|SPLIT|-|2",
          "value|link|0|http://a/PATH|-", "value|link|1|http://a/path|-",
          "summary|agreed=3|quote=0|majority=0|split=1|unresolved=0|"
          "inspection=0|single=0",
          "disposition|THIRD|single=-"))
TX = {"class": "PATCH", "narrow": False,
      "fields": [F("t1", "text"), F("t2", "text"), F("flag", "boolean")]}
rec("RC25 text: composed and decomposed agree; NO-BREAK SPACE does not",
    [o("a", {"t1": "e\u0301", "t2": "a b", "flag": True}),
     o("b", {"t1": "\u00e9", "t2": "a\u00a0b", "flag": True})], TX,
    lines(*OK2, "item|t1|AGREED|\u00e9|2", "item|t2|SPLIT|-|2",
          "value|t2|0|a b|-", "value|t2|1|a\u00a0b|-",
          "item|flag|AGREED|true|2",
          "summary|agreed=2|quote=0|majority=0|split=1|unresolved=0|"
          "inspection=0|single=0",
          "disposition|THIRD|single=-"))
rec("RC26 an empty object and a list are both shape",
    [o("a", {}), o("b", [])], SV,
    lines("output|0|a|MALFORMED|shape", "output|1|b|MALFORMED|shape",
          "disposition|REISSUE|0,1"))
rec("RC27 a quoted row column settles at L1 by its item id",
    [o("a", {"bench": [G, H]}), o("b", {"bench": [HB, w(GB, score="1.1")]})],
    RW,
    lines(*OK2, ROWS4[0], ROWS4[1], "item|bench[4:gpqa].score|QUOTE|11e-1|2",
          "value|bench[4:gpqa].score|0|1|UNKNOWN",
          "value|bench[4:gpqa].score|1|11e-1|PASS", ROWS4[3],
          "summary|agreed=3|quote=1|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"),
    [{}, {"bench[4:gpqa].score": "PASS"}])
rec("RC28 one output refused", [o("a", B1)], SV, "ERROR:outputs")
rec("RC29 duplicate accounts refused", [o("a", B1), o("a", B1)], SV,
    "ERROR:outputs")
rec("RC30 quotes not parallel to outputs refused", [o("a", B1), o("b", B1)],
    SV, "ERROR:quotes", [{}])
rec("RC31 a quote outcome MAYBE refused", [o("a", B1), o("b", B1)], SV,
    "ERROR:quotes", [{"score": "MAYBE"}, {}])
rec("RC32 a field named IV refused", [o("a", B1), o("b", B1)],
    {"class": "SURVEY", "narrow": False, "fields": [F("IV", "text")]},
    "ERROR:schema")
rec("RC33 a key column of class INSPECTION refused", [o("a", B1), o("b", B1)],
    {"class": "SURVEY", "narrow": False,
     "fields": [F("r", "rows", key=["k"],
                  columns=[F("k", "text", "INSPECTION")])]}, "ERROR:schema")
rec("RC34 OVERDUE carrying a result refused", [o("a", B1),
                                             o("b", B1, "OVERDUE")], SV,
    "ERROR:outputs")
rec("RC35 an unknown kind class refused", [o("a", B1), o("b", B1)],
    dict(SV, **{"class": "REPORT"}), "ERROR:schema")


# QUOTE AFFIX and QUOTE SCALE in the schema; the CONFIDENTIAL kind.
SVA = dict(SV, fields=[F("model", "text"), F("released", "date"),
                       F("score", "decimal", quote=True, affix="%",
                         scale="million"), F("link", "url")])
rec("RC36 a quoted decimal field declaring QUOTE AFFIX and QUOTE SCALE",
    [o("a", B1), o("b", w(B1, score="1.6"))], SVA,
    lines(*OK2, AG_M, AG_R, "item|score|QUOTE|15e-1|2",
          "value|score|0|15e-1|PASS", "value|score|1|16e-1|FAIL", AG_L,
          "summary|agreed=3|quote=1|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"),
    [{"score": "PASS"}, {"score": "FAIL"}])
rec("RC37 a quoted unitvalue field declaring a word affix and a scale",
    [o("a", {"mass": "5 kg"}), o("b", {"mass": "5.0 kg"})],
    {"class": "SURVEY", "narrow": False,
     "fields": [F("mass", "unitvalue", quote=True, affix="per cent",
                  scale="bn")]},
    lines(*OK2, "item|mass|AGREED|5 kg|2",
          "summary|agreed=1|quote=0|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"))
RWA = dict(RW, fields=[F("bench", "rows", key=["name"],
                         columns=[F("name", "text"),
                                  F("score", "decimal", quote=True,
                                    affix="%"),
                                  F("url", "url")])])
rec("RC38 QUOTE AFFIX on a quoted rows column",
    [o("a", {"bench": [G, H]}), o("b", {"bench": [HB, GB]})], RWA,
    lines(*OK2, *ROWS4,
          "summary|agreed=4|quote=0|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|AWAITING APPROVAL|single=-"))
for nm, spec in [
        ("RC39 QUOTE AFFIX on a text field refused",
         F("score", "text", quote=True, affix="%")),
        ("RC40 QUOTE SCALE without quote refused",
         F("score", "decimal", scale="million")),
        ("RC41 QUOTE SCALE with quote false refused",
         F("score", "decimal", quote=False, scale="million")),
        ("RC42 an unknown QUOTE SCALE refused",
         F("score", "decimal", quote=True, scale="lots")),
        ("RC43 a QUOTE AFFIX of letters refused",
         F("score", "decimal", quote=True, affix="ab")),
        ("RC44 a null QUOTE AFFIX refused",
         F("score", "decimal", quote=True, affix=None)),
        ("RC45 QUOTE SCALE on an enum field refused",
         F("score", "enum", quote=True, values=["a"], scale="k"))]:
    rec(nm, [o("a", {"score": "1"}), o("b", {"score": "1"})],
        {"class": "SURVEY", "narrow": False, "fields": [spec]},
        "ERROR:schema")
FDC = dict(FD, confidential=True)
RWC = dict(RW, **{"class": "FINDINGS", "confidential": True})
SNG = lines(*OK2, *ROWS4, "item|bench[4:mmlu]|SINGLE|-|0",
            "summary|agreed=4|quote=0|majority=0|split=0|unresolved=0|"
            "inspection=0|single=1")
rec("RC46 a CONFIDENTIAL kind's SINGLE row goes to the PRIVATE REPORT marked",
    [o("a", {"bench": [G, H, MM]}), o("b", {"bench": [HB, GB]})], RWC,
    lines(SNG, "disposition|PRIVATE REPORT|single=mark"))
rec("RC47 a CONFIDENTIAL SURVEY kind's SINGLE row is marked, not reissued",
    [o("a", {"bench": [G, H, MM]}), o("b", {"bench": [HB, GB]})],
    dict(RW, confidential=True),
    lines(SNG, "disposition|AWAITING APPROVAL|single=mark"))
rec("RC48 confidential false is the default (NARROW)",
    [o("a", {"bench": [G, H, MM]}), o("b", {"bench": [HB, GB]})],
    dict(RW, confidential=False),
    lines(SNG, "disposition|NARROW|single=narrow"))
rec("RC49 a CONFIDENTIAL FINDINGS kind without SINGLE rows",
    [o("a", {"verdict": "OK"}), o("b", {"verdict": "OK"})], FDC,
    lines(*OK2, "item|verdict|AGREED|OK|2",
          "summary|agreed=1|quote=0|majority=0|split=0|unresolved=0|"
          "inspection=0|single=0",
          "disposition|PRIVATE REPORT|single=-"))
rec("RC50 a CONFIDENTIAL kind still waits for a third (THIRD)",
    [o("a", {"verdict": "OK"}), o("b", {"verdict": "STALE"})], FDC,
    lines(*OK2, "item|verdict|SPLIT|-|2", "value|verdict|0|OK|-",
          "value|verdict|1|STALE|-",
          "summary|agreed=0|quote=0|majority=0|split=1|unresolved=0|"
          "inspection=0|single=0",
          "disposition|THIRD|single=-"))
rec("RC51 confidential given as a string refused",
    [o("a", {"verdict": "OK"}), o("b", {"verdict": "OK"})],
    dict(FD, confidential="yes"), "ERROR:schema")


# ---------------------------------------------------------------- stage
def L(t, agent="agent1"):
    return ["LANDED", t, agent]


def stage(name, events, as_of, setname, hand):
    add(name + " [set " + setname + "]", "stage",
        {"events": events, "as_of": as_of, "thresholds": SETS[setname]},
        hand)


def srender(st_, dorm, blocks, dates, span, last, grad=None):
    return lines("stage|" + st_, "dormant|" + ("true" if dorm else "false"),
                 "counts|%d|%d|%d|%s" % (blocks, dates, span,
                                         "-" if last is None else last),
                 "graduated|-" if grad is None
                 else "graduated|%d|%d|%d|%d" % grad)


V01 = [L(T0)] + [["EVALUATED", T0 + 7 * DAY * k] for k in range(1, 27)]
V02 = [L(T0 + 60 * k) for k in range(12)]
V03 = ([L(T0 + 30 * DAY + 60 * k) for k in range(5)]
       + [L(T0 + 31 * DAY + 60 * k) for k in range(5)])
V04 = [L(T0 + d * DAY + 60 * k) for d in (0, 3, 6, 9, 14) for k in (0, 1)]
V08 = [L(T0), L(T0 + 3 * DAY), L(T0 + 7 * DAY), L(T0 + 7 * DAY),
       L(T0 + 7 * DAY)]
V11 = ([L(T0 + d * DAY + 60 * k) for d in (0, 3, 6, 9) for k in (0, 1)]
       + [L(T0 + 1208736), L(T0 + 1208736 + 60)])
GA = (T0 + 14 * DAY + 60, 10, 5, 14 * DAY + 60)
GB4 = (T0 + 9 * DAY, 7, 4, 9 * DAY)
C04 = (10, 5, 14 * DAY + 60, T0 + 14 * DAY + 60)
for s in "ABC":
    stage("V01 the user's example: one agent, one UWC, one block, then six "
          "months away with weekly evaluations; registration is not counted",
          V01, T0 + 183 * DAY, s, srender("WORKING", True, 1, 1, 0, T0))
    stage("V02 a burst: 12 blocks in one day", V02, T0 + 2 * DAY, s,
          srender("WORKING", False, 12, 1, 660, T0 + 660))
    stage("V03 a sleeper: nothing for 30 days, then 10 blocks over two days",
          V03, T0 + 32 * DAY, s,
          srender("WORKING", False, 10, 2, DAY + 240, T0 + 31 * DAY + 240))
    stage("V04 steady: 10 blocks on 5 dates over 14 days", V04,
          T0 + 15 * DAY, s,
          srender("SERIOUS" if s != "C" else "WORKING", False, *C04,
                  grad={"A": GA, "B": GB4, "C": None}[s]))
    stage("V05 as V04, an agent SUSPENDED from day 10 (B graduated on day "
          "9 and keeps it: one-way)",
          V04 + [["SUSPENDED", T0 + 10 * DAY, "agent1"]], T0 + 15 * DAY, s,
          srender("SERIOUS" if s == "B" else "WORKING", False, *C04,
                  grad=GB4 if s == "B" else None))
    stage("V06 as V04, the sponsor BLOCKED from day 1",
          V04 + [["BLOCKED", T0 + DAY]], T0 + 15 * DAY, s,
          srender("WORKING", False, *C04))
    stage("V07 as V04, a confirmed VIOLATION on day 1",
          V04 + [["VIOLATION", T0 + DAY, "agent1"]], T0 + 15 * DAY, s,
          srender("WORKING", False, *C04))
    stage("V08 5 blocks on 3 dates over 7 days", V08, T0 + 8 * DAY, s,
          srender("SERIOUS" if s == "B" else "WORKING", False, 5, 3, 7 * DAY,
                  T0 + 7 * DAY,
                  grad=(T0 + 7 * DAY, 5, 3, 7 * DAY) if s == "B" else None))
    stage("V09 V04, then 100 days without a block: DORMANT lowers nothing",
          V04, T0 + 114 * DAY, s,
          srender("SERIOUS" if s != "C" else "WORKING", True, *C04,
                  grad={"A": GA, "B": GB4, "C": None}[s]))
    stage("V10 no block yet", [], T0, s, srender("NEW", False, 0, 0, 0, None))
    stage("V11 V04 with its span 13.99 days", V11, T0 + 15 * DAY, s,
          srender("SERIOUS" if s == "B" else "WORKING", False, 10, 5, 1208796,
                  T0 + 1208796, grad=GB4 if s == "B" else None))

# Boundaries: exactly each threshold, and one below each (hand formula).
for s in "ABC":
    b, dd, sd = (SETS[s]["blocks"], SETS[s]["dates"], SETS[s]["span_days"])
    days = [(j * sd) // (dd - 1) for j in range(dd)]
    base = [L(T0 + d * DAY) for d in days]
    extra = [L(T0 + 60 * (m + 1)) for m in range(b - dd)]
    last = T0 + sd * DAY
    ao = last + DAY
    stage("SB1 exactly B blocks, D dates, S days", base + extra, ao, s,
          srender("SERIOUS", False, b, dd, sd * DAY, last,
                  grad=(last, b, dd, sd * DAY)))
    stage("SB2 one block below B", base + extra[:-1], ao, s,
          srender("WORKING", False, b - 1, dd, sd * DAY, last))
    moved = [base[0], L(T0 + 30)] + base[2:]
    stage("SB3 one date below D", moved + extra, ao, s,
          srender("WORKING", False, b, dd - 1, sd * DAY, last))
    short = base[:-1] + [L(last - 1)]
    stage("SB4 one second below S days", short + extra, ao, s,
          srender("WORKING", False, b, dd, sd * DAY - 1, last - 1))

A = "A"
stage("V12 blocked when it would graduate; the next weekly evaluation "
      "graduates",
      V04 + [["BLOCKED", T0 + 13 * DAY], ["UNBLOCKED", T0 + 16 * DAY],
             ["EVALUATED", T0 + 21 * DAY]], T0 + 22 * DAY, A,
      srender("SERIOUS", False, *C04, grad=(T0 + 21 * DAY, 10, 5,
                                            14 * DAY + 60)))
stage("V13 as V12 with no weekly evaluation recorded: WORKING",
      V04 + [["BLOCKED", T0 + 13 * DAY], ["UNBLOCKED", T0 + 16 * DAY]],
      T0 + 22 * DAY, A, srender("WORKING", False, *C04))
stage("V14 a suspension cleared before the graduating landing",
      V04 + [["SUSPENDED", T0 + 10 * DAY, "agent1"],
             ["RESTORED", T0 + 12 * DAY, "agent1"]], T0 + 15 * DAY, A,
      srender("SERIOUS", False, *C04, grad=GA))
stage("V15 a landing and a suspension in the same second",
      V04 + [["SUSPENDED", T0 + 14 * DAY + 60, "agent2"]], T0 + 15 * DAY, A,
      srender("WORKING", False, *C04))
stage("V16 another agent's RESTORED does not clear the suspended one",
      V04 + [["SUSPENDED", T0 + 10 * DAY, "agent1"],
             ["RESTORED", T0 + 12 * DAY, "agent2"]], T0 + 15 * DAY, A,
      srender("WORKING", False, *C04))
stage("V17 events after as_of are not read", V04, T0 + 10 * DAY, A,
      srender("WORKING", False, 8, 4, 9 * DAY + 60, T0 + 9 * DAY + 60))
stage("V18 DORMANT at exactly dormant_days", [L(T0)], T0 + 90 * DAY, A,
      srender("WORKING", True, 1, 1, 0, T0))
stage("V19 not DORMANT one second before", [L(T0)], T0 + 90 * DAY - 1, A,
      srender("WORKING", False, 1, 1, 0, T0))
stage("V20 blocks of several agents count together",
      [L(T0 + d * DAY, "agent%d" % (d % 3)) for d in range(15)],
      T0 + 15 * DAY, A,
      srender("SERIOUS", False, 15, 15, 14 * DAY, T0 + 14 * DAY,
              grad=(T0 + 14 * DAY, 15, 15, 14 * DAY)))
stage("SE1 an unknown event kind refused", [["REGISTERED", T0]], T0, A,
      "ERROR:events")
stage("SE2 LANDED without its agent refused", [["LANDED", T0]], T0, A,
      "ERROR:events")
stage("SE3 EVALUATED with an agent refused", [["EVALUATED", T0, "a"]], T0, A,
      "ERROR:events")
stage("SE4 a negative time refused", [L(-1)], T0, A, "ERROR:events")
add("SE5 thresholds without dormant_days refused", "stage",
    {"events": [], "as_of": T0,
     "thresholds": {"blocks": 1, "dates": 1, "span_days": 0}}, "ERROR:thresholds")
add("SE6 blocks 0 refused", "stage",
    {"events": [], "as_of": T0,
     "thresholds": {"blocks": 0, "dates": 1, "span_days": 0,
                    "dormant_days": 1}}, "ERROR:thresholds")
stage("SE7 as_of given as a string refused", [], str(T0), A, "ERROR:as_of")

# ------------------------------------------------ quote match and check
# The TOKEN RULE, D150 (c) T1-T8 (tpc_falcon TK0-TK11). Hand expectations
# follow the rule text case by case.
MINUS = "\N{MINUS SIGN}"
NDASH = "\N{EN DASH}"
NBSP = "\N{NO-BREAK SPACE}"
DEG = "\N{DEGREE SIGN}"
PERMILLE = "\N{PER MILLE SIGN}"
CJK = "\N{CJK UNIFIED IDEOGRAPH-6771}\N{CJK UNIFIED IDEOGRAPH-4EAC}"
QMV = [
 # TK0: letters and digits around the occurrence
 ("TK0 the same token", "12", "12", None, False, "PASS"),
 ("TK0 12 inside 123", "12", "123", None, False, "FAIL"),
 ("TK0 12 after a letter (a12)", "12", "a12", None, False, "FAIL"),
 ("TK0 12 inside v12", "12", "v12", None, False, "FAIL"),
 ("TK0 a later occurrence passes", "12", "123 and 12", None, False, "PASS"),
 ("TK0 letters around a parenthesized value", "(12)", "x(12)y", None, False,
  "FAIL"),
 ("TK0 5 before letters (5cm)", "5", "5cm", None, False, "FAIL"),
 ("TK0 a superscript two is a digit", "12", "12\N{SUPERSCRIPT TWO}", None,
  False, "FAIL"),
 ("TK0 an Arabic-Indic digit after", "12", "12\N{ARABIC-INDIC DIGIT THREE}",
  None, False, "FAIL"),
 # TK1 (T1): signs
 ("T1 5 inside -5 (QM12 closed)", "5", "-5", None, False, "FAIL"),
 ("T1 5 inside +5", "5", "+5", None, False, "FAIL"),
 ("T1 5 after U+2212 MINUS SIGN", "5", MINUS + "5", None, False, "FAIL"),
 ("T1 5 after U+2013 EN DASH", "5", NDASH + "5", None, False, "FAIL"),
 ("T1 5 after U+00B1 PLUS-MINUS SIGN", "5", "\N{PLUS-MINUS SIGN}5", None,
  False, "FAIL"),
 ("T1 5 inside <5", "5", "<5", None, False, "FAIL"),
 ("T1 5 inside >5", "5", ">5", None, False, "FAIL"),
 ("T1 5 inside =5", "5", "=5", None, False, "FAIL"),
 ("T1 5 inside ~5", "5", "~5", None, False, "FAIL"),
 ("T1 5 after U+2248 ALMOST EQUAL TO", "5", "\N{ALMOST EQUAL TO}5", None,
  False, "FAIL"),
 ("T1 5 after U+2264", "5", "\N{LESS-THAN OR EQUAL TO}5", None, False,
  "FAIL"),
 ("T1 5 after U+2265", "5", "\N{GREATER-THAN OR EQUAL TO}5", None, False,
  "FAIL"),
 ("T1 5 inside <=5 (a run of signs)", "5", "<=5", None, False, "FAIL"),
 ("T1 a sign before a leading decimal point", ".5", "x -.5", None, False,
  "FAIL"),
 ("T1 -.5 as its own token", "-.5", "x -.5 y", None, False, "PASS"),
 ("T1 -5 matches -5", "-5", "-5", None, False, "PASS"),
 ("T1 -5 between spaces", "-5", "x -5 y", None, False, "PASS"),
 ("T1 -5 against +5", "-5", "+5", None, False, "FAIL"),
 ("T1 -5 after another sign (<-5)", "-5", "<-5", None, False, "FAIL"),
 ("T1 5 inside - 5 (a sign across one space)", "5", "- 5", None, False,
  "FAIL"),
 ("T1 -4 after a letter (GPT-4)", "-4", "GPT-4", None, False, "FAIL"),
 ("T1 4 inside GPT-4", "4", "GPT-4", None, False, "FAIL"),
 ("T1 the whole GPT-4", "GPT-4", "GPT-4 and more", None, False, "PASS"),
 ("T1 a hyphen between letters", "state", "state-of-the-art", None, False,
  "PASS"),
 # TK2 (T2): separators
 ("T2 5 inside .5", "5", ".5", None, False, "FAIL"),
 ("T2 5 inside ,5", "5", ",5", None, False, "FAIL"),
 ("T2 5 inside 1,5", "5", "1,5", None, False, "FAIL"),
 ("T2 5 inside 5.5", "5", "5.5", None, False, "FAIL"),
 ("T2 12 inside 12.5", "12", "12.5", None, False, "FAIL"),
 ("T2 12 inside 3.12", "12", "3.12", None, False, "FAIL"),
 ("T2 12.5 inside 12.50", "12.5", "12.50", None, False, "FAIL"),
 ("T2 234 inside 1,234", "234", "1,234", None, False, "FAIL"),
 ("T2 234 inside 1 234", "234", "1 234", None, False, "FAIL"),
 ("T2 234 after U+202F NARROW NO-BREAK SPACE", "234",
  "1\N{NARROW NO-BREAK SPACE}234", None, False, "FAIL"),
 ("T2 234 after U+2009 THIN SPACE", "234", "1\N{THIN SPACE}234", None,
  False, "FAIL"),
 ("T2 234 after U+00A0 NO-BREAK SPACE", "234", "1" + NBSP + "234", None,
  False, "FAIL"),
 ("T2 1 before an apostrophe group", "1", "1'000", None, False, "FAIL"),
 ("T2 1 before a U+2019 group", "1", "1\N{RIGHT SINGLE QUOTATION MARK}000",
  None, False, "FAIL"),
 ("T2 1 before an underscore group", "1", "1_000", None, False, "FAIL"),
 ("T2 two digits after a space are no group", "1", "1 23", None, False,
  "PASS"),
 ("T2 four digits after a space are no group", "1", "1 2345", None, False,
  "PASS"),
 ("T2 the grouped number whole", "1 234", "about 1 234 people", None,
  False, "PASS"),
 ("T2 1,000 inside 1,000,000", "1,000", "1,000,000", None, False, "FAIL"),
 ("T2 1,000 before a list comma", "1,000", "paid 1,000, then", None, False,
  "PASS"),
 ("T2 a list comma after 12", "12", "12, 13", None, False, "PASS"),
 ("T2 an underscore between letters", "a", "a_b", None, False, "PASS"),
 ("T2 an apostrophe after a word", "Claude", "Claude's", None, False, "PASS"),
 # TK3 (T3): joins
 ("T3 3 inside 3-5", "3", "3-5", None, False, "FAIL"),
 ("T3 5 inside 3-5", "5", "3-5", None, False, "FAIL"),
 ("T3 -5 inside 3-5 (a range)", "-5", "3-5", None, False, "FAIL"),
 ("T3 5 inside 3 U+2013 5", "5", "3" + NDASH + "5", None, False, "FAIL"),
 ("T3 1 inside 1/2", "1", "1/2", None, False, "FAIL"),
 ("T3 2 inside 2:1", "2", "2:1", None, False, "FAIL"),
 ("T3 2026 inside a date", "2026", "2026-09-25", None, False, "FAIL"),
 ("T3 the date whole", "2026-09-25", "on 2026-09-25.", None, False, "PASS"),
 ("T3 3 inside 3 - 5 (one space either side)", "3", "3 - 5", None, False,
  "FAIL"),
 ("T3 a dash before a letter (5-shot)", "5", "5-shot", None, False, "PASS"),
 # TK4 (T4): exponents
 ("T4 1 inside 1e-5", "1", "1e-5", None, False, "FAIL"),
 ("T4 5 inside 1e-5", "5", "1e-5", None, False, "FAIL"),
 ("T4 -5 inside 1e-5", "-5", "1e-5", None, False, "FAIL"),
 ("T4 1e-5 whole", "1e-5", "about 1e-5 s", None, False, "PASS"),
 ("T4 1 inside 1E5", "1", "1E5", None, False, "FAIL"),
 ("T4 1 inside 1 x 10^-5", "1", "1 x 10^-5", None, False, "FAIL"),
 ("T4 1 x 10^-5 whole", "1 x 10^-5", "rate 1 x 10^-5 per s", None, False,
  "PASS"),
 ("T4 10 inside 10^5", "10", "10^5", None, False, "FAIL"),
 ("T4 5 inside 10^5", "5", "10^5", None, False, "FAIL"),
 ("T4 1 before U+00D7 10 and superscript -5", "1",
  "1\N{MULTIPLICATION SIGN}10\N{SUPERSCRIPT MINUS}\N{SUPERSCRIPT FIVE}", None,
  False, "FAIL"),
 ("T4 10 before a superscript digit", "10", "10\N{SUPERSCRIPT FIVE}", None,
  False, "FAIL"),
 ("T4 2 inside 2*10^3", "2", "2*10^3", None, False, "FAIL"),
 ("T4 2 before U+00B7 10", "2", "2\N{MIDDLE DOT}10\N{SUPERSCRIPT THREE}",
  None, False, "FAIL"),
 ("T4 3 x 100 is no power of ten", "3", "3 x 100", None, False, "PASS"),
 # TK5 (T5): affixes
 ("T5 5 inside 5% (no affix declared)", "5", "5%", None, False, "FAIL"),
 ("T5 5 inside 5 % (no affix declared)", "5", "5 %", None, False, "FAIL"),
 ("T5 5 inside $5 (no affix declared)", "5", "$5", None, False, "FAIL"),
 ("T5 5 before U+2030 PER MILLE SIGN", "5", "5" + PERMILLE, None, False,
  "FAIL"),
 ("T5 5 before U+00B0 DEGREE SIGN", "5", "5" + DEG + "C", None, False,
  "FAIL"),
 ("T5 5 before U+2032 PRIME", "5", "5\N{PRIME}", None, False, "FAIL"),
 ("T5 5 after U+20AC EURO SIGN and a space", "5", "\N{EURO SIGN} 5", None,
  False, "FAIL"),
 ("T5 12.5 inside was 12.5% (no affix declared)", "12.5", "was 12.5%", None,
  False, "FAIL"),
 ("T5 72.4 in 72.4% with % declared", "72.4", "72.4%", "%", False, "PASS"),
 ("T5 72.4 in 72.4 % with % declared", "72.4", "72.4 %", "%", False, "PASS"),
 ("T5 72.4 across U+00A0 with % declared", "72.4", "72.4" + NBSP + "%", "%",
  False, "PASS"),
 ("T5 72.4 in 72.4% with $ declared", "72.4", "72.4%", "$", False, "FAIL"),
 ("T5 72.4 before per mille with % declared", "72.4", "72.4" + PERMILLE, "%",
  False, "FAIL"),
 ("T5 5 in $5 with $ declared", "5", "$5", "$", False, "PASS"),
 ("T5 5 in $ 5 with $ declared", "5", "$ 5", "$", False, "PASS"),
 ("T5 5 in -$5 with $ declared (a sign before the affix)", "5", "-$5", "$",
  False, "FAIL"),
 ("T5 5 in 5 U+00B0 C with U+00B0 C declared", "5", "5" + DEG + "C",
  DEG + "C", False, "PASS"),
 ("T5 5 in 5 U+00B0 C with U+00B0 alone declared", "5", "5" + DEG + "C", DEG,
  False, "FAIL"),
 ("T5 a declared affix not present", "72.4", "72.4 points", "%", False,
  "PASS"),
 ("T5 the value with its own affix", "72.4%", "72.4%", None, False, "PASS"),
 ("T5 an affix that begins another number", "5", "5%3", "%", False, "FAIL"),
 ("T5 a declared affix does not excuse a cut number", "72.4", "72.45%", "%",
  False, "FAIL"),
 # TK6 (T6): the value's own form
 ("T6 1234 against 1,234", "1234", "1,234", None, False, "FAIL"),
 ("T6 1.5 against 1,5", "1.5", "1,5", None, False, "FAIL"),
 ("T6 -5 against 5", "-5", "5", None, False, "FAIL"),
 ("T6 -5 against U+2212 5", "-5", MINUS + "5", None, False, "FAIL"),
 # TK7 (T7): outside the token
 ("T7 12 inside (12)", "12", "(12)", None, False, "PASS"),
 ("T7 12 before a full stop", "12", "is 12.", None, False, "PASS"),
 ("T7 12 in quotation marks", "12", '"12"', None, False, "PASS"),
 ("T7 12 in guillemets", "12",
  "\N{LEFT-POINTING DOUBLE ANGLE QUOTATION MARK}12"
  "\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}", None, False, "PASS"),
 ("T7 12 in brackets", "12", "[12]", None, False, "PASS"),
 ("T7 12 before a full stop and a letter", "12", "12.x", None, False,
  "PASS"),
 ("T5 5 inside 5 per cent (a per-cent word)", "5", "5 per cent", None,
  False, "FAIL"),
 ("T7 accounting: 5 inside (5)", "5", "(5)", None, True, "FAIL"),
 ("T7 accounting: -5 reads (5)", "-5", "net (5) in Q3", None, True, "PASS"),
 ("T7 no accounting: -5 against (5)", "-5", "(5)", None, False, "FAIL"),
 ("T7 accounting: 12 inside (12)", "12", "(12)", None, True, "FAIL"),
 ("T7 accounting: a grouped decimal", "-1,234.5", "(1,234.5)", None, True,
  "PASS"),
 ("T7 accounting: a value written (5)", "(5)", "(5)", None, True, "PASS"),
 # case, whitespace and non-ASCII under NFC
 ("text: no case folding", "Opus", "opus 5", None, False, "FAIL"),
 ("text: the same case", "Opus", "Claude Opus 5", None, False, "PASS"),
 ("text: whitespace runs collapse", "a  b", "x a\tb y", None, False, "PASS"),
 ("text: NFC on both sides", "caf\N{LATIN SMALL LETTER E WITH ACUTE}",
  "cafe\N{COMBINING ACUTE ACCENT} au lait", None, False, "PASS"),
 ("text: a letter after the occurrence", "caf",
  "cafe\N{COMBINING ACUTE ACCENT}", None, False, "FAIL"),
 ("text: sharp s is a letter", "Stra", "Stra\N{LATIN SMALL LETTER SHARP S}e",
  None, False, "FAIL"),
 ("text: a combining mark after", "x", "x\N{COMBINING ACUTE ACCENT} y", None,
  False, "FAIL"),
 ("text: a CJK letter after", CJK, CJK + "\N{CJK UNIFIED IDEOGRAPH-90FD}",
  None, False, "FAIL"),
 ("text: an ideographic comma after", CJK, CJK + "\N{IDEOGRAPHIC COMMA}",
  None, False, "PASS"),
 ("text: NO-BREAK SPACE is a boundary", "12", NBSP + "12" + NBSP, None, False,
  "PASS"),
 ("text: an empty value", "", "abc", None, False, "FAIL"),
 ("text: a whitespace-only value", " \t ", "abc", None, False, "FAIL"),
 ("text: a value longer than the quote", "abcd", "abc", None, False, "FAIL"),
 # refusals
 ("refused: a non-string value", 12, "12", None, False, "ERROR:text"),
 ("refused: a null quote", "12", None, None, False, "ERROR:text"),
 ("refused: an affix of letters", "5", "5", "ab", False, "ERROR:affix"),
 ("refused: an empty affix", "5", "5", "", False, "ERROR:affix"),
 ("refused: an affix given as a number", "5", "5", 5, False, "ERROR:affix"),
 ("refused: two symbols as an affix", "5", "5", "%%", False, "ERROR:affix"),
 ("refused: an affix of five characters", "5", "5", "%abcd", False,
  "ERROR:affix"),
 ("refused: accounting given as a string", "5", "5", None, "yes",
  "ERROR:accounting"),
]
# Second verification round: T8 SCALE, T5's per-cent words, Zs as the
# space, Cf removed, T3's slashes and spaced joins, T1 across a space.
# (name, value, quote, affix, scale, accounting, expected)
FIG = "\N{FIGURE SPACE}"
ENSP = "\N{EN SPACE}"
ZW = "\N{ZERO WIDTH SPACE}"
SHY = "\N{SOFT HYPHEN}"
QMS = [
 # the verifier's cases
 ("V 234 inside 1 U+2007 234 (T2, a figure space)", "234", "1" + FIG + "234",
  None, None, False, "FAIL"),
 ("V 5 inside 5 U+2002 % (T5, no affix declared)", "5", "5" + ENSP + "%",
  None, None, False, "FAIL"),
 ("V 5 inside 5 U+2044 10 (T3, fraction slash)", "5",
  "5\N{FRACTION SLASH}10", None, None, False, "FAIL"),
 ("V 5 inside - U+200B 5 (Cf removed, then T1)", "5", "-" + ZW + "5", None,
  None, False, "FAIL"),
 ("V 12 inside 12 U+200B 3 (Cf removed, then 123)", "12", "12" + ZW + "3",
  None, None, False, "FAIL"),
 # T8 SCALE, none declared
 ("T8 5 inside 5 million", "5", "5 million", None, None, False, "FAIL"),
 ("T8 5 inside 5 Million (any case)", "5", "5 Million", None, None, False,
  "FAIL"),
 ("T8 5 inside 5 MILLION", "5", "5 MILLION", None, None, False, "FAIL"),
 ("T8 5 inside 5 millions (a plural)", "5", "5 millions", None, None, False,
  "FAIL"),
 ("T8 5 inside 5 hundred", "5", "5 hundred", None, None, False, "FAIL"),
 ("T8 5 inside 5 thousand", "5", "5 thousand", None, None, False, "FAIL"),
 ("T8 5 inside 5 billion", "5", "5 billion", None, None, False, "FAIL"),
 ("T8 5 inside 5 trillion", "5", "5 trillion", None, None, False, "FAIL"),
 ("T8 5 inside 5 lakh", "5", "5 lakh", None, None, False, "FAIL"),
 ("T8 5 inside 5 crores", "5", "5 crores", None, None, False, "FAIL"),
 ("T8 5 inside 5 M", "5", "5 M", None, None, False, "FAIL"),
 ("T8 5 inside 5 k", "5", "5 k", None, None, False, "FAIL"),
 ("T8 5 inside 5 K", "5", "5 K", None, None, False, "FAIL"),
 ("T8 5 inside 5k (touching)", "5", "5k", None, None, False, "FAIL"),
 ("T8 5 inside 5 mn", "5", "5 mn", None, None, False, "FAIL"),
 ("T8 5 inside 5 bn", "5", "5 bn", None, None, False, "FAIL"),
 ("T8 5 inside 5 Bn (mn and bn in any case)", "5", "5 Bn", None, None,
  False, "FAIL"),
 ("T8 5 inside 5 B (billion)", "5", "5 B", None, None, False, "FAIL"),
 ("T8 5 inside 5 m (a bare m)", "5", "5 m", None, None, False, "FAIL"),
 ("T8 5 inside $5 billion with $ declared", "5", "$5 billion", "$", None,
  False, "FAIL"),
 ("T8 5 inside 5 E-3 (a spaced exponent)", "5", "5 E-3", None, None, False,
  "FAIL"),
 ("T8 5 inside 5 e 3", "5", "5 e 3", None, None, False, "FAIL"),
 ("T8 5 inside 5 E+3", "5", "5 E+3", None, None, False, "FAIL"),
 ("T8 5 inside 5E 3", "5", "5E 3", None, None, False, "FAIL"),
 ("T8 5 inside 5 times 10^3", "5", "5 times 10^3", None, None, False,
  "FAIL"),
 ("T8 10 inside 10**5", "10", "10**5", None, None, False, "FAIL"),
 ("T8 2 inside 2 ** 3", "2", "2 ** 3", None, None, False, "FAIL"),
 ("T8 5 inside 5 x 10^3", "5", "5 x 10^3", None, None, False, "FAIL"),
 ("T8 5 across U+00A0 before million", "5", "5" + NBSP + "million", None,
  None, False, "FAIL"),
 ("T8 5 across U+2007 before k", "5", "5" + FIG + "k", None, None, False,
  "FAIL"),
 # T8 and T7: words that are no scale
 ("T8 5 models: no scale", "5", "5 models", None, None, False, "PASS"),
 ("T8 5 km: no scale", "5", "5 km", None, None, False, "PASS"),
 ("T8 5 inside 5 thousandths (a fractional scale word)", "5",
  "5 thousandths", None, None, False, "FAIL"),
 ("T8 5 millionaires: no scale", "5", "5 millionaires", None, None, False,
  "PASS"),
 ("T8 5 each: no scale", "5", "5 each", None, None, False, "PASS"),
 ("T8 5 b: a lower-case b is no scale", "5", "5 b", None, None, False,
  "PASS"),
 ("T8 5 times 10 without ^ is no product", "5", "5 times 10", None, None,
  False, "PASS"),
 ("T7 12 metres: a unit in words", "12", "12 metres", None, None, False,
  "PASS"),
 # T8 with a QUOTE SCALE declared
 ("T8 5 in 5 million with million declared", "5", "5 million", None,
  "million", False, "PASS"),
 ("T8 5 in 5 Millions with million declared", "5", "5 Millions", None,
  "million", False, "PASS"),
 ("T8 5 in 5 million people with million declared", "5",
  "5 million people", None, "million", False, "PASS"),
 ("T8 5 in 5 million with billion declared", "5", "5 million", None,
  "billion", False, "FAIL"),
 ("T8 5 in 5k with k declared", "5", "5k", None, "k", False, "PASS"),
 ("T8 5 in 5 k with k declared", "5", "5 k", None, "k", False, "PASS"),
 ("T8 5 in 5 K with k declared (one letter exactly)", "5", "5 K", None, "k",
  False, "FAIL"),
 ("T8 5 in 5 M with M declared", "5", "5 M", None, "M", False, "PASS"),
 ("T8 5 in 5 BN with bn declared", "5", "5 BN", None, "bn", False, "PASS"),
 ("T8 12 in 12 lakh with lakh declared", "12", "12 lakh", None, "lakh",
  False, "PASS"),
 ("T8 3 in 3 crores with crore declared", "3", "3 crores", None, "crore",
  False, "PASS"),
 ("T8 5 in $5 billion with $ and billion declared", "5", "$5 billion", "$",
  "billion", False, "PASS"),
 ("T8 5 in $5 billion with billion alone declared", "5", "$5 billion", None,
  "billion", False, "FAIL"),
 ("T8 5 in 5 million% with % and million declared", "5", "5 million%", "%",
  "million", False, "PASS"),
 ("T8 a declared scale not present", "5", "5 people", None, "million",
  False, "PASS"),
 ("T8 a declared scale does not excuse a cut number", "5", "5.5 million",
  None, "million", False, "FAIL"),
 ("T8 a spaced exponent is never declarable", "5", "5 E-3", None, "m",
  False, "FAIL"),
 ("T8 the value with its own scale", "5 million", "about 5 million", None,
  None, False, "PASS"),
 # T5: the per-cent words
 ("T5 5 inside 5 percent", "5", "5 percent", None, None, False, "FAIL"),
 ("T5 5 inside 5 Percent", "5", "5 Percent", None, None, False, "FAIL"),
 ("T5 5 inside 5 pct", "5", "5 pct", None, None, False, "FAIL"),
 ("T5 5 inside 5 PCT", "5", "5 PCT", None, None, False, "FAIL"),
 ("T5 5 inside 5 per U+00A0 cent", "5", "5 per" + NBSP + "cent", None, None,
  False, "FAIL"),
 ("T5 5 inside percent 5 (before the number)", "5", "percent 5", None, None,
  False, "FAIL"),
 ("T5 5 inside per cent 5", "5", "per cent 5", None, None, False, "FAIL"),
 ("T5 5 in 5 per cent with per cent declared", "5", "5 per cent",
  "per cent", None, False, "PASS"),
 ("T5 5 in 5 Per Cent with per cent declared", "5", "5 Per Cent",
  "per cent", None, False, "PASS"),
 ("T5 5 in 5 percent with percent declared", "5", "5 percent", "percent",
  None, False, "PASS"),
 ("T5 5 in 5 percent with per cent declared", "5", "5 percent", "per cent",
  None, False, "FAIL"),
 ("T5 5 in 5 pct with pct declared", "5", "5 pct", "pct", None, False,
  "PASS"),
 ("T5 5 in pct 5 with pct declared", "5", "pct 5", "pct", None, False,
  "PASS"),
 ("T5 5 in 5% with percent declared (the word only)", "5", "5%", "percent",
  None, False, "FAIL"),
 ("T5 5 inside 5 percentage points (an affix after the number)", "5",
  "5 percentage points", None, None, False, "FAIL"),
 ("T5 5 per capita: no affix word", "5", "5 per capita", None, None, False,
  "PASS"),
 # Zs: any space separator
 ("Zs 5 in 5 U+2002 % with % declared", "5", "5" + ENSP + "%", "%", None,
  False, "PASS"),
 ("Zs 234 inside 1 U+3000 234", "234", "1\N{IDEOGRAPHIC SPACE}234", None,
  None, False, "FAIL"),
 ("Zs 5 inside - U+2003 5 (T1 across an em space)", "5",
  "-\N{EM SPACE}5", None, None, False, "FAIL"),
 ("Zs 5 inside 5 U+1680 million (ogham space mark)", "5",
  "5\N{OGHAM SPACE MARK}million", None, None, False, "FAIL"),
 ("Zs 3 inside 3 U+2007 - U+2007 5", "3", "3" + FIG + "-" + FIG + "5", None,
  None, False, "FAIL"),
 ("Zs 5 inside 5 U+205F / U+205F 10", "5",
  "5\N{MEDIUM MATHEMATICAL SPACE}/\N{MEDIUM MATHEMATICAL SPACE}10", None,
  None, False, "FAIL"),
 ("Zs a run of two spaces still joins (3 U+00A0 U+00A0 - 5)", "3",
  "3" + NBSP + NBSP + "- 5", None, None, False, "FAIL"),
 # T3: slashes, ratio and spaced joins
 ("T3 10 inside 5 U+2044 10", "10", "5\N{FRACTION SLASH}10", None, None,
  False, "FAIL"),
 ("T3 5 inside 5 U+2215 10 (division slash)", "5", "5\N{DIVISION SLASH}10",
  None, None, False, "FAIL"),
 ("T3 2 inside 2 U+2236 1 (ratio)", "2", "2\N{RATIO}1", None, None, False,
  "FAIL"),
 ("T3 5 inside 5 / 10", "5", "5 / 10", None, None, False, "FAIL"),
 ("T3 10 inside 5 / 10", "10", "5 / 10", None, None, False, "FAIL"),
 ("T3 5 inside 3 - 5", "5", "3 - 5", None, None, False, "FAIL"),
 ("T3 5 inside 5 :1", "5", "5 :1", None, None, False, "FAIL"),
 ("T3 a slash between words joins nothing", "5", "5 and/or", None, None,
  False, "PASS"),
 # T1 across one space
 ("T1 5 inside x - 5 (the sign follows no digit)", "5", "x - 5", None, None,
  False, "FAIL"),
 ("T1 5 inside score = 5", "5", "score = 5", None, None, False, "FAIL"),
 ("T1 2 in 1 + 1 = 2 (the sign follows a digit)", "2", "1 + 1 = 2", None,
  None, False, "PASS"),
 ("T1 5 inside ~ $5 with $ declared (a sign across a space)", "5",
  "~ $5", "$", None, False, "FAIL"),
 # Cf removed from both texts
 ("Cf 5 inside 5 U+00AD % (soft hyphen)", "5", "5" + SHY + "%", None, None,
  False, "FAIL"),
 ("Cf 12 after a U+2060 WORD JOINER and a letter", "12",
  "a\N{WORD JOINER}12", None, None, False, "FAIL"),
 ("Cf 12 inside 1 U+FEFF 2 3", "12", "1\N{ZERO WIDTH NO-BREAK SPACE}23",
  None, None, False, "FAIL"),
 ("Cf 5 inside - U+200E 5 (left-to-right mark)", "5",
  "-\N{LEFT-TO-RIGHT MARK}5", None, None, False, "FAIL"),
 ("Cf 5 inside 5 U+061C % (Arabic letter mark)", "5",
  "5\N{ARABIC LETTER MARK}%", None, None, False, "FAIL"),
 ("Cf removed from the value too", "1" + ZW + "2", "is 12 now", None, None,
  False, "PASS"),
 ("Cf a soft hyphen inside the quoted number", "12", "is 1" + SHY + "2 now",
  None, None, False, "PASS"),
 ("Cf a value of format characters only", ZW + SHY, "abc", None, None,
  False, "FAIL"),
 # the pinned class table (Unicode 15.1.0) in every residence
 ("table: a digit assigned in Unicode 15.0 (U+1E4F1)", "12",
  "12" + chr(0x1E4F1), None, None, False, "FAIL"),
 ("table: a letter assigned in Unicode 15.1 (U+2EBF0)", "12",
  chr(0x2EBF0) + "12", None, None, False, "FAIL"),
 # refusals of the scale
 ("refused: an unknown scale", "5", "5", None, "lots", False,
  "ERROR:scale"),
 ("refused: a plural scale", "5", "5", None, "millions", False,
  "ERROR:scale"),
 ("refused: a scale in upper case", "5", "5", None, "Million", False,
  "ERROR:scale"),
 ("refused: an empty scale", "5", "5", None, "", False, "ERROR:scale"),
 ("refused: a scale given as a number", "5", "5", None, 1000, False,
  "ERROR:scale"),
 ("refused: an affix word in upper case", "5", "5", "Percent", None, False,
  "ERROR:affix"),
]
QMV = QMV + QMS
# Pass 3 (pass3_fix_spec 0.1.0 SF1): the catalogues widened.
# (name, value, quote, affix, scale, accounting, expected)
QM3 = []
# T3: every Pd character, and the listed others, between two digits.
for cp, nm in [(0x2014, "EM DASH"), (0x2010, "HYPHEN"),
               (0x2011, "NON-BREAKING HYPHEN"), (0x2012, "FIGURE DASH"),
               (0x2015, "HORIZONTAL BAR"), (0x2E3A, "TWO-EM DASH"),
               (0x2E3B, "THREE-EM DASH"), (0x301C, "WAVE DASH"),
               (0xFE58, "SMALL EM DASH"), (0xFE63, "SMALL HYPHEN-MINUS"),
               (0xFF0D, "FULLWIDTH HYPHEN-MINUS"), (0x2212, "MINUS SIGN"),
               (0x007E, "TILDE"), (0xFF5E, "FULLWIDTH TILDE"),
               (0x00F7, "DIVISION SIGN"), (0x29F8, "BIG SOLIDUS"),
               (0x2053, "SWUNG DASH")]:
    QM3.append(("T3 5 inside 5 U+%04X 10 (%s)" % (cp, nm), "5",
                "5" + chr(cp) + "10", None, None, False, "FAIL"))
QM3 += [
 ("T3 5 inside 5 U+2014 10 with a space either side", "5",
  "5 " + chr(0x2014) + " 10", None, None, False, "FAIL"),
 ("T3 10 inside 5 U+2014 10", "10", "5" + chr(0x2014) + "10", None, None,
  False, "FAIL"),
 ("T3 5 inside 5 ~ 10", "5", "5 ~ 10", None, None, False, "FAIL"),
 ("T3 a dash before a word joins nothing (5 U+2014 shot)", "5",
  "5" + chr(0x2014) + "shot", None, None, False, "PASS"),
 # T3 mixed numbers
 ("T3 5 inside 5 U+00BD (a mixed number)", "5", "5 " + chr(0xBD), None,
  None, False, "FAIL"),
 ("T3 5 inside 5 1/2", "5", "5 1/2", None, None, False, "FAIL"),
 ("T3 5 inside 5 U+00A0 1 U+2044 2", "5",
  "5" + chr(0xA0) + "1" + chr(0x2044) + "2", None, None, False, "FAIL"),
 ("T3 5 inside 5 1 U+2215 2", "5", "5 1" + chr(0x2215) + "2", None, None,
  False, "FAIL"),
 ("T3 5 inside 5 U+2153 (one third)", "5", "5 " + chr(0x2153), None, None,
  False, "FAIL"),
 ("T3 5 inside 5 U+2189 (zero thirds)", "5", "5 " + chr(0x2189), None,
  None, False, "FAIL"),
 ("T3 12 inside 12 3/4 kg", "12", "12 3/4 kg", None, None, False, "FAIL"),
 ("T3 5 touching U+00BD joins by TK1", "5", "5" + chr(0xBD), None, None,
  False, "FAIL"),
 ("T3 a digit run with no slash is no fraction (5 12)", "5", "5 12", None,
  None, False, "PASS"),
 ("T3 a slash before a letter is no fraction (5 1/x)", "5", "5 1/x", None,
  None, False, "PASS"),
 # T7: the named limit, pinned
 ("T7 limit: 5 to 10 (a range in words) passes", "5", "5 to 10", None,
  None, False, "PASS"),
 ("T7 limit: 5 and a half (a fraction in words) passes", "5",
  "5 and a half", None, None, False, "PASS"),
 ("T7 limit: 5 km (a prefixed unit) passes", "5", "5 km", None, None,
  False, "PASS"),
 ("T7 limit: 5 mm (no scale, MM being exact case) passes", "5", "5 mm",
  None, None, False, "PASS"),
]
# T5 symbols, either side.
for cp, nm in [(0xFF05, "FULLWIDTH PERCENT SIGN"),
               (0xFE6A, "SMALL PERCENT SIGN"), (0x066A, "ARABIC PERCENT SIGN"),
               (0x0609, "ARABIC-INDIC PER MILLE SIGN"),
               (0x060A, "ARABIC-INDIC PER TEN THOUSAND SIGN")]:
    QM3.append(("T5 5 inside 5 U+%04X (%s)" % (cp, nm), "5", "5" + chr(cp),
                None, None, False, "FAIL"))
    QM3.append(("T5 5 in 5 U+%04X with it declared" % cp, "5", "5" + chr(cp),
                chr(cp), None, False, "PASS"))
QM3 += [
 ("T5 5 after U+066A (before the number)", "5", chr(0x66A) + "5", None,
  None, False, "FAIL"),
 ("T5 5 after U+066A with it declared", "5", chr(0x66A) + "5", chr(0x66A),
  None, False, "PASS"),
]
# T5 words after the number only.
for wd, text in [("per mille", "5 per mille"), ("permille", "5 permille"),
                 ("bp", "5 bp"), ("bps", "5 bps"),
                 ("basis point", "5 basis point"),
                 ("basis points", "5 basis points"), ("pp", "5 pp"),
                 ("percentage point", "5 percentage point"),
                 ("percentage points", "5 percentage points"),
                 ("ppm", "5 ppm"), ("ppb", "5 ppb"), ("ppt", "5 ppt"),
                 ("ppq", "5 ppq")]:
    QM3.append(("T5 5 inside %s" % text, "5", text, None, None, False,
                "FAIL"))
    QM3.append(("T5 5 in %s with %s declared" % (text, wd), "5", text, wd,
                None, False, "PASS"))
QM3 += [
 ("T5 5 inside 5 PPB (any case)", "5", "5 PPB", None, None, False, "FAIL"),
 ("T5 5 inside 5 Per Mille", "5", "5 Per Mille", None, None, False, "FAIL"),
 ("T5 5 inside 5 BPS", "5", "5 BPS", None, None, False, "FAIL"),
 ("T5 5 inside 5 basis U+00A0 points", "5", "5 basis" + chr(0xA0) + "points",
  None, None, False, "FAIL"),
 ("T5 basis point declared matches 5 basis points", "5", "5 basis points",
  "basis point", None, False, "PASS"),
 ("T5 basis points declared matches 5 basis point", "5", "5 basis point",
  "basis points", None, False, "PASS"),
 ("T5 percentage point declared matches 5 percentage points", "5",
  "5 percentage points", "percentage point", None, False, "PASS"),
 ("T5 percentage points declared matches 5 Percentage Point", "5",
  "5 Percentage Point", "percentage points", None, False, "PASS"),
 ("T5 bp declared does not match 5 bps (one word, no number)", "5", "5 bps",
  "bp", None, False, "FAIL"),
 ("T5 per mille declared does not match 5 %", "5", "5 %", "per mille",
  None, False, "FAIL"),
 ("T5 bps before the number joins nothing (after only)", "5", "bps 5",
  None, None, False, "PASS"),
 ("T5 5 in 5 million bps with million and bps declared", "5",
  "5 million bps", "bps", "million", False, "PASS"),
 ("T5 5 percentage: one word of a two-word form is none", "5",
  "5 percentage", None, None, False, "PASS"),
]
# T8 words and abbreviations.
for wd in ["quadrillion", "dozen", "tenth", "hundredth", "thousandth",
           "millionth", "billionth", "trillionth"]:
    QM3.append(("T8 5 inside 5 %ss" % wd, "5", "5 %ss" % wd, None, None,
                False, "FAIL"))
    QM3.append(("T8 5 inside 5 %s" % wd, "5", "5 %s" % wd, None, None, False,
                "FAIL"))
    QM3.append(("T8 5 in 5 %ss with %s declared" % (wd, wd), "5",
                "5 %ss" % wd, None, wd, False, "PASS"))
for ab, text in [("mln", "5 mln"), ("bln", "5 BLN"), ("tn", "5 tn"),
                 ("trn", "5 trn"), ("mio", "5 Mio"), ("mrd", "5 mrd"),
                 ("MM", "5 MM")]:
    QM3.append(("T8 5 inside %s" % text, "5", text, None, None, False,
                "FAIL"))
    QM3.append(("T8 5 in %s with %s declared" % (text, ab), "5", text, None,
                ab, False, "PASS"))
QM3 += [
 ("T8 5 inside 5 TN (any case)", "5", "5 TN", None, None, False, "FAIL"),
 ("T8 5 inside 5 MRD", "5", "5 MRD", None, None, False, "FAIL"),
 ("T8 5 in 5 mm with MM declared (no scale there)", "5", "5 mm", None, "MM",
  False, "PASS"),
 ("T8 5 in 5 MM with M declared", "5", "5 MM", None, "M", False, "FAIL"),
 ("T8 5 inside 5 Dozen", "5", "5 Dozen", None, None, False, "FAIL"),
 ("T8 0.5 inside 0.5 millionths", "0.5", "0.5 millionths", None, None,
  False, "FAIL"),
 ("refused: a plural scale entry (thousandths)", "5", "5", None,
  "thousandths", False, "ERROR:scale"),
 ("refused: a case-free abbreviation declared in upper case", "5", "5",
  None, "Mio", False, "ERROR:scale"),
 ("refused: an exact abbreviation not in the catalogue (mm)", "5", "5",
  None, "mm", False, "ERROR:scale"),
 ("refused: an affix word in title case (Basis Point)", "5", "5",
  "Basis Point", None, False, "ERROR:affix"),
 ("refused: points alone as an affix", "5", "5", "points", None, False,
  "ERROR:affix"),
]
QMV = QMV + QM3
for k, row in enumerate(QMV, 1):
    if len(row) == 6:
        nm, v, q, aff, acc, exp = row
        sc = None
    else:
        nm, v, q, aff, sc, acc, exp = row
    add("QM%03d %s" % (k, nm), "qmatch",
        {"value": v, "quote": q, "affix": aff, "scale": sc,
         "accounting": acc},
        exp if exp.startswith("ERROR") else "quote|" + exp)

PG = "GPQA Diamond: 87.3% (n = 198). Next line"
QCV = [
 ("the quote in the page, the value in the quote, % declared", PG,
  "GPQA Diamond: 87.3%", "87.3", "%", False, "PASS"),
 ("the same with no affix declared (T5)", PG, "GPQA Diamond: 87.3%", "87.3",
  None, False, "FAIL"),
 ("a whitespace-only difference", "GPQA  Diamond:\n87.3% (n = 198)",
  "GPQA Diamond: 87.3%", "87.3", "%", False, "PASS"),
 ("the value outside the quote", PG, "GPQA Diamond: 87.3%", "198", "%",
  False, "FAIL"),
 ("near-miss digits", PG, "GPQA Diamond: 87.3%", "87.4", "%", False, "FAIL"),
 ("no page TPX may read: UNKNOWN", None, "GPQA Diamond: 87.3%", "87.3", "%",
  False, "UNKNOWN"),
 ("a quote beginning mid-word", "Anthropic Claude", "hropic Claude",
  "Claude", None, False, "FAIL"),
 ("12 inside 123 within the quote", "123 items", "123 items", "12", None,
  False, "FAIL"),
 ("a quote that cuts a number", "score 87.35 on", "score 87.3", "87.3", None,
  False, "FAIL"),
 ("12 inside (12)", "limit (12) per day", "(12) per day", "12", None, False,
  "PASS"),
 ("a quote not on the page", "GPQA Diamond: 81.0%", "GPQA Diamond: 87.3%",
  "87.3", "%", False, "FAIL"),
 ("a quote ending at a declared affix", "GPQA 72.4% today", "GPQA 72.4",
  "72.4", "%", False, "PASS"),
 ("a quote ending at an undeclared affix", "GPQA 72.4% today", "GPQA 72.4",
  "72.4", None, False, "FAIL"),
 ("a sign error end to end (QM12)", "change -5 points", "change -5 points",
  "5", None, False, "FAIL"),
 ("the signed value end to end", "change -5 points", "change -5 points",
  "-5", None, False, "PASS"),
 ("accounting: -5 against (5)", "Net income (5) in Q3",
  "Net income (5) in Q3", "-5", None, True, "PASS"),
 ("no accounting: -5 against (5)", "Net income (5) in Q3",
  "Net income (5) in Q3", "-5", None, False, "FAIL"),
 ("accounting: 5 against (5)", "Net income (5) in Q3",
  "Net income (5) in Q3", "5", None, True, "FAIL"),
 ("T4 end to end: 1 inside 1 x 10^-5", "rate 1 x 10^-5 per s",
  "rate 1 x 10^-5 per s", "1", None, False, "FAIL"),
 ("T4 end to end: the whole power", "rate 1 x 10^-5 per s",
  "rate 1 x 10^-5 per s", "1 x 10^-5", None, False, "PASS"),
 ("refused: a non-string page", 5, "q", "v", None, False, "ERROR:text"),
 ("refused without a page: a non-string quote", None, 5, "v", None, False,
  "ERROR:text"),
 ("refused without a page: a bad affix", None, "q", "v", "ab", False,
  "ERROR:affix"),
 ("refused: accounting as a number", "p", "p", "p", None, 1,
  "ERROR:accounting"),
]
# (name, page, quote, value, affix, scale, accounting, expected)
PB = "Revenue was $5 billion in 2025."
QCS = [
 ("T8 end to end: $ and billion declared", PB, "Revenue was $5 billion",
  "5", "$", "billion", False, "PASS"),
 ("T8 end to end: $ alone declared", PB, "Revenue was $5 billion", "5",
  "$", None, False, "FAIL"),
 ("T8 end to end: a quote cut before the scale", PB, "Revenue was $5", "5",
  "$", None, False, "FAIL"),
 ("T8 end to end: the same cut with billion declared", PB,
  "Revenue was $5", "5", "$", "billion", False, "PASS"),
 ("Cf end to end: format characters in the page", "Score: 8" + ZW + "7.3%",
  "Score: 87.3%", "87.3", "%", None, False, "PASS"),
 ("Cf end to end: format characters in the quote", "Score: 87.3%",
  "Score: 8" + SHY + "7.3%", "87.3", "%", None, False, "PASS"),
 ("T3 end to end: a fraction slash", "ratio 5\N{FRACTION SLASH}10 here",
  "ratio 5\N{FRACTION SLASH}10", "5", None, None, False, "FAIL"),
 ("T5 end to end: per cent declared", "rose 5 per cent in May",
  "rose 5 per cent", "5", "per cent", None, False, "PASS"),
 ("refused without a page: a bad scale", None, "q", "v", None, "lots",
  False, "ERROR:scale"),
]
QCV = QCV + QCS
for k, row in enumerate(QCV, 1):
    if len(row) == 7:
        nm, pg, q, v, aff, acc, exp = row
        sc = None
    else:
        nm, pg, q, v, aff, sc, acc, exp = row
    add("QC%02d %s" % (k, nm), "qcheck",
        {"page": pg, "quote": q, "value": v, "affix": aff, "scale": sc,
         "accounting": acc},
        exp if exp.startswith("ERROR") else "quote|" + exp)


# --------------------------------------------------------- source match
SL = [["EXACT", "https://data.example.org/bench.csv"],
      ["PREFIX", "https://example.org/org/"],
      ["PREFIX", "https://docs.example.com/models"],
      ["EXACT", "https://api.example.net:8443/v1/list?format=csv"]]


def sm(name, cited, hand, sources=SL):
    add(name, "source", {"cited": cited, "sources": sources}, hand)


def ent(i, kind, declared, fetch=None):
    return lines("source|ENTRY|%d|%s|%s" % (i, kind, declared),
                 "fetch|" + (declared if fetch is None else fetch))


E0 = ent(0, "EXACT", "https://data.example.org/bench.csv")
sm("SM01 an exact entry", "https://data.example.org/bench.csv", E0)
sm("SM02 an exact entry after RFC 3986 syntax steps",
   "HTTPS://Data.Example.ORG/bench%2ecsv", E0)
sm("SM03 an unnamed query and the fragment dropped",
   "https://data.example.org/bench.csv?utm=x#top", E0)
sm("SM04 a path under a prefix", "https://example.org/org/team/page.html",
   ent(1, "PREFIX", "https://example.org/org/",
       "https://example.org/org/team/page.html"))
sm("SM05 /org2/ against the prefix /org/", "https://example.org/org2/x",
   "source|UNKNOWN|none")
sm("SM06 a dot segment in the cited path", "https://example.org/org/../secret",
   "source|UNKNOWN|path")
sm("SM07 an encoded dot segment in the cited path",
   "https://example.org/org/%2E%2E/secret", "source|UNKNOWN|path")
sm("SM08 an encoded slash refused", "https://example.org/org/..%2Fsecret",
   "source|UNKNOWN|path")
sm("SM09 the prefix itself (no trailing slash declared)",
   "https://docs.example.com/models",
   ent(2, "PREFIX", "https://docs.example.com/models"))
sm("SM10 a path under a prefix at a segment boundary",
   "https://docs.example.com/models/a/b",
   ent(2, "PREFIX", "https://docs.example.com/models",
       "https://docs.example.com/models/a/b"))
sm("SM11 /models does not cover /modelsx", "https://docs.example.com/modelsx",
   "source|UNKNOWN|none")
sm("SM12 another host", "https://evil.example.org/org/x",
   "source|UNKNOWN|none")
sm("SM13 a declared host as a label of another", 
   "https://example.org.evil.com/org/x", "source|UNKNOWN|none")
sm("SM14 another port", "https://example.org:444/org/x",
   "source|UNKNOWN|none")
sm("SM15 an explicit :443 against no declared port",
   "https://example.org:443/org/x", "source|UNKNOWN|none")
sm("SM16 the declared port and query",
   "https://api.example.net:8443/v1/list?format=csv",
   ent(3, "EXACT", "https://api.example.net:8443/v1/list?format=csv"))
sm("SM17 another query than the entry names",
   "https://api.example.net:8443/v1/list?format=json", "source|UNKNOWN|none")
sm("SM18 no query where the entry names one",
   "https://api.example.net:8443/v1/list", "source|UNKNOWN|none")
sm("SM19 the declared port missing",
   "https://api.example.net/v1/list?format=csv", "source|UNKNOWN|none")
sm("SM20 userinfo refused", "https://user@example.org/org/x",
   "source|UNKNOWN|userinfo")
sm("SM21 http refused", "http://example.org/org/x", "source|UNKNOWN|scheme")
sm("SM22 an IP literal refused", "https://[::1]/org/x", "source|UNKNOWN|host")
sm("SM23 a dotted IPv4 host refused", "https://127.0.0.1/org/x",
   "source|UNKNOWN|host")
sm("SM24 a space refused", "https://exa mple.org/", "source|UNKNOWN|url")
sm("SM25 a citation that is not a string", 5, "source|UNKNOWN|url")
sm("SM26 no authority", "https:/org/x", "source|UNKNOWN|url")
sm("SM27 a query and fragment under a prefix dropped",
   "https://example.org/org/a?b=1#c",
   ent(1, "PREFIX", "https://example.org/org/", "https://example.org/org/a"))
LP = [["PREFIX", "https://example.org/pub/"],
      ["PREFIX", "https://example.org/pub/org/"]]
sm("SM28 the longest prefix wins", "https://example.org/pub/org/x",
   ent(1, "PREFIX", "https://example.org/pub/org/",
       "https://example.org/pub/org/x"), LP)
sm("SM29 the shorter prefix elsewhere", "https://example.org/pub/other",
   ent(0, "PREFIX", "https://example.org/pub/",
       "https://example.org/pub/other"), LP)
sm("SM30 an exact entry beats a prefix", "https://example.org/org/page",
   ent(1, "EXACT", "https://example.org/org/page"),
   [["PREFIX", "https://example.org/org/"],
    ["EXACT", "https://example.org/org/page"]])
sm("SM31 an empty path is /", "https://example.com/",
   ent(0, "EXACT", "https://example.com/"), [["EXACT", "https://example.com"]])
sm("SM32 a trailing dot in the host", "https://example.org./org/x",
   "source|UNKNOWN|none")
sm("SM33 an empty list", "https://example.org/org/x", "source|UNKNOWN|none",
   [])
sm("SM34 a prefix with a trailing slash does not cover the bare path",
   "https://example.org/org", "source|UNKNOWN|none")
sm("SM35 unreserved characters decoded under a prefix",
   "https://example.org/org/%7Euser",
   ent(1, "PREFIX", "https://example.org/org/", "https://example.org/org/~user"))
for nm, bad_list in [
        ("SM36 a declared http entry refused", [["EXACT", "http://a.org/"]]),
        ("SM37 a declared entry with userinfo refused",
         [["EXACT", "https://u@a.org/"]]),
        ("SM38 a declared prefix with a query refused",
         [["PREFIX", "https://a.org/p?q=1"]]),
        ("SM39 a declared entry with a fragment refused",
         [["EXACT", "https://a.org/p#f"]]),
        ("SM40 a declared IP host refused", [["EXACT", "https://10.0.0.1/"]]),
        ("SM41 an unknown entry kind refused", [["LIKE", "https://a.org/"]]),
        ("SM42 an entry that is not a pair refused", ["https://a.org/"]),
        ("SM43 a declared empty port refused", [["EXACT", "https://a.org:/"]])]:
    sm(nm, "https://a.org/", "ERROR:sources", bad_list)


# SM2's path rules on the citation (second verification round).
E1 = "https://example.org/org/"
sm("SM44 ..; (a dot segment with a parameter)",
   "https://example.org/org/..;/secret", "source|UNKNOWN|path")
sm("SM45 %252E%252E (double-encoded dot segment)",
   "https://example.org/org/%252E%252E/secret", "source|UNKNOWN|path")
sm("SM46 a ; parameter", "https://example.org/org/page;jsessionid=1",
   "source|UNKNOWN|path")
sm("SM47 an encoded ; parameter", "https://example.org/org/a%3Bb",
   "source|UNKNOWN|path")
sm("SM48 an encoded backslash", "https://example.org/org/%5Csecret",
   "source|UNKNOWN|path")
sm("SM49 a double-encoded slash", "https://example.org/org/..%252Fsecret",
   "source|UNKNOWN|path")
sm("SM50 a single dot segment", "https://example.org/org/./x",
   "source|UNKNOWN|path")
sm("SM51 seven decodings settle",
   "https://example.org/org/%" + "25" * 6 + "41",
   ent(1, "PREFIX", E1, "https://example.org/org/%" + "25" * 6 + "41"))
sm("SM52 eight decodings do not settle",
   "https://example.org/org/%" + "25" * 7 + "41", "source|UNKNOWN|path")
sm("SM53 an encoded non-ASCII byte is harmless",
   "https://example.org/org/caf%C3%A9",
   ent(1, "PREFIX", E1, "https://example.org/org/caf%C3%A9"))
sm("SM54 three dots are no dot segment", "https://example.org/org/.../x",
   ent(1, "PREFIX", E1, "https://example.org/org/.../x"))
sm("SM55 a dot inside a segment", "https://example.org/org/v1.2/x",
   ent(1, "PREFIX", E1, "https://example.org/org/v1.2/x"))
sm("SM56 dots in the query are not the path",
   "https://example.org/org/x?p=../y",
   ent(1, "PREFIX", E1, "https://example.org/org/x"))
sm("SM57 the fetched URL equals the citation under a prefix",
   "https://example.org/org/a/b.html",
   ent(1, "PREFIX", E1, "https://example.org/org/a/b.html"))
# SM1, the PUBLISHER TEST, at the list's validation.
GH = ["CODE HOST", "github.com", "/*/*/tree/*/", "/*/*/blob/*/",
      "/*/*/commit/*/"]
BR = "https://github.com/org/repo/tree/main/"
LG = [GH, ["PREFIX", BR], ["EXACT", "https://data.example.org/bench.csv"],
      ["PREFIX", E1]]
sm("SM58 a PREFIX at a host root refused", "https://example.org/x",
   "ERROR:publisher", [["PREFIX", "https://example.org/"]])
sm("SM59 a PREFIX at a bare host refused", "https://example.org/x",
   "ERROR:publisher", [["PREFIX", "https://example.org"]])
sm("SM60 an EXACT entry at a host root is no PREFIX", "https://example.org",
   ent(0, "EXACT", "https://example.org/"),
   [["EXACT", "https://example.org/"]])
sm("SM61 a PREFIX at a named branch on a CODE HOST", BR + "data/r.csv",
   ent(1, "PREFIX", BR, BR + "data/r.csv"), LG)
sm("SM62 the repository's issues are outside the branch",
   "https://github.com/org/repo/issues/1", "source|UNKNOWN|none", LG)
sm("SM63 the branch prefix itself", BR, ent(1, "PREFIX", BR), LG)
sm("SM64 an organisation root on a CODE HOST refused", BR, "ERROR:publisher",
   [GH, ["PREFIX", "https://github.com/org/"]])
sm("SM65 a repository root on a CODE HOST refused", BR, "ERROR:publisher",
   [GH, ["PREFIX", "https://github.com/org/repo"]])
sm("SM66 tree without a branch refused", BR, "ERROR:publisher",
   [GH, ["PREFIX", "https://github.com/org/repo/tree/"]])
sm("SM67 a template segment is case-sensitive", BR, "ERROR:publisher",
   [GH, ["PREFIX", "https://github.com/org/repo/Tree/main/"]])
sm("SM68 a pinned commit on a CODE HOST",
   "https://github.com/org/repo/blob/0123abcd/data/r.csv",
   ent(1, "PREFIX", "https://github.com/org/repo/blob/0123abcd/",
       "https://github.com/org/repo/blob/0123abcd/data/r.csv"),
   [GH, ["PREFIX", "https://github.com/org/repo/blob/0123abcd/"]])
sm("SM69 a path below a named branch", BR + "docs/x",
   ent(1, "PREFIX", BR + "docs/", BR + "docs/x"),
   [GH, ["PREFIX", BR + "docs/"]])
sm("SM70 a CODE HOST without templates refuses every PREFIX", BR,
   "ERROR:publisher", [["CODE HOST", "github.com"], ["PREFIX", BR]])
sm("SM71 an EXACT entry on a CODE HOST needs no template",
   "https://github.com/org/repo/issues/1",
   ent(1, "EXACT", "https://github.com/org/repo/issues/1"),
   [["CODE HOST", "github.com"],
    ["EXACT", "https://github.com/org/repo/issues/1"]])
sm("SM72 a CODE HOST mark leaves other hosts to INSPECTION",
   "https://example.org/org/x",
   ent(1, "PREFIX", E1, "https://example.org/org/x"), [GH, ["PREFIX", E1]])
sm("SM73 a CODE HOST mark is itself no entry", "https://github.com/x",
   "source|UNKNOWN|none", [GH])
for nm, bad_list in [
        ("SM74 a CODE HOST named twice refused", [GH, GH]),
        ("SM75 a CODE HOST template without a final / refused",
         [["CODE HOST", "github.com", "/*/*/tree/*"]]),
        ("SM76 a CODE HOST in upper case refused",
         [["CODE HOST", "GitHub.com", "/*/*/tree/*/"]]),
        ("SM77 a CODE HOST given as an IP refused",
         [["CODE HOST", "10.0.0.1", "/*/"]]),
        ("SM78 a declared prefix holding ..; refused",
         [["PREFIX", "https://example.org/a/..;/"]]),
        ("SM79 a declared prefix holding an encoded dot segment refused",
         [["PREFIX", "https://example.org/a/%2E%2E/b/"]]),
        ("SM80 a declared entry holding ; refused",
         [["EXACT", "https://example.org/a;b"]])]:
    sm(nm, "https://a.org/", "ERROR:sources", bad_list)

# Pass 3: N2 (the branch slot "refs" refused on a CODE HOST) and N4 (a
# decoded path that is not valid UTF-8 refused).
RAW = ["CODE HOST", "raw.example.com", "/*/*/*/"]
sm("SM81 a branch slot of refs refused (pull-request refs)", BR,
   "ERROR:publisher",
   [GH, ["PREFIX", "https://github.com/org/repo/tree/refs/pull/1/"]])
sm("SM82 a branch slot of REFS refused (any case)", BR, "ERROR:publisher",
   [GH, ["PREFIX", "https://github.com/org/repo/blob/REFS/"]])
sm("SM83 a branch slot of refs written %72efs refused", BR,
   "ERROR:publisher",
   [GH, ["PREFIX", "https://github.com/org/repo/tree/%72efs/heads/main/"]])
sm("SM84 a commit slot of refs refused", BR, "ERROR:publisher",
   [GH, ["PREFIX", "https://github.com/org/repo/commit/Refs/"]])
sm("SM85 refs in the branch slot of a raw host refused",
   "https://raw.example.com/o/r/refs/pull/1/head/x", "ERROR:publisher",
   [RAW, ["PREFIX", "https://raw.example.com/o/r/refs/"]])
sm("SM86 a branch named refsx is a branch",
   "https://github.com/org/repo/tree/refsx/a",
   ent(1, "PREFIX", "https://github.com/org/repo/tree/refsx/",
       "https://github.com/org/repo/tree/refsx/a"),
   [GH, ["PREFIX", "https://github.com/org/repo/tree/refsx/"]])
sm("SM87 a folder named refs below a named branch",
   BR + "refs/a", ent(1, "PREFIX", BR + "refs/", BR + "refs/a"),
   [GH, ["PREFIX", BR + "refs/"]])
sm("SM88 an owner named refs (not the branch slot)",
   "https://github.com/refs/repo/tree/main/a",
   ent(1, "PREFIX", "https://github.com/refs/repo/tree/main/",
       "https://github.com/refs/repo/tree/main/a"),
   [GH, ["PREFIX", "https://github.com/refs/repo/tree/main/"]])
sm("SM89 %C0%AE%C0%AE (overlong dots) refused",
   "https://example.org/org/%C0%AE%C0%AE/secret", "source|UNKNOWN|path")
sm("SM90 an encoded surrogate refused", "https://example.org/org/%ED%A0%80",
   "source|UNKNOWN|path")
sm("SM91 a truncated sequence refused", "https://example.org/org/%E2%82x",
   "source|UNKNOWN|path")
sm("SM92 a code point above U+10FFFF refused",
   "https://example.org/org/%F4%90%80%80", "source|UNKNOWN|path")
sm("SM93 a lone continuation byte refused", "https://example.org/org/%80",
   "source|UNKNOWN|path")
sm("SM94 a double-encoded overlong form refused",
   "https://example.org/org/%25C0%25AE%25C0%25AE/x", "source|UNKNOWN|path")
sm("SM95 a valid four-byte sequence is harmless",
   "https://example.org/org/%F0%9F%98%80",
   ent(1, "PREFIX", E1, "https://example.org/org/%F0%9F%98%80"))
sm("SM96 a declared entry holding an overlong form refused",
   "https://a.org/", "ERROR:sources",
   [["PREFIX", "https://example.org/a/%C0%AE/"]])

# SM10 (0.1.1): SERVED entries, one per registry record TPX fetched under a
# REGISTRY DECLARATION and served in the packet (D172 (c), RD-17).
CR = "https://api.crossref.org/v1/works/10.1016/s0140-6736(97)11096-0"
AX = "https://export.arxiv.org/api/query?id_list=2601.12345v2"
SV = [["SERVED", CR], ["SERVED", AX], ["PREFIX", "https://api.crossref.org/v1/"],
      ["EXACT", "https://data.example.org/bench.csv"]]
sm("SM97 a citation equal to a SERVED registry record: read from the store",
   CR, lines("source|ENTRY|0|SERVED|" + CR, "fetch|STORE"), SV)
sm("SM98 SERVED wins over the PREFIX that also covers the citation",
   CR, lines("source|ENTRY|1|SERVED|" + CR, "fetch|STORE"),
   [["PREFIX", "https://api.crossref.org/v1/"], ["SERVED", CR]])
sm("SM99 an EXACT entry wins over SERVED",
   CR, lines("source|ENTRY|1|EXACT|" + CR, "fetch|" + CR),
   [["SERVED", CR], ["EXACT", CR]])
sm("SM100 a SERVED entry with a query matches the same query only",
   AX, lines("source|ENTRY|1|SERVED|" + AX, "fetch|STORE"), SV)
sm("SM101 another query does not match the SERVED entry (and no prefix covers it)",
   "https://export.arxiv.org/api/query?id_list=2601.12345v3", "source|UNKNOWN|none", SV)
sm("SM102 a query on a citation against a SERVED entry without one",
   CR + "?x=1", "source|UNKNOWN|none", [["SERVED", CR]])
sm("SM103 the fragment is dropped (SM7)", CR + "#top",
   lines("source|ENTRY|0|SERVED|" + CR, "fetch|STORE"), SV)
sm("SM104 the citation after RFC 3986 syntax steps (host case, an unreserved character decoded) equals the SERVED entry",
   "HTTPS://API.CROSSREF.ORG/v1/works/10.1016/s0140%2D6736(97)11096-0",
   lines("source|ENTRY|0|SERVED|" + CR, "fetch|STORE"), SV)
sm("SM104b a percent-encoded reserved character is not decoded, so the citation differs from the SERVED entry",
   "https://api.crossref.org/v1/works/10.1016/s0140-6736%2897%2911096-0",
   lines("source|ENTRY|2|PREFIX|https://api.crossref.org/v1/",
         "fetch|https://api.crossref.org/v1/works/10.1016/s0140-6736%2897%2911096-0"), SV)
sm("SM105 a SERVED entry with the registry's encoded semicolon (RD-15) is matched exactly; SM2's path rules do not apply to a store read",
   "https://api.crossref.org/v1/works/10.1002/(SICI)1097-0266(199708)18:7%3C509::AID-SMJ882%3E3.0.CO%3B2-Z",
   lines("source|ENTRY|0|SERVED|https://api.crossref.org/v1/works/10.1002/(SICI)1097-0266(199708)18:7%3C509::AID-SMJ882%3E3.0.CO%3B2-Z",
         "fetch|STORE"),
   [["SERVED", "https://api.crossref.org/v1/works/10.1002/(SICI)1097-0266(199708)18:7%3C509::AID-SMJ882%3E3.0.CO%3B2-Z"]])
sm("SM106 a citation under a SERVED URL is not a prefix match",
   CR + "/extra", "source|UNKNOWN|none", [["SERVED", CR]])
sm("SM106b a citation whose path SM2 refuses that equals no SERVED entry is UNKNOWN path",
   "https://api.crossref.org/v1/works/10.1000/a%3Bb", "source|UNKNOWN|path", SV)
sm("SM106c the same citation equal to a SERVED entry is read from the store",
   "https://api.crossref.org/v1/works/10.1000/a%3Bb",
   lines("source|ENTRY|0|SERVED|https://api.crossref.org/v1/works/10.1000/a%3Bb", "fetch|STORE"),
   [["SERVED", "https://api.crossref.org/v1/works/10.1000/a%3Bb"], ["PREFIX", "https://api.crossref.org/v1/"]])
sm("SM106d an EXACT entry holding an encoded semicolon is still refused (it would be fetched)",
   CR, "ERROR:sources", [["EXACT", "https://api.crossref.org/v1/works/10.1000/a%3Bb"]])
sm("SM107 a SERVED entry over http refused", CR, "ERROR:sources",
   [["SERVED", "http://api.crossref.org/v1/works/x"]])
sm("SM108 a SERVED entry with a fragment refused", CR, "ERROR:sources", [["SERVED", CR + "#x"]])
sm("SM109 a SERVED entry with three elements refused", CR, "ERROR:sources", [["SERVED", CR, CR]])
sm("SM110 a SERVED entry at a host root passes (no PUBLISHER TEST: nothing is fetched)",
   "https://doi.org/", lines("source|ENTRY|0|SERVED|https://doi.org/", "fetch|STORE"),
   [["SERVED", "https://doi.org/"]])
sm("SM111 a SERVED entry with an IP literal refused", CR, "ERROR:sources",
   [["SERVED", "https://[::1]/v1/works/x"]])
sm("SM112 a SERVED entry on a CODE HOST needs no template",
   "https://github.com/org/repo/raw/main/x", lines("source|ENTRY|1|SERVED|https://github.com/org/repo/raw/main/x", "fetch|STORE"),
   [GH, ["SERVED", "https://github.com/org/repo/raw/main/x"]])


# ------------------------------------------------------------ source hop
# SM9: every redirect hop matched again at entry level.
def hp(name, hop, matched, hand, sources=LG):
    add(name, "hop", {"hop": hop, "matched": matched, "sources": sources},
        hand)


hp("HP01 a hop inside the matched branch", BR + "data.csv", 1,
   lines(ent(1, "PREFIX", BR, BR + "data.csv"), "hop|SAME"))
hp("HP02 a hop to the same host outside the entry",
   "https://github.com/org/repo/issues/9", 1,
   lines("source|UNKNOWN|none", "hop|END"))
hp("HP03 a hop into another declared entry",
   "https://data.example.org/bench.csv", 1,
   lines(ent(2, "EXACT", "https://data.example.org/bench.csv"), "hop|OTHER"))
hp("HP04 a hop to an undeclared host", "https://cdn.example.net/r.csv", 1,
   lines("source|UNKNOWN|none", "hop|END"))
hp("HP05 a hop down to http", "http://github.com/org/repo/tree/main/x", 1,
   lines("source|UNKNOWN|scheme", "hop|END"))
hp("HP06 a hop through a double-encoded dot segment",
   "https://example.org/org/%252E%252E/x", 3,
   lines("source|UNKNOWN|path", "hop|END"))
hp("HP07 a hop back to the exact entry itself",
   "https://data.example.org/bench.csv", 2,
   lines(ent(2, "EXACT", "https://data.example.org/bench.csv"), "hop|SAME"))
hp("HP08 a hop with a query the exact entry does not name",
   "https://data.example.org/bench.csv?x=1", 2,
   lines(ent(2, "EXACT", "https://data.example.org/bench.csv"), "hop|SAME"))
hp("HP09 a hop that is not a string", 5, 1,
   lines("source|UNKNOWN|url", "hop|END"))
hp("HP10 matched naming the CODE HOST mark refused", BR, 0, "ERROR:matched")
hp("HP11 matched out of range refused", BR, 4, "ERROR:matched")
hp("HP12 matched given as a string refused", BR, "1", "ERROR:matched")
hp("HP13 a list failing the PUBLISHER TEST refused", BR, 0,
   "ERROR:publisher", [["PREFIX", "https://example.org/"]])
hp("HP14 a hop through an overlong form", "https://example.org/org/%C0%AE/x",
   3, lines("source|UNKNOWN|path", "hop|END"))
# SM9 at 0.1.1: a SERVED entry is read from the store, never fetched
LS = [["SERVED", CR], ["PREFIX", BR], ["EXACT", "https://data.example.org/bench.csv"],
      ["PREFIX", "https://api.crossref.org/v1/"]]
hp("HP15 matched naming a SERVED entry refused: no fetch leaves the store", BR, 0, "ERROR:matched", LS)
hp("HP16 a hop onto a SERVED URL is no candidate; the PREFIX that covers it matches", CR, 1,
   lines("source|ENTRY|3|PREFIX|https://api.crossref.org/v1/|" if False else
         "source|ENTRY|3|PREFIX|https://api.crossref.org/v1/", "fetch|" + CR, "hop|OTHER"), LS)
hp("HP17 a hop onto a SERVED URL covered by nothing else ends the fetch", CR, 1,
   lines("source|UNKNOWN|none", "hop|END"), [["SERVED", CR], ["PREFIX", BR]])
hp("HP18 a hop inside the matched branch with SERVED entries in the list", BR + "data.csv", 1,
   lines(ent(1, "PREFIX", BR, BR + "data.csv"), "hop|SAME"), LS)
hp("HP19 a list with a bad SERVED entry refused", BR, 1, "ERROR:sources", [["SERVED", "http://x.org/a"], ["PREFIX", BR]])


# ------------------------------------------------------------ kind stop
LIM = {"breaker": "0.3", "window": 20}


def iss(i):
    return ["ISSUED", i]


def fin(i, items, dec, unr=0):
    return ["FINISHED", i, items, dec, unr]


RES = ["RESUMED"]


def sp(name, counts, hand, limits=LIM):
    add(name, "stop", {"counts": counts, "limits": limits}, hand)


def srow(status, bad, total, n, alone="-", flying=0):
    return lines("stop|" + status, "share|%d|%d|%d" % (bad, total, n),
                 "alone|" + alone, "in_flight|%d" % flying)


STOP1 = [iss("f1"), fin("f1", 10, 4)]
sp("SP01 no falcon yet", [], srow("ISSUING", 0, 0, 0))
sp("SP02 a share of exactly 0.3 does not pass", [iss("f1"), fin("f1", 10, 2, 1)],
   srow("ISSUING", 3, 10, 1))
sp("SP03 a share above 0.3 stops", STOP1, srow("STOPPED", 4, 10, 1))
sp("SP04 a share below", [iss("f1"), fin("f1", 10, 1, 1)],
   srow("ISSUING", 2, 10, 1))
W3 = [iss("f1"), fin("f1", 10, 2), iss("f2"), fin("f2", 10, 2), iss("f3"),
      fin("f3", 10, 5)]
sp("SP05 the window: the last 2 falcons pass the share", W3,
   srow("STOPPED", 7, 20, 2), {"breaker": "0.3", "window": 2})
sp("SP06 the window: the last 3 falcons do not", W3,
   srow("ISSUING", 9, 30, 3), {"breaker": "0.3", "window": 3})
sp("SP07 falcons in flight at the stop finish; the kind stays stopped",
   [iss("f1"), iss("f2"), fin("f1", 10, 4), fin("f2", 10, 0)],
   srow("STOPPED", 4, 20, 2))
sp("SP08 an ISSUED while stopped is refused", STOP1 + [iss("f2")],
   "ERROR:counts")
sp("SP09 resumed with nothing in flight: one falcon may issue alone",
   STOP1 + [RES], srow("ISSUING", 0, 0, 0, "waiting"))
OLD = [iss("f1"), iss("f2"), fin("f1", 10, 4), RES]
sp("SP10 resumed with an older falcon in flight: the one waits for it", OLD,
   srow("STOPPED", 0, 0, 0, "waiting", 1))
sp("SP11 the older falcon finishes after the resumption and does not count",
   OLD + [fin("f2", 10, 10)], srow("ISSUING", 0, 0, 0, "waiting"))
sp("SP12 the one falcon issues alone", STOP1 + [RES, iss("f3")],
   srow("STOPPED", 0, 0, 0, "f3", 1))
sp("SP13 the one falcon finishes below the share: issuing again",
   STOP1 + [RES, iss("f3"), fin("f3", 10, 0)], srow("ISSUING", 0, 10, 1))
sp("SP14 the one falcon passes the share: the kind stops again",
   STOP1 + [RES, iss("f3"), fin("f3", 10, 4)], srow("STOPPED", 4, 10, 1))
sp("SP15 a second ISSUED beside the one falcon is refused",
   STOP1 + [RES, iss("f3"), iss("f4")], "ERROR:counts")
sp("SP16 RESUMED while issuing changes nothing",
   [iss("f1"), fin("f1", 10, 0), RES], srow("ISSUING", 0, 10, 1))
sp("SP17 a second RESUMED while the one falcon waits changes nothing",
   STOP1 + [RES, RES], srow("ISSUING", 0, 0, 0, "waiting"))
sp("SP18 FINISHED for a falcon never issued is refused", [fin("f1", 1, 0)],
   "ERROR:counts")
sp("SP19 an id issued twice is refused",
   [iss("f1"), fin("f1", 10, 0), iss("f1")], "ERROR:counts")
sp("SP20 FALCON BREAKER 1 never stops", [iss("f1"), fin("f1", 10, 10)],
   srow("ISSUING", 10, 10, 1), {"breaker": "1", "window": 20})
sp("SP21 FALCON BREAKER 0 stops on any item", [iss("f1"), fin("f1", 10, 1)],
   srow("STOPPED", 1, 10, 1), {"breaker": "0", "window": 20})
sp("SP22 a falcon closed without items: no share",
   [iss("f1"), fin("f1", 0, 0)], srow("ISSUING", 0, 0, 1))
sp("SP23 declined plus unresolved above items refused",
   [iss("f1"), fin("f1", 2, 2, 1)], "ERROR:counts")
TW = []
for k in range(1, 21):
    TW += [iss("g%d" % k), fin("g%d" % k, 10, 1)]
TW += [iss("g21"), fin("g21", 10, 10)]
sp("SP24 twenty-one falcons: only the last 20 count", TW,
   srow("ISSUING", 29, 200, 20))
sp("SP25 FALCON BREAKER 0.25 at exactly a quarter",
   [iss("f1"), fin("f1", 4, 1)], srow("ISSUING", 1, 4, 1),
   {"breaker": "0.25", "window": 20})
sp("SP26 FALCON BREAKER 0.25 above a quarter",
   [iss("f1"), fin("f1", 4, 2)], srow("STOPPED", 2, 4, 1),
   {"breaker": "0.25", "window": 20})
for nm, lim in [("SP27 five decimal places refused", {"breaker": "0.30001",
                                                       "window": 20}),
                ("SP28 a breaker above 1 refused", {"breaker": "1.5",
                                                     "window": 20}),
                ("SP29 a breaker given as a number refused",
                 {"breaker": 0.3, "window": 20}),
                ("SP30 a window of 0 refused", {"breaker": "0.3",
                                                 "window": 0}),
                ("SP31 limits without a window refused", {"breaker": "0.3"})]:
    sp(nm, [], "ERROR:limits", lim)
sp("SP32 an unknown record kind refused", [["STOPPED"]], "ERROR:counts")

# ------------------------------------------ the output-schema digest (C7)
# FS1-FS5 (agent RDC; S79): falcon_schema_canon(schema) and
# falcon_schema_digest(schema), rendered "canon|<text>", "digest|<hex>" or
# "ERROR:schema". Expectations are literals: RDC's (V1-V7, the eighteen
# kinds) and, for the FSX and FSR cases, canonical texts written here by
# hand from FS2 and FS3. The digest of each is taken below over the D97
# netstring built by hand, never by tpc_falcon.
# Agent RDC's vectors V1-V7 (tpm_falcon_schema_digest_vectors 0.1.0), transcribed
# literally: (name, input, canonical text or None for a refusal, digest, preimage
# length). RDC hand-derived each digest: the preimage written and hashed apart
# from tpc_digest.
FS_RDC = [
 ('V1 minimal',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'name': 'A', 'type': 'text'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","name":"A","type":"text"}]}',
  '4f74820e227f3884d8f6c1c5095488810c2ce3833df2b1da12e2ba9e3f410d5a', 105),
 ('V2 same meaning as V1',
  {'class': 'FINDINGS', 'confidential': False, 'fields': [{'class': 'MECHANICAL', 'name': 'A', 'quote': False, 'type': 'text'}], 'narrow': True},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","name":"A","type":"text"}]}',
  '4f74820e227f3884d8f6c1c5095488810c2ce3833df2b1da12e2ba9e3f410d5a', 105),
 ('V3 enum, quote, affix, confidential',
  {'class': 'SURVEY', 'confidential': True, 'fields': [{'affix': '%', 'class': 'MECHANICAL', 'name': 'score', 'quote': True, 'type': 'decimal'}, {'class': 'INSPECTION', 'name': 'state', 'type': 'enum', 'values': ['B', 'A']}], 'narrow': False},
  '{"class":"SURVEY","confidential":true,"fields":[{"affix":"%","class":"MECHANICAL","name":"score","quote":true,"type":"decimal"},{"class":"INSPECTION","name":"state","type":"enum","values":["B","A"]}]}',
  '0064adccf8048bdd4c254c0c374ba7be5c08c5eb625a81c703aa955e19bd5a64', 227),
 ('V4 rows',
  {'class': 'PATCH', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'k', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'v', 'type': 'text'}], 'key': ['k'], 'name': 'R', 'type': 'rows'}], 'narrow': False},
  '{"class":"PATCH","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"k","type":"text"},{"class":"INSPECTION","name":"v","type":"text"}],"key":["k"],"name":"R","type":"rows"}]}',
  'dc3232347cf35e2ba87c241b749263d243179a07ce83404ef54fa5ad6c946208', 223),
 ('V5 NFC and escapes: refused under FS1 0.2.0, its first value not in NFC (D213.1)',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'name': 'A', 'type': 'enum', 'values': ['e\u0301', 'tab\there', 'q"b\\s']}], 'narrow': False},
  None,
  None, None),
 ('V6 unknown key refused',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'extra': True, 'name': 'A', 'type': 'text'}], 'narrow': False},
  None,
  None, None),
 ('V7 IV as a field name refused',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'name': 'IV', 'type': 'text'}], 'narrow': False},
  None,
  None, None),
 # FS specification 0.2.0 (agent TR; D213.1): V5b and V8-V11
 ('V5b V5 with U+00E9 precomposed: accepted, V5\'s old canonical text and digest (FS 0.2.0)',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'name': 'A', 'type': 'enum', 'values': ['\u00e9', 'tab\there', 'q"b\\s']}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","name":"A","type":"enum","values":["\xe9","tab\\u0009here","q\\"b\\\\s"]}]}',
  '51c9421a2453950a5c26ef1d0b0c2057c664422c03197a2634fbd2320cc29b97', 148),
 ('V8 an enumeration value holding a lone CR: refused (FS 0.2.0)',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'name': 'A', 'type': 'enum', 'values': ['a\rb']}], 'narrow': False},
  None,
  None, None),
 ('V9 an enumeration value holding CRLF: refused (FS 0.2.0)',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'name': 'A', 'type': 'enum', 'values': ['a\r\nb']}], 'narrow': False},
  None,
  None, None),
 ('V10 an enumeration value in NFD (e + U+0301): refused (FS 0.2.0)',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'name': 'A', 'type': 'enum', 'values': ['e\u0301']}], 'narrow': False},
  None,
  None, None),
 ('V11 a QUOTE AFFIX not in NFC (U+00A4 + U+212B ANGSTROM SIGN): refused (FS 0.2.0)',
  {'class': 'SURVEY', 'fields': [{'affix': '\u00a4\u212b', 'class': 'MECHANICAL', 'name': 's', 'quote': True, 'type': 'decimal'}], 'narrow': False},
  None,
  None, None),
]
# The eighteen FALCON KIND schemas of tpm_decl_fk_schemas 0.1.0 (agent RDC), with
# the canonical text and the digest RDC computed for each, transcribed literally.
FS_KINDS = [
 ('FK-01',
  {'class': 'PATCH', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'old', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'site_class', 'type': 'enum', 'values': ['VERSION', 'STATUS']}, {'class': 'MECHANICAL', 'name': 'new', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'reason', 'type': 'text'}], 'key': ['page', 'old'], 'name': 'EXACT_SITES', 'type': 'rows'}, {'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'old', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'new', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'reason', 'type': 'text'}], 'key': ['page', 'old'], 'name': 'PROSE_SITES', 'type': 'rows'}], 'narrow': False},
  '{"class":"PATCH","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"old","type":"text"},{"class":"MECHANICAL","name":"site_class","type":"enum","values":["VERSION","STATUS"]},{"class":"MECHANICAL","name":"new","type":"text"},{"class":"INSPECTION","name":"reason","type":"text"}],"key":["page","old"],"name":"EXACT_SITES","type":"rows"},{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"old","type":"text"},{"class":"INSPECTION","name":"new","type":"text"},{"class":"INSPECTION","name":"reason","type":"text"}],"key":["page","old"],"name":"PROSE_SITES","type":"rows"}]}',
  '792e8d2222226e6594421ed2df33e7deabc288eb5cc35eafcd0ca9407d61284b'),
 ('FK-02',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'section', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'action', 'type': 'enum', 'values': ['NONE', 'STATUS', 'REWRITE PASSAGE', 'NEW PAGE']}, {'class': 'INSPECTION', 'name': 'anchor', 'type': 'text'}], 'key': ['page', 'section', 'action'], 'name': 'IMPACTS', 'type': 'rows'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"section","type":"text"},{"class":"MECHANICAL","name":"action","type":"enum","values":["NONE","STATUS","REWRITE PASSAGE","NEW PAGE"]},{"class":"INSPECTION","name":"anchor","type":"text"}],"key":["page","section","action"],"name":"IMPACTS","type":"rows"}]}',
  '42b5d6206435a7d5a5dedf14f4b3a515a3b4ab6f64a21585693467a916d85a58'),
 ('FK-03',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'anchor', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'finding_class', 'type': 'enum', 'values': ['CONTRADICTS', 'RESTATES FIGURE', 'OUTDATED', 'MINIMUM MISSING']}, {'class': 'MECHANICAL', 'name': 'section', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'span', 'type': 'text'}], 'key': ['page', 'anchor', 'finding_class'], 'name': 'FINDINGS', 'type': 'rows'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"anchor","type":"text"},{"class":"MECHANICAL","name":"finding_class","type":"enum","values":["CONTRADICTS","RESTATES FIGURE","OUTDATED","MINIMUM MISSING"]},{"class":"MECHANICAL","name":"section","type":"text"},{"class":"INSPECTION","name":"span","type":"text"}],"key":["page","anchor","finding_class"],"name":"FINDINGS","type":"rows"}]}',
  '8c1cd6eca2eb99476cf828e97d039aeb4665962c653e09ffcc4ab231284edd39'),
 ('FK-04',
  {'class': 'SURVEY', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'url'}, {'class': 'MECHANICAL', 'name': 'name', 'quote': True, 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'field_place', 'type': 'enum', 'values': ['MATHEMATICS', 'PHYSICS', 'CHEMISTRY', 'BIOCHEMISTRY', 'MIXED SCIENCE', 'OTHER']}, {'class': 'MECHANICAL', 'name': 'runner', 'quote': True, 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'item_count', 'quote': True, 'type': 'decimal'}, {'affix': '%', 'class': 'MECHANICAL', 'name': 'top_score', 'quote': True, 'type': 'decimal'}, {'class': 'MECHANICAL', 'name': 'uncertainty', 'type': 'enum', 'values': ['SE', 'ITEM COUNT ONLY', 'NONE']}, {'class': 'MECHANICAL', 'name': 'per_item_per_run', 'type': 'enum', 'values': ['PUBLISHED', 'PRIVATE', 'NOT STATED']}, {'class': 'MECHANICAL', 'name': 'licence_line', 'quote': True, 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'last_update', 'quote': True, 'type': 'date'}, {'class': 'INSPECTION', 'name': 'saturation', 'type': 'enum', 'values': ['SEPARATES', 'NEAR CEILING', 'SATURATED']}, {'class': 'INSPECTION', 'name': 'assessment', 'type': 'text'}], 'key': ['page'], 'name': 'BENCHMARKS', 'type': 'rows'}, {'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'url'}, {'class': 'MECHANICAL', 'name': 'dataset_url', 'type': 'url'}, {'class': 'MECHANICAL', 'name': 'format', 'type': 'enum', 'values': ['CSV', 'ZIP OF CSV', 'JSON', 'PARQUET', 'OTHER', 'NONE']}, {'class': 'MECHANICAL', 'name': 'licence_line', 'quote': True, 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'last_update', 'quote': True, 'type': 'date'}], 'key': ['page'], 'name': 'SOURCES', 'type': 'rows'}, {'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'url', 'type': 'url'}, {'class': 'INSPECTION', 'name': 'reason', 'type': 'text'}], 'key': ['url'], 'name': 'PROPOSED_SOURCES', 'type': 'rows'}, {'class': 'INSPECTION', 'name': 'ALIAS', 'type': 'text'}], 'narrow': False},
  '{"class":"SURVEY","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"url"},{"class":"MECHANICAL","name":"name","quote":true,"type":"text"},{"class":"MECHANICAL","name":"field_place","type":"enum","values":["MATHEMATICS","PHYSICS","CHEMISTRY","BIOCHEMISTRY","MIXED SCIENCE","OTHER"]},{"class":"MECHANICAL","name":"runner","quote":true,"type":"text"},{"class":"MECHANICAL","name":"item_count","quote":true,"type":"decimal"},{"affix":"%","class":"MECHANICAL","name":"top_score","quote":true,"type":"decimal"},{"class":"MECHANICAL","name":"uncertainty","type":"enum","values":["SE","ITEM COUNT ONLY","NONE"]},{"class":"MECHANICAL","name":"per_item_per_run","type":"enum","values":["PUBLISHED","PRIVATE","NOT STATED"]},{"class":"MECHANICAL","name":"licence_line","quote":true,"type":"text"},{"class":"MECHANICAL","name":"last_update","quote":true,"type":"date"},{"class":"INSPECTION","name":"saturation","type":"enum","values":["SEPARATES","NEAR CEILING","SATURATED"]},{"class":"INSPECTION","name":"assessment","type":"text"}],"key":["page"],"name":"BENCHMARKS","type":"rows"},{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"url"},{"class":"MECHANICAL","name":"dataset_url","type":"url"},{"class":"MECHANICAL","name":"format","type":"enum","values":["CSV","ZIP OF CSV","JSON","PARQUET","OTHER","NONE"]},{"class":"MECHANICAL","name":"licence_line","quote":true,"type":"text"},{"class":"MECHANICAL","name":"last_update","quote":true,"type":"date"}],"key":["page"],"name":"SOURCES","type":"rows"},{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"url","type":"url"},{"class":"INSPECTION","name":"reason","type":"text"}],"key":["url"],"name":"PROPOSED_SOURCES","type":"rows"},{"class":"INSPECTION","name":"ALIAS","type":"text"}]}',
  '1a9c3ebec4c2e60e386ed3b2db09f47c755435ff5ff27ad476bc917ad77bfaca'),
 ('FK-05',
  {'class': 'SURVEY', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'vendor', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'identifier', 'quote': True, 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'family', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'listed', 'type': 'boolean'}, {'class': 'MECHANICAL', 'name': 'owner', 'quote': True, 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'alias_of', 'quote': True, 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'deprecation_date', 'quote': True, 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'retirement_date', 'quote': True, 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'serving_provider', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'quantization', 'type': 'text'}], 'key': ['vendor', 'identifier'], 'name': 'MODELS', 'type': 'rows'}, {'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'url', 'type': 'url'}, {'class': 'INSPECTION', 'name': 'reason', 'type': 'text'}], 'key': ['url'], 'name': 'PROPOSED_SOURCES', 'type': 'rows'}, {'class': 'INSPECTION', 'name': 'ALIAS', 'type': 'text'}], 'narrow': False},
  '{"class":"SURVEY","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"vendor","type":"text"},{"class":"MECHANICAL","name":"identifier","quote":true,"type":"text"},{"class":"MECHANICAL","name":"family","type":"text"},{"class":"MECHANICAL","name":"listed","type":"boolean"},{"class":"MECHANICAL","name":"owner","quote":true,"type":"text"},{"class":"MECHANICAL","name":"alias_of","quote":true,"type":"text"},{"class":"MECHANICAL","name":"deprecation_date","quote":true,"type":"text"},{"class":"MECHANICAL","name":"retirement_date","quote":true,"type":"text"},{"class":"MECHANICAL","name":"serving_provider","type":"text"},{"class":"MECHANICAL","name":"quantization","type":"text"}],"key":["vendor","identifier"],"name":"MODELS","type":"rows"},{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"url","type":"url"},{"class":"INSPECTION","name":"reason","type":"text"}],"key":["url"],"name":"PROPOSED_SOURCES","type":"rows"},{"class":"INSPECTION","name":"ALIAS","type":"text"}]}',
  '4cee00aca0cbf8b5d9e9bf877b8c2f01fec69d22fc77b5d724b2086ea6eebf44'),
 ('FK-06',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'citation', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'verdict', 'type': 'enum', 'values': ['SUPPORTS', 'DOES NOT SUPPORT', 'UNRESOLVED']}, {'class': 'INSPECTION', 'name': 'support', 'type': 'text'}], 'key': ['page', 'citation'], 'name': 'CITATIONS', 'type': 'rows'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"citation","type":"text"},{"class":"MECHANICAL","name":"verdict","type":"enum","values":["SUPPORTS","DOES NOT SUPPORT","UNRESOLVED"]},{"class":"INSPECTION","name":"support","type":"text"}],"key":["page","citation"],"name":"CITATIONS","type":"rows"}]}',
  '78e634a74e311ac53ad5889d50ea536ef5e6bcec334f417f54aab777f664a49b'),
 ('FK-07',
  {'class': 'PATCH', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'term', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'source_section', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'entry', 'type': 'text'}], 'key': ['page', 'term'], 'name': 'ENTRIES', 'type': 'rows'}], 'narrow': False},
  '{"class":"PATCH","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"term","type":"text"},{"class":"MECHANICAL","name":"source_section","type":"text"},{"class":"INSPECTION","name":"entry","type":"text"}],"key":["page","term"],"name":"ENTRIES","type":"rows"}]}',
  '5bf528998328cd7f488ba2a8c8b3a8f72747ca820c9c51109702703f6e531fda'),
 ('FK-08',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'revision', 'type': 'decimal'}, {'class': 'MECHANICAL', 'name': 'offset', 'type': 'decimal'}, {'class': 'MECHANICAL', 'name': 'length', 'type': 'decimal'}, {'class': 'MECHANICAL', 'name': 'pattern_class', 'type': 'enum', 'values': ['INSTRUCTION', 'ROLE CHANGE', 'HIDDEN MARKUP', 'OTHER']}], 'key': ['page', 'revision', 'offset', 'length'], 'name': 'SPANS', 'type': 'rows'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"revision","type":"decimal"},{"class":"MECHANICAL","name":"offset","type":"decimal"},{"class":"MECHANICAL","name":"length","type":"decimal"},{"class":"MECHANICAL","name":"pattern_class","type":"enum","values":["INSTRUCTION","ROLE CHANGE","HIDDEN MARKUP","OTHER"]}],"key":["page","revision","offset","length"],"name":"SPANS","type":"rows"}]}',
  'aea5fef0b95601d04cc50f3fbfc23022e3f20190ff95f32a072ac2a23a9a5b9b'),
 ('FK-10',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'question', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'answer', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'passage', 'type': 'text'}], 'key': ['question'], 'name': 'ANSWERS', 'type': 'rows'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"question","type":"text"},{"class":"MECHANICAL","name":"answer","type":"text"},{"class":"INSPECTION","name":"passage","type":"text"}],"key":["question"],"name":"ANSWERS","type":"rows"}]}',
  '43c3796e90cf1cc5f6174770ca7bebb9a5352bf77a9b0bd9549d124ef2b1a320'),
 ('FK-11',
  {'class': 'PATCH', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'verdict', 'type': 'enum', 'values': ['CURRENT', 'NEEDS UPDATE']}, {'class': 'INSPECTION', 'name': 'finding', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'new_source', 'type': 'text'}], 'key': ['page'], 'name': 'DIAGRAMS', 'type': 'rows'}], 'narrow': False},
  '{"class":"PATCH","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"verdict","type":"enum","values":["CURRENT","NEEDS UPDATE"]},{"class":"INSPECTION","name":"finding","type":"text"},{"class":"INSPECTION","name":"new_source","type":"text"}],"key":["page"],"name":"DIAGRAMS","type":"rows"}]}',
  'bf13fffa040e0515c61d7a595038ecf44cd6baaca6dab965c471388facb7f1c2'),
 ('FK-12',
  {'class': 'PATCH', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'ref', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'summary', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'reader_action', 'type': 'text'}], 'key': ['ref'], 'name': 'CHANGES', 'type': 'rows'}, {'class': 'INSPECTION', 'name': 'PAGE_TEXT', 'type': 'text'}], 'narrow': False},
  '{"class":"PATCH","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"ref","type":"text"},{"class":"INSPECTION","name":"summary","type":"text"},{"class":"INSPECTION","name":"reader_action","type":"text"}],"key":["ref"],"name":"CHANGES","type":"rows"},{"class":"INSPECTION","name":"PAGE_TEXT","type":"text"}]}',
  '3666045633e12ef8aa4ab31a6444a9daa004904beddafc56f74dc06668184b26'),
 ('FK-13',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'criterion', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'location', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'severity', 'type': 'enum', 'values': ['HIGH', 'MEDIUM', 'LOW']}, {'class': 'INSPECTION', 'name': 'finding', 'type': 'text'}], 'key': ['page', 'criterion', 'location'], 'name': 'FINDINGS', 'type': 'rows'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"criterion","type":"text"},{"class":"MECHANICAL","name":"location","type":"text"},{"class":"MECHANICAL","name":"severity","type":"enum","values":["HIGH","MEDIUM","LOW"]},{"class":"INSPECTION","name":"finding","type":"text"}],"key":["page","criterion","location"],"name":"FINDINGS","type":"rows"}]}',
  'b25a4c1f8af2c08169867f9d835d9cc67fb704b3618bf6e11fc41c0c8bb0208f'),
 ('FK-14',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'step', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'verdict', 'type': 'enum', 'values': ['MATCHES', 'DIFFERS', 'MISSING']}, {'class': 'INSPECTION', 'name': 'note', 'type': 'text'}], 'key': ['step'], 'name': 'STEPS', 'type': 'rows'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"step","type":"text"},{"class":"MECHANICAL","name":"verdict","type":"enum","values":["MATCHES","DIFFERS","MISSING"]},{"class":"INSPECTION","name":"note","type":"text"}],"key":["step"],"name":"STEPS","type":"rows"}]}',
  '8c6fc55e4597c5561a979bb2c6fae18b0533a2c8b9e14231bca474cc52a9b031'),
 ('FK-15',
  {'class': 'PATCH', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'code', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'meaning', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'fix', 'type': 'text'}], 'key': ['code'], 'name': 'CODES', 'type': 'rows'}], 'narrow': False},
  '{"class":"PATCH","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"code","type":"text"},{"class":"INSPECTION","name":"meaning","type":"text"},{"class":"INSPECTION","name":"fix","type":"text"}],"key":["code"],"name":"CODES","type":"rows"}]}',
  'f312357c5dba5900fe4b31b21b333bc9801a2fa7003ec99eda9d47e90d5eec84'),
 ('FK-16',
  {'class': 'PATCH', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'figure', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'value', 'quote': True, 'type': 'decimal'}], 'key': ['figure'], 'name': 'FIGURES', 'type': 'rows'}, {'class': 'INSPECTION', 'name': 'SUMMARY_TEXT', 'type': 'text'}], 'narrow': False},
  '{"class":"PATCH","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"figure","type":"text"},{"class":"MECHANICAL","name":"value","quote":true,"type":"decimal"}],"key":["figure"],"name":"FIGURES","type":"rows"},{"class":"INSPECTION","name":"SUMMARY_TEXT","type":"text"}]}',
  '568aeb9cb18634ecf7841a8c0ae23a234d950e52973a7d3cceed1435f6165616'),
 ('FK-17',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'url', 'type': 'url'}, {'class': 'MECHANICAL', 'name': 'liveness', 'type': 'enum', 'values': ['LIVE', 'DEAD', 'NOT SERVED']}, {'class': 'MECHANICAL', 'name': 'support', 'type': 'enum', 'values': ['SUPPORTS', 'DOES NOT SUPPORT', 'NOT CHECKABLE']}, {'class': 'INSPECTION', 'name': 'note', 'type': 'text'}], 'key': ['page', 'url'], 'name': 'LINKS', 'type': 'rows'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"url","type":"url"},{"class":"MECHANICAL","name":"liveness","type":"enum","values":["LIVE","DEAD","NOT SERVED"]},{"class":"MECHANICAL","name":"support","type":"enum","values":["SUPPORTS","DOES NOT SUPPORT","NOT CHECKABLE"]},{"class":"INSPECTION","name":"note","type":"text"}],"key":["page","url"],"name":"LINKS","type":"rows"}]}',
  'd279a25fa472b812746f954b132c4ca25b7da69468c33578862323b8f559d145'),
 ('FK-19',
  {'class': 'PATCH', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'page', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'kind', 'type': 'enum', 'values': ['STUB', 'PREV NEXT LINKS', 'CATEGORY']}, {'class': 'INSPECTION', 'name': 'new_text', 'type': 'text'}, {'class': 'INSPECTION', 'name': 'reason', 'type': 'text'}], 'key': ['page', 'kind'], 'name': 'EDITS', 'type': 'rows'}], 'narrow': False},
  '{"class":"PATCH","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"page","type":"text"},{"class":"MECHANICAL","name":"kind","type":"enum","values":["STUB","PREV NEXT LINKS","CATEGORY"]},{"class":"INSPECTION","name":"new_text","type":"text"},{"class":"INSPECTION","name":"reason","type":"text"}],"key":["page","kind"],"name":"EDITS","type":"rows"}]}',
  'b3216a4d320e13530bb67a21986cd47749ed89e697bf6643dc39085cf6ba6228'),
 ('FK-20',
  {'class': 'FINDINGS', 'fields': [{'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'kind', 'type': 'enum', 'values': ['DOI', 'ARXIV', 'ISBN', 'ISSN', 'PMID', 'PMCID']}, {'class': 'MECHANICAL', 'name': 'identifier', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'result', 'type': 'enum', 'values': ['SOURCE AGREES', 'SOURCE DIFFERS', 'NOT FOUND']}], 'key': ['kind', 'identifier'], 'name': 'RESULTS', 'type': 'rows'}, {'class': 'MECHANICAL', 'columns': [{'class': 'MECHANICAL', 'name': 'kind', 'type': 'enum', 'values': ['DOI', 'ARXIV', 'ISBN', 'ISSN', 'PMID', 'PMCID']}, {'class': 'MECHANICAL', 'name': 'identifier', 'type': 'text'}, {'class': 'MECHANICAL', 'name': 'field', 'type': 'enum', 'values': ['year', 'title', 'family', 'doi', 'issn', 'isbn', 'arxiv']}, {'class': 'MECHANICAL', 'name': 'value', 'quote': True, 'type': 'text'}], 'key': ['kind', 'identifier', 'field'], 'name': 'SOURCE_FIELDS', 'type': 'rows'}, {'class': 'INSPECTION', 'name': 'NOTE', 'type': 'text'}], 'narrow': False},
  '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"kind","type":"enum","values":["DOI","ARXIV","ISBN","ISSN","PMID","PMCID"]},{"class":"MECHANICAL","name":"identifier","type":"text"},{"class":"MECHANICAL","name":"result","type":"enum","values":["SOURCE AGREES","SOURCE DIFFERS","NOT FOUND"]}],"key":["kind","identifier"],"name":"RESULTS","type":"rows"},{"class":"MECHANICAL","columns":[{"class":"MECHANICAL","name":"kind","type":"enum","values":["DOI","ARXIV","ISBN","ISSN","PMID","PMCID"]},{"class":"MECHANICAL","name":"identifier","type":"text"},{"class":"MECHANICAL","name":"field","type":"enum","values":["year","title","family","doi","issn","isbn","arxiv"]},{"class":"MECHANICAL","name":"value","quote":true,"type":"text"}],"key":["kind","identifier","field"],"name":"SOURCE_FIELDS","type":"rows"},{"class":"INSPECTION","name":"NOTE","type":"text"}]}',
  '4e43f86acab3dcf8df0ad437fdde4b187a75d0e963c34be4701ee42a68ce4eee'),
]


def fs_hand_digest(text):
    """SHA-256 over the netstring of ["TPC-FALCON-SCHEMA", "1", text], built
    here, apart from tpc_falcon and tpc_digest (FS4)."""
    pre = b"".join(str(len(b)).encode("ascii") + b":" + b
                   for b in (f.encode("utf-8") for f in
                             ["TPC-FALCON-SCHEMA", "1", text]))
    return hashlib.sha256(pre).hexdigest(), len(pre)


def fs(name, schema, canon, digest=None, plen=None):
    """canon: the literal canonical text, None for a refusal ("schema")."""
    if canon is None:
        add(name + " (canon)", "schema_canon", {"schema": schema}, "ERROR:schema")
        add(name + " (digest)", "schema_digest", {"schema": schema}, "ERROR:schema")
        return
    d, n = fs_hand_digest(canon)
    assert digest is None or digest == d, name + ": the source's digest"
    assert plen is None or plen == n, name + ": the source's preimage length"
    add(name + " (canon)", "schema_canon", {"schema": schema}, "canon|" + canon)
    add(name + " (digest)", "schema_digest", {"schema": schema}, "digest|" + d)


for nm, inp, canon, dg, plen in FS_RDC:
    fs("FS " + nm, inp, canon, dg, plen)
for kind, sch, canon, dg in FS_KINDS:
    fs("FS " + kind + " the kind's schema (tpm_decl_fk_schemas 0.1.0)", sch, canon, dg)

MF = {"class": "MECHANICAL", "name": "A", "type": "text"}
V1C = '{"class":"FINDINGS","fields":[{"class":"MECHANICAL","name":"A","type":"text"}]}'


def en(*values, **kw):
    s = {"class": "FINDINGS", "narrow": False,
         "fields": [{"class": "MECHANICAL", "name": "A", "type": "enum", "values": list(values)}]}
    s.update(kw)
    return s


def en_c(*escaped):
    """The canonical text of en(...), its values already written as FS3
    spells them."""
    return ('{"class":"FINDINGS","fields":[{"class":"MECHANICAL","name":"A","type":"enum",'
            '"values":[' + ",".join('"' + v + '"' for v in escaped) + ']}]}')


fs("FSX01 members in any order: V1's text (FS3 sorts by key)",
   {"narrow": False, "fields": [{"type": "text", "name": "A", "class": "MECHANICAL"}],
    "class": "FINDINGS"}, V1C, "4f74820e227f3884d8f6c1c5095488810c2ce3833df2b1da12e2ba9e3f410d5a")
fs("FSX02 confidential true is kept, narrow true is dropped (FS2)",
   {"class": "FINDINGS", "narrow": True, "confidential": True,
    "fields": [{"name": "A", "type": "boolean", "class": "INSPECTION"}]},
   '{"class":"FINDINGS","confidential":true,"fields":[{"class":"INSPECTION","name":"A",'
   '"type":"boolean"}]}')
fs("FSX03 a rows column's quote false is dropped; quote true and a scale are kept",
   {"class": "SURVEY", "narrow": False, "fields": [
       {"name": "R", "type": "rows", "class": "MECHANICAL", "key": ["k"], "columns": [
           {"name": "k", "type": "text", "class": "MECHANICAL", "quote": False},
           {"name": "v", "type": "decimal", "class": "MECHANICAL", "quote": True,
            "scale": "million"}]}]},
   '{"class":"SURVEY","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL",'
   '"name":"k","type":"text"},{"class":"MECHANICAL","name":"v","quote":true,"scale":"million",'
   '"type":"decimal"}],"key":["k"],"name":"R","type":"rows"}]}')
fs("FSX04 line ends CRLF and CR: refused under FS1 0.2.0 (D213.1)",
   en("a\r\nb", "c\rd", "e\nf"), None)
fs("FSX05a a CRLF value: refused under FS1 0.2.0 (D213.1)", en("x\r\ny"), None)
fs("FSX05b a value with LF: accepted, LF written \\u000a", en("x\ny"), en_c("x\\u000ay"))
fs("FSX06 DEL, the bar and U+2028 are written as themselves",
   en("a\u007fb", "p|q", "l\u2028m"), en_c("a\u007fb", "p|q", "l\u2028m"))
fs("FSX07 a character beyond the BMP is written as itself",
   en("\U0001f600"), en_c("\U0001f600"))
fs("FSX08 key and column order are kept (they carry meaning)",
   {"class": "PATCH", "narrow": False, "fields": [
       {"name": "R", "type": "rows", "class": "MECHANICAL", "key": ["b", "a"], "columns": [
           {"name": "b", "type": "date", "class": "MECHANICAL"},
           {"name": "a", "type": "url", "class": "MECHANICAL"}]}]},
   '{"class":"PATCH","fields":[{"class":"MECHANICAL","columns":[{"class":"MECHANICAL",'
   '"name":"b","type":"date"},{"class":"MECHANICAL","name":"a","type":"url"}],'
   '"key":["b","a"],"name":"R","type":"rows"}]}')
fs("FSX09 two values NFC would merge: refused under FS1 0.2.0, the first not in NFC (C-TP-4, D213.1)",
   en("e\u0301", "\u00e9"), None)
fs("FSX10 the quotation mark, the backslash and every C0 control",
   en("\"", "\\", "\u0000\u001f\t"), en_c('\\"', "\\\\", "\\u0000\\u001f\\u0009"))
# increment 12 (D213.1; FS 0.2.0): NFC, not NFKC; singletons; Hangul; an affix
fs("FSX11 an enumeration value holding U+212B ANGSTROM SIGN (NFC maps it to U+00C5): refused",
   en("\u212b"), None)
fs("FSX12 a Hangul syllable written as conjoining jamo (U+1100 U+1161): refused",
   en("\u1100\u1161"), None)
fs("FSX13 the same syllable precomposed (U+AC00): accepted", en("\uac00"), en_c("\uac00"))
fs("FSX14 q + U+0301, NFC with a combining mark (no precomposed form): accepted",
   en("q\u0301"), en_c("q\u0301"))
fs("FSX15 U+FB01 LATIN SMALL LIGATURE FI, NFC though not NFKC: accepted",
   en("\ufb01"), en_c("\ufb01"))
fs("FSX16 a QUOTE AFFIX in NFC (U+00A4 + U+00C5): accepted",
   {"class": "SURVEY", "narrow": False,
    "fields": [{"class": "MECHANICAL", "name": "s", "type": "decimal", "quote": True,
                "affix": "\u00a4\u00c5"}]},
   '{"class":"SURVEY","fields":[{"affix":"\u00a4\u00c5","class":"MECHANICAL","name":"s",'
   '"quote":true,"type":"decimal"}]}')

fs("FSR01 a list is not a schema", ["class", "fields"], None)
fs("FSR02 a string is not a schema", "FINDINGS", None)
fs("FSR03 null is not a schema", None, None)
fs("FSR04 an empty object: class, narrow and fields missing", {}, None)
fs("FSR05 narrow missing", {"class": "FINDINGS", "fields": [MF]}, None)
fs("FSR06 narrow given as a string", {"class": "FINDINGS", "narrow": "false", "fields": [MF]}, None)
fs("FSR07 confidential not a boolean",
   {"class": "FINDINGS", "narrow": False, "confidential": "no", "fields": [MF]}, None)
fs("FSR08 no field", {"class": "FINDINGS", "narrow": False, "fields": []}, None)
fs("FSR09 fields given as a JSON object (C-MS-5's shape)",
   {"class": "FINDINGS", "narrow": False, "fields": {}}, None)
fs("FSR10 a field given as a JSON array",
   {"class": "FINDINGS", "narrow": False, "fields": [[]]}, None)
fs("FSR11 two fields of one name",
   {"class": "FINDINGS", "narrow": False, "fields": [MF, dict(MF, type="date")]}, None)
fs("FSR12 quote given as a string",
   {"class": "FINDINGS", "narrow": False, "fields": [dict(MF, quote="false")]}, None)
fs("FSR13 an unknown top-level member",
   {"class": "FINDINGS", "narrow": False, "version": "1", "fields": [MF]}, None)
fs("FSR14 a class outside FINDINGS, PATCH and SURVEY",
   {"class": "REPORT", "narrow": False, "fields": [MF]}, None)
fs("FSR15 a rows field of class INSPECTION",
   {"class": "PATCH", "narrow": False, "fields": [
       {"name": "R", "type": "rows", "class": "INSPECTION", "key": ["k"],
        "columns": [dict(MF, name="k")]}]}, None)
fs("FSR16 an enumeration without values", en(), None)
fs("FSR17 a number among the values (a schema holds no number)", en("a", 1), None)
fs("FSR18 an affix without quote true",
   {"class": "SURVEY", "narrow": False,
    "fields": [{"class": "MECHANICAL", "name": "s", "type": "decimal", "affix": "%"}]}, None)
fs("FSR19 rows inside rows",
   {"class": "PATCH", "narrow": False, "fields": [
       {"name": "R", "type": "rows", "class": "MECHANICAL", "key": ["k"], "columns": [
           dict(MF, name="k"),
           {"name": "S", "type": "rows", "class": "MECHANICAL", "key": ["j"],
            "columns": [dict(MF, name="j")]}]}]}, None)
fs("FSR20 a field name that is not an identifier",
   {"class": "FINDINGS", "narrow": False, "fields": [dict(MF, name="a-b")]}, None)

# ---------------------------------------------------------------- write
out = []
bad = 0
for name, fn, args, hand in cases:
    exp = run_case(fn, args)
    if hand is not None and exp != hand:
        bad += 1
        print("HAND MISMATCH", name)
        print(exp)
        print("--- hand:")
        print(hand)
    out.append({"name": name, "fn": fn, "args": args,
                "hand_checked": hand is not None, "expected": exp})
if bad:
    print("refusing to write: %d mismatches" % bad)
    sys.exit(1)
names = [c["name"] for c in out]
assert len(set(names)) == len(names), "duplicate case names"
json.dump({"corpus": "falcon", "version": "0.2.0", "cases": out},
          open("falcon_corpus.json", "w", encoding="utf-8", newline="\n"), indent=1, sort_keys=True)
print(len(out), "cases,", sum(1 for c in out if c["hand_checked"]),
      "hand-checked")