Jump to content

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

From The Invariance Itself
TPC-PY source gen_module_corpus.py (0.2.0), SHA-256 1031a1d314abf755; conformed TPS 0.3.0; PROPOSED, not declared (CO-8)
 
m 1 revision imported
 
(2 intermediate revisions by the same user not shown)
Line 6: Line 6:
tpm_module_spec 0.2.0 before either residence was changed to it. The
tpm_module_spec 0.2.0 before either residence was changed to it. The
generator never imports or runs tpc_module. It writes the corpus in the
generator never imports or runs tpc_module. It writes the corpus in the
RUN_CASE form of lint_corpus: {"corpus": "module", "version": "0.2.0",
RUN_CASE form of lint_corpus: {"corpus": "module", "version": "0.2.1",
"cases": [{"name", "fn", "args", "expected", "hand_checked"}]}, "expected"
"cases": [{"name", "fn", "args", "expected", "hand_checked"}]}, "expected"
being the rendered result (tpm_module_spec 2.3, 6.4 RENDER).
being the rendered result (tpm_module_spec 2.3, 6.4 RENDER).
Line 33: Line 33:
Every physical value in the fixtures is a FIXTURE, not a declaration
Every physical value in the fixtures is a FIXTURE, not a declaration
(tpm_module_spec section 8): the ISO 2533:1975 values are VERIFY-HIGH (S6).
(tpm_module_spec section 8): the ISO 2533:1975 values are VERIFY-HIGH (S6).
0.2.1 (increment 11, 2026-09-28): X64-X67, the base TPK read as the JSON
is written (C-MS-5); the corpus is written with LF line ends on every
platform (S65). No earlier case moves.
"""
"""
import copy
import copy
Line 50: Line 53:
import tpc_eom as E          # noqa: E402  existing: eom_map (assert only)
import tpc_eom as E          # noqa: E402  existing: eom_map (assert only)


CORPUS_VERSION = "0.2.0"
CORPUS_VERSION = "0.2.1"
CASES = []
CASES = []


Line 1,656: Line 1,659:
   [C_SUMP, dict(C_SHARE, VALUES={"k_q": {"VALUE": "2", "UNIT": "kg m-1"}})], [X_SUMP, X_SHARE],
   [C_SUMP, dict(C_SHARE, VALUES={"k_q": {"VALUE": "2", "UNIT": "kg m-1"}})], [X_SUMP, X_SHARE],
   REF("EXPAND: TEST-SHARE 1.0.0 and TEST-SUMP 1.0.0 both emit k_q, differently"))
   REF("EXPAND: TEST-SHARE 1.0.0 and TEST-SUMP 1.0.0 both emit k_q, differently"))
# 0.2.1 (C-MS-5, increment 11): the base TPK reaches T1 as the JSON reads, {} apart from []
# (tpm_module_spec C-MS-5: TPC-PHP's bridge into the zone functions now keeps the distinction)
_X_DOF = {"NAME": "x", "DIMENSION": "L1"}
assert t1_first({"DOF": {}}) == "TPK kind DOF is not a list"
X("X64 a base TPK kind that is a JSON {} is not a list (C-MS-5)", {"TPK": {"DOF": {}}}, [], [],
  REF("BASE: the TPK fails T1: TPK kind DOF is not a list"))
X("X65 a base TPK kind keyed 0 is not a list (C-MS-5)", {"TPK": {"DOF": {"0": _X_DOF}}}, [], [],
  REF("BASE: the TPK fails T1: TPK kind DOF is not a list"))
X("X66 an empty PARAMETERS object in the base is valid, D_VARIABLES ONE (C-MS-5)",
  {"TPK": {"PARAMETERS": {}, "DOF": [_X_DOF]}}, [], [],
  XR({"DOF": [_X_DOF]}, [], [], []))
X("X67 an entry that is a JSON [] is not an object (C-MS-5)", {"TPK": {"DOF": [[]]}}, [], [],
  REF("BASE: the TPK fails T1: TPK kind DOF: entry is not an object"))


# ============================================================== catalogue lines
# ============================================================== catalogue lines
Line 2,382: Line 2,399:
                 "docstring); no tpc_module exists"),
                 "docstring); no tpc_module exists"),
       "cases": CASES}
       "cases": CASES}
with open(os.path.join(HERE, "vectors", "module_corpus.json"), "w", encoding="utf-8") as f:
with open(os.path.join(HERE, "vectors", "module_corpus.json"), "w", encoding="utf-8", newline="\n") as f:
     json.dump(out, f, indent=1, sort_keys=True, ensure_ascii=True)
     json.dump(out, f, indent=1, sort_keys=True, ensure_ascii=True)
     f.write("\n")
     f.write("\n")

Latest revision as of 02:11, 30 September 2026

"""gen_module_corpus 0.2.0: builds vectors/module_corpus.json for tpc_module
(D178.1 (a)-(g); plan 0.8.0 P2-04; specification tpm_module_spec 0.2.0, which
applies the supervisor's rulings S48-S54 on RV's review 0.1.0).

VECTORS FIRST. Every expectation below is a literal written from
tpm_module_spec 0.2.0 before either residence was changed to it. The
generator never imports or runs tpc_module. It writes the corpus in the
RUN_CASE form of lint_corpus: {"corpus": "module", "version": "0.2.1",
"cases": [{"name", "fn", "args", "expected", "hand_checked"}]}, "expected"
being the rendered result (tpm_module_spec 2.3, 6.4 RENDER).

WHAT IS COMPUTED, AND WITH WHAT (tpm_module_spec section 9). Only values
that are digests are computed, and only by EXISTING, verified TPC-PY
functions over data written here:
  - a module digest (S49; spec section 4): tpc_digest.digest_fields over
    ["TPC-MODULE", "1", T], T the canonical text built by CT() below from a
    fixture dict; CT is anchored by the canonical texts of MATH 1.0.0 and of
    GRAV-UNIFORM 1.0.0 written out literally below (asserted equal), and by
    the escape anchors;
  - a D(TPK): tpc_zone.d_tpk over the expanded TPK written literally below
    (EXP_* dicts: every composed EQUATION, REFS, STATEMENT and emitted entry
    typed by hand); RV's oracle values for X54 and X57 are asserted equal;
  - a catalogue STATE DIGEST: tpc_digest.digest_fields.
CHECKS OF THE HAND DERIVATION (asserts; existing functions only): every
expanded TPK an expectation calls PASS runs through tpc_tests.kernel_tests
and tpc_tests.t3_consistency (S_MAX 1); every FAIL row's text is compared
with the existing function's; every eom label a refusal quotes is compared
with tpc_eom.eom_map; every canonical spelling written by hand is compared
with tpc_form.parse_form; every quoted T1 problem with
tpc_zone.t1_well_formed. The generator REFUSES TO WRITE when an assert
fails. Deterministic: no randomness, no clock.

Every physical value in the fixtures is a FIXTURE, not a declaration
(tpm_module_spec section 8): the ISO 2533:1975 values are VERIFY-HIGH (S6).
0.2.1 (increment 11, 2026-09-28): X64-X67, the base TPK read as the JSON
is written (C-MS-5); the corpus is written with LF line ends on every
platform (S65). No earlier case moves.
"""
import copy
import hashlib
import json
import os
import re
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import tpc_zone as Z          # noqa: E402  existing: _node, _tree_tpk, d_tpk, canon_decimal
import tpc_digest as G        # noqa: E402  existing: digest_fields
import tpc_form as F          # noqa: E402  existing: parse_form (assert only)
import tpc_units as U         # noqa: E402  existing: unit_parse (canonical unit)
import tpc_tests as K         # noqa: E402  existing: kernel_tests, t3_consistency (assert only)
import tpc_eom as E           # noqa: E402  existing: eom_map (assert only)

CORPUS_VERSION = "0.2.1"
CASES = []


def J(v):
    """tpm_module_spec 2.3: the rendering of a result."""
    return json.dumps(v, ensure_ascii=True, separators=(",", ":"))


def OK(*items):
    return J(["OK"] + list(items))


def REF(msg):
    return J(["REFUSED", msg])


_HEX64 = re.compile(r"[0-9a-f]{64}")


def add(name, fn, args, expected):
    """hand_checked is false exactly where the expectation holds a digest or a
    D(TPK): those are computed by existing TPC-PY functions over literal data
    (the docstring); every other character of every expectation is typed here."""
    c = {"name": name, "fn": fn, "args": args, "expected": expected,
         "hand_checked": _HEX64.search(expected) is None}
    if not c["hand_checked"]:
        c["computed"] = "digests by existing TPC-PY functions over literal data"
    CASES.append(c)


# ------------------------------------------------ existing-function helpers
# The canonical text of a module (tpm_module_spec 0.2.0 section 4, S49): the
# module schema's objects in FIELD ORDER, every other object by key in code
# point order, strings as tpc_zone._text escaped only for '"', '\\' and the
# controls (\u00XX lower case), integers in decimal, no white space.
FIELD_ORDER = {
    "MODULE": ["ID", "VERSION", "TITLE", "SOURCE", "PREREQUISITES", "PARAMETERS", "SUMS", "TERMS",
               "ASSUMPTIONS", "BOUNDARIES", "NOT_CHOSEN", "NOT_INCLUDED", "EXCLUDES",
               "INTERACTS_WITH", "TEST_BINDINGS"],
    "PREREQUISITES": ["SLOT", "KIND", "DIMENSION", "MIN", "MAX", "LABEL"],
    "PARAMETERS": ["NAME", "DIMENSION", "SUPPLY", "LABEL", "VALUE", "UNIT", "UNCERTAINTY",
                   "OVERRIDABLE"],
    "SUMS": ["NAME", "OVER", "EACH"],
    "TERMS": ["TARGET", "SIGN", "EXPR", "LABEL"],
    "ASSUMPTIONS": ["NAME", "PROPOSITION", "REFS"],
    "BOUNDARIES": ["NAME", "PREDICATE", "CONDITION"],
    "INTERACTS_WITH": ["WITH", "QUESTION_ID", "QUESTION", "YES_LINE", "NO_LINE"],
    "TEST_BINDINGS": ["NAME", "TPK", "BIND", "VALUES", "ANSWERS"],
}


def JS(s):
    out = ['"']
    for ch in Z._text(s):
        if ch == '"' or ch == "\\":
            out.append("\\" + ch)
        elif ord(ch) < 0x20:
            out.append("\\u%04x" % ord(ch))
        else:
            out.append(ch)
    return "".join(out) + '"'


def GV(v):
    """A value outside the module schema's objects: keys in code point order."""
    if type(v) is str:
        return JS(v)
    if type(v) is int:
        return str(v)
    if type(v) is list:
        return "[" + ",".join(GV(x) for x in v) + "]"
    assert type(v) is dict, v
    return "{" + ",".join(JS(k) + ":" + GV(v[k]) for k in sorted(v)) + "}"


def SO(d, order):
    return "{" + ",".join(JS(k) + ":" + GV(d[k]) for k in order if k in d) + "}"


def CT(m):
    parts = []
    for k in FIELD_ORDER["MODULE"]:
        v = m[k]
        if k in FIELD_ORDER:
            parts.append(JS(k) + ":[" + ",".join(SO(e, FIELD_ORDER[k]) for e in v) + "]")
        else:
            parts.append(JS(k) + ":" + GV(v))
    return "{" + ",".join(parts) + "}"


def MDIG(m):
    """A module digest (S49): digest_fields(["TPC-MODULE", "1", CT(m)])."""
    return G.digest_fields(["TPC-MODULE", "1", CT(m)])


assert JS('a"b\\c\td\ne\r\nf\u007f') == '"a\\"b\\\\c\\u0009d\\u000ae\\u000af\u007f"'
assert JS("cafe\u0301") == '"caf\u00e9"'


def DTPK(tpk):
    return Z.d_tpk(tpk)


def CD(s):
    r = Z.canon_decimal(s)
    assert r is not None, s
    return r


def CU(u):
    r = U.unit_parse(u)
    assert r[0] == "OK", u
    return r[1]


def SD(offer):
    """STATE DIGEST of an OFFERED list (tpm_module_spec 6.8)."""
    f = ["TPC-MODULE-OFFER", "1"]
    for i, v, d in offer:
        f += [i, v, d]
    return G.digest_fields(f)


def HX(label):
    """An arbitrary 64-hex digest for lifecycle-only lines (data)."""
    return hashlib.sha256(label.encode("ascii")).hexdigest()


def canon_is(text, mode):
    r = F.parse_form(text, mode)
    assert r == ["OK", text], (text, r)
    return text


def must_pass(tpk, tag):
    kt = K.kernel_tests(tpk)
    assert kt[0] is True, (tag, kt)
    t3 = K.t3_consistency(tpk, {"PARAMETERS": {"S_MAX": 1}})
    assert t3[0] is True, (tag, t3)


def first_kt_problem(tpk):
    for name, passed, probs in K.kernel_tests(tpk)[1]:
        if passed is not True and passed != "NO FORM":
            return name + ": " + probs[0]
    return None


def label_of(tpk, q):
    m = E.eom_map({"TPK": tpk})
    assert m[0] == "OK", m
    r = dict((row[0], row[1:]) for row in m[1])
    for dof, label, law in r["MAP"][0]:
        if dof == q:
            return label, law
    raise AssertionError(q)


def t1_first(tpk):
    r = Z.t1_well_formed(tpk)
    assert r[0] is False
    return r[1][0]


# =================================================================== TPKs
def law(n, eq, role, refs, st):
    return {"NAME": n, "STATEMENT": st, "REFS": refs, "ROLE": role, "EQUATION": eq}


def dof(n, d, role=None):
    e = {"NAME": n, "DIMENSION": d}
    if role:
        e["ROLE"] = role
    return e


P2 = {
    "DOF": [dof("t", "T1"), dof("x", "L1"), dof("z", "L1"), dof("v_x", "L1 T-1"),
            dof("v_z", "L1 T-1")],
    "CONSTANT": [{"NAME": "m", "DIMENSION": "M1"}],
    "LAW": [law("KIN_X", "D(x,t)=v_x", "EOM(x)", ["t", "v_x", "x"], "dx/dt = v_x"),
            law("KIN_Z", "D(z,t)=v_z", "EOM(z)", ["t", "v_z", "z"], "dz/dt = v_z"),
            law("EOM_X", "m*D(v_x,t)=0", "EOM(v_x)", ["m", "t", "v_x"], "m dv_x/dt = 0"),
            law("EOM_Z", "m*D(v_z,t)=0", "EOM(v_z)", ["m", "t", "v_z"], "m dv_z/dt = 0")],
    "ASSUMPTION": [{"NAME": "point_mass", "PROPOSITION": "the body is treated as a point mass",
                    "REFS": ["m"]}],
}
must_pass(P2, "P2")

TABLE = {
    "DOF": [dof("t", "T1"), dof("x", "L1"), dof("z", "L1"), dof("v_x", "L1 T-1"),
            dof("v_z", "L1 T-1")],
    "CONSTANT": [{"NAME": "m", "DIMENSION": "M1"}],
    "LAW": [law("KIN_X", "D(x,t)=v_x", "EOM(x)", ["t", "v_x", "x"], "dx/dt = v_x"),
            law("KIN_Z", "D(z,t)=v_z", "CONSTRAINT", ["t", "v_z", "z"], "dz/dt = v_z"),
            law("EOM_X", "m*D(v_x,t)=0", "EOM(v_x)", ["m", "t", "v_x"], "m dv_x/dt = 0")],
    "CONSTRAINT": [{"NAME": "C_Z", "TARGET": "z", "ACTION": "FIX", "VALUE": "0", "UNIT": "m",
                    "STATEMENT": "the puck stays on the table top, z = 0"},
                   {"NAME": "C_VZ", "TARGET": "v_z", "ACTION": "FIX", "VALUE": "0",
                    "UNIT": "m s-1", "STATEMENT": "the puck has no vertical velocity"}],
}
must_pass(TABLE, "TABLE")
assert label_of(TABLE, "v_z") == ("FIXED", "")

SOLO = {
    "DOF": [dof("t", "T1"), dof("q", "L1")],
    "LAW": [law("EQ_Q", "D(q,t)=0", "EOM(q)", ["q", "t"], "dq/dt = 0")],
}
must_pass(SOLO, "SOLO")


def tpk_mod(base, laws=None, add_entries=(), drop_laws=(), extra=None):
    """Plumbing over literal values: base with some laws replaced by literal
    (EQUATION, REFS, STATEMENT) triples and literal entries appended."""
    t = copy.deepcopy(base)
    for i, e in enumerate(t.get("LAW", [])):
        if laws and e["NAME"] in laws:
            eq, refs, st = laws[e["NAME"]]
            t["LAW"][i] = {"NAME": e["NAME"], "STATEMENT": st, "REFS": refs, "ROLE": e["ROLE"],
                           "EQUATION": eq}
    if drop_laws:
        t["LAW"] = [e for e in t["LAW"] if e["NAME"] not in drop_laws]
    for kind, e in add_entries:
        t.setdefault(kind, []).append(copy.deepcopy(e))
    if extra:
        extra(t)
    return t


# ======================================================== module fixtures
# ---- GRAV-UNIFORM 1.0.0
GRAV_TITLE = "Uniform gravity at the standard value g_n"
GRAV_SOURCE = ("g_n = 9.80665 m s-2, exact by definition: 3rd CGPM (1901), declaration on "
               "the unit of mass and on the definition of weight")
L_MASS = "the mass of the body"
L_UPV = "the velocity component pointing up; its equation of motion receives the weight"
L_HEIGHT = "the position along the same upward axis"
L_GN = "the standard acceleration of gravity"
L_HMAX = "the largest height, above or below the origin, over which the field is taken as uniform"
PROP_GU = "the gravitational field is uniform over the region abs({HEIGHT}) < h_max"
PROP_GS = "the local gravitational acceleration equals the conventional standard value g_n"
PROP_FI = "the frame in which {HEIGHT} is measured is treated as inertial"
PRED_GR = "abs({HEIGHT}) < h_max"
GRAV_NC = "No gravity: no weight acts on any DOF."
GRAV_NI = ["No variation of gravity with height or latitude beyond what g_n contains.",
           "No Earth rotation (Coriolis or centrifugal force) beyond what g_n contains.",
           "No gravity of bodies other than the Earth."]
Q_B = "Does the air's buoyancy (upthrust) act on the body in the system you mean?"
YES_B = ("Buoyancy acts (your answer): no module adds it; it acts in this kernel only where "
         "one of your own laws states it.")
NO_B = "No buoyancy: the air's upthrust on the body is not included."

PRE_MASS = {"SLOT": "MASS", "KIND": "CONSTANT", "DIMENSION": "M1", "MIN": 1, "MAX": 1,
            "LABEL": L_MASS}
PRE_UPV = {"SLOT": "UP_VELOCITY", "KIND": "DOF", "DIMENSION": "L1 T-1", "MIN": 1, "MAX": 1,
           "LABEL": L_UPV}
PRE_HEIGHT = {"SLOT": "HEIGHT", "KIND": "DOF", "DIMENSION": "L1", "MIN": 1, "MAX": 1,
              "LABEL": L_HEIGHT}
PAR_GN = {"NAME": "g_n", "DIMENSION": "L1 T-2", "SUPPLY": "DECLARED", "VALUE": "9.80665",
          "UNIT": "m s-2", "OVERRIDABLE": "NO", "LABEL": L_GN}
PAR_HMAX = {"NAME": "h_max", "DIMENSION": "L1", "SUPPLY": "AUTHOR", "LABEL": L_HMAX}
GRAV_BIND = {"HEIGHT": ["z"], "MASS": ["m"], "UP_VELOCITY": ["v_z"]}
TB_GRAV_P2 = {"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": GRAV_BIND,
              "VALUES": {"h_max": {"VALUE": "100", "UNIT": "m"}}, "ANSWERS": {}}
TB_GRAV_TABLE = {"NAME": "TABLE", "TPK": TABLE, "BIND": GRAV_BIND,
                 "VALUES": {"h_max": {"VALUE": "1", "UNIT": "m"}}, "ANSWERS": {"FIXED:v_z": "YES"}}
GRAV = {
    "ID": "GRAV-UNIFORM", "VERSION": "1.0.0", "TITLE": GRAV_TITLE, "SOURCE": GRAV_SOURCE,
    "PREREQUISITES": [PRE_MASS, PRE_UPV, PRE_HEIGHT],
    "PARAMETERS": [PAR_GN, PAR_HMAX],
    "SUMS": [],
    "TERMS": [{"TARGET": "UP_VELOCITY", "SIGN": "-", "EXPR": "MASS*g_n", "LABEL": "the weight"}],
    "ASSUMPTIONS": [{"NAME": "gravity_uniform", "PROPOSITION": PROP_GU,
                     "REFS": ["HEIGHT", "h_max"]},
                    {"NAME": "gravity_standard", "PROPOSITION": PROP_GS, "REFS": ["g_n"]},
                    {"NAME": "frame_inertial", "PROPOSITION": PROP_FI, "REFS": ["HEIGHT"]}],
    "BOUNDARIES": [{"NAME": "gravity_region", "PREDICATE": PRED_GR,
                    "CONDITION": "abs(HEIGHT) < h_max"}],
    "NOT_CHOSEN": GRAV_NC, "NOT_INCLUDED": GRAV_NI, "EXCLUDES": [],
    "INTERACTS_WITH": [{"WITH": "AIR-*", "QUESTION_ID": "BUOYANCY", "QUESTION": Q_B,
                        "YES_LINE": YES_B, "NO_LINE": NO_B}],
    "TEST_BINDINGS": [TB_GRAV_P2, TB_GRAV_TABLE],
}
assert CD("9.80665") == "980665e-5" and CU("m s-2") == "m s-2" and CD("100") == "1e2"
canon_is("MASS*g_n", "expression")
canon_is("abs(HEIGHT)<h_max", "predicate")

# ---- GRAV-UNIFORM 1.1.0 (the same physics; only TITLE revised)
GRAV11_TITLE = "Uniform gravity at the standard value g_n (text revised)"
GRAV11 = copy.deepcopy(GRAV)
GRAV11["VERSION"] = "1.1.0"
GRAV11["TITLE"] = GRAV11_TITLE

# ---- GRAV-UNIFORM 1.2.0 (one assumption added: a different expansion)
PROP_NT = "no tidal force acts on the body"
GRAV12 = copy.deepcopy(GRAV)
GRAV12["VERSION"] = "1.2.0"
GRAV12["ASSUMPTIONS"].append({"NAME": "gravity_no_tides", "PROPOSITION": PROP_NT,
                              "REFS": ["HEIGHT"]})

# ---- GRAV-UNIFORM 2.0.0 (a new AUTHOR parameter z_ref)
L_ZREF = "the height of the origin above sea level"
GRAV20 = copy.deepcopy(GRAV)
GRAV20["VERSION"] = "2.0.0"
GRAV20["PARAMETERS"].append({"NAME": "z_ref", "DIMENSION": "L1", "SUPPLY": "AUTHOR",
                             "LABEL": L_ZREF})
GRAV20["TEST_BINDINGS"][0]["VALUES"] = {"h_max": {"VALUE": "100", "UNIT": "m"}, "z_ref": {}}
GRAV20["TEST_BINDINGS"][1]["VALUES"] = {"h_max": {"VALUE": "1", "UNIT": "m"}, "z_ref": {}}

# ---- AIR-QUADRATIC 1.0.0 and AIR-LINEAR 1.0.0 (ISO 2533:1975 values VERIFY-HIGH)
AIR_SOURCE = ("ISO 2533:1975, Standard Atmosphere, sea-level values: density 1.225 kg m-3, "
              "dynamic viscosity 1.7894e-5 Pa s")
AIRQ_TITLE = "Air resistance at sea level, quadratic in the speed"
AIRL_TITLE = "Air resistance at sea level, linear in the speed (Stokes drag on a sphere)"
L_VEL = "the velocity components of the body, one per axis"
L_RHO = "the density of air at sea level"
L_MU = "the dynamic viscosity of air at sea level"
L_CD = "the drag coefficient of the body"
L_AREF = "the reference area of the drag coefficient"
L_LREF = "the reference length of the Reynolds number"
L_PI = "pi, to 16 significant digits"
L_RS = "the radius of the sphere"
PROP_AS = "the air is at rest in the frame of the body's velocity"
PROP_AU = "the air density is uniform over the region of motion"
PROP_CD = "the drag coefficient C_d is constant over the motion"
PROP_NL = "the body experiences no lift force"
PROP_NM = "the body experiences no Magnus force"
PROP_SP = "the body is a rigid sphere of radius R_s"
PRED_Q = "1e3 < rho_air |v| L_ref / mu_air < 2e5: the Reynolds window of the quadratic law"
PRED_L = "2 rho_air |v| R_s / mu_air < 1: the regime of the linear law"
COND_Q = ("mu_air != 0 and SPEED2 >= 0 and rho_air*sqrt(SPEED2)*L_ref/mu_air > 1000 "
          "and rho_air*sqrt(SPEED2)*L_ref/mu_air < 200000")
COND_Qc = ("mu_air!=0 and SPEED2>=0 and rho_air*sqrt(SPEED2)*L_ref/mu_air>1e3 and "
           "rho_air*sqrt(SPEED2)*L_ref/mu_air<2e5")
COND_L = "mu_air != 0 and SPEED2 >= 0 and 2*rho_air*sqrt(SPEED2)*R_s/mu_air < 1"
COND_Lc = "mu_air!=0 and SPEED2>=0 and 2*rho_air*sqrt(SPEED2)*R_s/mu_air<1"
assert F.parse_form(COND_Q, "predicate") == ["OK", COND_Qc]
assert F.parse_form(COND_L, "predicate") == ["OK", COND_Lc]
EXPR_Q = "0.5*rho_air*C_d*A_ref*sqrt(SPEED2)*VELOCITY"
EXPR_Qc = "5e-1*rho_air*C_d*A_ref*sqrt(SPEED2)*VELOCITY"
assert F.parse_form(EXPR_Q, "expression") == ["OK", EXPR_Qc]
EXPR_L = "6*pi_c*mu_air*R_s*VELOCITY"
canon_is(EXPR_L, "expression")
canon_is("VELOCITY^2", "expression")
AIRQ_NC = "No quadratic air resistance."
AIRL_NC = "No linear (Stokes) air resistance."
AIR_NI_WIND = "No wind: the air is at rest."
AIR_NI_ALT = "No change of air density or viscosity with altitude or temperature."
AIRQ_NI = [AIR_NI_WIND, AIR_NI_ALT, "No lift, Magnus or added-mass force.",
           "No drag outside the Reynolds window of the quadratic law."]
AIRL_NI = [AIR_NI_WIND, AIR_NI_ALT, "No drag outside the regime of the linear law."]

PRE_VEL = {"SLOT": "VELOCITY", "KIND": "DOF", "DIMENSION": "L1 T-1", "MIN": 1, "MAX": 3,
           "LABEL": L_VEL}
PAR_RHO = {"NAME": "rho_air", "DIMENSION": "L-3 M1", "SUPPLY": "DECLARED", "VALUE": "1.225",
           "UNIT": "kg m-3", "OVERRIDABLE": "NO", "LABEL": L_RHO}
PAR_MU = {"NAME": "mu_air", "DIMENSION": "L-1 M1 T-1", "SUPPLY": "DECLARED",
          "VALUE": "1.7894e-5", "UNIT": "Pa s", "OVERRIDABLE": "NO", "LABEL": L_MU}
PAR_PI = {"NAME": "pi_c", "DIMENSION": "1", "SUPPLY": "DECLARED", "VALUE": "3.141592653589793",
          "UNIT": "1", "OVERRIDABLE": "NO", "LABEL": L_PI}
SUM_S2 = {"NAME": "SPEED2", "OVER": "VELOCITY", "EACH": "VELOCITY^2"}
ASM_AS = {"NAME": "air_still", "PROPOSITION": PROP_AS, "REFS": ["VELOCITY"]}
ASM_AU = {"NAME": "air_uniform", "PROPOSITION": PROP_AU, "REFS": ["rho_air"]}
AIRQ = {
    "ID": "AIR-QUADRATIC", "VERSION": "1.0.0", "TITLE": AIRQ_TITLE, "SOURCE": AIR_SOURCE,
    "PREREQUISITES": [PRE_VEL],
    "PARAMETERS": [PAR_RHO, PAR_MU,
                   {"NAME": "C_d", "DIMENSION": "1", "SUPPLY": "AUTHOR", "LABEL": L_CD},
                   {"NAME": "A_ref", "DIMENSION": "L2", "SUPPLY": "AUTHOR", "LABEL": L_AREF},
                   {"NAME": "L_ref", "DIMENSION": "L1", "SUPPLY": "AUTHOR", "LABEL": L_LREF}],
    "SUMS": [SUM_S2],
    "TERMS": [{"TARGET": "VELOCITY", "SIGN": "-", "EXPR": EXPR_Q, "LABEL": "the quadratic drag"}],
    "ASSUMPTIONS": [ASM_AS, ASM_AU,
                    {"NAME": "drag_cd_constant", "PROPOSITION": PROP_CD, "REFS": ["C_d"]},
                    {"NAME": "drag_no_lift", "PROPOSITION": PROP_NL, "REFS": ["VELOCITY"]},
                    {"NAME": "drag_no_magnus", "PROPOSITION": PROP_NM, "REFS": ["VELOCITY"]}],
    "BOUNDARIES": [{"NAME": "drag_quadratic_regime", "PREDICATE": PRED_Q, "CONDITION": COND_Q}],
    "NOT_CHOSEN": AIRQ_NC, "NOT_INCLUDED": AIRQ_NI, "EXCLUDES": ["AIR-LINEAR"],
    "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {"VELOCITY": ["v_x", "v_z"]},
                       "VALUES": {"A_ref": {}, "C_d": {}, "L_ref": {}}, "ANSWERS": {}}],
}
assert CD("1.225") == "1225e-3" and CU("kg m-3") == "m-3 kg"
assert CD("1.7894e-5") == "17894e-9" and CU("Pa s") == "s Pa"
AIRL = {
    "ID": "AIR-LINEAR", "VERSION": "1.0.0", "TITLE": AIRL_TITLE, "SOURCE": AIR_SOURCE,
    "PREREQUISITES": [PRE_VEL],
    "PARAMETERS": [PAR_RHO, PAR_MU, PAR_PI,
                   {"NAME": "R_s", "DIMENSION": "L1", "SUPPLY": "AUTHOR", "LABEL": L_RS}],
    "SUMS": [SUM_S2],
    "TERMS": [{"TARGET": "VELOCITY", "SIGN": "-", "EXPR": EXPR_L,
               "LABEL": "the linear (Stokes) drag"}],
    "ASSUMPTIONS": [ASM_AS, ASM_AU,
                    {"NAME": "drag_sphere", "PROPOSITION": PROP_SP, "REFS": ["R_s"]}],
    "BOUNDARIES": [{"NAME": "drag_linear_regime", "PREDICATE": PRED_L, "CONDITION": COND_L}],
    "NOT_CHOSEN": AIRL_NC, "NOT_INCLUDED": AIRL_NI, "EXCLUDES": [], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {"VELOCITY": ["v_x", "v_z"]},
                       "VALUES": {"R_s": {}}, "ANSWERS": {}}],
}
assert CD("3.141592653589793") == "3141592653589793e-15" and CU("1") == "1"

# ---- MATH 1.0.0
MATH_TITLE = "The mathematical constant pi, as the CONSTANT pi_c"
MATH_SOURCE = "pi to 16 significant digits, 3.141592653589793, the nearest IEEE 754 binary64 value"
MATH_NC = "No mathematical constant: pi exists only if you declare it."
MATH_NI = ["pi_c is pi rounded to 16 significant digits; no other mathematical constant is "
           "declared."]
MATH = {
    "ID": "MATH", "VERSION": "1.0.0", "TITLE": MATH_TITLE, "SOURCE": MATH_SOURCE,
    "PREREQUISITES": [], "PARAMETERS": [PAR_PI], "SUMS": [], "TERMS": [], "ASSUMPTIONS": [],
    "BOUNDARIES": [], "NOT_CHOSEN": MATH_NC, "NOT_INCLUDED": MATH_NI, "EXCLUDES": [],
    "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {}, "VALUES": {},
                       "ANSWERS": {}}],
}

# ---- synthetic fixtures (lifecycle, conflict, sign, override, fail rows)
TPI_TITLE = "Test: a rough pi"
TEST_PI = {
    "ID": "TEST-PI", "VERSION": "1.0.0", "TITLE": TPI_TITLE, "SOURCE": "synthetic fixture",
    "PREREQUISITES": [],
    "PARAMETERS": [{"NAME": "pi_c", "DIMENSION": "1", "SUPPLY": "DECLARED", "VALUE": "3.14",
                    "UNIT": "1", "OVERRIDABLE": "NO", "LABEL": "pi, roughly"}],
    "SUMS": [], "TERMS": [], "ASSUMPTIONS": [], "BOUNDARIES": [],
    "NOT_CHOSEN": "No rough pi.", "NOT_INCLUDED": [], "EXCLUDES": [], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {}, "VALUES": {},
                       "ANSWERS": {}}],
}
assert CD("3.14") == "314e-2"

TX_TITLE = "Test interaction X"
TEST_X = {
    "ID": "TEST-X", "VERSION": "1.0.0", "TITLE": TX_TITLE, "SOURCE": "synthetic fixture",
    "PREREQUISITES": [],
    "PARAMETERS": [{"NAME": "x_test", "DIMENSION": "1", "SUPPLY": "DECLARED", "VALUE": "1",
                    "UNIT": "1", "OVERRIDABLE": "NO", "LABEL": "a test constant"}],
    "SUMS": [], "TERMS": [], "ASSUMPTIONS": [], "BOUNDARIES": [],
    "NOT_CHOSEN": "No test interaction X.", "NOT_INCLUDED": ["Test X adds only a constant."],
    "EXCLUDES": [], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "SOLO", "TPK": SOLO, "BIND": {}, "VALUES": {}, "ANSWERS": {}}],
}

TE_TITLE = "Test: excludes MATH, one way"
TEST_EXCL = {
    "ID": "TEST-EXCL", "VERSION": "1.0.0", "TITLE": TE_TITLE, "SOURCE": "synthetic fixture",
    "PREREQUISITES": [], "PARAMETERS": [], "SUMS": [], "TERMS": [], "ASSUMPTIONS": [],
    "BOUNDARIES": [], "NOT_CHOSEN": "No exclusion test.", "NOT_INCLUDED": [],
    "EXCLUDES": ["MATH"], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {}, "VALUES": {},
                       "ANSWERS": {}}],
}

TP_TITLE = "Test: a push and a pull"
L_PUSHED = "the velocity that receives the push"
L_PULLED = "the velocity that receives the pull"
L_FA = "the first applied force"
L_FB = "the second applied force"
TEST_PUSH = {
    "ID": "TEST-PUSH", "VERSION": "1.0.0", "TITLE": TP_TITLE, "SOURCE": "synthetic fixture",
    "PREREQUISITES": [{"SLOT": "PUSHED", "KIND": "DOF", "DIMENSION": "L1 T-1", "MIN": 1, "MAX": 1,
                       "LABEL": L_PUSHED},
                      {"SLOT": "PULLED", "KIND": "DOF", "DIMENSION": "L1 T-1", "MIN": 1, "MAX": 1,
                       "LABEL": L_PULLED}],
    "PARAMETERS": [{"NAME": "F_a", "DIMENSION": "L1 M1 T-2", "SUPPLY": "AUTHOR", "LABEL": L_FA},
                   {"NAME": "F_b", "DIMENSION": "L1 M1 T-2", "SUPPLY": "AUTHOR", "LABEL": L_FB}],
    "SUMS": [],
    "TERMS": [{"TARGET": "PUSHED", "SIGN": "+", "EXPR": "F_a+F_b", "LABEL": "the push"},
              {"TARGET": "PULLED", "SIGN": "-", "EXPR": "F_a+F_b", "LABEL": "the pull"}],
    "ASSUMPTIONS": [], "BOUNDARIES": [], "NOT_CHOSEN": "No push or pull.", "NOT_INCLUDED": [],
    "EXCLUDES": [], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2,
                       "BIND": {"PULLED": ["v_z"], "PUSHED": ["v_x"]},
                       "VALUES": {"F_a": {}, "F_b": {}}, "ANSWERS": {}}],
}

TO_TITLE = "Test: an overridable declared value"
TEST_OVR = {
    "ID": "TEST-OVR", "VERSION": "1.0.0", "TITLE": TO_TITLE, "SOURCE": "synthetic fixture",
    "PREREQUISITES": [],
    "PARAMETERS": [{"NAME": "k_loc", "DIMENSION": "1", "SUPPLY": "DECLARED", "VALUE": "2",
                    "UNIT": "1", "OVERRIDABLE": "YES", "LABEL": "a local factor"}],
    "SUMS": [], "TERMS": [], "ASSUMPTIONS": [], "BOUNDARIES": [],
    "NOT_CHOSEN": "No local factor.", "NOT_INCLUDED": [], "EXCLUDES": [], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {}, "VALUES": {},
                       "ANSWERS": {}}],
}

TB_TITLE = "Test: a boundary without its guard"
TEST_BADB = {
    "ID": "TEST-BADB", "VERSION": "1.0.0", "TITLE": TB_TITLE, "SOURCE": "synthetic fixture",
    "PREREQUISITES": [{"SLOT": "SPEEDY", "KIND": "DOF", "DIMENSION": "L1 T-1", "MIN": 1, "MAX": 1,
                       "LABEL": "a velocity"}],
    "PARAMETERS": [{"NAME": "k_b", "DIMENSION": "1", "SUPPLY": "AUTHOR", "LABEL": "a number"},
                   {"NAME": "h_b", "DIMENSION": "1", "SUPPLY": "AUTHOR", "LABEL": "a divisor"}],
    "SUMS": [], "TERMS": [], "ASSUMPTIONS": [],
    "BOUNDARIES": [{"NAME": "bad_region", "PREDICATE": "k_b / h_b > 1",
                    "CONDITION": "k_b/h_b > 1"}],
    "NOT_CHOSEN": "No guard test.", "NOT_INCLUDED": [], "EXCLUDES": [], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {"SPEEDY": ["v_x"]},
                       "VALUES": {"h_b": {}, "k_b": {}}, "ANSWERS": {}},
                      {"NAME": "WRONG", "TPK": P2, "BIND": {"SPEEDY": ["z"]},
                       "VALUES": {"h_b": {}, "k_b": {}}, "ANSWERS": {}}],
}
canon_is("k_b/h_b>1", "predicate")

# ---- fixtures of RV's review (rv_additions.py; F-9) and of S51
TEST_SUMP = {
    "ID": "TEST-SUMP", "VERSION": "1.0.0", "TITLE": "Test: a sum inside a product",
    "SOURCE": "synthetic fixture", "PREREQUISITES": [PRE_VEL],
    "PARAMETERS": [{"NAME": "k_q", "DIMENSION": "L-1 M1", "SUPPLY": "AUTHOR",
                    "LABEL": "a quadratic coefficient"}],
    "SUMS": [SUM_S2],
    "TERMS": [{"TARGET": "VELOCITY", "SIGN": "-", "EXPR": "k_q*SPEED2", "LABEL": "the test force"}],
    "ASSUMPTIONS": [], "BOUNDARIES": [], "NOT_CHOSEN": "No sum test.", "NOT_INCLUDED": [],
    "EXCLUDES": [], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {"VELOCITY": ["v_x", "v_z"]},
                       "VALUES": {"k_q": {}}, "ANSWERS": {}}],
}
TEST_NOAIR = {
    "ID": "TEST-NOAIR", "VERSION": "1.0.0", "TITLE": "Test: excludes every AIR module",
    "SOURCE": "synthetic fixture", "PREREQUISITES": [], "PARAMETERS": [], "SUMS": [], "TERMS": [],
    "ASSUMPTIONS": [], "BOUNDARIES": [], "NOT_CHOSEN": "No pattern test.", "NOT_INCLUDED": [],
    "EXCLUDES": ["AIR-*"], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {}, "VALUES": {},
                       "ANSWERS": {}}],
}
FIX2 = {"DOF": [dof("t", "T1"), dof("x", "L1")],
        "LAW": [law("EQ_X", "D(x,t)=0", "EOM(x)", ["t", "x"], "dx/dt = 0")],
        "CONSTRAINT": [{"NAME": "C1", "TARGET": "x", "ACTION": "FIX", "VALUE": "0", "UNIT": "m",
                        "STATEMENT": "x is held at 0"},
                       {"NAME": "C2", "TARGET": "x", "ACTION": "FIX", "VALUE": "1", "UNIT": "m",
                        "STATEMENT": "x is held at 1"}]}
TEST_T3 = {
    "ID": "TEST-T3", "VERSION": "1.0.0", "TITLE": "Test: a binding failing T3",
    "SOURCE": "synthetic fixture", "PREREQUISITES": [],
    "PARAMETERS": [{"NAME": "k_t", "DIMENSION": "1", "SUPPLY": "DECLARED", "VALUE": "1",
                    "UNIT": "1", "OVERRIDABLE": "NO", "LABEL": "a constant"}],
    "SUMS": [], "TERMS": [], "ASSUMPTIONS": [], "BOUNDARIES": [], "NOT_CHOSEN": "No T3 test.",
    "NOT_INCLUDED": [], "EXCLUDES": [], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "FIX2", "TPK": FIX2, "BIND": {}, "VALUES": {}, "ANSWERS": {}}],
}
TEST_SHARE = {
    "ID": "TEST-SHARE", "VERSION": "1.0.0", "TITLE": "Test: an author parameter shared by name",
    "SOURCE": "synthetic fixture", "PREREQUISITES": [],
    "PARAMETERS": [{"NAME": "k_q", "DIMENSION": "L-1 M1", "SUPPLY": "AUTHOR",
                    "LABEL": "a quadratic coefficient, named by another module too"}],
    "SUMS": [], "TERMS": [], "ASSUMPTIONS": [], "BOUNDARIES": [],
    "NOT_CHOSEN": "No shared-name test.", "NOT_INCLUDED": [], "EXCLUDES": [], "INTERACTS_WITH": [],
    "TEST_BINDINGS": [{"NAME": "PROJECTILE_2D", "TPK": P2, "BIND": {}, "VALUES": {"k_q": {}},
                       "ANSWERS": {}}],
}
TEST_KQ1 = copy.deepcopy(TEST_SHARE)
TEST_KQ1["ID"] = "TEST-KQ1"
TEST_KQ1["TITLE"] = "Test: an author parameter of another dimension"
TEST_KQ1["PARAMETERS"][0]["DIMENSION"] = "1"
TEST_KQ1["NOT_CHOSEN"] = "No dimension test."

# ---- texts and digests
def TXT(d):
    return json.dumps(d, ensure_ascii=True)


X_GRAV, X_GRAV11, X_GRAV12, X_GRAV20 = TXT(GRAV), TXT(GRAV11), TXT(GRAV12), TXT(GRAV20)
X_AIRQ, X_AIRL, X_MATH = TXT(AIRQ), TXT(AIRL), TXT(MATH)
X_TPI, X_TX, X_TEXCL, X_TPUSH = TXT(TEST_PI), TXT(TEST_X), TXT(TEST_EXCL), TXT(TEST_PUSH)
X_TOVR, X_TBADB = TXT(TEST_OVR), TXT(TEST_BADB)
D_GRAV, D_GRAV11, D_GRAV12, D_GRAV20 = MDIG(GRAV), MDIG(GRAV11), MDIG(GRAV12), MDIG(GRAV20)
D_AIRQ, D_AIRL, D_MATH = MDIG(AIRQ), MDIG(AIRL), MDIG(MATH)
D_TPI, D_TX, D_TEXCL, D_TPUSH = MDIG(TEST_PI), MDIG(TEST_X), MDIG(TEST_EXCL), MDIG(TEST_PUSH)
D_TOVR, D_TBADB = MDIG(TEST_OVR), MDIG(TEST_BADB)
X_SUMP, X_NOAIR, X_T3, X_SHARE, X_KQ1 = (TXT(TEST_SUMP), TXT(TEST_NOAIR), TXT(TEST_T3),
                                         TXT(TEST_SHARE), TXT(TEST_KQ1))
D_SUMP, D_NOAIR, D_T3, D_SHARE, D_KQ1 = (MDIG(TEST_SUMP), MDIG(TEST_NOAIR), MDIG(TEST_T3),
                                         MDIG(TEST_SHARE), MDIG(TEST_KQ1))

# ---- the anchors of CT (tpm_module_spec 0.2.0 section 4), typed by hand from the spec
_DOF5 = ('"DOF":[{"DIMENSION":"T1","NAME":"t"},{"DIMENSION":"L1","NAME":"x"},'
         '{"DIMENSION":"L1","NAME":"z"},{"DIMENSION":"L1 T-1","NAME":"v_x"},'
         '{"DIMENSION":"L1 T-1","NAME":"v_z"}]')
_KINX = ('{"EQUATION":"D(x,t)=v_x","NAME":"KIN_X","REFS":["t","v_x","x"],"ROLE":"EOM(x)",'
         '"STATEMENT":"dx/dt = v_x"}')
_EOMX = ('{"EQUATION":"m*D(v_x,t)=0","NAME":"EOM_X","REFS":["m","t","v_x"],"ROLE":"EOM(v_x)",'
         '"STATEMENT":"m dv_x/dt = 0"}')
P2_CT = ('{"ASSUMPTION":[{"NAME":"point_mass","PROPOSITION":"the body is treated as a point mass",'
         '"REFS":["m"]}],"CONSTANT":[{"DIMENSION":"M1","NAME":"m"}],' + _DOF5 + ',"LAW":['
         + _KINX + ',{"EQUATION":"D(z,t)=v_z","NAME":"KIN_Z","REFS":["t","v_z","z"],"ROLE":"EOM(z)",'
         '"STATEMENT":"dz/dt = v_z"},' + _EOMX + ',{"EQUATION":"m*D(v_z,t)=0","NAME":"EOM_Z",'
         '"REFS":["m","t","v_z"],"ROLE":"EOM(v_z)","STATEMENT":"m dv_z/dt = 0"}]}')
TABLE_CT = ('{"CONSTANT":[{"DIMENSION":"M1","NAME":"m"}],"CONSTRAINT":[{"ACTION":"FIX","NAME":"C_Z",'
            '"STATEMENT":"the puck stays on the table top, z = 0","TARGET":"z","UNIT":"m",'
            '"VALUE":"0"},{"ACTION":"FIX","NAME":"C_VZ","STATEMENT":"the puck has no vertical '
            'velocity","TARGET":"v_z","UNIT":"m s-1","VALUE":"0"}],' + _DOF5 + ',"LAW":[' + _KINX
            + ',{"EQUATION":"D(z,t)=v_z","NAME":"KIN_Z","REFS":["t","v_z","z"],"ROLE":"CONSTRAINT",'
            '"STATEMENT":"dz/dt = v_z"},' + _EOMX + ']}')
MATH_CT = ('{"ID":"MATH","VERSION":"1.0.0","TITLE":"The mathematical constant pi, as the CONSTANT '
           'pi_c","SOURCE":"pi to 16 significant digits, 3.141592653589793, the nearest IEEE 754 '
           'binary64 value","PREREQUISITES":[],"PARAMETERS":[{"NAME":"pi_c","DIMENSION":"1",'
           '"SUPPLY":"DECLARED","LABEL":"pi, to 16 significant digits","VALUE":"3.141592653589793",'
           '"UNIT":"1","OVERRIDABLE":"NO"}],"SUMS":[],"TERMS":[],"ASSUMPTIONS":[],"BOUNDARIES":[],'
           '"NOT_CHOSEN":"No mathematical constant: pi exists only if you declare it.",'
           '"NOT_INCLUDED":["pi_c is pi rounded to 16 significant digits; no other mathematical '
           'constant is declared."],"EXCLUDES":[],"INTERACTS_WITH":[],"TEST_BINDINGS":[{"NAME":'
           '"PROJECTILE_2D","TPK":' + P2_CT + ',"BIND":{},"VALUES":{},"ANSWERS":{}}]}')
_GBIND = '"BIND":{"HEIGHT":["z"],"MASS":["m"],"UP_VELOCITY":["v_z"]}'
GRAV_CT = ('{"ID":"GRAV-UNIFORM","VERSION":"1.0.0","TITLE":"Uniform gravity at the standard value '
           'g_n","SOURCE":"g_n = 9.80665 m s-2, exact by definition: 3rd CGPM (1901), declaration '
           'on the unit of mass and on the definition of weight","PREREQUISITES":[{"SLOT":"MASS",'
           '"KIND":"CONSTANT","DIMENSION":"M1","MIN":1,"MAX":1,"LABEL":"the mass of the body"},'
           '{"SLOT":"UP_VELOCITY","KIND":"DOF","DIMENSION":"L1 T-1","MIN":1,"MAX":1,"LABEL":"the '
           'velocity component pointing up; its equation of motion receives the weight"},'
           '{"SLOT":"HEIGHT","KIND":"DOF","DIMENSION":"L1","MIN":1,"MAX":1,"LABEL":"the position '
           'along the same upward axis"}],"PARAMETERS":[{"NAME":"g_n","DIMENSION":"L1 T-2",'
           '"SUPPLY":"DECLARED","LABEL":"the standard acceleration of gravity","VALUE":"9.80665",'
           '"UNIT":"m s-2","OVERRIDABLE":"NO"},{"NAME":"h_max","DIMENSION":"L1","SUPPLY":"AUTHOR",'
           '"LABEL":"the largest height, above or below the origin, over which the field is taken '
           'as uniform"}],"SUMS":[],"TERMS":[{"TARGET":"UP_VELOCITY","SIGN":"-","EXPR":"MASS*g_n",'
           '"LABEL":"the weight"}],"ASSUMPTIONS":[{"NAME":"gravity_uniform","PROPOSITION":"the '
           'gravitational field is uniform over the region abs({HEIGHT}) < h_max","REFS":["HEIGHT",'
           '"h_max"]},{"NAME":"gravity_standard","PROPOSITION":"the local gravitational '
           'acceleration equals the conventional standard value g_n","REFS":["g_n"]},{"NAME":'
           '"frame_inertial","PROPOSITION":"the frame in which {HEIGHT} is measured is treated as '
           'inertial","REFS":["HEIGHT"]}],"BOUNDARIES":[{"NAME":"gravity_region","PREDICATE":'
           '"abs({HEIGHT}) < h_max","CONDITION":"abs(HEIGHT) < h_max"}],"NOT_CHOSEN":"No gravity: '
           'no weight acts on any DOF.","NOT_INCLUDED":["No variation of gravity with height or '
           'latitude beyond what g_n contains.","No Earth rotation (Coriolis or centrifugal force) '
           'beyond what g_n contains.","No gravity of bodies other than the Earth."],"EXCLUDES":[],'
           '"INTERACTS_WITH":[{"WITH":"AIR-*","QUESTION_ID":"BUOYANCY","QUESTION":"Does the '
           "air's buoyancy (upthrust) act on the body in the system you mean?\",\"YES_LINE\":"
           '"Buoyancy acts (your answer): no module adds it; it acts in this kernel only where one '
           'of your own laws states it.","NO_LINE":"No buoyancy: the air' + "'" + 's upthrust on the '
           'body is not included."}],"TEST_BINDINGS":[{"NAME":"PROJECTILE_2D","TPK":' + P2_CT + ','
           + _GBIND + ',"VALUES":{"h_max":{"UNIT":"m","VALUE":"100"}},"ANSWERS":{}},{"NAME":'
           '"TABLE","TPK":' + TABLE_CT + ',' + _GBIND + ',"VALUES":{"h_max":{"UNIT":"m","VALUE":'
           '"1"}},"ANSWERS":{"FIXED:v_z":"YES"}}]}')
assert GV(P2) == P2_CT and GV(TABLE) == TABLE_CT
assert CT(MATH) == MATH_CT, (CT(MATH), MATH_CT)
assert CT(GRAV) == GRAV_CT
assert D_MATH == G.digest_fields(["TPC-MODULE", "1", MATH_CT])
assert D_GRAV == G.digest_fields(["TPC-MODULE", "1", GRAV_CT])
ALLD = [D_GRAV, D_GRAV11, D_GRAV12, D_GRAV20, D_AIRQ, D_AIRL, D_MATH, D_TPI, D_TX, D_TEXCL,
        D_TPUSH, D_TOVR, D_TBADB, D_SUMP, D_NOAIR, D_T3, D_SHARE, D_KQ1]
assert len(set(ALLD)) == len(ALLD)
M4 = [X_AIRL, X_AIRQ, X_GRAV, X_MATH]        # the first catalogue's texts


def claim(i, v, d, bind, values, answers):
    return {"ID": i, "VERSION": v, "DIGEST": d, "BIND": bind, "VALUES": values,
            "ANSWERS": answers}


C_GRAV = claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND,
               {"h_max": {"VALUE": "100", "UNIT": "m"}}, {})
C_GRAV_NO = claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND,
                  {"h_max": {"VALUE": "100", "UNIT": "m"}}, {"BUOYANCY": "NO"})
C_GRAV_YES = claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND,
                   {"h_max": {"VALUE": "100", "UNIT": "m"}}, {"BUOYANCY": "YES"})
C_AIRQ = claim("AIR-QUADRATIC", "1.0.0", D_AIRQ, {"VELOCITY": ["v_x", "v_z"]},
               {"A_ref": {}, "C_d": {}, "L_ref": {}}, {})
C_AIRL = claim("AIR-LINEAR", "1.0.0", D_AIRL, {"VELOCITY": ["v_x", "v_z"]}, {"R_s": {}}, {})
C_MATH = claim("MATH", "1.0.0", D_MATH, {}, {}, {})
C_TPI = claim("TEST-PI", "1.0.0", D_TPI, {}, {}, {})
C_TX = claim("TEST-X", "1.0.0", D_TX, {}, {}, {})
C_TEXCL = claim("TEST-EXCL", "1.0.0", D_TEXCL, {}, {}, {})
C_TPUSH = claim("TEST-PUSH", "1.0.0", D_TPUSH, {"PULLED": ["v_z"], "PUSHED": ["v_x"]},
                {"F_a": {}, "F_b": {}}, {})

# ================================================== the expanded TPKs (literal)
E_GN = {"NAME": "g_n", "DIMENSION": "L1 T-2", "VALUE": "9.80665", "UNIT": "m s-2"}
E_HMAX100 = {"NAME": "h_max", "DIMENSION": "L1", "VALUE": "100", "UNIT": "m"}
E_HMAX1 = {"NAME": "h_max", "DIMENSION": "L1", "VALUE": "1", "UNIT": "m"}
E_HMAXS = {"NAME": "h_max", "DIMENSION": "L1"}
E_GU = {"NAME": "gravity_uniform",
        "PROPOSITION": "the gravitational field is uniform over the region abs(z) < h_max",
        "REFS": ["h_max", "z"]}
E_GS = {"NAME": "gravity_standard", "PROPOSITION": PROP_GS, "REFS": ["g_n"]}
E_FI = {"NAME": "frame_inertial",
        "PROPOSITION": "the frame in which z is measured is treated as inertial", "REFS": ["z"]}
E_GR = {"NAME": "gravity_region", "PREDICATE": "abs(z) < h_max", "REFS": ["h_max", "z"],
        "CONDITION": "abs(z)<h_max"}
E_NT = {"NAME": "gravity_no_tides", "PROPOSITION": PROP_NT, "REFS": ["z"]}
E_ZREF = {"NAME": "z_ref", "DIMENSION": "L1"}
E_FIXVZ = {"NAME": "fixed_v_z_balanced",
           "PROPOSITION": "a constraint force along v_z balances every other force along v_z",
           "REFS": ["v_z"]}
GRAV_EMIT_100 = [("CONSTANT", E_GN), ("CONSTANT", E_HMAX100), ("ASSUMPTION", E_GU),
                 ("ASSUMPTION", E_GS), ("ASSUMPTION", E_FI), ("BOUNDARY", E_GR)]
E_RHO = {"NAME": "rho_air", "DIMENSION": "L-3 M1", "VALUE": "1.225", "UNIT": "kg m-3"}
E_MU = {"NAME": "mu_air", "DIMENSION": "L-1 M1 T-1", "VALUE": "1.7894e-5", "UNIT": "Pa s"}
E_PI = {"NAME": "pi_c", "DIMENSION": "1", "VALUE": "3.141592653589793", "UNIT": "1"}
E_PIR = {"NAME": "pi_c", "DIMENSION": "1", "VALUE": "3.14", "UNIT": "1"}
E_CD = {"NAME": "C_d", "DIMENSION": "1"}
E_AREF = {"NAME": "A_ref", "DIMENSION": "L2"}
E_LREF = {"NAME": "L_ref", "DIMENSION": "L1"}
E_RS = {"NAME": "R_s", "DIMENSION": "L1"}
E_AS2 = {"NAME": "air_still", "PROPOSITION": PROP_AS, "REFS": ["v_x", "v_z"]}
E_AU = {"NAME": "air_uniform", "PROPOSITION": PROP_AU, "REFS": ["rho_air"]}
E_DCD = {"NAME": "drag_cd_constant", "PROPOSITION": PROP_CD, "REFS": ["C_d"]}
E_NL2 = {"NAME": "drag_no_lift", "PROPOSITION": PROP_NL, "REFS": ["v_x", "v_z"]}
E_NM2 = {"NAME": "drag_no_magnus", "PROPOSITION": PROP_NM, "REFS": ["v_x", "v_z"]}
E_SPH = {"NAME": "drag_sphere", "PROPOSITION": PROP_SP, "REFS": ["R_s"]}
COND_Q2 = ("mu_air!=0 and v_x^2+v_z^2>=0 and rho_air*sqrt(v_x^2+v_z^2)*L_ref/mu_air>1e3 and "
           "rho_air*sqrt(v_x^2+v_z^2)*L_ref/mu_air<2e5")
COND_L2 = "mu_air!=0 and v_x^2+v_z^2>=0 and 2*rho_air*sqrt(v_x^2+v_z^2)*R_s/mu_air<1"
canon_is(COND_Q2, "predicate")
canon_is(COND_L2, "predicate")
E_BQ2 = {"NAME": "drag_quadratic_regime", "PREDICATE": PRED_Q,
         "REFS": ["L_ref", "mu_air", "rho_air", "v_x", "v_z"], "CONDITION": COND_Q2}
E_BL2 = {"NAME": "drag_linear_regime", "PREDICATE": PRED_L,
         "REFS": ["R_s", "mu_air", "rho_air", "v_x", "v_z"], "CONDITION": COND_L2}
AIRQ_EMIT_2 = [("CONSTANT", E_RHO), ("CONSTANT", E_MU), ("CONSTANT", E_CD), ("CONSTANT", E_AREF),
               ("CONSTANT", E_LREF), ("ASSUMPTION", E_AS2), ("ASSUMPTION", E_AU),
               ("ASSUMPTION", E_DCD), ("ASSUMPTION", E_NL2), ("ASSUMPTION", E_NM2),
               ("BOUNDARY", E_BQ2)]
AIRL_EMIT_2 = [("CONSTANT", E_RHO), ("CONSTANT", E_MU), ("CONSTANT", E_PI), ("CONSTANT", E_RS),
               ("ASSUMPTION", E_AS2), ("ASSUMPTION", E_AU), ("ASSUMPTION", E_SPH),
               ("BOUNDARY", E_BL2)]

EQ_GZ = "m*D(v_z,t)=-m*g_n"
EQ_QX = "m*D(v_x,t)=-5e-1*rho_air*C_d*A_ref*sqrt(v_x^2+v_z^2)*v_x"
EQ_QZ = "m*D(v_z,t)=-5e-1*rho_air*C_d*A_ref*sqrt(v_x^2+v_z^2)*v_z"
EQ_QZG = "m*D(v_z,t)=-5e-1*rho_air*C_d*A_ref*sqrt(v_x^2+v_z^2)*v_z-m*g_n"
EQ_LX = "m*D(v_x,t)=-6*pi_c*mu_air*R_s*v_x"
EQ_LZ = "m*D(v_z,t)=-6*pi_c*mu_air*R_s*v_z"
EQ_LZG = "m*D(v_z,t)=-6*pi_c*mu_air*R_s*v_z-m*g_n"
for _eq in (EQ_GZ, EQ_QX, EQ_QZ, EQ_QZG, EQ_LX, EQ_LZ, EQ_LZG):
    canon_is(_eq, "equation")
TQ_X = "-5e-1*rho_air*C_d*A_ref*sqrt(v_x^2+v_z^2)*v_x"
TQ_Z = "-5e-1*rho_air*C_d*A_ref*sqrt(v_x^2+v_z^2)*v_z"
R_QX = ["A_ref", "C_d", "m", "rho_air", "t", "v_x", "v_z"]
R_QZ = ["A_ref", "C_d", "m", "rho_air", "t", "v_x", "v_z"]
R_QZG = ["A_ref", "C_d", "g_n", "m", "rho_air", "t", "v_x", "v_z"]
R_LX = ["R_s", "m", "mu_air", "pi_c", "t", "v_x"]
R_LZ = ["R_s", "m", "mu_air", "pi_c", "t", "v_z"]
R_LZG = ["R_s", "g_n", "m", "mu_air", "pi_c", "t", "v_z"]
S_W = "; with the weight: -m*g_n"
S_QX = "; with the quadratic drag: " + TQ_X
S_QZ = "; with the quadratic drag: " + TQ_Z
S_LX = "; with the linear (Stokes) drag: -6*pi_c*mu_air*R_s*v_x"
S_LZ = "; with the linear (Stokes) drag: -6*pi_c*mu_air*R_s*v_z"

EXP_GRAV = tpk_mod(P2, {"EOM_Z": (EQ_GZ, ["g_n", "m", "t", "v_z"], "m dv_z/dt = 0" + S_W)},
                   GRAV_EMIT_100)
EXP_AIRQ = tpk_mod(P2, {"EOM_X": (EQ_QX, R_QX, "m dv_x/dt = 0" + S_QX),
                        "EOM_Z": (EQ_QZ, R_QZ, "m dv_z/dt = 0" + S_QZ)}, AIRQ_EMIT_2)
EXP_AIRL = tpk_mod(P2, {"EOM_X": (EQ_LX, R_LX, "m dv_x/dt = 0" + S_LX),
                        "EOM_Z": (EQ_LZ, R_LZ, "m dv_z/dt = 0" + S_LZ)}, AIRL_EMIT_2)
EXP_MATH = tpk_mod(P2, None, [("CONSTANT", E_PI)])
EXP_GRAV_AIRQ = tpk_mod(P2, {"EOM_X": (EQ_QX, R_QX, "m dv_x/dt = 0" + S_QX),
                             "EOM_Z": (EQ_QZG, R_QZG, "m dv_z/dt = 0" + S_QZ + S_W)},
                        AIRQ_EMIT_2 + GRAV_EMIT_100)
EXP_GRAV_AIRL = tpk_mod(P2, {"EOM_X": (EQ_LX, R_LX, "m dv_x/dt = 0" + S_LX),
                             "EOM_Z": (EQ_LZG, R_LZG, "m dv_z/dt = 0" + S_LZ + S_W)},
                        AIRL_EMIT_2 + GRAV_EMIT_100)
EXP_GRAV_MATH = tpk_mod(P2, {"EOM_Z": (EQ_GZ, ["g_n", "m", "t", "v_z"], "m dv_z/dt = 0" + S_W)},
                        GRAV_EMIT_100 + [("CONSTANT", E_PI)])
EXP_AIRQ_MATH = tpk_mod(P2, {"EOM_X": (EQ_QX, R_QX, "m dv_x/dt = 0" + S_QX),
                             "EOM_Z": (EQ_QZ, R_QZ, "m dv_z/dt = 0" + S_QZ)},
                        AIRQ_EMIT_2 + [("CONSTANT", E_PI)])
EXP_GRAV_AIRL_MATH = EXP_GRAV_AIRL                    # pi_c merges: one entry
EXP_TABLE = tpk_mod(TABLE, None, [("CONSTANT", E_GN), ("CONSTANT", E_HMAX1), ("ASSUMPTION", E_GU),
                                  ("ASSUMPTION", E_GS), ("ASSUMPTION", E_FI), ("BOUNDARY", E_GR),
                                  ("ASSUMPTION", E_FIXVZ)])
EXP_TPI = tpk_mod(P2, None, [("CONSTANT", E_PIR)])
EXP_AIRQ_TPI = tpk_mod(P2, {"EOM_X": (EQ_QX, R_QX, "m dv_x/dt = 0" + S_QX),
                            "EOM_Z": (EQ_QZ, R_QZ, "m dv_z/dt = 0" + S_QZ)},
                       AIRQ_EMIT_2 + [("CONSTANT", E_PIR)])
EXP_GRAV_TPI = tpk_mod(P2, {"EOM_Z": (EQ_GZ, ["g_n", "m", "t", "v_z"], "m dv_z/dt = 0" + S_W)},
                       GRAV_EMIT_100 + [("CONSTANT", E_PIR)])
EXP_TX = tpk_mod(SOLO, None, [("CONSTANT", {"NAME": "x_test", "DIMENSION": "1", "VALUE": "1",
                                            "UNIT": "1"})])
EXP_GRAV12 = tpk_mod(P2, {"EOM_Z": (EQ_GZ, ["g_n", "m", "t", "v_z"], "m dv_z/dt = 0" + S_W)},
                     GRAV_EMIT_100[:5] + [("ASSUMPTION", E_NT), ("BOUNDARY", E_GR)])
for _t, _n in [(EXP_GRAV, "EXP_GRAV"), (EXP_AIRQ, "EXP_AIRQ"), (EXP_AIRL, "EXP_AIRL"),
               (EXP_MATH, "EXP_MATH"), (EXP_GRAV_AIRQ, "EXP_GRAV_AIRQ"),
               (EXP_GRAV_AIRL, "EXP_GRAV_AIRL"), (EXP_GRAV_MATH, "EXP_GRAV_MATH"),
               (EXP_AIRQ_MATH, "EXP_AIRQ_MATH"), (EXP_TABLE, "EXP_TABLE"), (EXP_TPI, "EXP_TPI"),
               (EXP_AIRQ_TPI, "EXP_AIRQ_TPI"), (EXP_GRAV_TPI, "EXP_GRAV_TPI"), (EXP_TX, "EXP_TX"),
               (EXP_GRAV12, "EXP_GRAV12")]:
    must_pass(_t, _n)


def SRC(*mods):
    return list(mods)


G1 = "GRAV-UNIFORM 1.0.0"
AQ1 = "AIR-QUADRATIC 1.0.0"
AL1 = "AIR-LINEAR 1.0.0"
MA1 = "MATH 1.0.0"


def rows_of(emit, src):
    return [[k, e["NAME"], [src]] for k, e in emit]


def XR(tpk, composed, emitted, dropped):
    """The render of module_expand (tpm_module_spec 6.4 RENDER)."""
    return OK(["D(TPK)", DTPK(tpk)], ["COMPOSED", composed], ["EMITTED", emitted],
              ["DROPPED", dropped])


R_GRAV = XR(EXP_GRAV, [["EOM_Z", EQ_GZ]], rows_of(GRAV_EMIT_100, G1), [])
R_AIRQ = XR(EXP_AIRQ, [["EOM_X", EQ_QX], ["EOM_Z", EQ_QZ]], rows_of(AIRQ_EMIT_2, AQ1), [])
R_AIRL = XR(EXP_AIRL, [["EOM_X", EQ_LX], ["EOM_Z", EQ_LZ]], rows_of(AIRL_EMIT_2, AL1), [])
R_MATH = XR(EXP_MATH, [], [["CONSTANT", "pi_c", [MA1]]], [])
R_GRAV_AIRQ = XR(EXP_GRAV_AIRQ, [["EOM_X", EQ_QX], ["EOM_Z", EQ_QZG]],
                 rows_of(AIRQ_EMIT_2, AQ1) + rows_of(GRAV_EMIT_100, G1), [])
_airl_math_rows = rows_of(AIRL_EMIT_2, AL1)
_airl_math_rows[2] = ["CONSTANT", "pi_c", [AL1, MA1]]
R_AIRL_MATH = XR(EXP_AIRL, [["EOM_X", EQ_LX], ["EOM_Z", EQ_LZ]], _airl_math_rows, [])
R_GRAV_AIRL_MATH = XR(EXP_GRAV_AIRL_MATH, [["EOM_X", EQ_LX], ["EOM_Z", EQ_LZG]],
                      _airl_math_rows + rows_of(GRAV_EMIT_100, G1), [])

# ============================================================ module_parse
def SUMMARY_GRAV():
    return OK("GRAV-UNIFORM", "1.0.0",
              [["MASS", "CONSTANT", "M1", 1, 1], ["UP_VELOCITY", "DOF", "L1 T-1", 1, 1],
               ["HEIGHT", "DOF", "L1", 1, 1]],
              [["g_n", "DECLARED", "L1 T-2"], ["h_max", "AUTHOR", "L1"]],
              [], [["UP_VELOCITY", "-", "MASS*g_n"]],
              ["gravity_uniform", "gravity_standard", "frame_inertial"],
              [["gravity_region", "abs(HEIGHT)<h_max"]], [], [["AIR-*", "BUOYANCY"]],
              ["PROJECTILE_2D", "TABLE"])


add("P01 GRAV-UNIFORM 1.0.0 parses", "module_parse", {"text": X_GRAV}, SUMMARY_GRAV())
add("P02 AIR-QUADRATIC 1.0.0 parses; the template spelled canonically", "module_parse",
    {"text": X_AIRQ},
    OK("AIR-QUADRATIC", "1.0.0", [["VELOCITY", "DOF", "L1 T-1", 1, 3]],
       [["rho_air", "DECLARED", "L-3 M1"], ["mu_air", "DECLARED", "L-1 M1 T-1"],
        ["C_d", "AUTHOR", "1"], ["A_ref", "AUTHOR", "L2"], ["L_ref", "AUTHOR", "L1"]],
       [["SPEED2", "VELOCITY", "VELOCITY^2"]], [["VELOCITY", "-", EXPR_Qc]],
       ["air_still", "air_uniform", "drag_cd_constant", "drag_no_lift", "drag_no_magnus"],
       [["drag_quadratic_regime", COND_Qc]], ["AIR-LINEAR"], [], ["PROJECTILE_2D"]))
add("P03 AIR-LINEAR 1.0.0 parses", "module_parse", {"text": X_AIRL},
    OK("AIR-LINEAR", "1.0.0", [["VELOCITY", "DOF", "L1 T-1", 1, 3]],
       [["rho_air", "DECLARED", "L-3 M1"], ["mu_air", "DECLARED", "L-1 M1 T-1"],
        ["pi_c", "DECLARED", "1"], ["R_s", "AUTHOR", "L1"]],
       [["SPEED2", "VELOCITY", "VELOCITY^2"]], [["VELOCITY", "-", EXPR_L]],
       ["air_still", "air_uniform", "drag_sphere"], [["drag_linear_regime", COND_Lc]], [], [],
       ["PROJECTILE_2D"]))
add("P04 MATH 1.0.0 parses", "module_parse", {"text": X_MATH},
    OK("MATH", "1.0.0", [], [["pi_c", "DECLARED", "1"]], [], [], [], [], [], [],
       ["PROJECTILE_2D"]))
add("P05 GRAV pretty-printed: white space outside strings changes nothing", "module_parse",
    {"text": json.dumps(GRAV, indent=2)}, SUMMARY_GRAV())
_dimvar = copy.deepcopy(GRAV)
_dimvar["PREREQUISITES"][1]["DIMENSION"] = "T-1 L1"
add("P06 a DIMENSION written T-1 L1 is kept normalized", "module_parse", {"text": TXT(_dimvar)},
    SUMMARY_GRAV())
add("P07 the text is not a string", "module_parse", {"text": GRAV},
    REF("MODULE: the text is not a string"))
add("P08 a text of 262145 bytes", "module_parse", {"text_parts": [["x", 262145]]},
    REF("MODULE: the text is longer than 262144 bytes"))
add("P09 131073 two-byte letters: 262146 bytes", "module_parse",
    {"text_parts": [["\u00e9", 131073]]}, REF("MODULE: the text is longer than 262144 bytes"))
add("P10 exactly 262144 bytes, an array: read, then not an object", "module_parse",
    {"text_parts": [["[", 1], [" ", 262142], ["]", 1]]},
    REF("MODULE: the text is not a JSON object"))
NOTJSON = REF("MODULE: the text is not JSON (RFC 8259, strict)")
add("P11 a trailing comma", "module_parse", {"text": '{"ID": "MATH",}'}, NOTJSON)
add("P12 NaN", "module_parse", {"text": '{"ID": NaN}'}, NOTJSON)
add("P13 a duplicate key", "module_parse", {"text": '{"ID": "MATH", "ID": "MATH"}'}, NOTJSON)
add("P14 a duplicate key after NFC", "module_parse",
    {"text": '{"caf\\u00e9": 1, "cafe\\u0301": 2}'}, NOTJSON)
add("P15 a lone surrogate escape", "module_parse", {"text": '{"TITLE": "a\\ud800b"}'}, NOTJSON)
add("P16 a byte-order mark before the object", "module_parse", {"text": "\ufeff{}"}, NOTJSON)
add("P17 a raw control character in a string", "module_parse", {"text": '{"TITLE": "a\u0001b"}'},
    NOTJSON)
add("P18 65 nested arrays: depth 65 refused", "module_parse",
    {"text": '{"ID": ' + "[" * 65 + "]" * 65 + "}"}, NOTJSON)
add("P19 64 nested arrays are read; then the first missing field", "module_parse",
    {"text": '{"ID": ' + "[" * 64 + "]" * 64 + "}"}, REF("MODULE: missing field VERSION"))
add("P20 an empty text", "module_parse", {"text": ""}, NOTJSON)


def mut(base, fn):
    d = copy.deepcopy(base)
    fn(d)
    return TXT(d)


def P(name, base, fn, msg):
    add(name, "module_parse", {"text": mut(base, fn)}, REF(msg))


P("P21 an unknown key (the least named)", GRAV,
  lambda d: d.update({"LAWS": [], "DOF": []}), "MODULE: unknown field DOF")
P("P22 a missing key", GRAV, lambda d: d.pop("TEST_BINDINGS"),
  "MODULE: missing field TEST_BINDINGS")
P("P23 ID in lower case", GRAV, lambda d: d.update({"ID": "grav-uniform"}),
  "MODULE: ID grav-uniform is not a module ID")
P("P24 ID ending in a hyphen", GRAV, lambda d: d.update({"ID": "GRAV-"}),
  "MODULE: ID GRAV- is not a module ID")
P("P25 ID of 33 characters", GRAV, lambda d: d.update({"ID": "A" * 33}),
  "MODULE: ID " + "A" * 33 + " is not a module ID")
P("P26 VERSION of two parts", GRAV, lambda d: d.update({"VERSION": "1.0"}),
  "MODULE: VERSION 1.0 is not a version")
P("P27 VERSION with a leading zero", GRAV, lambda d: d.update({"VERSION": "1.01.0"}),
  "MODULE: VERSION 1.01.0 is not a version")
P("P28 VERSION with a five-digit part", GRAV, lambda d: d.update({"VERSION": "10000.0.0"}),
  "MODULE: VERSION 10000.0.0 is not a version")
P("P29 TITLE blank", GRAV, lambda d: d.update({"TITLE": " \t "}), "MODULE: field TITLE is blank")
P("P30 TITLE with a bidi override", GRAV, lambda d: d.update({"TITLE": "g\u202en"}),
  "MODULE: field TITLE contains disallowed character U+202E")
P("P31 TITLE a number", GRAV, lambda d: d.update({"TITLE": 7}),
  "MODULE: field TITLE has the wrong type")
P("P32 PREREQUISITES holding a string", GRAV,
  lambda d: d.update({"PREREQUISITES": ["MASS"]}), "MODULE: field PREREQUISITES has the wrong type")
P("P33 PREREQUISITES of 17 entries", GRAV,
  lambda d: d.update({"PREREQUISITES": [PRE_MASS] * 17}),
  "MODULE: field PREREQUISITES has more than 16 entries")
P("P34 a slot name in lower case", GRAV,
  lambda d: d["PREREQUISITES"][0].update({"SLOT": "mass"}),
  "PREREQUISITE:mass: SLOT mass is not a slot name")
P("P35 KIND not DOF or CONSTANT", GRAV,
  lambda d: d["PREREQUISITES"][0].update({"KIND": "ENTRY"}),
  "PREREQUISITE:MASS: KIND ENTRY is not DOF or CONSTANT")
P("P36 a DIMENSION that does not parse", GRAV,
  lambda d: d["PREREQUISITES"][0].update({"DIMENSION": "Q1"}),
  "PREREQUISITE:MASS: DIMENSION Q1 is not a dimension")
P("P37 MIN 0", GRAV, lambda d: d["PREREQUISITES"][0].update({"MIN": 0}),
  "PREREQUISITE:MASS: MIN is not an integer from 1 to 16")
P("P38 MIN a string", GRAV, lambda d: d["PREREQUISITES"][0].update({"MIN": "1"}),
  "PREREQUISITE:MASS: MIN is not an integer from 1 to 16")
assert '"MIN": 1, "MAX": 1, "LABEL": "the mass' in X_GRAV
add("P39 MIN written 1.0 (not an integer in the strict reader)", "module_parse",
    {"text": X_GRAV.replace('"MIN": 1, "MAX": 1, "LABEL": "the mass', '"MIN": 1.0, "MAX": 1, '
                            '"LABEL": "the mass', 1)},
    REF("PREREQUISITE:MASS: MIN is not an integer from 1 to 16"))
add("P40 MAX written 1e0", "module_parse",
    {"text": X_GRAV.replace('"MAX": 1, "LABEL": "the mass', '"MAX": 1e0, "LABEL": "the mass', 1)},
    REF("PREREQUISITE:MASS: MAX is not an integer from 1 to 16"))
add("P41 MIN true", "module_parse",
    {"text": X_GRAV.replace('"MIN": 1, "MAX": 1, "LABEL": "the mass', '"MIN": true, "MAX": 1, '
                            '"LABEL": "the mass', 1)},
    REF("PREREQUISITE:MASS: MIN is not an integer from 1 to 16"))
P("P42 MIN greater than MAX", GRAV,
  lambda d: d["PREREQUISITES"][1].update({"MIN": 2}),
  "PREREQUISITE:UP_VELOCITY: MIN is greater than MAX")
P("P43 a CONSTANT slot of MAX 2", GRAV,
  lambda d: d["PREREQUISITES"][0].update({"MAX": 2}),
  "PREREQUISITE:MASS: a CONSTANT slot takes exactly one name")
P("P44 an unknown field in a prerequisite", GRAV,
  lambda d: d["PREREQUISITES"][0].update({"ROLE": "INPUT"}),
  "PREREQUISITE:MASS: unknown field ROLE")
P("P45 a prerequisite without LABEL", GRAV, lambda d: d["PREREQUISITES"][0].pop("LABEL"),
  "PREREQUISITE:MASS: missing field LABEL")
P("P46 a duplicate slot", GRAV,
  lambda d: d["PREREQUISITES"].append(copy.deepcopy(PRE_MASS)), "MODULE: duplicate slot MASS")
P("P47 a parameter name without a lower-case letter", GRAV,
  lambda d: d["PARAMETERS"][0].update({"NAME": "G_N"}),
  "PARAMETER:G_N: NAME G_N is not an entry name (an identifier with a lower-case letter)")
P("P48 SUPPLY not DECLARED or AUTHOR", GRAV,
  lambda d: d["PARAMETERS"][0].update({"SUPPLY": "FIXED"}),
  "PARAMETER:g_n: SUPPLY FIXED is not DECLARED or AUTHOR")
P("P49 DECLARED without OVERRIDABLE", GRAV, lambda d: d["PARAMETERS"][0].pop("OVERRIDABLE"),
  "PARAMETER:g_n: DECLARED requires OVERRIDABLE")
P("P50 AUTHOR with a VALUE", GRAV, lambda d: d["PARAMETERS"][1].update({"VALUE": "100"}),
  "PARAMETER:h_max: AUTHOR forbids VALUE")
P("P51 a VALUE that is not a decimal", GRAV,
  lambda d: d["PARAMETERS"][0].update({"VALUE": "9,80665"}),
  "PARAMETER:g_n: VALUE 9,80665 is not a decimal")
P("P52 a UNIT of another dimension", GRAV,
  lambda d: d["PARAMETERS"][0].update({"UNIT": "m s-3"}),
  "PARAMETER:g_n: UNIT m s-3 has dimension L1 T-3, not L1 T-2")
assert U.unit_parse("m/s2") == ["REFUSED", "UNIT m/s2 is not a unit"]
P("P53 a UNIT that does not parse (tpc_units' reason)", GRAV,
  lambda d: d["PARAMETERS"][0].update({"UNIT": "m/s2"}), "PARAMETER:g_n: UNIT m/s2 is not a unit")
P("P54 a negative UNCERTAINTY", GRAV,
  lambda d: d["PARAMETERS"][0].update({"UNCERTAINTY": "-0.1"}),
  "PARAMETER:g_n: UNCERTAINTY -0.1 is negative")
P("P55 OVERRIDABLE in lower case", GRAV,
  lambda d: d["PARAMETERS"][0].update({"OVERRIDABLE": "yes"}),
  "PARAMETER:g_n: OVERRIDABLE yes is not YES or NO")
P("P56 a duplicate parameter", GRAV, lambda d: d["PARAMETERS"].append(copy.deepcopy(PAR_GN)),
  "MODULE: duplicate parameter g_n")
P("P57 a sum over a name that is no slot", AIRQ, lambda d: d["SUMS"][0].update({"OVER": "V"}),
  "SUM:SPEED2: OVER V is not a DOF slot")
P("P58 a sum naming another name", AIRQ,
  lambda d: d["SUMS"][0].update({"EACH": "VELOCITY^2+HEIGHT"}),
  "SUM:SPEED2: EACH names HEIGHT, which is not VELOCITY or a parameter")
P("P59 a sum using D", AIRQ, lambda d: d["SUMS"][0].update({"EACH": "D(VELOCITY,VELOCITY)"}),
  "SUM:SPEED2: EACH uses D")
P("P60 a sum named as a slot", AIRQ, lambda d: d["SUMS"][0].update({"NAME": "VELOCITY"}),
  "SUM:VELOCITY: NAME VELOCITY is the name of a slot")
P("P61 a term on a CONSTANT slot", GRAV, lambda d: d["TERMS"][0].update({"TARGET": "MASS"}),
  "TERM:1: TARGET MASS is not a DOF slot")
P("P62 a SIGN that is neither + nor -", GRAV, lambda d: d["TERMS"][0].update({"SIGN": "*"}),
  "TERM:1: SIGN * is not + or -")
assert F.parse_form("MASS*", "expression") == ["REFUSED", "unexpected end"]
P("P63 an EXPR that does not parse (tpc_form's reason)", GRAV,
  lambda d: d["TERMS"][0].update({"EXPR": "MASS*"}), "TERM:1: EXPR does not parse: unexpected end")
P("P64 an EXPR beginning with a minus sign", GRAV,
  lambda d: d["TERMS"][0].update({"EXPR": "-MASS*g_n"}),
  "TERM:1: EXPR begins with a minus sign; the sign is SIGN's")
P("P65 an EXPR using D", GRAV,
  lambda d: d["TERMS"][0].update({"EXPR": "MASS*D(HEIGHT,UP_VELOCITY)"}), "TERM:1: EXPR uses D")
P("P66 an EXPR naming an unknown name (its own unknown reference)", GRAV,
  lambda d: d["TERMS"][0].update({"EXPR": "MASS*g"}),
  "TERM:1: EXPR names g, which is not a slot, parameter or sum of this module")


def _other(d):
    d["PREREQUISITES"].append({"SLOT": "OTHER", "KIND": "DOF", "DIMENSION": "L1 T-1", "MIN": 1,
                               "MAX": 3, "LABEL": "other velocities"})
    d["TERMS"][0]["EXPR"] = "0.5*rho_air*C_d*A_ref*sqrt(SPEED2)*OTHER"


P("P67 an EXPR naming a list slot outside its own TERM", AIRQ, _other,
  "TERM:1: EXPR names the list slot OTHER outside its own TERM")
P("P68 an assumption name without a lower-case letter", GRAV,
  lambda d: d["ASSUMPTIONS"][0].update({"NAME": "GRAVITY"}),
  "ASSUMPTION:GRAVITY: NAME GRAVITY is not an entry name (an identifier with a lower-case letter)")
P("P69 a placeholder naming no slot", GRAV,
  lambda d: d["ASSUMPTIONS"][0].update({"PROPOSITION": "uniform near {MASSES}"}),
  "ASSUMPTION:gravity_uniform: PROPOSITION: {MASSES} is not a slot of this module")
P("P70 a stray closing brace", GRAV,
  lambda d: d["ASSUMPTIONS"][0].update({"PROPOSITION": "a brace } here"}),
  "ASSUMPTION:gravity_uniform: PROPOSITION: a brace outside a {SLOT} placeholder")
P("P71 an unclosed placeholder", GRAV,
  lambda d: d["ASSUMPTIONS"][0].update({"PROPOSITION": "uniform over {HEIGHT"}),
  "ASSUMPTION:gravity_uniform: PROPOSITION: a brace outside a {SLOT} placeholder")
P("P72 REFS naming no slot or parameter", GRAV,
  lambda d: d["ASSUMPTIONS"][0].update({"REFS": ["HEIGHT", "g"]}),
  "ASSUMPTION:gravity_uniform: REFS g is not a slot or parameter of this module")
P("P73 duplicate REFS", GRAV, lambda d: d["ASSUMPTIONS"][0].update({"REFS": ["h_max", "h_max"]}),
  "ASSUMPTION:gravity_uniform: field REFS has duplicate entries")
P("P74 a CONDITION that does not parse", GRAV,
  lambda d: d["BOUNDARIES"][0].update({"CONDITION": "abs(HEIGHT) <"}),
  "BOUNDARY:gravity_region: CONDITION does not parse: unexpected end")
P("P75 a CONDITION naming a list slot outside a sum", AIRQ,
  lambda d: d["BOUNDARIES"][0].update({"CONDITION": "VELOCITY > 0 and mu_air != 0"}),
  "BOUNDARY:drag_quadratic_regime: CONDITION names the list slot VELOCITY outside a sum")
P("P76 a CONDITION naming an unknown name", GRAV,
  lambda d: d["BOUNDARIES"][0].update({"CONDITION": "abs(HEIGHT) < h_min"}),
  "BOUNDARY:gravity_region: CONDITION names h_min, which is not a slot, parameter or sum of "
  "this module")
P("P77 an assumption named as a parameter", GRAV,
  lambda d: d["ASSUMPTIONS"][1].update({"NAME": "g_n"}), "MODULE: duplicate entry name g_n")
P("P78 NOT_CHOSEN blank", GRAV, lambda d: d.update({"NOT_CHOSEN": ""}),
  "MODULE: field NOT_CHOSEN is blank")
P("P79 a blank NOT_INCLUDED line", GRAV, lambda d: d.update({"NOT_INCLUDED": ["a", "  "]}),
  "MODULE: NOT_INCLUDED 2 is blank")
P("P80 duplicate NOT_INCLUDED lines", GRAV, lambda d: d.update({"NOT_INCLUDED": ["a", "a"]}),
  "MODULE: field NOT_INCLUDED has duplicate entries")
P("P81 a zero-width space in a NOT_INCLUDED line", GRAV,
  lambda d: d.update({"NOT_INCLUDED": ["a", "b\u200bc"]}),
  "MODULE: field NOT_INCLUDED contains disallowed character U+200B")
P("P82 an EXCLUDES pattern without its hyphen", AIRQ, lambda d: d.update({"EXCLUDES": ["AIR*"]}),
  "MODULE: EXCLUDES AIR* is not a module ID or pattern")
P("P83 EXCLUDES naming the module itself", AIRQ,
  lambda d: d.update({"EXCLUDES": ["AIR-QUADRATIC"]}),
  "MODULE: EXCLUDES AIR-QUADRATIC matches the module's own ID")
P("P84 an EXCLUDES pattern matching the module itself", AIRQ,
  lambda d: d.update({"EXCLUDES": ["AIR-*"]}), "MODULE: EXCLUDES AIR-* matches the module's own ID")
P("P85 INTERACTS_WITH matching the module itself", GRAV,
  lambda d: d["INTERACTS_WITH"][0].update({"WITH": "GRAV-*"}),
  "INTERACTS_WITH:1: WITH GRAV-* matches the module's own ID")
P("P86 a question ID in lower case", GRAV,
  lambda d: d["INTERACTS_WITH"][0].update({"QUESTION_ID": "buoyancy"}),
  "INTERACTS_WITH:1: QUESTION_ID buoyancy is not a question ID")
P("P87 a duplicate question", GRAV,
  lambda d: d["INTERACTS_WITH"].append(copy.deepcopy(d["INTERACTS_WITH"][0])),
  "MODULE: duplicate question BUOYANCY")
P("P88 no test binding", GRAV, lambda d: d.update({"TEST_BINDINGS": []}),
  "MODULE: field TEST_BINDINGS is empty (a module declares at least one test binding)")
P("P89 nine test bindings", MATH, lambda d: d.update({"TEST_BINDINGS": [
    dict(d["TEST_BINDINGS"][0], NAME="B%d" % i) for i in range(9)]}),
  "MODULE: field TEST_BINDINGS has more than 8 entries")
_badtpk = copy.deepcopy(P2)
_badtpk["LAW"][3]["REFS"] = ["t", "v_z"]
assert t1_first(_badtpk) == "LAW:EOM_Z: EQUATION names m, which is not in REFS"
P("P90 a test binding whose TPK fails T1 (T1's first problem)", GRAV,
  lambda d: d["TEST_BINDINGS"][0].update({"TPK": _badtpk}),
  "TEST_BINDING:PROJECTILE_2D: TPK fails T1: LAW:EOM_Z: EQUATION names m, which is not in REFS")
P("P91 a test binding with a slot unbound", GRAV,
  lambda d: d["TEST_BINDINGS"][0].update({"BIND": {"MASS": ["m"], "UP_VELOCITY": ["v_z"]}}),
  "TEST_BINDING:PROJECTILE_2D: BIND: slot HEIGHT is not bound")
P("P92 a test binding with an unknown slot", GRAV,
  lambda d: d["TEST_BINDINGS"][0]["BIND"].update({"WEIGHT": ["m"]}),
  "TEST_BINDING:PROJECTILE_2D: BIND: unknown slot WEIGHT")
P("P93 a test binding with an unknown parameter", GRAV,
  lambda d: d["TEST_BINDINGS"][0]["VALUES"].update({"g": {}}),
  "TEST_BINDING:PROJECTILE_2D: VALUES: unknown parameter g")
P("P94 a test binding value without UNIT", GRAV,
  lambda d: d["TEST_BINDINGS"][0].update({"VALUES": {"h_max": {"VALUE": "100"}}}),
  "TEST_BINDING:PROJECTILE_2D: VALUES: h_max: VALUE requires UNIT")
P("P95 an answer key that is no question", GRAV,
  lambda d: d["TEST_BINDINGS"][1].update({"ANSWERS": {"FIXED:v z": "YES"}}),
  "TEST_BINDING:TABLE: ANSWERS: FIXED:v z is not a question")
P("P96 an answer that is neither YES nor NO", GRAV,
  lambda d: d["TEST_BINDINGS"][0].update({"ANSWERS": {"BUOYANCY": "yes"}}),
  "TEST_BINDING:PROJECTILE_2D: ANSWERS: BUOYANCY is not YES or NO")
P("P97 a duplicate test binding", GRAV,
  lambda d: d["TEST_BINDINGS"].append(copy.deepcopy(TB_GRAV_P2)),
  "MODULE: duplicate test binding PROJECTILE_2D")
P("P98 a binding name in lower case", GRAV,
  lambda d: d["TEST_BINDINGS"][0].update({"NAME": "projectile"}),
  "TEST_BINDING:projectile: NAME projectile is not a binding name")
P("P99 LAWS key: a module emits no law (B2)", MATH, lambda d: d.update({"LAWS": []}),
  "MODULE: unknown field LAWS")
# ---- RV's vectors (review 0.1.0 F-9): refusals without a case in 0.1.0
_dmsg = F.parse_form("D(HEIGHT,UP_VELOCITY) < h_max", "predicate")
assert _dmsg == ["REFUSED", "D is not permitted in a CONDITION at 1"], _dmsg
P("P100 a CONDITION using D: tpc_form's own refusal (S53, F-6)", GRAV,
  lambda d: d["BOUNDARIES"][0].update({"CONDITION": "D(HEIGHT,UP_VELOCITY) < h_max"}),
  "BOUNDARY:gravity_region: CONDITION does not parse: D is not permitted in a CONDITION at 1")
P("P101 an UNCERTAINTY that is not a decimal", GRAV,
  lambda d: d["PARAMETERS"][0].update({"UNCERTAINTY": "0.1.2"}),
  "PARAMETER:g_n: UNCERTAINTY 0.1.2 is not a decimal")
P("P102 a SUM NAME that is not a slot name", AIRQ, lambda d: d["SUMS"][0].update({"NAME": "speed2"}),
  "SUM:speed2: NAME speed2 is not a slot name")
_emsg = F.parse_form("VELOCITY^", "expression")
assert _emsg == ["REFUSED", "^ takes an integer or a parenthesized rational exponent at 9"], _emsg
P("P103 a SUM EACH that does not parse", AIRQ, lambda d: d["SUMS"][0].update({"EACH": "VELOCITY^"}),
  "SUM:SPEED2: EACH does not parse: ^ takes an integer or a parenthesized rational exponent at 9")
P("P104 a duplicate sum", AIRQ, lambda d: d["SUMS"].append(copy.deepcopy(d["SUMS"][0])),
  "MODULE: duplicate sum SPEED2")
P("P105 duplicate EXCLUDES", AIRQ, lambda d: d.update({"EXCLUDES": ["AIR-LINEAR", "AIR-LINEAR"]}),
  "MODULE: field EXCLUDES has duplicate entries")
P("P106 a WITH that is no ID or pattern", GRAV,
  lambda d: d["INTERACTS_WITH"][0].update({"WITH": "air-*"}),
  "INTERACTS_WITH:1: WITH air-* is not a module ID or pattern")
P("P107 a PREDICATE placeholder naming no slot", GRAV,
  lambda d: d["BOUNDARIES"][0].update({"PREDICATE": "abs({HIGHT}) < h_max"}),
  "BOUNDARY:gravity_region: PREDICATE: {HIGHT} is not a slot of this module")
P("P108 a test binding's BIND slot of the wrong type", GRAV,
  lambda d: d["TEST_BINDINGS"][0]["BIND"].update({"MASS": "m"}),
  "TEST_BINDING:PROJECTILE_2D: BIND: slot MASS has the wrong type")

# ============================================================ module_digest
add("G01 GRAV-UNIFORM 1.0.0", "module_digest", {"text": X_GRAV}, OK(D_GRAV))
add("G02 AIR-QUADRATIC 1.0.0", "module_digest", {"text": X_AIRQ}, OK(D_AIRQ))
add("G03 AIR-LINEAR 1.0.0", "module_digest", {"text": X_AIRL}, OK(D_AIRL))
add("G04 MATH 1.0.0", "module_digest", {"text": X_MATH}, OK(D_MATH))
add("G05 white space outside strings: one digest", "module_digest",
    {"text": json.dumps(GRAV, indent=2)}, OK(D_GRAV))
_rev = dict(reversed(list(GRAV.items())))
add("G06 key order: one digest", "module_digest", {"text": TXT(_rev)}, OK(D_GRAV))
_spell = copy.deepcopy(GRAV)
_spell["PREREQUISITES"][1]["DIMENSION"] = "T-1 L1"
_spell["PARAMETERS"][0]["VALUE"] = "9.806650"
_spell["TERMS"][0]["EXPR"] = "MASS * g_n"
_spell["BOUNDARIES"][0]["CONDITION"] = "(abs(HEIGHT) < h_max)"
_spell["TEST_BINDINGS"][0]["VALUES"]["h_max"]["VALUE"] = "1e2"
assert CD("9.806650") == "980665e-5" and CD("1e2") == "1e2"
assert F.parse_form("(abs(HEIGHT) < h_max)", "predicate") == ["OK", "abs(HEIGHT)<h_max"]
D_SPELL = MDIG(_spell)
assert D_SPELL != D_GRAV
add("G07 a DIMENSION, decimal and forms spelled otherwise: another digest (verbatim, S49)",
    "module_digest", {"text": TXT(_spell)}, OK(D_SPELL))
add("G08 GRAV-UNIFORM 1.1.0 (TITLE revised)", "module_digest", {"text": X_GRAV11}, OK(D_GRAV11))
_v101 = copy.deepcopy(GRAV)
_v101["VERSION"] = "1.0.1"
add("G09 the same text at another VERSION: another digest", "module_digest",
    {"text": TXT(_v101)}, OK(MDIG(_v101)))
assert MDIG(_v101) != D_GRAV
_ws = copy.deepcopy(GRAV)
_ws["TITLE"] = "Uniform  gravity at the standard value g_n"
add("G10 white space inside a string is text: another digest", "module_digest",
    {"text": TXT(_ws)}, OK(MDIG(_ws)))
assert MDIG(_ws) != D_GRAV
_nfc1 = copy.deepcopy(TEST_X)
_nfc1["TITLE"] = "Test interaction X, caf\u00e9"
_nfc2 = copy.deepcopy(TEST_X)
_nfc2["TITLE"] = "Test interaction X, cafe\u0301"
assert MDIG(_nfc1) == MDIG(_nfc2) and CT(_nfc1) == CT(_nfc2)
add("G11 a precomposed letter", "module_digest", {"text": TXT(_nfc1)}, OK(MDIG(_nfc1)))
add("G12 the decomposed spelling: one digest (NFC)", "module_digest", {"text": TXT(_nfc2)},
    OK(MDIG(_nfc1)))
add("G13 a refused text: module_parse's refusal", "module_digest", {"text": '{"ID": 1'}, NOTJSON)
add("G14 GRAV-UNIFORM 1.2.0", "module_digest", {"text": X_GRAV12}, OK(D_GRAV12))
add("G15 GRAV-UNIFORM 2.0.0", "module_digest", {"text": X_GRAV20}, OK(D_GRAV20))
add("G16 TEST-PUSH 1.0.0", "module_digest", {"text": X_TPUSH}, OK(D_TPUSH))
_esc = copy.deepcopy(TEST_X)
_esc["TITLE"] = 'Test "X", a\\b\tc'
_esc["NOT_INCLUDED"] = ["one line\nand the next"]
assert '"TITLE":"Test \\"X\\", a\\\\b\\u0009c"' in CT(_esc)
assert '"NOT_INCLUDED":["one line\\u000aand the next"]' in CT(_esc)
add("G17 a quotation mark, a backslash, a TAB and an LF inside strings: escaped (section 4)",
    "module_digest", {"text": TXT(_esc)}, OK(MDIG(_esc)))
_crlf = copy.deepcopy(_esc)
_crlf["NOT_INCLUDED"] = ["one line\r\nand the next"]
assert CT(_crlf) == CT(_esc)
add("G18 CRLF inside a string: the digest of LF (2.5)", "module_digest", {"text": TXT(_crlf)},
    OK(MDIG(_esc)))
_refs = copy.deepcopy(GRAV)
_refs["ASSUMPTIONS"][0]["REFS"] = ["h_max", "HEIGHT"]
assert MDIG(_refs) != D_GRAV
add("G19 REFS in another order: another digest (lists keep the author's order)", "module_digest",
    {"text": TXT(_refs)}, OK(MDIG(_refs)))

# ============================================================== module_bind
B_OK = OK([["SLOT", "MASS", ["m"]], ["SLOT", "UP_VELOCITY", ["v_z"]], ["SLOT", "HEIGHT", ["z"]]],
          [["PARAMETER", "g_n", "DECLARED", "9.80665", "m s-2", ""],
           ["PARAMETER", "h_max", "AUTHOR", "100", "m", ""]])


def B(name, bind, values, expected, text=X_GRAV, tpk=P2):
    add(name, "module_bind", {"tpk": tpk, "text": text, "bind": bind, "values": values}, expected)


H100 = {"h_max": {"VALUE": "100", "UNIT": "m"}}
B("N01 GRAV bound on the projectile", GRAV_BIND, H100, B_OK)
B("N02 an author value left symbolic", GRAV_BIND, {"h_max": {}},
  OK([["SLOT", "MASS", ["m"]], ["SLOT", "UP_VELOCITY", ["v_z"]], ["SLOT", "HEIGHT", ["z"]]],
     [["PARAMETER", "g_n", "DECLARED", "9.80665", "m s-2", ""],
      ["PARAMETER", "h_max", "SYMBOLIC", "", "", ""]]))
B("N03 an author value with its uncertainty", GRAV_BIND,
  {"h_max": {"VALUE": "100", "UNIT": "m", "UNCERTAINTY": "0.5"}},
  OK([["SLOT", "MASS", ["m"]], ["SLOT", "UP_VELOCITY", ["v_z"]], ["SLOT", "HEIGHT", ["z"]]],
     [["PARAMETER", "g_n", "DECLARED", "9.80665", "m s-2", ""],
      ["PARAMETER", "h_max", "AUTHOR", "100", "m", "0.5"]]))
_p2bad = copy.deepcopy(P2)
_p2bad["LAW"][3] = law("EOM_Z", "m*D(v_z,t)=-m*g_n", "EOM(v_z)", ["g_n", "m", "t", "v_z"], "x")
assert t1_first(_p2bad) == "LAW:EOM_Z: REFS g_n does not resolve"
B("N04 a base TPK failing T1", GRAV_BIND, H100,
  REF("BASE: the TPK fails T1: LAW:EOM_Z: REFS g_n does not resolve"), tpk=_p2bad)
B("N05 bind not an object", [["m"]], H100, REF("BIND: not an object"))
B("N06 an unknown slot", dict(GRAV_BIND, SPEED=["v_x"]), H100, REF("BIND: unknown slot SPEED"))
B("N07 a slot unbound (declared order)", {"MASS": ["m"], "UP_VELOCITY": ["v_z"]}, H100,
  REF("BIND: slot HEIGHT is not bound"))
B("N08 a slot bound to a string", dict(GRAV_BIND, MASS="m"), H100,
  REF("BIND: slot MASS has the wrong type"))
B("N09 a slot bound to no name", dict(GRAV_BIND, MASS=[]), H100,
  REF("BIND: slot MASS takes from 1 to 1 names, not 0"))
B("N10 a scalar slot bound to two names", dict(GRAV_BIND, MASS=["m", "m2"]), H100,
  REF("BIND: slot MASS takes from 1 to 1 names, not 2"))
B("N11 a CONSTANT slot bound to a DOF", dict(GRAV_BIND, MASS=["x"]), H100,
  REF("BIND: slot MASS: x is not a CONSTANT of the TPK"))
B("N12 a DOF slot bound to a CONSTANT", dict(GRAV_BIND, UP_VELOCITY=["m"]), H100,
  REF("BIND: slot UP_VELOCITY: m is not a DOF of the TPK"))
B("N13 a DOF of another dimension", dict(GRAV_BIND, UP_VELOCITY=["z"]), H100,
  REF("BIND: slot UP_VELOCITY: z has dimension L1, not L1 T-1"))
B("N14 a name the TPK lacks", dict(GRAV_BIND, UP_VELOCITY=["w"]), H100,
  REF("BIND: slot UP_VELOCITY: w is not a DOF of the TPK"))
AQV = {"A_ref": {}, "C_d": {}, "L_ref": {}}
B("N15 list names out of code point order", {"VELOCITY": ["v_z", "v_x"]}, AQV,
  REF("BIND: slot VELOCITY: the names are not in code point order without repeats"), text=X_AIRQ)
B("N16 list names repeated", {"VELOCITY": ["v_x", "v_x"]}, AQV,
  REF("BIND: slot VELOCITY: the names are not in code point order without repeats"), text=X_AIRQ)
B("N17 a list slot of four names", {"VELOCITY": ["a", "b", "c", "d"]}, AQV,
  REF("BIND: slot VELOCITY takes from 1 to 3 names, not 4"), text=X_AIRQ)
B("N18 a list slot bound", {"VELOCITY": ["v_x", "v_z"]}, AQV,
  OK([["SLOT", "VELOCITY", ["v_x", "v_z"]]],
     [["PARAMETER", "rho_air", "DECLARED", "1.225", "kg m-3", ""],
      ["PARAMETER", "mu_air", "DECLARED", "1.7894e-5", "Pa s", ""],
      ["PARAMETER", "C_d", "SYMBOLIC", "", "", ""], ["PARAMETER", "A_ref", "SYMBOLIC", "", "", ""],
      ["PARAMETER", "L_ref", "SYMBOLIC", "", "", ""]]), text=X_AIRQ)
B("N19 one name in two slots", {"PULLED": ["v_x"], "PUSHED": ["v_x"]}, {"F_a": {}, "F_b": {}},
  REF("BIND: v_x is bound to two slots, PUSHED and PULLED"), text=X_TPUSH)
B("N20 values not an object", GRAV_BIND, [], REF("VALUES: not an object"))
B("N21 an unknown parameter", GRAV_BIND, dict(H100, g={}), REF("VALUES: unknown parameter g"))
B("N22 a value for a parameter that is not OVERRIDABLE", GRAV_BIND,
  dict(H100, g_n={"VALUE": "9.81", "UNIT": "m s-2"}), REF("VALUES: g_n is not OVERRIDABLE"))
B("N23 an author parameter without a value", GRAV_BIND, {},
  REF("VALUES: h_max is author supplied: give a value, or {} for a symbolic constant"))
B("N24 a value that is not an object", GRAV_BIND, {"h_max": "100"},
  REF("VALUES: h_max has the wrong type"))
B("N25 VALUE without UNIT", GRAV_BIND, {"h_max": {"VALUE": "100"}},
  REF("VALUES: h_max: VALUE requires UNIT"))
B("N26 UNIT without VALUE", GRAV_BIND, {"h_max": {"UNIT": "m"}},
  REF("VALUES: h_max: UNIT requires VALUE"))
B("N27 UNCERTAINTY alone", GRAV_BIND, {"h_max": {"UNCERTAINTY": "1"}},
  REF("VALUES: h_max: UNCERTAINTY requires VALUE"))
B("N28 an unknown field in a value", GRAV_BIND,
  {"h_max": {"VALUE": "100", "UNIT": "m", "NOTE": "x"}}, REF("VALUES: h_max: unknown field NOTE"))
B("N29 a VALUE that is not a decimal", GRAV_BIND, {"h_max": {"VALUE": "1OO", "UNIT": "m"}},
  REF("VALUES: h_max: VALUE 1OO is not a decimal"))
B("N30 a UNIT of another dimension", GRAV_BIND, {"h_max": {"VALUE": "100", "UNIT": "s"}},
  REF("VALUES: h_max: UNIT s has dimension T1, not L1"))
assert U.unit_parse("furlong") == ["REFUSED", "UNIT furlong: furlong is not a unit symbol"]
B("N31 a UNIT that does not parse (tpc_units' reason)", GRAV_BIND,
  {"h_max": {"VALUE": "100", "UNIT": "furlong"}},
  REF("VALUES: h_max: UNIT furlong: furlong is not a unit symbol"))
B("N32 a negative UNCERTAINTY", GRAV_BIND,
  {"h_max": {"VALUE": "100", "UNIT": "m", "UNCERTAINTY": "-1"}},
  REF("VALUES: h_max: UNCERTAINTY -1 is negative"))
B("N33 a VALUE given as a number", GRAV_BIND, {"h_max": {"VALUE": 100, "UNIT": "m"}},
  REF("VALUES: h_max: field VALUE has the wrong type"))
B("N34 a bidi override in a VALUE", GRAV_BIND, {"h_max": {"VALUE": "1\u202e00", "UNIT": "m"}},
  REF("VALUES: h_max: field VALUE contains disallowed character U+202E"))
B("N35 an OVERRIDABLE value overridden", {}, {"k_loc": {"VALUE": "3", "UNIT": "1"}},
  OK([], [["PARAMETER", "k_loc", "OVERRIDDEN", "3", "1", ""]]), text=X_TOVR)
B("N36 an OVERRIDABLE value kept", {}, {},
  OK([], [["PARAMETER", "k_loc", "DECLARED", "2", "1", ""]]), text=X_TOVR)
B("N37 an empty override", {}, {"k_loc": {}},
  REF("VALUES: k_loc: an override needs VALUE and UNIT"), text=X_TOVR)
B("N38 a module text that is not JSON", GRAV_BIND, H100, NOTJSON, text="{")
B("N39 an author UNCERTAINTY that is not a decimal (RV F-9)", GRAV_BIND,
  {"h_max": {"VALUE": "100", "UNIT": "m", "UNCERTAINTY": "x"}},
  REF("VALUES: h_max: UNCERTAINTY x is not a decimal"))

# ============================================================ module_expand
def X(name, draft, claims, modules, expected):
    add(name, "module_expand", {"draft": draft, "claims": claims, "modules": modules}, expected)


D_P2 = {"TPK": P2}
X("X01 GRAV-UNIFORM on the projectile: the weight composed into EOM_Z", D_P2, [C_GRAV], M4,
  R_GRAV)
X("X02 AIR-QUADRATIC: one term per member, the speed a sum", D_P2, [C_AIRQ], M4, R_AIRQ)
X("X03 AIR-LINEAR", D_P2, [C_AIRL], M4, R_AIRL)
X("X04 MATH: a constant only; no structure check needed", D_P2, [C_MATH], M4, R_MATH)
X("X05 GRAV and AIR-QUADRATIC, buoyancy NO: terms in module-ID order", D_P2,
  [C_GRAV_NO, C_AIRQ], M4, R_GRAV_AIRQ)
X("X06 the other selection order: one D(TPK)", D_P2, [C_AIRQ, C_GRAV_NO], M4, R_GRAV_AIRQ)
X("X07 buoyancy YES changes no expansion (RD-MS-7)", D_P2, [C_GRAV_YES, C_AIRQ], M4, R_GRAV_AIRQ)
X("X08 buoyancy not answered", D_P2, [C_GRAV, C_AIRQ], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: question BUOYANCY is not answered"))
X("X09 AIR-LINEAR and MATH: pi_c emitted identically merges", D_P2, [C_AIRL, C_MATH], M4,
  R_AIRL_MATH)
X("X10 the other order", D_P2, [C_MATH, C_AIRL], M4, R_AIRL_MATH)
X("X11 three modules (buoyancy NO)", D_P2, [C_MATH, C_GRAV_NO, C_AIRL], M4, R_GRAV_AIRL_MATH)
X("X12 pi_c emitted differently is refused", D_P2, [C_TPI, C_MATH], M4 + [X_TPI],
  REF("EXPAND: MATH 1.0.0 and TEST-PI 1.0.0 both emit pi_c, differently"))
X("X13 AIR-QUADRATIC EXCLUDES AIR-LINEAR", D_P2, [C_AIRQ, C_AIRL], M4,
  REF("CLAIMS: AIR-LINEAR 1.0.0 and AIR-QUADRATIC 1.0.0 exclude each other (EXCLUDES)"))
X("X14 EXCLUDES is symmetric: MATH names nothing, TEST-EXCL names MATH", D_P2,
  [C_MATH, C_TEXCL], M4 + [X_TEXCL],
  REF("CLAIMS: MATH 1.0.0 and TEST-EXCL 1.0.0 exclude each other (EXCLUDES)"))
C_TAB_YES = claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND,
                  {"h_max": {"VALUE": "1", "UNIT": "m"}}, {"FIXED:v_z": "YES"})
C_TAB_NO = claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND,
                 {"h_max": {"VALUE": "1", "UNIT": "m"}}, {"FIXED:v_z": "NO"})
C_TAB_NONE = claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND,
                   {"h_max": {"VALUE": "1", "UNIT": "m"}}, {})
D_TAB = {"TPK": TABLE}
X("X15 weight on a FIXED velocity, support answered YES: dropped, the balance assumed", D_TAB,
  [C_TAB_YES], M4,
  XR(EXP_TABLE, [], rows_of(GRAV_EMIT_100[:1], G1)
     + [["CONSTANT", "h_max", [G1]]] + rows_of(GRAV_EMIT_100[2:], G1)
     + [["ASSUMPTION", "fixed_v_z_balanced", [G1]]], [[G1, "v_z"]]))
X("X16 the support-force question answered NO", D_TAB, [C_TAB_NO], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: v_z is FIXED and the answer to FIXED:v_z is NO: a force "
      "along a fixed DOF with no constraint force balancing it contradicts the constraint"))
X("X17 the support-force question not answered", D_TAB, [C_TAB_NONE], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: question FIXED:v_z is not answered (v_z is FIXED: does a "
      "constraint force balance the forces this module adds along it?)"))
X("X18 a FIXED answer where nothing is fixed", D_P2,
  [claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND, H100, {"FIXED:v_z": "YES"})], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: answer FIXED:v_z answers no question raised"))
X("X19 a buoyancy answer without an air module", D_P2, [C_GRAV_NO], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: answer BUOYANCY answers no question raised"))
X("X20 an answer neither YES nor NO", D_P2,
  [claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND, H100, {"BUOYANCY": "MAYBE"}), C_AIRQ], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: answer BUOYANCY is not YES or NO"))
X("X21 an answer key that is no question", D_P2,
  [claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND, H100, {"buoyancy": "NO"}), C_AIRQ], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: answer buoyancy is not a question"))
_p2rm = tpk_mod(P2, extra=lambda t: t.update({"CONSTRAINT": [
    {"NAME": "C_RVZ", "TARGET": "v_z", "ACTION": "REMOVE", "STATEMENT": "v_z is not modelled"}]}))
assert Z.t1_well_formed(_p2rm)[0] and label_of(_p2rm, "v_z")[0] == "REMOVED"
X("X22 a REMOVED target", {"TPK": _p2rm}, [C_GRAV], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: v_z is REMOVED; no module is bound to a removed DOF"))
_p2in = copy.deepcopy(P2)
_p2in["DOF"][4]["ROLE"] = "INPUT"
assert Z.t1_well_formed(_p2in)[0] and label_of(_p2in, "v_z")[0] == "INPUT"
X("X23 an INPUT target", {"TPK": _p2in}, [C_GRAV], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: v_z is an INPUT DOF and has no equation of motion"))
_puw = {"DOF": [dof("u", "L1 T-1"), dof("w", "L1 T-1")],
        "LAW": [law("EQ_W", "D(w,u)=0", "EOM(w)", ["u", "w"], "dw/du = 0")]}
assert Z.t1_well_formed(_puw)[0] and label_of(_puw, "u")[0] == "INDEPENDENT"
X("X24 the independent variable as a target", {"TPK": _puw},
  [claim("TEST-PUSH", "1.0.0", D_TPUSH, {"PULLED": ["w"], "PUSHED": ["u"]},
         {"F_a": {}, "F_b": {}}, {})], [X_TPUSH],
  REF("CLAIM TEST-PUSH 1.0.0: TERM 1: u is the independent variable"))
_p2noeom = tpk_mod(P2, drop_laws=("EOM_Z",))
assert Z.t1_well_formed(_p2noeom)[0] and label_of(_p2noeom, "v_z") == ("NO LAW", "")
X("X25 no equation of motion for the target (NO LAW)", {"TPK": _p2noeom}, [C_GRAV], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: v_z has no equation of motion (NO LAW); write it, then "
      "bind the module"))
_p2prose = copy.deepcopy(P2)
for _l in _p2prose["LAW"]:
    del _l["EQUATION"]
assert Z.t1_well_formed(_p2prose)[0] and label_of(_p2prose, "v_z") == ("NO LAW", "")
X("X26 laws in prose only (analysis NO FORM: NO LAW)", {"TPK": _p2prose}, [C_GRAV], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: v_z has no equation of motion (NO LAW); write it, "
      "then bind the module"))
_p2pde = copy.deepcopy(P2)
_p2pde["PARAMETERS"] = {"D_VARIABLES": "SEVERAL"}
_p2pde["DOF"].append(dof("s", "T1"))
_p2pde["LAW"][0] = law("KIN_X", "D(x,s)=v_x", "EOM(x)", ["s", "v_x", "x"], "dx/ds = v_x")
assert Z.t1_well_formed(_p2pde)[0] and label_of(_p2pde, "v_z") == ("NOT ANALYZED", "")
X("X26b two D variables (analysis PDE: NOT ANALYZED)", {"TPK": _p2pde}, [C_GRAV], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: v_z has no equation of motion (NOT ANALYZED); write "
      "it, then bind the module"))
_p2role = copy.deepcopy(P2)
_p2role["LAW"][3]["ROLE"] = "CONSTITUTIVE"
assert Z.t1_well_formed(_p2role)[0] and label_of(_p2role, "v_z") == ("DETERMINED", "EOM_Z")
X("X27 the determining law's ROLE is not EOM(v_z)", {"TPK": _p2role}, [C_GRAV], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: v_z's equation of motion LAW EOM_Z has ROLE "
      "CONSTITUTIVE, not EOM(v_z)"))
_p2both = copy.deepcopy(P2)
_p2both["CONSTANT"].append({"NAME": "c_v", "DIMENSION": "M1"})
_p2both["LAW"][3] = law("EOM_Z", "m*D(v_z,t)=c_v*D(v_z,t)", "EOM(v_z)", ["c_v", "m", "t", "v_z"],
                        "m dv_z/dt = c_v dv_z/dt")
assert Z.t1_well_formed(_p2both)[0] and label_of(_p2both, "v_z") == ("DETERMINED", "EOM_Z")
X("X28 both sides of the equation-of-motion shape", {"TPK": _p2both}, [C_GRAV], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: LAW EOM_Z has no single side of the equation-of-motion "
      "shape for v_z"))
_p2right = copy.deepcopy(P2)
_p2right["LAW"][3] = law("EOM_Z", "0 = m*D(v_z, t)", "EOM(v_z)", ["m", "t", "v_z"], "0 = m dv_z/dt")
assert label_of(_p2right, "v_z") == ("DETERMINED", "EOM_Z")
_EXP_RIGHT = tpk_mod(_p2right, {"EOM_Z": (canon_is("-m*g_n=m*D(v_z,t)", "equation"),
                                          ["g_n", "m", "t", "v_z"], "0 = m dv_z/dt" + S_W)},
                     GRAV_EMIT_100)
must_pass(_EXP_RIGHT, "_EXP_RIGHT")
X("X29 the D side on the right: the force side is the left", {"TPK": _p2right}, [C_GRAV], M4,
  XR(_EXP_RIGHT, [["EOM_Z", "-m*g_n=m*D(v_z,t)"]], rows_of(GRAV_EMIT_100, G1), []))
SPRING = {"DOF": [dof("t", "T1"), dof("z", "L1"), dof("v_z", "L1 T-1")],
          "CONSTANT": [{"NAME": "m", "DIMENSION": "M1"}, {"NAME": "k_s", "DIMENSION": "M1 T-2"}],
          "LAW": [law("KIN", "D(z,t)=v_z", "EOM(z)", ["t", "v_z", "z"], "dz/dt = v_z"),
                  law("N2", "m*D(v_z,t)=-k_s*z", "EOM(v_z)", ["k_s", "m", "t", "v_z", "z"],
                      "m dv_z/dt = -k_s z")],
          "ASSUMPTION": [{"NAME": "linear_spring", "PROPOSITION": "the spring force is linear in z",
                          "REFS": ["z"]}]}
must_pass(SPRING, "SPRING")
_EXP_SPRING = tpk_mod(SPRING, {"N2": (canon_is("m*D(v_z,t)=-k_s*z-m*g_n", "equation"),
                                      ["g_n", "k_s", "m", "t", "v_z", "z"],
                                      "m dv_z/dt = -k_s z" + S_W)},
                      [("CONSTANT", E_GN), ("CONSTANT", E_HMAXS), ("ASSUMPTION", E_GU),
                       ("ASSUMPTION", E_GS), ("ASSUMPTION", E_FI), ("BOUNDARY", E_GR)])
must_pass(_EXP_SPRING, "_EXP_SPRING")
X("X30 a force side that is not 0: the term appended; h_max symbolic", {"TPK": SPRING},
  [claim("GRAV-UNIFORM", "1.0.0", D_GRAV, GRAV_BIND, {"h_max": {}}, {})], M4,
  XR(_EXP_SPRING, [["N2", "m*D(v_z,t)=-k_s*z-m*g_n"]], rows_of(GRAV_EMIT_100, G1), []))
_EXP_PUSH = tpk_mod(P2, {"EOM_X": (canon_is("m*D(v_x,t)=F_a+F_b", "equation"),
                                   ["F_a", "F_b", "m", "t", "v_x"],
                                   "m dv_x/dt = 0; with the push: +(F_a+F_b)"),
                         "EOM_Z": (canon_is("m*D(v_z,t)=-(F_a+F_b)", "equation"),
                                   ["F_a", "F_b", "m", "t", "v_z"],
                                   "m dv_z/dt = 0; with the pull: -(F_a+F_b)")},
                    [("CONSTANT", {"NAME": "F_a", "DIMENSION": "L1 M1 T-2"}),
                     ("CONSTANT", {"NAME": "F_b", "DIMENSION": "L1 M1 T-2"})])
must_pass(_EXP_PUSH, "_EXP_PUSH")
TP1 = "TEST-PUSH 1.0.0"
X("X31 SIGN +, and a sum wrapped in parentheses under SIGN -", D_P2, [C_TPUSH], [X_TPUSH],
  XR(_EXP_PUSH, [["EOM_X", "m*D(v_x,t)=F_a+F_b"], ["EOM_Z", "m*D(v_z,t)=-(F_a+F_b)"]],
     [["CONSTANT", "F_a", [TP1]], ["CONSTANT", "F_b", [TP1]]], []))
_p2damp = copy.deepcopy(P2)
_p2damp["CONSTANT"].append({"NAME": "c_x", "DIMENSION": "M1 T-1"})
_p2damp["LAW"][2] = law("EOM_X", "m*D(v_x,t)=-c_x*v_x", "EOM(v_x)", ["c_x", "m", "t", "v_x"],
                        "m dv_x/dt = -c_x v_x")
_p2damp["LAW"][3] = law("EOM_Z", "m*D(v_z,t)=-c_x*v_z", "EOM(v_z)", ["c_x", "m", "t", "v_z"],
                        "m dv_z/dt = -c_x v_z")
must_pass(_p2damp, "_p2damp")
_EXP_DAMP = tpk_mod(_p2damp, {"EOM_X": (canon_is("m*D(v_x,t)=-c_x*v_x+(F_a+F_b)", "equation"),
                                        ["F_a", "F_b", "c_x", "m", "t", "v_x"],
                                        "m dv_x/dt = -c_x v_x; with the push: +(F_a+F_b)"),
                              "EOM_Z": (canon_is("m*D(v_z,t)=-c_x*v_z-(F_a+F_b)", "equation"),
                                        ["F_a", "F_b", "c_x", "m", "t", "v_z"],
                                        "m dv_z/dt = -c_x v_z; with the pull: -(F_a+F_b)")},
                    [("CONSTANT", {"NAME": "F_a", "DIMENSION": "L1 M1 T-2"}),
                     ("CONSTANT", {"NAME": "F_b", "DIMENSION": "L1 M1 T-2"})])
must_pass(_EXP_DAMP, "_EXP_DAMP")
X("X32 appended to a force side that is not 0: +(...) and -(...)", {"TPK": _p2damp}, [C_TPUSH],
  [X_TPUSH],
  XR(_EXP_DAMP, [["EOM_X", "m*D(v_x,t)=-c_x*v_x+(F_a+F_b)"],
                 ["EOM_Z", "m*D(v_z,t)=-c_x*v_z-(F_a+F_b)"]],
     [["CONSTANT", "F_a", [TP1]], ["CONSTANT", "F_b", [TP1]]], []))
FALL = {"DOF": [dof("t", "T1"), dof("z", "L1"), dof("v_z", "L1 T-1")],
        "CONSTANT": [{"NAME": "m", "DIMENSION": "M1"}],
        "LAW": [law("KIN_Z", "D(z,t)=v_z", "EOM(z)", ["t", "v_z", "z"], "dz/dt = v_z"),
                law("EOM_Z", "m*D(v_z,t)=0", "EOM(v_z)", ["m", "t", "v_z"], "m dv_z/dt = 0")]}
_TQ1 = "-5e-1*rho_air*C_d*A_ref*sqrt(v_z^2)*v_z"
_COND1 = ("mu_air!=0 and v_z^2>=0 and rho_air*sqrt(v_z^2)*L_ref/mu_air>1e3 and "
          "rho_air*sqrt(v_z^2)*L_ref/mu_air<2e5")
canon_is(_COND1, "predicate")
_EXP_FALL = tpk_mod(FALL, {"EOM_Z": (canon_is("m*D(v_z,t)=" + _TQ1, "equation"),
                                     ["A_ref", "C_d", "m", "rho_air", "t", "v_z"],
                                     "m dv_z/dt = 0; with the quadratic drag: " + _TQ1)},
                    [("CONSTANT", E_RHO), ("CONSTANT", E_MU), ("CONSTANT", E_CD),
                     ("CONSTANT", E_AREF), ("CONSTANT", E_LREF),
                     ("ASSUMPTION", {"NAME": "air_still", "PROPOSITION": PROP_AS, "REFS": ["v_z"]}),
                     ("ASSUMPTION", E_AU), ("ASSUMPTION", E_DCD),
                     ("ASSUMPTION", {"NAME": "drag_no_lift", "PROPOSITION": PROP_NL,
                                     "REFS": ["v_z"]}),
                     ("ASSUMPTION", {"NAME": "drag_no_magnus", "PROPOSITION": PROP_NM,
                                     "REFS": ["v_z"]}),
                     ("BOUNDARY", {"NAME": "drag_quadratic_regime", "PREDICATE": PRED_Q,
                                   "REFS": ["L_ref", "mu_air", "rho_air", "v_z"],
                                   "CONDITION": _COND1})])
must_pass(_EXP_FALL, "_EXP_FALL")
X("X33 a list slot of one member: the sum of one square", {"TPK": FALL},
  [claim("AIR-QUADRATIC", "1.0.0", D_AIRQ, {"VELOCITY": ["v_z"]}, AQV, {})], M4,
  XR(_EXP_FALL, [["EOM_Z", "m*D(v_z,t)=" + _TQ1]], rows_of(AIRQ_EMIT_2, AQ1), []))
_p2gn = copy.deepcopy(P2)
_p2gn["CONSTANT"].append(copy.deepcopy(E_GN))
X("X34 an emitted name the author's TPK already has", {"TPK": _p2gn}, [C_GRAV], M4,
  REF("EXPAND: GRAV-UNIFORM 1.0.0 emits CONSTANT g_n, but the TPK already has an entry named g_n"))
X("X35 a claim whose DIGEST is not its text's", D_P2,
  [dict(C_GRAV, DIGEST=D_GRAV11)], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: DIGEST does not match the module text"))
X("X36 a claimed version with no text", D_P2,
  [dict(C_GRAV, VERSION="1.1.0", DIGEST=D_GRAV11)], M4,
  REF("CLAIM GRAV-UNIFORM 1.1.0: no module text"))
X("X37 two claims of one module", D_P2, [C_GRAV, C_GRAV], M4,
  REF("CLAIMS: duplicate module GRAV-UNIFORM"))
X("X38 claims not a list", D_P2, C_GRAV, M4, REF("CLAIMS: not a list"))
_cm = dict(C_GRAV)
del _cm["ANSWERS"]
X("X39 a claim without ANSWERS", D_P2, [_cm], M4, REF("CLAIM 1: missing field ANSWERS"))
X("X40 a claim with an unknown field", D_P2, [dict(C_GRAV, NOTE="x")], M4,
  REF("CLAIM 1: unknown field NOTE"))
X("X41 a claim whose DIGEST is not a digest", D_P2, [dict(C_GRAV, DIGEST="XYZ")], M4,
  REF("CLAIM 1: DIGEST is not a digest"))
X("X42 the draft not an object", [P2], [C_GRAV], M4, REF("DRAFT: not an object"))
X("X43 an unknown zone", {"TPK": P2, "RR": {}}, [C_GRAV], M4, REF("DRAFT: unknown zone RR"))
X("X44 no TPK", {"TMS": {}}, [C_GRAV], M4, REF("DRAFT: missing zone TPK"))
X("X45 a base TPK failing T1", {"TPK": _p2bad}, [C_GRAV], M4,
  REF("BASE: the TPK fails T1: LAW:EOM_Z: REFS g_n does not resolve"))
X("X46 modules not a list", D_P2, [C_GRAV], X_GRAV, REF("MODULES: not a list of texts"))
X("X47 a module text that is not JSON", D_P2, [C_GRAV], [X_GRAV, "{"],
  REF("MODULES 2: MODULE: the text is not JSON (RFC 8259, strict)"))
X("X48 one text twice", D_P2, [C_GRAV], [X_GRAV, json.dumps(GRAV, indent=1)],
  REF("MODULES: duplicate text of GRAV-UNIFORM 1.0.0"))
X("X49 no claim: the base unchanged (the NONE answer)", D_P2, [], M4,
  XR(P2, [], [], []))
X("X50 a binding refused inside the expansion", D_P2,
  [dict(C_GRAV, BIND=dict(GRAV_BIND, UP_VELOCITY=["z"]))], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: BIND: slot UP_VELOCITY: z has dimension L1, not L1 T-1"))
_long = "m*D(v_z,t)=" + "-c_1*v_z" * 510
assert len(_long) == 4091
_p2long = copy.deepcopy(P2)
_p2long["CONSTANT"].append({"NAME": "c_1", "DIMENSION": "M1 T-1"})
_p2long["LAW"][3] = law("EOM_Z", _long, "EOM(v_z)", ["c_1", "m", "t", "v_z"], "a long force side")
assert Z.t1_well_formed(_p2long)[0] and label_of(_p2long, "v_z") == ("DETERMINED", "EOM_Z")
assert F.parse_form(_long + "-m*g_n", "equation") == [
    "REFUSED", "the form is longer than 4096 code points"]
X("X51 a composed EQUATION over 4096 code points (D186.1)", {"TPK": _p2long}, [C_GRAV], M4,
  REF("EXPAND: LAW EOM_Z: the composed EQUATION is refused: the form is longer than 4096 code "
      "points"))
X("X52 one name bound to two slots, inside the expansion", D_P2,
  [claim("TEST-PUSH", "1.0.0", D_TPUSH, {"PULLED": ["v_x"], "PUSHED": ["v_x"]},
         {"F_a": {}, "F_b": {}}, {})], [X_TPUSH],
  REF("CLAIM TEST-PUSH 1.0.0: BIND: v_x is bound to two slots, PUSHED and PULLED"))
X("X53 TrM and TMS in the draft: the expansion as X01 (the RENDER shows D(TPK) only)",
  {"TPK": P2, "TrM": {"HEADER": {}}, "TMS": {"PARAMETERS": {}}}, [C_GRAV], M4, R_GRAV)
# ---- RV's vectors (review 0.1.0 F-9; rv_additions.py), expectations re-derived here
TP1_ = "TEST-PUSH 1.0.0"
C_TPUSH_TAB = claim("TEST-PUSH", "1.0.0", D_TPUSH, {"PULLED": ["v_z"], "PUSHED": ["v_x"]},
                    {"F_a": {}, "F_b": {}}, {"FIXED:v_z": "YES"})
E_FA = {"NAME": "F_a", "DIMENSION": "L1 M1 T-2"}
E_FB = {"NAME": "F_b", "DIMENSION": "L1 M1 T-2"}
_EXP_TAB2 = tpk_mod(TABLE, {"EOM_X": ("m*D(v_x,t)=F_a+F_b", ["F_a", "F_b", "m", "t", "v_x"],
                                      "m dv_x/dt = 0; with the push: +(F_a+F_b)")},
                    [("CONSTANT", E_GN), ("CONSTANT", E_HMAX1), ("ASSUMPTION", E_GU),
                     ("ASSUMPTION", E_GS), ("ASSUMPTION", E_FI), ("BOUNDARY", E_GR),
                     ("ASSUMPTION", E_FIXVZ), ("CONSTANT", E_FA), ("CONSTANT", E_FB)])
must_pass(_EXP_TAB2, "_EXP_TAB2")
assert DTPK(_EXP_TAB2) == "4b845b45a26745ae30d77643b7a7fb4270259c8febcc096d32cdec4ddefdd382"
X("X54 two modules drop terms on one FIXED DOF: the balance assumption merges", D_TAB,
  [C_TPUSH_TAB, C_TAB_YES], M4 + [X_TPUSH],
  XR(_EXP_TAB2, [["EOM_X", "m*D(v_x,t)=F_a+F_b"]],
     rows_of(GRAV_EMIT_100[:1], G1) + [["CONSTANT", "h_max", [G1]]]
     + rows_of(GRAV_EMIT_100[2:], G1) + [["ASSUMPTION", "fixed_v_z_balanced", [G1, TP1_]],
                                         ["CONSTANT", "F_a", [TP1_]], ["CONSTANT", "F_b", [TP1_]]],
     [[G1, "v_z"], [TP1_, "v_z"]]))
_und = copy.deepcopy(P2)
_und["DOF"].append(dof("w", "L1 M1 T-2"))
_und["LAW"][3] = law("EOM_Z", "m*D(v_z,t)=w", "EOM(v_z)", ["m", "t", "v_z", "w"], "m dv_z/dt = w")
assert Z.t1_well_formed(_und)[0] and label_of(_und, "v_z")[0] == "UNDER"
X("X55 the target in the UNDER part", {"TPK": _und}, [C_GRAV], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: v_z's equation of motion is not determined (UNDER); "
      "resolve it, then bind the module"))
_ovr = copy.deepcopy(P2)
_ovr["LAW"].append(law("EOM_Z2", "m*D(v_z,t)=0", "EOM(v_z)", ["m", "t", "v_z"],
                       "m dv_z/dt = 0, again"))
assert Z.t1_well_formed(_ovr)[0] and label_of(_ovr, "v_z")[0] == "OVER"
X("X56 two equations of motion for the target (OVER)", {"TPK": _ovr}, [C_GRAV], M4,
  REF("CLAIM GRAV-UNIFORM 1.0.0: TERM 1: v_z's equation of motion is not determined (OVER); "
      "resolve it, then bind the module"))
SUMP1 = "TEST-SUMP 1.0.0"
C_SUMP = claim("TEST-SUMP", "1.0.0", D_SUMP, {"VELOCITY": ["v_x", "v_z"]}, {"k_q": {}}, {})
E_KQ = {"NAME": "k_q", "DIMENSION": "L-1 M1"}
_EQ_SX = canon_is("m*D(v_x,t)=-k_q*(v_x^2+v_z^2)", "equation")
_EQ_SZ = canon_is("m*D(v_z,t)=-k_q*(v_x^2+v_z^2)", "equation")
_EXP_SUMP = tpk_mod(P2, {"EOM_X": (_EQ_SX, ["k_q", "m", "t", "v_x", "v_z"],
                                   "m dv_x/dt = 0; with the test force: -k_q*(v_x^2+v_z^2)"),
                         "EOM_Z": (_EQ_SZ, ["k_q", "m", "t", "v_x", "v_z"],
                                   "m dv_z/dt = 0; with the test force: -k_q*(v_x^2+v_z^2)")},
                    [("CONSTANT", E_KQ)])
must_pass(_EXP_SUMP, "_EXP_SUMP")
assert DTPK(_EXP_SUMP) == "e1cff39ece3129f139b8b74d9e94d7e33718d2a94920165f2f5b6cd471acf187"
X("X57 a sum inside a product keeps its parentheses (3.4)", D_P2, [C_SUMP], [X_SUMP],
  XR(_EXP_SUMP, [["EOM_X", _EQ_SX], ["EOM_Z", _EQ_SZ]], [["CONSTANT", "k_q", [SUMP1]]], []))
X("X58 a claim that is not an object", D_P2, ["GRAV-UNIFORM"], M4, REF("CLAIM 1: not an object"))
X("X59 a claim ID that is not a module ID", D_P2, [dict(C_GRAV, ID="grav")], M4,
  REF("CLAIM 1: ID is not a module ID"))
X("X60 a claim VERSION that is not a version", D_P2, [dict(C_GRAV, VERSION="1")], M4,
  REF("CLAIM 1: VERSION is not a version"))
C_NOAIR = claim("TEST-NOAIR", "1.0.0", D_NOAIR, {}, {}, {})
X("X61 EXCLUDES by pattern (AIR-*)", D_P2, [C_NOAIR, C_AIRQ], M4 + [X_NOAIR],
  REF("CLAIMS: AIR-QUADRATIC 1.0.0 and TEST-NOAIR 1.0.0 exclude each other (EXCLUDES)"))
# ---- S51: same-named AUTHOR parameters left symbolic by two modules merge
SHARE1 = "TEST-SHARE 1.0.0"
C_SHARE = claim("TEST-SHARE", "1.0.0", D_SHARE, {}, {"k_q": {}}, {})
X("X62 two same-named AUTHOR parameters left symbolic: one CONSTANT (S51)", D_P2,
  [C_SUMP, C_SHARE], [X_SUMP, X_SHARE],
  XR(_EXP_SUMP, [["EOM_X", _EQ_SX], ["EOM_Z", _EQ_SZ]], [["CONSTANT", "k_q", [SHARE1, SUMP1]]], []))
X("X63 the same, one given a value: they differ, refused", D_P2,
  [C_SUMP, dict(C_SHARE, VALUES={"k_q": {"VALUE": "2", "UNIT": "kg m-1"}})], [X_SUMP, X_SHARE],
  REF("EXPAND: TEST-SHARE 1.0.0 and TEST-SUMP 1.0.0 both emit k_q, differently"))

# 0.2.1 (C-MS-5, increment 11): the base TPK reaches T1 as the JSON reads, {} apart from []
# (tpm_module_spec C-MS-5: TPC-PHP's bridge into the zone functions now keeps the distinction)
_X_DOF = {"NAME": "x", "DIMENSION": "L1"}
assert t1_first({"DOF": {}}) == "TPK kind DOF is not a list"
X("X64 a base TPK kind that is a JSON {} is not a list (C-MS-5)", {"TPK": {"DOF": {}}}, [], [],
  REF("BASE: the TPK fails T1: TPK kind DOF is not a list"))
X("X65 a base TPK kind keyed 0 is not a list (C-MS-5)", {"TPK": {"DOF": {"0": _X_DOF}}}, [], [],
  REF("BASE: the TPK fails T1: TPK kind DOF is not a list"))
X("X66 an empty PARAMETERS object in the base is valid, D_VARIABLES ONE (C-MS-5)",
  {"TPK": {"PARAMETERS": {}, "DOF": [_X_DOF]}}, [], [],
  XR({"DOF": [_X_DOF]}, [], [], []))
X("X67 an entry that is a JSON [] is not an object (C-MS-5)", {"TPK": {"DOF": [[]]}}, [], [],
  REF("BASE: the TPK fails T1: TPK kind DOF: entry is not an object"))

# ============================================================== catalogue lines
DAY1, DAY2, DAY3, DAY4, DAY5 = "2026-10-01", "2026-10-02", "2026-10-03", "2026-10-04", "2026-10-05"


def LADD(i, v, d, date=DAY1):
    return {"ACT": "MODULE ADD", "ID": i, "VERSION": v, "DIGEST": d, "DATE": date}


def LVER(i, v, d, date):
    return {"ACT": "MODULE VERSION", "ID": i, "VERSION": v, "DIGEST": d, "DATE": date}


def LRET(i, v, date):
    return {"ACT": "MODULE RETIRE", "ID": i, "VERSION": v, "DATE": date}


def LREI(i, v, date):
    return {"ACT": "MODULE REINSTATE", "ID": i, "VERSION": v, "DATE": date}


def LWD(i, v, reason, date, repl=None):
    d = {"ACT": "MODULE WITHDRAW", "ID": i, "VERSION": v, "REASON": reason, "DATE": date}
    if repl is not None:
        d["REPLACED_BY"] = repl
    return d


GU, AQ, AL, MA, TX = "GRAV-UNIFORM", "AIR-QUADRATIC", "AIR-LINEAR", "MATH", "TEST-X"
L4 = [LADD(AL, "1.0.0", D_AIRL), LADD(AQ, "1.0.0", D_AIRQ), LADD(GU, "1.0.0", D_GRAV),
      LADD(MA, "1.0.0", D_MATH)]
OF_AL, OF_AQ, OF_GU, OF_MA = ([AL, "1.0.0", D_AIRL], [AQ, "1.0.0", D_AIRQ], [GU, "1.0.0", D_GRAV],
                              [MA, "1.0.0", D_MATH])
OFFER4 = [OF_AL, OF_AQ, OF_GU, OF_MA]
OF_TX = [TX, "1.0.0", D_TX]
OF_GU11 = [GU, "1.1.0", D_GRAV11]
L5X = L4 + [LADD(TX, "1.0.0", D_TX, DAY2)]
L5V = L4 + [LVER(GU, "1.1.0", D_GRAV11, DAY2)]
L5R = L4 + [LRET(MA, "1.0.0", DAY2)]
REASON = "the weight's sign was reversed in the binding page's preview"
L5W = L4 + [LWD(GU, "1.0.0", REASON, DAY2)]


def VROW(i, v, d, st, date, note=""):
    return [i, v, d, st, date, note]


def ST(n_offers):
    return [[n + 1, SD(o)] for n, o in enumerate(n_offers)]


def CS(name, lines, expected):
    add(name, "catalogue_state", {"lines": lines}, expected)


ROWS4 = [VROW(AL, "1.0.0", D_AIRL, "OFFERED", DAY1), VROW(AQ, "1.0.0", D_AIRQ, "OFFERED", DAY1),
         VROW(GU, "1.0.0", D_GRAV, "OFFERED", DAY1), VROW(MA, "1.0.0", D_MATH, "OFFERED", DAY1)]
STATES4 = ST([[OF_AL], [OF_AL, OF_AQ], [OF_AL, OF_AQ, OF_GU], OFFER4])
CS("S01 no line: an empty catalogue", [], OK(["VERSIONS", []], ["OFFERED", []], ["STATES", []]))
CS("S02 the first catalogue: four MODULE ADD lines", L4,
   OK(["VERSIONS", ROWS4], ["OFFERED", OFFER4], ["STATES", STATES4]))
CS("S03 MODULE VERSION: the previous version SUPERSEDED in the same act", L5V,
   OK(["VERSIONS", ROWS4[:2] + [VROW(GU, "1.0.0", D_GRAV, "SUPERSEDED", DAY2),
                                VROW(GU, "1.1.0", D_GRAV11, "OFFERED", DAY2), ROWS4[3]]],
      ["OFFERED", [OF_AL, OF_AQ, OF_GU11, OF_MA]],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_GU11, OF_MA])]]]))
CS("S04 MODULE RETIRE", L5R,
   OK(["VERSIONS", ROWS4[:3] + [VROW(MA, "1.0.0", D_MATH, "RETIRED", DAY2)]],
      ["OFFERED", [OF_AL, OF_AQ, OF_GU]],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_GU])]]]))
CS("S05 MODULE REINSTATE of a RETIRED version; equal offers, equal state digests",
   L5R + [LREI(MA, "1.0.0", DAY3)],
   OK(["VERSIONS", ROWS4[:3] + [VROW(MA, "1.0.0", D_MATH, "OFFERED", DAY3)]],
      ["OFFERED", OFFER4],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_GU])], [6, SD(OFFER4)]]]))
CS("S06 MODULE REINSTATE of a SUPERSEDED version: the OFFERED one SUPERSEDED",
   L5V + [LREI(GU, "1.0.0", DAY3)],
   OK(["VERSIONS", ROWS4[:2] + [VROW(GU, "1.0.0", D_GRAV, "OFFERED", DAY3),
                                VROW(GU, "1.1.0", D_GRAV11, "SUPERSEDED", DAY3), ROWS4[3]]],
      ["OFFERED", OFFER4],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_GU11, OF_MA])], [6, SD(OFFER4)]]]))
CS("S07 MODULE WITHDRAW of the OFFERED version: the ID offers nothing", L5W,
   OK(["VERSIONS", ROWS4[:2] + [VROW(GU, "1.0.0", D_GRAV, "WITHDRAWN", DAY2, REASON), ROWS4[3]]],
      ["OFFERED", [OF_AL, OF_AQ, OF_MA]],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_MA])]]]))
CS("S08 MODULE WITHDRAW of a SUPERSEDED version", L5V + [LWD(GU, "1.0.0", REASON, DAY3)],
   OK(["VERSIONS", ROWS4[:2] + [VROW(GU, "1.0.0", D_GRAV, "WITHDRAWN", DAY3, REASON),
                                VROW(GU, "1.1.0", D_GRAV11, "OFFERED", DAY2), ROWS4[3]]],
      ["OFFERED", [OF_AL, OF_AQ, OF_GU11, OF_MA]],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_GU11, OF_MA])],
                            [6, SD([OF_AL, OF_AQ, OF_GU11, OF_MA])]]]))
CS("S09 MODULE WITHDRAW naming REPLACED_BY a SUPERSEDED version",
   L5V + [LWD(GU, "1.1.0", REASON, DAY3, "1.0.0")],
   OK(["VERSIONS", ROWS4[:2] + [VROW(GU, "1.0.0", D_GRAV, "OFFERED", DAY3),
                                VROW(GU, "1.1.0", D_GRAV11, "WITHDRAWN", DAY3, REASON), ROWS4[3]]],
      ["OFFERED", OFFER4],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_GU11, OF_MA])], [6, SD(OFFER4)]]]))
_L_ret_ver = L4 + [LRET(GU, "1.0.0", DAY2), LVER(GU, "1.1.0", D_GRAV11, DAY3)]
CS("S10 MODULE VERSION of an ID whose versions are all RETIRED", _L_ret_ver,
   OK(["VERSIONS", ROWS4[:2] + [VROW(GU, "1.0.0", D_GRAV, "RETIRED", DAY2),
                                VROW(GU, "1.1.0", D_GRAV11, "OFFERED", DAY3), ROWS4[3]]],
      ["OFFERED", [OF_AL, OF_AQ, OF_GU11, OF_MA]],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_MA])],
                            [6, SD([OF_AL, OF_AQ, OF_GU11, OF_MA])]]]))
_L_repl_ret = L4 + [LVER(GU, "1.1.0", D_GRAV11, DAY2), LRET(GU, "1.1.0", DAY3),
                    LREI(GU, "1.0.0", DAY4), LWD(GU, "1.0.0", REASON, DAY5, "1.1.0")]
CS("S11 REPLACED_BY a RETIRED version", _L_repl_ret,
   OK(["VERSIONS", ROWS4[:2] + [VROW(GU, "1.0.0", D_GRAV, "WITHDRAWN", DAY5, REASON),
                                VROW(GU, "1.1.0", D_GRAV11, "OFFERED", DAY5), ROWS4[3]]],
      ["OFFERED", [OF_AL, OF_AQ, OF_GU11, OF_MA]],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_GU11, OF_MA])],
                            [6, SD([OF_AL, OF_AQ, OF_MA])], [7, SD(OFFER4)],
                            [8, SD([OF_AL, OF_AQ, OF_GU11, OF_MA])]]]))
CS("S12 MODULE ADD of an ID already declared", L4 + [LADD(GU, "2.0.0", HX("a"), DAY2)],
   REF("LINE 5: MODULE ADD of GRAV-UNIFORM, an ID already declared (no ID is reissued)"))
CS("S13 MODULE ADD of an ID whose only version is RETIRED",
   L4 + [LRET(GU, "1.0.0", DAY2), LADD(GU, "1.0.0", HX("b"), DAY3)],
   REF("LINE 6: MODULE ADD of GRAV-UNIFORM, an ID already declared (no ID is reissued)"))
CS("S14 MODULE VERSION of an ID never added", [LVER(GU, "1.0.0", HX("c"), DAY1)],
   REF("LINE 1: MODULE VERSION of GRAV-UNIFORM, an ID never added"))
CS("S15 MODULE VERSION reissuing a version", L4 + [LVER(GU, "1.0.0", HX("d"), DAY2)],
   REF("LINE 5: GRAV-UNIFORM 1.0.0 is already declared (no version is reissued)"))
CS("S16 MODULE VERSION not greater", L4 + [LVER(GU, "0.9.0", HX("e"), DAY2)],
   REF("LINE 5: GRAV-UNIFORM 0.9.0 is not greater than GRAV-UNIFORM 1.0.0"))
CS("S17 greater as integers: 1.10.0 after 1.9.0",
   [LADD("TEST-V", "1.9.0", HX("v19")), LVER("TEST-V", "1.10.0", HX("v110"), DAY2)],
   OK(["VERSIONS", [VROW("TEST-V", "1.10.0", HX("v110"), "OFFERED", DAY2),
                    VROW("TEST-V", "1.9.0", HX("v19"), "SUPERSEDED", DAY2)]],
      ["OFFERED", [["TEST-V", "1.10.0", HX("v110")]]],
      ["STATES", [[1, SD([["TEST-V", "1.9.0", HX("v19")]])],
                  [2, SD([["TEST-V", "1.10.0", HX("v110")]])]]]))
CS("S18 MODULE REINSTATE of a WITHDRAWN version", L5W + [LREI(GU, "1.0.0", DAY3)],
   REF("LINE 6: MODULE REINSTATE of GRAV-UNIFORM 1.0.0, which is WITHDRAWN (WITHDRAWN is final)"))
CS("S19 MODULE REINSTATE of the OFFERED version", L4 + [LREI(GU, "1.0.0", DAY2)],
   REF("LINE 5: MODULE REINSTATE of GRAV-UNIFORM 1.0.0, which is already OFFERED"))
CS("S20 MODULE RETIRE of a SUPERSEDED version", L5V + [LRET(GU, "1.0.0", DAY3)],
   REF("LINE 6: MODULE RETIRE of GRAV-UNIFORM 1.0.0, which is SUPERSEDED, not OFFERED"))
CS("S21 MODULE RETIRE of a version never declared", L4 + [LRET(GU, "3.0.0", DAY2)],
   REF("LINE 5: GRAV-UNIFORM 3.0.0 is not declared"))
_nor = LWD(GU, "1.0.0", REASON, DAY2)
del _nor["REASON"]
CS("S22 MODULE WITHDRAW without a reason", L4 + [_nor],
   REF("LINE 5: MODULE WITHDRAW without a reason"))
CS("S23 MODULE WITHDRAW with a blank reason", L4 + [LWD(GU, "1.0.0", "  ", DAY2)],
   REF("LINE 5: MODULE WITHDRAW without a reason"))
CS("S24 WITHDRAWN is final: withdrawn twice", L5W + [LWD(GU, "1.0.0", REASON, DAY3)],
   REF("LINE 6: GRAV-UNIFORM 1.0.0 is already WITHDRAWN (WITHDRAWN is final)"))
CS("S25 REPLACED_BY when the withdrawn version is not OFFERED",
   L5V + [LWD(GU, "1.0.0", REASON, DAY3, "1.1.0")],
   REF("LINE 6: REPLACED_BY needs GRAV-UNIFORM 1.0.0 OFFERED; it is SUPERSEDED"))
CS("S26 REPLACED_BY a version never declared", L4 + [LWD(GU, "1.0.0", REASON, DAY2, "0.5.0")],
   REF("LINE 5: REPLACED_BY GRAV-UNIFORM 0.5.0 is not declared"))
CS("S27 REPLACED_BY a WITHDRAWN version",
   L5V + [LWD(GU, "1.0.0", REASON, DAY3), LWD(GU, "1.1.0", REASON, DAY4, "1.0.0")],
   REF("LINE 7: REPLACED_BY GRAV-UNIFORM 1.0.0 is WITHDRAWN, not SUPERSEDED or RETIRED"))
CS("S28 an act that is not a module act",
   [{"ACT": "MODULE DELETE", "ID": GU, "VERSION": "1.0.0", "DATE": DAY1}],
   REF("LINE 1: ACT MODULE DELETE is not a module act"))
CS("S29 an unknown field", [dict(LADD(GU, "1.0.0", D_GRAV), NOTE="x")],
   REF("LINE 1: unknown field NOTE"))
CS("S30 a field of another act: DIGEST on RETIRE", L4 + [dict(LRET(GU, "1.0.0", DAY2),
                                                           DIGEST=D_GRAV)],
   REF("LINE 5: unknown field DIGEST"))
CS("S31 no ACT", [{"ID": GU, "VERSION": "1.0.0", "DATE": DAY1}], REF("LINE 1: missing field ACT"))
CS("S32 ACT not a string", [{"ACT": 1, "ID": GU}], REF("LINE 1: field ACT has the wrong type"))
_nod = LADD(GU, "1.0.0", D_GRAV)
del _nod["DIGEST"]
CS("S33 MODULE ADD without DIGEST", [_nod], REF("LINE 1: missing field DIGEST"))
CS("S34 a DIGEST in upper case", [LADD(GU, "1.0.0", D_GRAV.upper())],
   REF("LINE 1: DIGEST is not a digest"))
CS("S35 a DIGEST already declared", [LADD(AL, "1.0.0", D_AIRL), LADD(AQ, "1.0.0", D_AIRL)],
   REF("LINE 2: DIGEST already declared for AIR-LINEAR 1.0.0"))
CS("S36 a month 13", [LADD(GU, "1.0.0", D_GRAV, "2026-13-01")],
   REF("LINE 1: DATE 2026-13-01 is not a date"))
CS("S37 29 February of a common year", [LADD(GU, "1.0.0", D_GRAV, "2026-02-29")],
   REF("LINE 1: DATE 2026-02-29 is not a date"))
CS("S38 29 February of a leap year", [LADD(GU, "1.0.0", D_GRAV, "2028-02-29")],
   OK(["VERSIONS", [VROW(GU, "1.0.0", D_GRAV, "OFFERED", "2028-02-29")]], ["OFFERED", [OF_GU]],
      ["STATES", [[1, SD([OF_GU])]]]))
CS("S39 a line dated before the previous one",
   [LADD(AL, "1.0.0", D_AIRL, DAY2), LADD(AQ, "1.0.0", D_AIRQ, DAY1)],
   REF("LINE 2: DATE 2026-10-01 is before the previous line's"))
CS("S40 a VERSION of two parts", [LADD(GU, "1.0", D_GRAV)],
   REF("LINE 1: VERSION 1.0 is not a version"))
CS("S41 an ID in lower case", [LADD("grav", "1.0.0", D_GRAV)],
   REF("LINE 1: ID grav is not a module ID"))
CS("S42 lines not a list", {"ACT": "MODULE ADD"}, REF("LINES: not a list"))
CS("S43 a line not an object", [["MODULE ADD"]], REF("LINE 1: not an object"))
CS("S44 a VERSION given as a number", [dict(LADD(GU, "1.0.0", D_GRAV), VERSION=1)],
   REF("LINE 1: field VERSION has the wrong type"))
CS("S45 a REASON with a bidi override", L4 + [LWD(GU, "1.0.0", "bad \u202e sign", DAY2)],
   REF("LINE 5: field REASON contains disallowed character U+202E"))
CS("S46 a REPLACED_BY that is not a version", L4 + [LWD(GU, "1.0.0", REASON, DAY2, "one")],
   REF("LINE 5: REPLACED_BY one is not a version"))
# ---- RV's vectors (review 0.1.0 F-9)
CS("S47 MODULE WITHDRAW of a RETIRED version", L5R + [LWD(MA, "1.0.0", REASON, DAY3)],
   OK(["VERSIONS", ROWS4[:3] + [VROW(MA, "1.0.0", D_MATH, "WITHDRAWN", DAY3, REASON)]],
      ["OFFERED", [OF_AL, OF_AQ, OF_GU]],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_GU])], [6, SD([OF_AL, OF_AQ, OF_GU])]]]))
CS("S48 MODULE VERSION after the OFFERED version was WITHDRAWN (J F-4's repair)",
   L5W + [LVER(GU, "1.1.0", D_GRAV11, DAY3)],
   OK(["VERSIONS", ROWS4[:2] + [VROW(GU, "1.0.0", D_GRAV, "WITHDRAWN", DAY2, REASON),
                                VROW(GU, "1.1.0", D_GRAV11, "OFFERED", DAY3), ROWS4[3]]],
      ["OFFERED", [OF_AL, OF_AQ, OF_GU11, OF_MA]],
      ["STATES", STATES4 + [[5, SD([OF_AL, OF_AQ, OF_MA])],
                            [6, SD([OF_AL, OF_AQ, OF_GU11, OF_MA])]]]))
CS("S49 MODULE WITHDRAW of a version never declared", L4 + [LWD(GU, "3.0.0", REASON, DAY2)],
   REF("LINE 5: GRAV-UNIFORM 3.0.0 is not declared"))
CS("S50 MODULE REINSTATE of a version never declared", L4 + [LREI(MA, "2.0.0", DAY2)],
   REF("LINE 5: MATH 2.0.0 is not declared"))
CS("S51 MODULE VERSION whose DIGEST is already declared", L4 + [LVER(GU, "1.1.0", D_GRAV, DAY3)],
   REF("LINE 5: DIGEST already declared for GRAV-UNIFORM 1.0.0"))
CS("S52 REPLACED_BY naming the withdrawn version itself",
   L4 + [LWD(GU, "1.0.0", REASON, DAY2, "1.0.0")],
   REF("LINE 5: REPLACED_BY GRAV-UNIFORM 1.0.0 is OFFERED, not SUPERSEDED or RETIRED"))

# ============================================================ interview_check
def IC(name, answer, claims, lines, modules, carried, expected, modified=()):
    add(name, "interview_check", {"answer": answer, "claims": claims, "lines": lines,
                                  "modules": modules, "carried": carried,
                                  "modified": list(modified)}, expected)


def RND(at, covered, chosen, reselect=None):
    r = {"AT": at, "COVERED": covered, "CHOSEN": chosen}
    if reselect is not None:
        r["RESELECT"] = reselect
    return r


A_NONE4 = [RND(4, OFFER4, "NONE")]
A_GRAV4 = [RND(4, OFFER4, [GU])]
A_GQ4 = [RND(4, OFFER4, [AQ, GU])]
IC("I01 NONE over the first catalogue", A_NONE4, [], L4, M4, [],
   OK(["ASK", []], ["CLAIMS", []]))
IC("I02 GRAV-UNIFORM chosen and claimed: PINNED", A_GRAV4, [C_GRAV], L4, M4, [],
   OK(["ASK", []], ["CLAIMS", [[GU, "1.0.0", "PINNED"]]]))
IC("I03 two chosen, buoyancy answered", A_GQ4, [C_GRAV_NO, C_AIRQ], L4, M4, [],
   OK(["ASK", []], ["CLAIMS", [[AQ, "1.0.0", "PINNED"], [GU, "1.0.0", "PINNED"]]]))
M5X = M4 + [X_TX]
IC("I04 a module offered after the answer is asked about (the delta)", A_NONE4, [], L5X, M5X, [],
   OK(["ASK", [OF_TX]], ["CLAIMS", []]))
IC("I05 the delta answered in a second round", A_NONE4 + [RND(5, [OF_TX], "NONE")], [], L5X,
   M5X, [], OK(["ASK", []], ["CLAIMS", []]))
IC("I06 a second round covering the whole offer", A_NONE4 + [RND(5, OFFER4 + [OF_TX], "NONE")], [],
   L5X, M5X, [], REF("ANSWER ROUND 2: COVERED is not the offer the round was given over"))
IC("I07 a second round not after the first", A_NONE4 + [RND(4, [OF_TX], "NONE")], [], L5X, M5X,
   [], REF("ANSWER ROUND 2: AT is not after the previous round's"))
M5V = M4 + [X_GRAV11]
IC("I08 a second round with nothing new (a new VERSION is no new module)",
   A_NONE4 + [RND(5, [], "NONE")], [], L5V, M5V, [],
   REF("ANSWER ROUND 2: no module was added since the previous round"))
IC("I09 AT beyond the lines", [RND(7, OFFER4, "NONE")], [], L4, M4, [],
   REF("ANSWER ROUND 1: AT is not a line count from 0 to 4"))
IC("I10 no round: the interaction question not answered", [], [], L4, M4, [],
   REF("INTERACTIONS: not answered"))
IC("I11 answer not a list", RND(4, OFFER4, "NONE"), [], L4, M4, [], REF("ANSWER: not a list"))
IC("I12 CHOSEN empty", [RND(4, OFFER4, [])], [], L4, M4, [],
   REF("ANSWER ROUND 1: CHOSEN is empty; the exclusive answer is NONE"))
IC("I13 CHOSEN out of order", [RND(4, OFFER4, [MA, GU])], [C_GRAV, C_MATH], L4, M4, [],
   REF("ANSWER ROUND 1: CHOSEN is not in code point order without repeats"))
IC("I14 CHOSEN an ID the round did not cover", [RND(4, OFFER4, [TX])], [], L4, M4, [],
   REF("ANSWER ROUND 1: CHOSEN TEST-X is not in COVERED"))
IC("I15 CHOSEN a word other than NONE", [RND(4, OFFER4, "ALL")], [], L4, M4, [],
   REF("ANSWER ROUND 1: field CHOSEN has the wrong type"))
IC("I16 NONE, and a claim", A_NONE4, [C_GRAV], L4, M4, [],
   REF("CLAIM GRAV-UNIFORM 1.0.0: the answer did not choose GRAV-UNIFORM"))
IC("I17 chosen but not claimed", A_GRAV4, [], L4, M4, [],
   REF("ANSWER: GRAV-UNIFORM is chosen but not claimed"))
IC("I18 a pinned claim whose version was SUPERSEDED after the answer: kept", A_GRAV4, [C_GRAV],
   L5V, M5V, [], OK(["ASK", []], ["CLAIMS", [[GU, "1.0.0", "PINNED"]]]))
OFFER5V = [OF_AL, OF_AQ, OF_GU11, OF_MA]
IC("I19 a new claim of a version not OFFERED is refused", [RND(5, OFFER5V, [GU])], [C_GRAV], L5V,
   M5V, [], REF("CLAIM GRAV-UNIFORM 1.0.0: not OFFERED when chosen (a new claim names the OFFERED "
                "version)"))
IC("I20 the same claim carried by COPY-FORWARD is admitted", [RND(5, OFFER5V, [GU])], [C_GRAV],
   L5V, M5V, [OF_GU], OK(["ASK", []], ["CLAIMS", [[GU, "1.0.0", "CARRIED"]]]))
OFFER5R = [OF_AL, OF_AQ, OF_GU]
IC("I21 a carried claim of an ID no longer offered", [RND(5, OFFER5R, "NONE")], [C_MATH], L5R, M4,
   [OF_MA], OK(["ASK", []], ["CLAIMS", [[MA, "1.0.0", "CARRIED"]]]))
IC("I22 the same claim not carried", [RND(5, OFFER5R, "NONE")], [C_MATH], L5R, M4, [],
   REF("CLAIM MATH 1.0.0: MATH is not in the answer"))
IC("I23 two excluding modules chosen", [RND(4, OFFER4, [AL, AQ])], [C_AIRQ, C_AIRL], L4, M4, [],
   REF("CLAIMS: AIR-LINEAR 1.0.0 and AIR-QUADRATIC 1.0.0 exclude each other (EXCLUDES)"))
IC("I24 the buoyancy question unanswered", A_GQ4, [C_GRAV, C_AIRQ], L4, M4, [],
   REF("CLAIM GRAV-UNIFORM 1.0.0: question BUOYANCY is not answered"))
IC("I25 a buoyancy answer with no air module", A_GRAV4, [C_GRAV_NO], L4, M4, [],
   REF("CLAIM GRAV-UNIFORM 1.0.0: answer BUOYANCY answers no question raised"))
IC("I26 a FIXED answer is the expansion's to judge", A_GRAV4,
   [claim(GU, "1.0.0", D_GRAV, GRAV_BIND, H100, {"FIXED:v_z": "YES"})], L4, M4, [],
   OK(["ASK", []], ["CLAIMS", [[GU, "1.0.0", "PINNED"]]]))
IC("I27 a claim DIGEST not the declared one", A_GRAV4, [dict(C_GRAV, DIGEST=D_GRAV11)], L4, M4, [],
   REF("CLAIM GRAV-UNIFORM 1.0.0: DIGEST does not match the declaration"))
IC("I28 carried not a list of triples", A_NONE4, [], L4, M4, [GU],
   REF("CARRIED: not a list of [ID, VERSION, DIGEST]"))
IC("I29 carried naming a version not declared", A_NONE4, [], L4, M4, [[GU, "9.0.0", D_GRAV]],
   REF("CARRIED GRAV-UNIFORM 9.0.0: not declared"))
IC("I30 COVERED in another order", [RND(4, [OF_AQ, OF_AL, OF_GU, OF_MA], "NONE")], [], L4, M4, [],
   REF("ANSWER ROUND 1: COVERED is not the offer the round was given over"))
IC("I31 an empty catalogue answered NONE at AT 0", [RND(0, [], "NONE")], [], [], [], [],
   OK(["ASK", []], ["CLAIMS", []]))
IC("I32 answered before any module: every module asked", [RND(0, [], "NONE")], [], L4, M4, [],
   OK(["ASK", OFFER4], ["CLAIMS", []]))
IC("I33 a pinned claim whose version was WITHDRAWN after the answer: kept", A_GRAV4, [C_GRAV],
   L5W, M4, [], OK(["ASK", []], ["CLAIMS", [[GU, "1.0.0", "PINNED"]]]))
IC("I34 a claim with no module text", A_GRAV4, [C_GRAV], L4, [X_AIRL, X_AIRQ, X_MATH], [],
   REF("CLAIM GRAV-UNIFORM 1.0.0: no module text"))
IC("I35 a text of a version not declared", A_NONE4, [], L4, M4 + [X_GRAV11], [],
   REF("MODULES: GRAV-UNIFORM 1.1.0 is not declared"))
IC("I36 a catalogue that is refused", A_NONE4, [], [["x"]], M4, [],
   REF("CATALOGUE: LINE 1: not an object"))
IC("I37 a claim of a version never declared", A_GRAV4, [dict(C_GRAV, VERSION="1.0.5")], L4, M4, [],
   REF("CLAIM GRAV-UNIFORM 1.0.5: not declared"))
# ---- RV's vectors (review 0.1.0 F-9)
L5RG_ = L4 + [LRET(GU, "1.0.0", DAY2)]
IC("I38 a pinned claim whose version was RETIRED after the answer: kept", A_GRAV4, [C_GRAV],
   L5RG_, M4, [], OK(["ASK", []], ["CLAIMS", [[GU, "1.0.0", "PINNED"]]]))
_bad_cov = copy.deepcopy(A_GRAV4)
_bad_cov[0]["COVERED"] = "all"
IC("I39 COVERED of the wrong type", _bad_cov, [C_GRAV], L4, M4, [],
   REF("ANSWER ROUND 1: field COVERED has the wrong type"))
IC("I40 CARRIED with a DIGEST not the declared one", A_GRAV4, [C_GRAV], L4, M4,
   [[GU, "1.0.0", D_MATH]], REF("CARRIED GRAV-UNIFORM 1.0.0: DIGEST does not match the declaration"))
# ---- S50: RESELECT (RV F-2, RD-RV-3)
C_GRAV11 = claim(GU, "1.1.0", D_GRAV11, GRAV_BIND, H100, {})
A_RESEL = A_GRAV4 + [RND(5, [OF_GU11], [GU], [GU])]
IC("I41 a re-selection round takes the OFFERED version: PINNED to it", A_RESEL, [C_GRAV11], L5V,
   M5V, [], OK(["ASK", []], ["CLAIMS", [[GU, "1.1.0", "PINNED"]]]))
IC("I42 the same round, the claim still on the old version: refused (DR-7)", A_RESEL, [C_GRAV],
   L5V, M5V, [], REF("CLAIM GRAV-UNIFORM 1.0.0: not OFFERED when chosen (a new claim names the "
                     "OFFERED version)"))
IC("I43 RESELECT of an ID no earlier round covered", A_NONE4 + [RND(5, [OF_TX], "NONE", [TX])],
   [], L5X, M5X, [], REF("ANSWER ROUND 2: RESELECT TEST-X was not covered before"))
# ---- S50: MODIFIED (RV F-3, C-RV-2)
IC("I44 a chosen module the author edited by hand: MODIFIED, not claimed", A_GRAV4, [], L4, M4, [],
   OK(["ASK", []], ["CLAIMS", []]), modified=[[GU, "1.0.0"]])
IC("I45 MODIFIED not a list of pairs", A_GRAV4, [], L4, M4, [],
   REF("MODIFIED: not a list of [ID, VERSION]"), modified=[[GU, "1.0.0", D_GRAV]])
IC("I46 MODIFIED naming a version never declared", A_GRAV4, [], L4, M4, [],
   REF("MODIFIED GRAV-UNIFORM 9.0.0: not declared"), modified=[[GU, "9.0.0"]])
IC("I47 MODIFIED and claimed at once", A_GRAV4, [C_GRAV], L4, M4, [],
   REF("MODIFIED GRAV-UNIFORM 1.0.0: the ID is also claimed"), modified=[[GU, "1.0.0"]])
IC("I48 MODIFIED naming one module twice", A_GRAV4, [], L4, M4, [],
   REF("MODIFIED: duplicate module GRAV-UNIFORM"), modified=[[GU, "1.0.0"], [GU, "1.0.0"]])
# ---- RESELECT's own refusals, and the round's shape (5.4)
IC("I49 RESELECT of the wrong type", A_GRAV4 + [RND(5, [OF_GU11], [GU], GU)], [C_GRAV11], L5V,
   M5V, [], REF("ANSWER ROUND 2: field RESELECT has the wrong type"))
IC("I50 RESELECT out of order", A_NONE4 + [RND(5, OFFER5V, "NONE", [MA, GU])], [], L5V, M5V, [],
   REF("ANSWER ROUND 2: RESELECT is not in code point order without repeats"))
L6WV = L5V + [LWD(GU, "1.1.0", REASON, DAY3)]
IC("I51 RESELECT of an ID no longer offered", A_GRAV4 + [RND(6, [], "NONE", [GU])], [C_GRAV],
   L6WV, M5V, [], REF("ANSWER ROUND 2: RESELECT GRAV-UNIFORM is not offered at AT"))
IC("I52 a round that is not an object", [OFFER4], [], L4, M4, [],
   REF("ANSWER ROUND 1: not an object"))
IC("I53 a round with an unknown field", [dict(RND(4, OFFER4, "NONE"), NOTE="x")], [], L4, M4, [],
   REF("ANSWER ROUND 1: unknown field NOTE"))
_nochosen = RND(4, OFFER4, "NONE")
del _nochosen["CHOSEN"]
IC("I54 a round without CHOSEN", [_nochosen], [], L4, M4, [],
   REF("ANSWER ROUND 1: missing field CHOSEN"))
IC("I55 a re-selection round answered NONE: the module dropped, nothing claimed", A_GRAV4
   + [RND(5, [OF_GU11], "NONE", [GU])], [], L5V, M5V, [], OK(["ASK", []], ["CLAIMS", []]))

# ========================================================= not_included_lines
def NI(name, answer, claims, lines, modules, carried, expected, modified=()):
    add(name, "not_included_lines", {"answer": answer, "claims": claims, "lines": lines,
                                     "modules": modules, "carried": carried,
                                     "modified": list(modified)}, expected)


CATCH = ["-", "Any interaction not listed under INCLUDED is absent from this kernel."]
NC_AL, NC_AQ, NC_GU, NC_MA = ([AL1, AIRL_NC], [AQ1, AIRQ_NC], [G1, GRAV_NC], [MA1, MATH_NC])
NI("K01 NONE: one NOT_CHOSEN line per covered module, then the closing line", A_NONE4, [], L4, M4,
   [], OK([NC_AL, NC_AQ, NC_GU, NC_MA, CATCH]))
NI("K02 GRAV-UNIFORM chosen: its NOT_INCLUDED lines", A_GRAV4, [C_GRAV], L4, M4, [],
   OK([NC_AL, NC_AQ, NC_MA] + [[G1, x] for x in GRAV_NI] + [CATCH]))
NI("K03 GRAV-UNIFORM and AIR-QUADRATIC, buoyancy NO", A_GQ4, [C_GRAV_NO, C_AIRQ], L4, M4, [],
   OK([NC_AL, NC_MA] + [[AQ1, x] for x in AIRQ_NI] + [[G1, x] for x in GRAV_NI]
      + [[G1, NO_B], CATCH]))
NI("K04 the same, buoyancy YES", A_GQ4, [C_GRAV_YES, C_AIRQ], L4, M4, [],
   OK([NC_AL, NC_MA] + [[AQ1, x] for x in AIRQ_NI] + [[G1, x] for x in GRAV_NI]
      + [[G1, YES_B], CATCH]))
NI("K05 a module RETIRED after the answer keeps its line", A_NONE4, [], L5R, M4, [],
   OK([NC_AL, NC_AQ, NC_GU, NC_MA, CATCH]))
NI("K06 a module offered after the answer has no line until answered", A_NONE4, [], L5X, M5X, [],
   OK([NC_AL, NC_AQ, NC_GU, NC_MA, CATCH]))
TX1 = "TEST-X 1.0.0"
NI("K07 answered in a second round: its line, in ID order", A_NONE4 + [RND(5, [OF_TX], "NONE")],
   [], L5X, M5X, [], OK([NC_AL, NC_AQ, NC_GU, NC_MA, [TX1, "No test interaction X."], CATCH]))
NI("K08 chosen in the second round", A_NONE4 + [RND(5, [OF_TX], [TX])], [C_TX], L5X, M5X, [],
   OK([NC_AL, NC_AQ, NC_GU, NC_MA, [TX1, "Test X adds only a constant."], CATCH]))
NI("K09 the interview refused: its refusal", [], [], L4, M4, [],
   REF("INTERACTIONS: not answered"))
NI("K10 a carried claim of an ID no longer offered keeps its lines", [RND(5, OFFER5R, "NONE")],
   [C_MATH], L5R, M4, [OF_MA], OK([NC_AL, NC_AQ, NC_GU] + [[MA1, x] for x in MATH_NI] + [CATCH]))
NI("K11 a covered module without its text", A_NONE4, [], L4, [X_AIRL, X_AIRQ, X_GRAV], [],
   REF("MODULES: no text of MATH 1.0.0, covered by the answer"))
# ---- S50: MODIFIED and RESELECT in the check page's lines
_MODL = [G1, "Modified from GRAV-UNIFORM 1.0.0: its entries are the author's own."]
NI("K12 a module the author edited by hand: its Modified line after the claims' lines", A_GQ4,
   [C_AIRQ], L4, M4, [], OK([NC_AL, NC_MA] + [[AQ1, x] for x in AIRQ_NI] + [_MODL, CATCH]),
   modified=[[GU, "1.0.0"]])
NI("K13 a re-selection round answered NONE: the latest round's line", A_GRAV4
   + [RND(5, [OF_GU11], "NONE", [GU])], [], L5V, M5V, [],
   OK([NC_AL, NC_AQ, ["GRAV-UNIFORM 1.1.0", GRAV_NC], NC_MA, CATCH]))
NI("K14 a re-selection round choosing the new version: its lines", A_RESEL, [C_GRAV11], L5V, M5V,
   [], OK([NC_AL, NC_AQ, NC_MA] + [["GRAV-UNIFORM 1.1.0", x] for x in GRAV_NI] + [CATCH]))

# ============================================================== module_verify
def V(name, base, claims, modules, lines, draft, expected):
    add(name, "module_verify", {"base": base, "claims": claims, "modules": modules,
                                "lines": lines, "draft": draft}, expected)


W_GRAV = DTPK(EXP_GRAV)
V("V01 the draft carries the expansion: MATCH, OFFERED", D_P2, [C_GRAV], M4, L4,
  {"TPK": EXP_GRAV}, OK("MATCH", W_GRAV, W_GRAV, [[GU, "1.0.0", "OFFERED"]]))
V("V02 re-expanded whatever the status: SUPERSEDED", D_P2, [C_GRAV], M5V, L5V, {"TPK": EXP_GRAV},
  OK("MATCH", W_GRAV, W_GRAV, [[GU, "1.0.0", "SUPERSEDED"]]))
L5RG = L4 + [LRET(GU, "1.0.0", DAY2)]
V("V03 RETIRED", D_P2, [C_GRAV], M4, L5RG, {"TPK": EXP_GRAV},
  OK("MATCH", W_GRAV, W_GRAV, [[GU, "1.0.0", "RETIRED"]]))
V("V04 WITHDRAWN", D_P2, [C_GRAV], M4, L5W, {"TPK": EXP_GRAV},
  OK("MATCH", W_GRAV, W_GRAV, [[GU, "1.0.0", "WITHDRAWN"]]))
_ed = copy.deepcopy(EXP_GRAV)
_ed["CONSTANT"][2]["VALUE"] = "200"
assert _ed["CONSTANT"][2]["NAME"] == "h_max"
V("V05 an emitted value edited by hand: MISMATCH", D_P2, [C_GRAV], M4, L4, {"TPK": _ed},
  OK("MISMATCH", W_GRAV, DTPK(_ed), [[GU, "1.0.0", "OFFERED"]]))
_noterm = tpk_mod(P2, None, GRAV_EMIT_100)
V("V06 the entries without the composed term: MISMATCH", D_P2, [C_GRAV], M4, L4,
  {"TPK": _noterm}, OK("MISMATCH", W_GRAV, DTPK(_noterm), [[GU, "1.0.0", "OFFERED"]]))
_nwf = copy.deepcopy(EXP_GRAV)
_nwf["LAW"][3]["REFS"] = ["m", "t", "v_z"]
assert Z.t1_well_formed(_nwf)[0] is False
V("V07 a draft TPK failing T1: NOT WELL-FORMED", D_P2, [C_GRAV], M4, L4, {"TPK": _nwf},
  OK("MISMATCH", W_GRAV, "NOT WELL-FORMED", [[GU, "1.0.0", "OFFERED"]]))
V("V08 a claim of a version never declared", D_P2, [dict(C_GRAV, VERSION="1.1.0",
                                                         DIGEST=D_GRAV11)], M4, L4,
  {"TPK": EXP_GRAV}, REF("CLAIM GRAV-UNIFORM 1.1.0: not declared"))
V("V09 a claim DIGEST not the declared one", D_P2, [dict(C_GRAV, DIGEST=HX("z"))], M4, L4,
  {"TPK": EXP_GRAV}, REF("CLAIM GRAV-UNIFORM 1.0.0: DIGEST does not match the declaration"))
V("V10 a catalogue refused", D_P2, [C_GRAV], M4, [["x"]], {"TPK": EXP_GRAV},
  REF("CATALOGUE: LINE 1: not an object"))
V("V11 a draft without TPK", D_P2, [C_GRAV], M4, L4, {"TMS": {}}, REF("DRAFT: missing zone TPK"))
W_GQ = DTPK(EXP_GRAV_AIRQ)
V("V12 two claims in either order: MATCH", D_P2, [C_AIRQ, C_GRAV_NO], M4, L4,
  {"TPK": EXP_GRAV_AIRQ},
  OK("MATCH", W_GQ, W_GQ, [[AQ, "1.0.0", "OFFERED"], [GU, "1.0.0", "OFFERED"]]))
V("V13 the expansion refused: its refusal", D_P2, [C_GRAV, C_AIRQ], M4, L4,
  {"TPK": EXP_GRAV_AIRQ}, REF("CLAIM GRAV-UNIFORM 1.0.0: question BUOYANCY is not answered"))
_L4bad = copy.deepcopy(L4)
_L4bad[0]["DIGEST"] = HX("other")
V("V14 a text whose digest is not its declaration's", D_P2, [C_GRAV], M4, _L4bad,
  {"TPK": EXP_GRAV},
  REF("MODULES: AIR-LINEAR 1.0.0: the text's digest does not match the declaration"))
V("V15 no claim: the base itself", D_P2, [], M4, L4, D_P2,
  OK("MATCH", DTPK(P2), DTPK(P2), []))

# ======================================================= draft_module_status
def DS(name, base, claims, modules, lines, expected):
    add(name, "draft_module_status", {"base": base, "claims": claims, "modules": modules,
                                      "lines": lines}, expected)


L_SUP_DIFF = L4 + [LVER(GU, "1.2.0", D_GRAV12, DAY2)]
L_SUP_NE = L4 + [LVER(GU, "2.0.0", D_GRAV20, DAY2)]
L_SUP_NONE = L5V + [LRET(GU, "1.1.0", DAY3)]
GATE_Q = ("This draft uses GRAV-UNIFORM 1.0.0, withdrawn on 2026-10-02 for: " + REASON
          + ". Declare with it?")
assert DTPK(EXP_GRAV12) != W_GRAV
DS("D01 every claim OFFERED", D_P2, [C_GRAV], M4, L4,
   OK(["CLAIMS", [[GU, "1.0.0", "OFFERED"]]], ["GATE", []]))
DS("D02 SUPERSEDED by a version that expands alike: SAME", D_P2, [C_GRAV], M5V, L5V,
   OK(["CLAIMS", [[GU, "1.0.0", "SUPERSEDED", "1.1.0", "SAME"]]], ["GATE", []]))
DS("D03 SUPERSEDED by a version that expands otherwise: DIFFERS", D_P2, [C_GRAV],
   M4 + [X_GRAV12], L_SUP_DIFF,
   OK(["CLAIMS", [[GU, "1.0.0", "SUPERSEDED", "1.2.0", "DIFFERS"]]], ["GATE", []]))
DS("D04 SUPERSEDED by a version this draft's binding cannot expand", D_P2, [C_GRAV],
   M4 + [X_GRAV20], L_SUP_NE,
   OK(["CLAIMS", [[GU, "1.0.0", "SUPERSEDED", "2.0.0",
                   "NOT EXPANDED: CLAIM GRAV-UNIFORM 2.0.0: VALUES: z_ref is author supplied: "
                   "give a value, or {} for a symbolic constant"]]], ["GATE", []]))
DS("D05 SUPERSEDED, and no version OFFERED now", D_P2, [C_GRAV], M5V, L_SUP_NONE,
   OK(["CLAIMS", [[GU, "1.0.0", "SUPERSEDED", "NONE OFFERED"]]], ["GATE", []]))
DS("D06 RETIRED", D_P2, [C_GRAV], M4, L5RG,
   OK(["CLAIMS", [[GU, "1.0.0", "RETIRED", DAY2]]], ["GATE", []]))
DS("D07 WITHDRAWN: the line for H-DECL's one question", D_P2, [C_GRAV], M4, L5W,
   OK(["CLAIMS", [[GU, "1.0.0", "WITHDRAWN", DAY2, REASON]]], ["GATE", [GATE_Q]]))
DS("D08 two claims, one WITHDRAWN", D_P2, [C_GRAV_NO, C_AIRQ], M4, L5W,
   OK(["CLAIMS", [[AQ, "1.0.0", "OFFERED"], [GU, "1.0.0", "WITHDRAWN", DAY2, REASON]]],
      ["GATE", [GATE_Q]]))
DS("D09 the OFFERED successor's text missing", D_P2, [C_GRAV], M4, L5V,
   REF("MODULES: no text of GRAV-UNIFORM 1.1.0 (OFFERED)"))
DS("D10 the draft's own expansion refused: its refusal", D_P2,
   [dict(C_GRAV, BIND=dict(GRAV_BIND, MASS=["x"]))], M4, L4,
   REF("CLAIM GRAV-UNIFORM 1.0.0: BIND: slot MASS: x is not a CONSTANT of the TPK"))
DS("D11 a catalogue refused", D_P2, [C_GRAV], M4, "L4", REF("CATALOGUE: LINES: not a list"))

# ============================================================ module_use_view
def UV(name, certificates, lines, expected):
    add(name, "module_use_view", {"certificates": certificates, "lines": lines}, expected)


def CERT(p, used):
    return {"PREFIX": p, "MODULES_USED": used}


CERTS = [CERT("RR", [OF_GU, OF_AQ]), CERT("QQ", [OF_MA]), CERT("AA", [OF_GU])]
UV("U01 rows by ID, VERSION, PREFIX", CERTS, L4,
   OK(["ROWS", [[AQ, "1.0.0", "RR", "OFFERED"], [GU, "1.0.0", "AA", "OFFERED"],
                [GU, "1.0.0", "RR", "OFFERED"], [MA, "1.0.0", "QQ", "OFFERED"]]],
      ["UWCS ON WITHDRAWN", 0]))
UV("U02 a version WITHDRAWN: the certificates are unchanged, the view shows it", CERTS, L5W,
   OK(["ROWS", [[AQ, "1.0.0", "RR", "OFFERED"], [GU, "1.0.0", "AA", "WITHDRAWN"],
                [GU, "1.0.0", "RR", "WITHDRAWN"], [MA, "1.0.0", "QQ", "OFFERED"]]],
      ["UWCS ON WITHDRAWN", 2]))
_LV = [LADD("TEST-V", "1.2.0", HX("v12")), LVER("TEST-V", "1.10.0", HX("v110"), DAY2)]
UV("U03 versions in code point order: 1.10.0 before 1.2.0",
   [CERT("BB", [["TEST-V", "1.2.0", HX("v12")]]), CERT("CC", [["TEST-V", "1.10.0", HX("v110")]])],
   _LV, OK(["ROWS", [["TEST-V", "1.10.0", "CC", "OFFERED"],
                     ["TEST-V", "1.2.0", "BB", "SUPERSEDED"]]],
           ["UWCS ON WITHDRAWN", 0]))
UV("U04 no certificate", [], L4, OK(["ROWS", []], ["UWCS ON WITHDRAWN", 0]))
UV("U05 a version never declared", [CERT("RR", [[GU, "9.9.9", D_GRAV]])], L4,
   REF("CERTIFICATE RR: GRAV-UNIFORM 9.9.9 is not declared"))
UV("U06 a digest not the declared one", [CERT("RR", [[GU, "1.0.0", D_GRAV11]])], L4,
   REF("CERTIFICATE RR: GRAV-UNIFORM 1.0.0: DIGEST does not match the declaration"))
UV("U07 a prefix twice", [CERT("RR", [OF_GU]), CERT("RR", [OF_MA])], L4,
   REF("CERTIFICATES: duplicate prefix RR"))
UV("U08 a module twice in one certificate", [CERT("RR", [OF_GU, OF_GU])], L4,
   REF("CERTIFICATE RR: duplicate module GRAV-UNIFORM"))
UV("U09 certificates not a list", CERT("RR", []), L4, REF("CERTIFICATES: not a list"))
UV("U10 a certificate without MODULES_USED", [{"PREFIX": "RR"}], L4,
   REF("CERTIFICATE 1: missing field MODULES_USED"))
UV("U11 a prefix that is no identifier", [CERT("R-R", [])], L4,
   REF("CERTIFICATE 1: PREFIX R-R is not a prefix"))
UV("U12 MODULES_USED not a list of triples", [CERT("RR", [GU])], L4,
   REF("CERTIFICATE 1: field MODULES_USED has the wrong type"))
UV("U13 a catalogue refused", CERTS, [["x"]], REF("CATALOGUE: LINE 1: not an object"))
UV("U14 a certificate that is not an object (RV F-9)", ["RR"], L4,
   REF("CERTIFICATE 1: not an object"))
UV("U15 a certificate with an unknown field (RV F-9)",
   [{"PREFIX": "RR", "MODULES_USED": [], "NOTE": "x"}], L4, REF("CERTIFICATE 1: unknown field NOTE"))

# =============================================================== module_check
def MC(name, text, act, date, lines, modules, expected):
    add(name, "module_check", {"text": text, "act": act, "date": date, "lines": lines,
                               "modules": modules}, expected)


def PASSROWS(dg, refs, bindings, pairs, atomicity):
    rows = [["DIGEST", dg], ["REFERENCES", refs]] + bindings + pairs + [["ATOMICITY", atomicity]]
    bad = any(r[-1] not in ("PASS", "EXCLUDED") for r in rows[1:-1])
    return OK("FAIL" if bad else "PASS", rows)


ATOM_AL = ["air_still", "air_uniform", "drag_sphere"]
ATOM_AQ = ["air_still", "air_uniform", "drag_cd_constant", "drag_no_lift", "drag_no_magnus"]
ATOM_GU = ["gravity_uniform", "gravity_standard", "frame_inertial"]
L1_ = L4[:1]
L2_ = L4[:2]
L3_ = L4[:3]
MC("M01 ADD AIR-LINEAR to an empty catalogue: PASS", X_AIRL, "MODULE ADD", DAY1, [], [],
   PASSROWS(D_AIRL, "PASS", [["BINDING", "PROJECTILE_2D", "PASS"]], [], ATOM_AL))
MC("M02 ADD AIR-QUADRATIC: EXCLUDES a declared ID; the excluded pair not combined", X_AIRQ,
   "MODULE ADD", DAY1, L1_, [X_AIRL],
   PASSROWS(D_AIRQ, "PASS", [["BINDING", "PROJECTILE_2D", "PASS"]],
            [["PAIR", AL, "1.0.0", "EXCLUDED"]], ATOM_AQ))
MC("M03 ADD GRAV-UNIFORM: both bindings, both air pairs on the shared binding", X_GRAV,
   "MODULE ADD", DAY1, L2_, [X_AIRL, X_AIRQ],
   PASSROWS(D_GRAV, "PASS", [["BINDING", "PROJECTILE_2D", "PASS"], ["BINDING", "TABLE", "PASS"]],
            [["PAIR", AL, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", AQ, "1.0.0", "PROJECTILE_2D", "PASS"]], ATOM_GU))
MC("M04 ADD MATH: combined with every offered module (the pi_c merge with AIR-LINEAR)", X_MATH,
   "MODULE ADD", DAY1, L3_, [X_AIRL, X_AIRQ, X_GRAV],
   PASSROWS(D_MATH, "PASS", [["BINDING", "PROJECTILE_2D", "PASS"]],
            [["PAIR", AL, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", AQ, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", GU, "1.0.0", "PROJECTILE_2D", "PASS"]], []))
MC("M05 VERSION GRAV-UNIFORM 1.1.0: never paired with its own ID", X_GRAV11, "MODULE VERSION",
   DAY2, L4, M4,
   PASSROWS(D_GRAV11, "PASS", [["BINDING", "PROJECTILE_2D", "PASS"], ["BINDING", "TABLE", "PASS"]],
            [["PAIR", AL, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", AQ, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", MA, "1.0.0", "PROJECTILE_2D", "PASS"]], ATOM_GU))
MC("M06 ADD GRAV-UNIFORM before any air module: WITH matches nothing", X_GRAV, "MODULE ADD", DAY1,
   [], [],
   PASSROWS(D_GRAV, "WITH AIR-* matches no declared module",
            [["BINDING", "PROJECTILE_2D", "PASS"], ["BINDING", "TABLE", "PASS"]], [], ATOM_GU))
MC("M07 ADD AIR-QUADRATIC before AIR-LINEAR: EXCLUDES matches nothing", X_AIRQ, "MODULE ADD", DAY1,
   [], [],
   PASSROWS(D_AIRQ, "EXCLUDES AIR-LINEAR matches no declared module",
            [["BINDING", "PROJECTILE_2D", "PASS"]], [], ATOM_AQ))
MC("M08 ADD TEST-PI: two pairs refused (pi_c differently)", X_TPI, "MODULE ADD", DAY2, L4, M4,
   PASSROWS(D_TPI, "PASS", [["BINDING", "PROJECTILE_2D", "PASS"]],
            [["PAIR", AL, "1.0.0", "PROJECTILE_2D",
              "EXPAND: AIR-LINEAR 1.0.0 and TEST-PI 1.0.0 both emit pi_c, differently"],
             ["PAIR", AQ, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", GU, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", MA, "1.0.0", "PROJECTILE_2D",
              "EXPAND: MATH 1.0.0 and TEST-PI 1.0.0 both emit pi_c, differently"]], []))
MC("M09 ADD TEST-X, whose one binding no offered module shares", X_TX, "MODULE ADD", DAY2, L4, M4,
   PASSROWS(D_TX, "PASS", [["BINDING", "SOLO", "PASS"]],
            [["PAIR", AL, "1.0.0", "-", "NO SHARED BINDING"],
             ["PAIR", AQ, "1.0.0", "-", "NO SHARED BINDING"],
             ["PAIR", GU, "1.0.0", "-", "NO SHARED BINDING"],
             ["PAIR", MA, "1.0.0", "-", "NO SHARED BINDING"]], []))
_badb_p2 = tpk_mod(P2, None, [("CONSTANT", {"NAME": "k_b", "DIMENSION": "1"}),
                              ("CONSTANT", {"NAME": "h_b", "DIMENSION": "1"}),
                              ("BOUNDARY", {"NAME": "bad_region", "PREDICATE": "k_b / h_b > 1",
                                            "REFS": ["h_b", "k_b"], "CONDITION": "k_b/h_b>1"})])
_badb_row = "T6: BOUNDARY:bad_region: CONDITION uses / on h_b with no guard h_b != 0 (T6)"
assert first_kt_problem(_badb_p2) == _badb_row
MC("M10 a binding failing T6, a binding failing its BIND", X_TBADB, "MODULE ADD", DAY1, [], [],
   PASSROWS(D_TBADB, "PASS",
            [["BINDING", "PROJECTILE_2D", _badb_row],
             ["BINDING", "WRONG",
              "CLAIM TEST-BADB 1.0.0: BIND: slot SPEEDY: z has dimension L1, not L1 T-1"]], [],
            []))
MC("M11 ADD of an ID already declared", X_GRAV, "MODULE ADD", DAY2, L4, M4,
   REF("CATALOGUE: LINE 5: MODULE ADD of GRAV-UNIFORM, an ID already declared (no ID is "
       "reissued)"))
MC("M12 VERSION reissuing a version", X_GRAV, "MODULE VERSION", DAY2, L4, M4,
   REF("CATALOGUE: LINE 5: GRAV-UNIFORM 1.0.0 is already declared (no version is reissued)"))
MC("M13 an act that is neither ADD nor VERSION", X_GRAV, "MODULE RETIRE", DAY2, L4, M4,
   REF("ACT: MODULE RETIRE is not MODULE ADD or MODULE VERSION"))
MC("M14 a proposal that does not parse", "{", "MODULE ADD", DAY2, L4, M4, NOTJSON)
MC("M15 a text missing for an OFFERED version", X_TPI, "MODULE ADD", DAY2, L4,
   [X_AIRL, X_AIRQ, X_GRAV], REF("MODULES: no text of MATH 1.0.0 (OFFERED)"))
MC("M16 a catalogue refused", X_TPI, "MODULE ADD", DAY2, [["x"]], M4,
   REF("CATALOGUE: LINE 1: not an object"))
MC("M17 a date that is no date", X_TPI, "MODULE ADD", "2026-99-01", L4, M4,
   REF("CATALOGUE: LINE 5: DATE 2026-99-01 is not a date"))
MC("M18 a date before the last line's", X_TPI, "MODULE ADD", "2026-09-30", L4, M4,
   REF("CATALOGUE: LINE 5: DATE 2026-09-30 is before the previous line's"))
MC("M19 ADD TEST-PUSH: SIGN + and a wrapped sum pass on the shared binding", X_TPUSH,
   "MODULE ADD", DAY2, L4, M4,
   PASSROWS(D_TPUSH, "PASS", [["BINDING", "PROJECTILE_2D", "PASS"]],
            [["PAIR", AL, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", AQ, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", GU, "1.0.0", "PROJECTILE_2D", "PASS"],
             ["PAIR", MA, "1.0.0", "PROJECTILE_2D", "PASS"]], []))
# ---- RV's vector (review 0.1.0 F-9): a binding failing T3 at depth 1
_exp_t3 = tpk_mod(FIX2, None, [("CONSTANT", {"NAME": "k_t", "DIMENSION": "1", "VALUE": "1",
                                             "UNIT": "1"})])
assert K.kernel_tests(_exp_t3)[0] is True
_t3row = "T3: CONSTRAINT:C1 and CONSTRAINT:C2: x fixed at 0 m and at 1 m (T3)"
assert "T3: " + K.t3_consistency(_exp_t3, {"PARAMETERS": {"S_MAX": 1}})[1][0] == _t3row
MC("M20 a binding failing T3 at depth 1", X_T3, "MODULE ADD", DAY1, [], [],
   PASSROWS(D_T3, "PASS", [["BINDING", "FIX2", _t3row]], [], []))
# ---- S51: a SHARED PARAMETER row; differing DIMENSIONs refuse the pair
L_SUMP = [LADD("TEST-SUMP", "1.0.0", D_SUMP)]
_exp_share = tpk_mod(P2, None, [("CONSTANT", E_KQ)])
must_pass(_exp_share, "_exp_share")
_exp_kq1 = tpk_mod(P2, None, [("CONSTANT", {"NAME": "k_q", "DIMENSION": "1"})])
must_pass(_exp_kq1, "_exp_kq1")
MC("M21 ADD TEST-SHARE: an AUTHOR parameter named as TEST-SUMP's: the question for H-DECL",
   X_SHARE, "MODULE ADD", DAY2, L_SUMP, [X_SUMP],
   OK("PASS", [["DIGEST", D_SHARE], ["REFERENCES", "PASS"], ["BINDING", "PROJECTILE_2D", "PASS"],
               ["PAIR", "TEST-SUMP", "1.0.0", "PROJECTILE_2D", "PASS"],
               ["SHARED PARAMETER", "TEST-SHARE", "1.0.0", "TEST-SUMP", "1.0.0", "k_q"],
               ["ATOMICITY", []]]))
MC("M22 ADD TEST-KQ1: the same name, another DIMENSION: the pair refused (S51)", X_KQ1,
   "MODULE ADD", DAY2, L_SUMP, [X_SUMP],
   OK("FAIL", [["DIGEST", D_KQ1], ["REFERENCES", "PASS"], ["BINDING", "PROJECTILE_2D", "PASS"],
               ["PAIR", "TEST-SUMP", "1.0.0", "-", "SHARED PARAMETER k_q: DIMENSION 1 in TEST-KQ1 "
                "1.0.0, L-1 M1 in TEST-SUMP 1.0.0"],
               ["ATOMICITY", []]]))

# hand derivation of the PASS pairs: every pair expansion, both orders one TPK, passes
_PAIR_TPKS = {
    ("AIR-LINEAR", "GRAV-UNIFORM"): EXP_GRAV_AIRL, ("AIR-QUADRATIC", "GRAV-UNIFORM"): EXP_GRAV_AIRQ,
    ("AIR-LINEAR", "MATH"): EXP_AIRL, ("AIR-QUADRATIC", "MATH"): EXP_AIRQ_MATH,
    ("GRAV-UNIFORM", "MATH"): EXP_GRAV_MATH, ("AIR-QUADRATIC", "TEST-PI"): EXP_AIRQ_TPI,
    ("GRAV-UNIFORM", "TEST-PI"): EXP_GRAV_TPI}
_PAIR_TPKS[("TEST-SHARE", "TEST-SUMP")] = _EXP_SUMP
for _k, _t in _PAIR_TPKS.items():
    must_pass(_t, str(_k))
_PUSH_PAIRS = {
    "AIR-LINEAR": tpk_mod(P2, {"EOM_X": ("m*D(v_x,t)=-6*pi_c*mu_air*R_s*v_x+(F_a+F_b)",
                                         ["F_a", "F_b", "R_s", "m", "mu_air", "pi_c", "t", "v_x"],
                                         "m dv_x/dt = 0" + S_LX + "; with the push: +(F_a+F_b)"),
                               "EOM_Z": ("m*D(v_z,t)=-6*pi_c*mu_air*R_s*v_z-(F_a+F_b)",
                                         ["F_a", "F_b", "R_s", "m", "mu_air", "pi_c", "t", "v_z"],
                                         "m dv_z/dt = 0" + S_LZ + "; with the pull: -(F_a+F_b)")},
                          AIRL_EMIT_2 + [("CONSTANT", {"NAME": "F_a", "DIMENSION": "L1 M1 T-2"}),
                                         ("CONSTANT", {"NAME": "F_b", "DIMENSION": "L1 M1 T-2"})]),
    "GRAV-UNIFORM": tpk_mod(P2, {"EOM_X": ("m*D(v_x,t)=F_a+F_b", ["F_a", "F_b", "m", "t", "v_x"],
                                           "m dv_x/dt = 0; with the push: +(F_a+F_b)"),
                                 "EOM_Z": ("m*D(v_z,t)=-m*g_n-(F_a+F_b)",
                                           ["F_a", "F_b", "g_n", "m", "t", "v_z"],
                                           "m dv_z/dt = 0" + S_W + "; with the pull: -(F_a+F_b)")},
                            GRAV_EMIT_100 + [("CONSTANT", {"NAME": "F_a",
                                                           "DIMENSION": "L1 M1 T-2"}),
                                             ("CONSTANT", {"NAME": "F_b",
                                                           "DIMENSION": "L1 M1 T-2"})]),
}
_PUSH_PAIRS["AIR-QUADRATIC"] = tpk_mod(
    P2, {"EOM_X": (EQ_QX + "+(F_a+F_b)", ["F_a", "F_b"] + R_QX,
                   "m dv_x/dt = 0" + S_QX + "; with the push: +(F_a+F_b)"),
         "EOM_Z": (EQ_QZ + "-(F_a+F_b)", ["F_a", "F_b"] + R_QZ,
                   "m dv_z/dt = 0" + S_QZ + "; with the pull: -(F_a+F_b)")},
    AIRQ_EMIT_2 + [("CONSTANT", {"NAME": "F_a", "DIMENSION": "L1 M1 T-2"}),
                   ("CONSTANT", {"NAME": "F_b", "DIMENSION": "L1 M1 T-2"})])
_PUSH_PAIRS["MATH"] = tpk_mod(_EXP_PUSH, None, [("CONSTANT", E_PI)])
for _k, _t in _PUSH_PAIRS.items():
    for _l in _t["LAW"]:
        canon_is(_l["EQUATION"], "equation")
    must_pass(_t, "push+" + _k)

# ---------------------------------------------------------------- write
names = [c["name"] for c in CASES]
assert len(set(names)) == len(names), "duplicate case names"
for c in CASES:
    json.loads(c["expected"])                    # every expectation is JSON text
    assert all(ord(ch) < 128 for ch in c["expected"])
counts = {}
for c in CASES:
    counts[c["fn"]] = counts.get(c["fn"], 0) + 1
out = {"corpus": "module", "version": CORPUS_VERSION,
       "spec": "tpm_module_spec 0.2.0",
       "note": ("vectors first: every expectation a literal of the generator; digests and D(TPK) "
                "computed by existing TPC-PY functions over literal data (see the generator's "
                "docstring); no tpc_module exists"),
       "cases": CASES}
with open(os.path.join(HERE, "vectors", "module_corpus.json"), "w", encoding="utf-8", newline="\n") as f:
    json.dump(out, f, indent=1, sort_keys=True, ensure_ascii=True)
    f.write("\n")
print("wrote %d module cases" % len(CASES))
for fn in sorted(counts):
    print("  %-20s %d" % (fn, counts[fn]))