Jump to content

The Principia Machina/TPC-PY/gen dis corpus.py: Difference between revisions

From The Invariance Itself
TPC-PY source gen_dis_corpus.py (0.1.0), SHA-256 b6ee0e453a2bca3e; conformed TPS 0.4.0; PROPOSED, not declared (TPC-PY 0.2.0)
 
m 1 revision imported
(No difference)

Revision as of 02:11, 30 September 2026

# -*- coding: utf-8 -*-
"""gen_dis_corpus 0.1.0: writes vectors/dis_corpus.json, the vectors of
tpc_digest 0.2.0's three functions for the TPS digest and the instruction
set digest (D203.1, taken 2026-09-28: tpm_post_tpcpy_pause 0.1.0, D203
Option 1 (a) and (b); tpm_tps_digest_spec 0.1.0):

  pre_texts(wikitext)             the <pre> contents of one page, in order
  d_tps(version, pages)           D(TPS) over the pages of the TPS tree
  d_is(version, d_tps, members)   D(IS) over D(TPS) and the other members
                                  of the index's instruction-artifact class

A corpus of its own beside digest_corpus.json, which is written by hand
and stays byte-identical at its tpc_digest 0.1.0 label (sr3_scope's
precedent). BASE form: {"id", "fn", "input", "expect"}, the result being
canon() of the function's answer; digest cases also carry "preimage", the
exact bytes hashed (ASCII here), so a reader can check any of them with
printf %s <preimage> | sha256sum.

VECTORS FIRST. Every expectation below is HAND-DERIVED: refusals written
literally; each digest from a field list written literally below, the
page hashes and the netstring built by this file's own code (sha256_hex,
netstring), apart from tpc_digest. The generator refuses to write the
corpus if tpc_digest disagrees with any expectation. The page sets are
synthetic: the real TPS 0.3.0 page set is a fixture outside tpc/ (ruling
S97; dis_fixtures/ at the tree root, not published with TPC-PY).

canon() renders a string as itself, so a pre_texts answer whose blocks
hold "," or "]" would read ambiguously; the pre_texts vectors keep such
characters out of their blocks, and the d_tps vectors pin what the
blocks hash to. Deterministic; standard library only; Python 3.8+.
Run: cd tpc && python3 gen_dis_corpus.py (Windows: py -3 gen_dis_corpus.py).
"""
import hashlib
import json
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.dont_write_bytecode = True
import tpc_strike as S   # noqa: E402  canon()
import tpc_digest as G   # noqa: E402

GEN_VERSION = "0.1.0"
R = "The Principia Machina/TPS"          # the tree's root title (D34.1)
HEX_A = "a" * 64
HEX_B = "0123456789abcdef" * 4


def sha256_hex(text):
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def netstring(fields):
    """D97's encoding, built here apart from tpc_digest: each field its
    UTF-8 byte length in decimal, ":", its bytes; an int in base 10."""
    out = []
    for f in fields:
        s = str(f) if type(f) is int else f
        out.append("%d:%s" % (len(s.encode("utf-8")), s))
    return "".join(out)


cases = []


def case(cid, fn, inp, expect, preimage=None):
    c = {"id": cid, "fn": fn, "input": inp}
    if preimage is not None:
        c["preimage"] = preimage
    c["expect"] = expect
    cases.append(c)


def ok_digest(preimage):
    return "[OK,%s]" % sha256_hex(preimage)


# ------------------------------------------------------------ pre_texts
P = "pre_texts"
REF_PRE = "[REFUSED,pre]"
case("pre/one-block", P,
     {"wikitext": "''banner''\n\n<pre>\nA\nB\n</pre>\n\n[[Category:X]]\n"},
     "[OK,[A\nB\n]]")
case("pre/two-blocks", P,
     {"wikitext": "<pre>\nX\n</pre>\nbetween\n<pre>\nY\n</pre>\n"},
     "[OK,[X\n,Y\n]]")
case("pre/no-block", P, {"wikitext": "just text\n"}, "[OK,[]]")
case("pre/empty-text", P, {"wikitext": ""}, "[OK,[]]")
case("pre/empty-block (canon writes [] as for no block; d_tps pins the difference)", P,
     {"wikitext": "<pre>\n</pre>"}, "[OK,[]]")
case("pre/text-after-closing-ignored", P,
     {"wikitext": "<pre>\nA\n</pre>tail <b>x</b>\n"}, "[OK,[A\n]]")
case("pre/prefix-inside-is-text (TPS 0.3.0 writes <prefix>-n)", P,
     {"wikitext": "<pre>\n<prefix>-n and </prefix> x\n</pre>\n"},
     "[OK,[<prefix>-n and </prefix> x\n]]")
case("pre/pretty-and-preamble-outside-are-text", P,
     {"wikitext": "<pretty> </preamble>\n<pre>\nA\n</pre>\n<presence/>\n"}, "[OK,[A\n]]")
case("pre/non-ascii", P,
     {"wikitext": "<pre>\ncaf\u00e9 \u2014 \u00b5 \U0001f600\n</pre>\n"},
     "[OK,[caf\u00e9 \u2014 \u00b5 \U0001f600\n]]")
case("pre/crlf-inside-kept-as-is", P,
     {"wikitext": "<pre>\nA\r\nB\n</pre>"}, "[OK,[A\r\nB\n]]")
case("pre/opening-without-lf", P, {"wikitext": "<pre>A\n</pre>"}, REF_PRE)
case("pre/opening-then-cr", P, {"wikitext": "<pre>\r\nA\n</pre>"}, REF_PRE)
case("pre/unclosed", P, {"wikitext": "<pre>\nA\n"}, REF_PRE)
case("pre/closing-before-opening", P, {"wikitext": "</pre>\n<pre>\nA\n</pre>"}, REF_PRE)
case("pre/second-opening-inside", P, {"wikitext": "<pre>\nA\n<pre>\nB\n</pre>"}, REF_PRE)
case("pre/upper-case-tags", P, {"wikitext": "<PRE>\nA\n</PRE>"}, REF_PRE)
case("pre/mixed-case-opening", P, {"wikitext": "<Pre>\nA\n</pre>"}, REF_PRE)
case("pre/attribute", P, {"wikitext": "<pre class=\"x\">\nA\n</pre>"}, REF_PRE)
case("pre/self-closing", P, {"wikitext": "<pre />\n"}, REF_PRE)
case("pre/self-closing-no-space", P, {"wikitext": "<pre/>\n"}, REF_PRE)
case("pre/tab-in-opening", P, {"wikitext": "<pre\t>\nA\n</pre>"}, REF_PRE)
case("pre/space-in-closing", P, {"wikitext": "<pre>\nA\n</pre >"}, REF_PRE)
case("pre/upper-case-closing", P, {"wikitext": "<pre>\nA\n</PRE>"}, REF_PRE)
case("pre/tag-name-at-end-of-text", P, {"wikitext": "x <pre"}, REF_PRE)
case("pre/closing-name-at-end-of-text", P, {"wikitext": "<pre>\nA\n</pre"}, REF_PRE)
case("pre/tag-in-comment-is-read-as-tag (named limit)", P,
     {"wikitext": "<!-- <pre> -->\nx\n"}, REF_PRE)
case("pre/not-a-string", P, {"wikitext": 5}, REF_PRE)
case("pre/null", P, {"wikitext": None}, REF_PRE)

# ------------------------------------------------------------ d_tps
T = "d_tps"
ROOT_TEXT = "''root banner''\n<pre>\nROOT ONE\n</pre>\nmiddle\n<pre>\nROOT TWO\n</pre>\n"
ROOT_TEXT_B = "''another banner, [[a link]]''\n\n<pre>\nROOT ONE\n</pre>\n<pre>\nROOT TWO\n</pre>"
S01 = R + "/0.1 Front"
S59 = R + "/5.9 Nine"
S510 = R + "/5.10 Ten"
CT = R + "/Contents"
P_ROOT = [R, ROOT_TEXT]
P_01 = [S01, "<pre>\nFRONT\n</pre>"]
P_59 = [S59, "''b''\n<pre>\nSECTION 5.9\n</pre>\n"]
P_510 = [S510, "''b''\n<pre>\nSECTION 5.10\n</pre>\n"]
H_ROOT = sha256_hex("ROOT ONE\n" + "\n" + "ROOT TWO\n")   # blocks joined by one LF
H_01 = sha256_hex("FRONT\n")
H_59 = sha256_hex("SECTION 5.9\n")
H_510 = sha256_hex("SECTION 5.10\n")
BASE_PAGES = [P_510, P_ROOT, P_59, P_01]
BASE_FIELDS = ["TPC-TPS", "1", "0.3.0", 4, R, H_ROOT, S01, H_01, S59, H_59, S510, H_510]
BASE_PRE = netstring(BASE_FIELDS)
BASE_EXPECT = ok_digest(BASE_PRE)
D_TPS_BASE = sha256_hex(BASE_PRE)

case("tps/root-and-sections-out-of-order: 0.1 < 5.9 < 5.10 as integers", T,
     {"version": "0.3.0", "pages": BASE_PAGES}, BASE_EXPECT, BASE_PRE)
case("tps/input-order-reversed: equal", T,
     {"version": "0.3.0", "pages": list(reversed(BASE_PAGES))}, BASE_EXPECT, BASE_PRE)
case("tps/banners-changed: equal", T,
     {"version": "0.3.0", "pages": [[S510, "<pre>\nSECTION 5.10\n</pre>"], [R, ROOT_TEXT_B],
                                    [S59, "new banner\n<pre>\nSECTION 5.9\n</pre>\n\n[[Category:Y]]"],
                                    [S01, "x\n<pre>\nFRONT\n</pre>\ny"]]},
     BASE_EXPECT, BASE_PRE)
case("tps/contents-without-block: left out, equal", T,
     {"version": "0.3.0", "pages": BASE_PAGES + [[CT, "== Parts ==\n* a list\n"]]},
     BASE_EXPECT, BASE_PRE)
case("tps/contents-with-a-block: left out by title, equal", T,
     {"version": "0.3.0", "pages": BASE_PAGES + [[CT, "<pre>\nNOT RULE TEXT\n</pre>\n"]]},
     BASE_EXPECT, BASE_PRE)
case("tps/section-without-block: left out, equal", T,
     {"version": "0.3.0", "pages": BASE_PAGES + [[R + "/6.1 Empty", "no block here\n"]]},
     BASE_EXPECT, BASE_PRE)
f = ["TPC-TPS", "1", "0.3.0", 4, R, H_ROOT, S01, H_01, S59, sha256_hex("SECTION 5.8\n"), S510, H_510]
case("tps/one-character-inside-a-block-changed: moves", T,
     {"version": "0.3.0", "pages": [P_510, P_ROOT, [S59, "''b''\n<pre>\nSECTION 5.8\n</pre>\n"], P_01]},
     ok_digest(netstring(f)), netstring(f))
f = ["TPC-TPS", "1", "0.3.0", 4, R, H_ROOT, S01, H_01, S59, H_510, S510, H_59]
case("tps/two-pages-contents-swapped-between-titles: moves", T,
     {"version": "0.3.0", "pages": [[S510, P_59[1]], P_ROOT, [S59, P_510[1]], P_01]},
     ok_digest(netstring(f)), netstring(f))
f = ["TPC-TPS", "1", "0.3.0", 5, R, H_ROOT, S01, H_01, S59, H_59, S510, H_510,
     R + "/6.1 Empty", sha256_hex("")]
case("tps/section-with-an-empty-block: kept, n = 5", T,
     {"version": "0.3.0", "pages": BASE_PAGES + [[R + "/6.1 Empty", "<pre>\n</pre>\n"]]},
     ok_digest(netstring(f)), netstring(f))
f = ["TPC-TPS", "1", "0.3.1", 4, R, H_ROOT, S01, H_01, S59, H_59, S510, H_510]
case("tps/version-inside: 0.3.1 over the same pages moves", T,
     {"version": "0.3.1", "pages": BASE_PAGES}, ok_digest(netstring(f)), netstring(f))
f = ["TPC-TPS", "1", "10.20.30", 4, R, H_ROOT, S01, H_01, S59, H_59, S510, H_510]
case("tps/version-with-several-digits", T,
     {"version": "10.20.30", "pages": BASE_PAGES}, ok_digest(netstring(f)), netstring(f))
A51, A512, A52 = R + "/5.1 A", R + "/5.1.2 B", R + "/5.2 C"
f = ["TPC-TPS", "1", "0.4.0", 4, R, H_ROOT, A51, sha256_hex("a\n"), A512, sha256_hex("b\n"),
     A52, sha256_hex("c\n")]
case("tps/three-part-numbers: 5.1 < 5.1.2 < 5.2", T,
     {"version": "0.4.0", "pages": [[A52, "<pre>\nc\n</pre>"], [A512, "<pre>\nb\n</pre>"], P_ROOT,
                                    [A51, "<pre>\na\n</pre>"]]},
     ok_digest(netstring(f)), netstring(f))
f = ["TPC-TPS", "1", "0.3.0", 1, R, H_ROOT]
case("tps/root-only: n = 1", T, {"version": "0.3.0", "pages": [P_ROOT]},
     ok_digest(netstring(f)), netstring(f))
f = ["TPC-TPS", "1", "0.3.0", 1, S59, H_59]
case("tps/root-without-block: left out, n = 1", T,
     {"version": "0.3.0", "pages": [[R, "no block\n"], P_59]}, ok_digest(netstring(f)), netstring(f))
f = ["TPC-TPS", "1", "0.3.0", 2, R, H_ROOT, R + "/9999.9999 Last", sha256_hex("z\n")]
case("tps/four-digit-parts", T,
     {"version": "0.3.0", "pages": [[R + "/9999.9999 Last", "<pre>\nz\n</pre>"], P_ROOT]},
     ok_digest(netstring(f)), netstring(f))
f = ["TPC-TPS", "1", "0.3.0", 2, R, H_ROOT, R + "/0.0 Zero", sha256_hex("0\n")]
case("tps/zero-parts", T,
     {"version": "0.3.0", "pages": [P_ROOT, [R + "/0.0 Zero", "<pre>\n0\n</pre>"]]},
     ok_digest(netstring(f)), netstring(f))
NU = R + "/2.1 Caf\u00e9 and more"
f = ["TPC-TPS", "1", "0.3.0", 2, R, H_ROOT, NU, sha256_hex("\u00e9\n")]
case("tps/non-ascii-title-and-text", T,
     {"version": "0.3.0", "pages": [[NU, "<pre>\n\u00e9\n</pre>"], P_ROOT]},
     ok_digest(netstring(f)), netstring(f))
# refusals, in the reasons' order: version, pages, title, root, pre
for cid, v in (("tps/version-two-parts", "0.3"), ("tps/version-prefixed", "v0.3.0"),
               ("tps/version-leading-zero", "0.03.0"), ("tps/version-suffix", "0.3.0-rc1"),
               ("tps/version-trailing-lf", "0.3.0\n"), ("tps/version-empty", "")):
    case(cid, T, {"version": v, "pages": BASE_PAGES}, "[REFUSED,version]")
case("tps/version-not-a-string", T, {"version": 3, "pages": BASE_PAGES}, "[REFUSED,version]")
case("tps/version-checked-before-pages", T, {"version": "x", "pages": {}}, "[REFUSED,version]")
case("tps/pages-an-object", T, {"version": "0.3.0", "pages": {}}, "[REFUSED,pages]")
case("tps/pages-a-string", T, {"version": "0.3.0", "pages": "x"}, "[REFUSED,pages]")
case("tps/page-not-a-pair", T, {"version": "0.3.0", "pages": [[R]]}, "[REFUSED,pages]")
case("tps/page-of-three", T, {"version": "0.3.0", "pages": [[R, "", ""]]}, "[REFUSED,pages]")
case("tps/page-text-not-a-string", T, {"version": "0.3.0", "pages": [[R, 5]]}, "[REFUSED,pages]")
case("tps/page-as-an-object", T,
     {"version": "0.3.0", "pages": [{"title": R, "text": ROOT_TEXT}]}, "[REFUSED,pages]")
case("tps/duplicate-title", T, {"version": "0.3.0", "pages": BASE_PAGES + [P_59]}, "[REFUSED,title]")
case("tps/duplicate-section-number", T,
     {"version": "0.3.0", "pages": BASE_PAGES + [[R + "/5.9 Other", "<pre>\nq\n</pre>"]]},
     "[REFUSED,title]")
case("tps/duplicate-root", T, {"version": "0.3.0", "pages": [P_ROOT, [R, "<pre>\nq\n</pre>"]]},
     "[REFUSED,title]")
for cid, t in (("tps/title-outside-the-tree", "Main Page"),
               ("tps/title-with-underscores", "The_Principia_Machina/TPS"),
               ("tps/section-with-underscore", R + "/5.9_Nine"),
               ("tps/section-leading-zero", R + "/05.9 Nine"),
               ("tps/section-one-part", R + "/5 Nine"),
               ("tps/section-without-name", R + "/5.9"),
               ("tps/section-five-digit-part", R + "/5.10000 Big"),
               ("tps/section-subpage", R + "/5.9 Nine/sub"),
               ("tps/section-two-spaces", R + "/5.9  Nine"),
               ("tps/section-trailing-space", R + "/5.9 Nine "),
               ("tps/section-trailing-dot", R + "/5.9. Nine"),
               ("tps/contents-in-lower-case", R + "/contents"),
               ("tps/root-with-trailing-slash", R + "/"),
               ("tps/title-with-lf", R + "\n"),
               ("tps/section-unicode-digit", R + "/5.\u0669 Nine"),
               ("tps/title-empty", "")):
    case(cid, T, {"version": "0.3.0", "pages": BASE_PAGES + [[t, "<pre>\nq\n</pre>"]]},
         "[REFUSED,title]")
case("tps/root-absent", T, {"version": "0.3.0", "pages": [P_59, P_510]}, "[REFUSED,root]")
case("tps/no-page", T, {"version": "0.3.0", "pages": []}, "[REFUSED,root]")
case("tps/root-absent-contents-present", T, {"version": "0.3.0", "pages": [[CT, "x"], P_59]},
     "[REFUSED,root]")
case("tps/a-page-pre-refused", T,
     {"version": "0.3.0", "pages": BASE_PAGES + [[R + "/6.1 Bad", "<pre>\nunclosed\n"]]},
     "[REFUSED,pre]")
case("tps/contents-pre-refused-too", T,
     {"version": "0.3.0", "pages": BASE_PAGES + [[CT, "<PRE>x</PRE>"]]}, "[REFUSED,pre]")
case("tps/title-reason-before-pre, bad title last", T,
     {"version": "0.3.0", "pages": [[R + "/6.1 Bad", "<pre>\nx\n"], P_ROOT, ["Main Page", "x"]]},
     "[REFUSED,title]")
case("tps/title-reason-before-pre, bad title first", T,
     {"version": "0.3.0", "pages": [["Main Page", "x"], P_ROOT, [R + "/6.1 Bad", "<pre>\nx\n"]]},
     "[REFUSED,title]")
case("tps/root-reason-before-pre", T,
     {"version": "0.3.0", "pages": [[R + "/6.1 Bad", "<pre>\nx\n"]]}, "[REFUSED,root]")

# ------------------------------------------------------------ d_is
I = "d_is"
f = ["TPC-DIS", "1", "0.3.0", D_TPS_BASE, 0]
case("dis/m-0 over tps/root-and-sections-out-of-order", I,
     {"version": "0.3.0", "d_tps": D_TPS_BASE, "members": []}, ok_digest(netstring(f)), netstring(f))
f = ["TPC-DIS", "1", "0.3.0", D_TPS_BASE, 1, "The Principia Machina/Member A", HEX_A]
case("dis/m-1", I, {"version": "0.3.0", "d_tps": D_TPS_BASE,
                    "members": [["The Principia Machina/Member A", HEX_A]]},
     ok_digest(netstring(f)), netstring(f))
f = ["TPC-DIS", "1", "0.3.0", D_TPS_BASE, 2, "The Principia Machina/Member A", HEX_A,
     "The Principia Machina/Member B", HEX_B]
case("dis/m-2 in index order", I, {"version": "0.3.0", "d_tps": D_TPS_BASE,
                                   "members": [["The Principia Machina/Member A", HEX_A],
                                               ["The Principia Machina/Member B", HEX_B]]},
     ok_digest(netstring(f)), netstring(f))
f = ["TPC-DIS", "1", "0.3.0", D_TPS_BASE, 2, "The Principia Machina/Member B", HEX_B,
     "The Principia Machina/Member A", HEX_A]
case("dis/m-2 the other order: moves (the caller's order is kept)", I,
     {"version": "0.3.0", "d_tps": D_TPS_BASE,
      "members": [["The Principia Machina/Member B", HEX_B],
                  ["The Principia Machina/Member A", HEX_A]]},
     ok_digest(netstring(f)), netstring(f))
f = ["TPC-DIS", "1", "0.4.0", D_TPS_BASE, 0]
case("dis/version-inside", I, {"version": "0.4.0", "d_tps": D_TPS_BASE, "members": []},
     ok_digest(netstring(f)), netstring(f))
case("dis/version-bad", I, {"version": "0.4", "d_tps": D_TPS_BASE, "members": []},
     "[REFUSED,version]")
case("dis/d_tps-upper-case", I, {"version": "0.3.0", "d_tps": D_TPS_BASE.upper(), "members": []},
     "[REFUSED,digest]")
case("dis/d_tps-63-characters", I, {"version": "0.3.0", "d_tps": D_TPS_BASE[:63], "members": []},
     "[REFUSED,digest]")
case("dis/d_tps-not-a-string", I, {"version": "0.3.0", "d_tps": 0, "members": []},
     "[REFUSED,digest]")
case("dis/members-an-object", I, {"version": "0.3.0", "d_tps": D_TPS_BASE, "members": {}},
     "[REFUSED,members]")
case("dis/member-not-a-pair", I, {"version": "0.3.0", "d_tps": D_TPS_BASE, "members": [[HEX_A]]},
     "[REFUSED,members]")
case("dis/member-hash-upper-case", I,
     {"version": "0.3.0", "d_tps": D_TPS_BASE, "members": [["M", HEX_A.upper()]]},
     "[REFUSED,members]")
case("dis/member-title-empty", I, {"version": "0.3.0", "d_tps": D_TPS_BASE, "members": [["", HEX_A]]},
     "[REFUSED,members]")
case("dis/member-title-not-a-string", I,
     {"version": "0.3.0", "d_tps": D_TPS_BASE, "members": [[7, HEX_A]]}, "[REFUSED,members]")
case("dis/member-titles-duplicate", I,
     {"version": "0.3.0", "d_tps": D_TPS_BASE, "members": [["M", HEX_A], ["M", HEX_B]]},
     "[REFUSED,members]")

# spot check of the hand netstring against a preimage written out in full
assert netstring(["TPC-DIS", "1", "0.3.0", HEX_A, 0]) == (
    "7:TPC-DIS1:15:0.3.064:" + HEX_A + "1:0")


def dispatch(fn, inp):
    if fn == "pre_texts":
        return G.pre_texts(inp["wikitext"])
    if fn == "d_tps":
        return G.d_tps(inp["version"], inp["pages"])
    if fn == "d_is":
        return G.d_is(inp["version"], inp["d_tps"], inp["members"])
    raise KeyError(fn)


def main():
    bad = 0
    ids = [c["id"] for c in cases]
    assert len(set(ids)) == len(ids), "duplicate ids"
    for c in cases:
        got = S.canon(dispatch(c["fn"], c["input"]))
        if got != c["expect"]:
            bad += 1
            print("MISMATCH %s\n  expect %r\n  got    %r" % (c["id"], c["expect"], got))
    if bad:
        print("refusing to write: %d of %d cases disagree with their hand-derived expectations"
              % (bad, len(cases)))
        return 1
    corpus = {
        "meta": {
            "name": "tpc_dis_corpus", "version": "0.1.0",
            "status": "PROPOSED 2026-09-28 (D203.1, taken 2026-09-28; tpm_post_tpcpy_pause 0.1.0 D203 "
                      "Option 1); not declared",
            "conforms_tps": "0.4.0 (proposed; revision 7 states D203.1)",
            "module": "tpc_digest 0.2.0 (proposed with TPC-PY 0.2.0)",
            "spec": "tpm_tps_digest_spec 0.1.0",
            "note": ("Every expectation is hand-derived: refusals written literally, each digest from a field "
                     "list written in gen_dis_corpus.py and hashed apart from tpc_digest; 'preimage' is the "
                     "exact bytes hashed. The page sets are synthetic; the real TPS 0.3.0 page set is a "
                     "fixture outside tpc/ (S97). Both residences MUST reproduce every expect.")
        },
        "cases": cases,
    }
    with open(os.path.join(HERE, "vectors", "dis_corpus.json"), "w", encoding="utf-8",
              newline="\n") as fh:
        fh.write(json.dumps(corpus, indent=1, ensure_ascii=True) + "\n")
    print("wrote", len(cases), "dis cases; all match hand-derived expectations")
    return 0


if __name__ == "__main__":
    sys.exit(main())

# END OF ARTIFACT --- gen_dis_corpus 0.1.0
# If this line is absent, the payload arrived truncated.