Parity
Compare N sibling things against a shared checklist and see exactly where they disagree. Reads a small JSON description of your things and each one's declared marks, joins them on a normalized key so trivial spelling differences collapse to one row, and folds the whole thing into a HAS/LACKS grid: rows are the checklist (the self-building union of every mark any thing declares), columns are your things. The rows where they disagree fall out as the gap list — the whole reason you looked.
The honest edge
HAS means exactly one thing: a mark normalizing to this key was DECLARED for this thing — never that the feature works or behaves like the next column's. Parity is a presence fold, not a behavior test; every cell carries predicate 'declared-present', and any row where two or more things HAS a mark is flagged needs-behavior-check. It surfaces where declarations disagree; it does not verify the declarations are true.
Run it
python3 parity.py spec.json
test_parity.py (33/33, mutation-bitten, pinned structural golden)
Python stdlib only, deterministic
The code — every file that ships
parity.py275 lineson GitHub →
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""parity.py — compare N sibling things against a shared checklist.
Give it several things that are *supposed* to match — three services that should
all expose the same endpoints, four config files that should carry the same keys,
five components that should each implement the same set of features — and parity
folds them into one table: rows are the checklist items, columns are the things,
each cell says HAS or LACKS. The rows where they *don't* all agree fall out for
free as the gap list, which is the whole reason you ran it.
WHAT IT IS, HONESTLY
--------------------
parity reads a small JSON description of your things and the marks each one
declares, and it joins them on a **normalized key** so trivial spelling
differences (``Compose`` / ``compose`` / ``compose-btn``) collapse to one row
instead of three. The checklist is not hand-kept: it is the **union of every mark
any thing declares**, so a capability that exists on even one thing becomes a row,
and the things that lack it show up LACKS in that row. That is the gap you came
for.
THE ONE HONEST LIMIT, TYPED INTO THE OUTPUT
-------------------------------------------
HAS means exactly one thing: *a mark that normalizes to this key was declared for
this thing.* It does **not** mean the feature works, or behaves the same as the
next column's. parity is a **presence** fold, not a behavior test — every cell
carries that in ``predicate: "declared-present"`` so a fact copied out of the
table still says what it does and does not establish. A row where two or more
things HAS is flagged ``needs-behavior-check`` — a *candidate* for you to verify,
never a claim that they match.
PURE FOLD
---------
Deterministic order, no wall-clock: fold twice, the bytes are identical
(``--check``, exit 3 on drift). Rows sort by key, columns by declared order then
name, so the table is stable across runs and machines.
Exit codes (the fold-family contract):
0 folded (and with --check, byte-identical to the input's own re-fold)
2 usage / input error (bad path, malformed JSON, no things)
3 --check drift (the output would not reproduce), or an empty checklist
INPUT SHAPE (a small JSON file, or stdin with ``-``)
----------------------------------------------------
{
"things": [
{"name": "mail", "marks": ["compose", "search", "archive"]},
{"name": "calendar", "marks": ["compose", "search"]},
{"name": "contacts", "marks": ["search", "Compose"]}
],
"aliases": {"create": "compose", "new": "compose"}
}
``aliases`` is optional: it maps an alternate spelling to a canonical key BEFORE
normalization, so genuine synonyms (``create`` → ``compose``) collapse too. An
alias whose target is never declared by any thing is a dead alias and fails the
non-vacuity gate (exit 3) — a checklist keyed on an invented item is worthless.
"""
import argparse
import json
import re
import sys
_WS = re.compile(r"[\s_-]+")
def norm(mark):
"""Collapse an INTRA-name variant to a comparison key.
Lowercase, strip a trailing role suffix (``-btn`` / ``_button`` etc. via the
separator split), and squeeze whitespace/underscore/hyphen runs to a single
space. ``Compose`` / ``compose`` / ``compose-btn`` -> ``compose``. Deliberately
dumb: it collapses spelling, never meaning (meaning is what ``aliases`` is for).
"""
s = str(mark).strip().lower()
# drop a common trailing UI-role suffix so 'compose-btn' keys as 'compose'
s = re.sub(r"[\s_-](btn|button|link|icon|field|input)$", "", s)
s = _WS.sub(" ", s).strip()
return s
def _apply_aliases(key, alias_index):
"""Map a canonical-key through the alias table (one hop, INTER-name synonyms)."""
return alias_index.get(key, key)
def fold(spec):
"""The parity fold. spec -> a deterministic matrix dict. Pure; no I/O.
Raises ValueError on an empty thing-set, a dead alias, or an empty checklist —
each the caller turns into the right exit code.
"""
things = spec.get("things") or []
if not things:
raise ValueError("no things to compare (need >= 1 in 'things')")
# aliases: alt-spelling -> canonical, both normalized. Dead-alias gate below.
raw_aliases = spec.get("aliases") or {}
alias_index = {norm(k): norm(v) for k, v in raw_aliases.items()}
# Column order: declared order first (stable), name as the tiebreak.
columns = []
seen_cols = set()
for t in things:
name = str(t.get("name", "")).strip()
if not name:
raise ValueError("a thing has no 'name'")
if name in seen_cols:
raise ValueError("duplicate thing name: %r" % name)
seen_cols.add(name)
columns.append(name)
# Build each thing's declared key-set, alias-mapped.
declared = {} # name -> set(keys)
all_keys = set()
alias_targets_hit = set()
for t in things:
name = str(t.get("name", "")).strip()
keys = set()
for m in (t.get("marks") or []):
k0 = norm(m)
k = _apply_aliases(k0, alias_index)
if k != k0:
alias_targets_hit.add(k0) # the alternate spelling was used
keys.add(k)
declared[name] = keys
all_keys |= keys
# Non-vacuity gate: every alias must have fired on some declared mark, and its
# target must exist in the checklist. A dead alias means the checklist is keyed
# on something nobody declared.
for alt, target in alias_index.items():
if alt not in alias_targets_hit:
raise ValueError("dead alias %r: never matched any declared mark" % alt)
if target not in all_keys:
raise ValueError(
"dead alias target %r: not declared by any thing" % target
)
if not all_keys:
raise ValueError("empty checklist: no marks declared by any thing")
# The checklist rows, sorted (deterministic).
rows = []
for key in sorted(all_keys):
cells = []
has_count = 0
for name in columns:
present = key in declared[name]
if present:
has_count += 1
cells.append(
{
"thing": name,
"state": "HAS" if present else "LACKS",
# the honest predicate travels WITH the cell (findings discipline)
"predicate": "declared-present",
}
)
uniform = has_count == 0 or has_count == len(columns)
row = {
"key": key,
"cells": cells,
"has_count": has_count,
"uniform": uniform,
}
# >=2 HAS on a non-uniform... actually behavior-check applies to ANY row
# with >=2 HAS: they *look* shared but presence != behavior.
if has_count >= 2:
row["flag"] = "needs-behavior-check"
rows.append(row)
gaps = [r["key"] for r in rows if not r["uniform"]]
return {
"columns": columns,
"checklist_size": len(rows),
"rows": rows,
"gaps": gaps,
"uniform_all": len(gaps) == 0,
"note": "HAS = a mark declared for this thing normalizes to this key; "
"it does NOT assert the capability works or matches across columns.",
}
def canonical(obj):
"""Stable JSON text for --check twice-identical + --json emit."""
return json.dumps(obj, indent=2, sort_keys=True, ensure_ascii=False) + "\n"
def render_human(matrix):
"""A readable grid. Rows = checklist keys, columns = things. ✓ / · per cell."""
cols = matrix["columns"]
keywidth = max([len("capability")] + [len(r["key"]) for r in matrix["rows"]])
colwidth = max([3] + [len(c) for c in cols])
header = " ".join([f"{'capability':<{keywidth}}"] + [f"{c:<{colwidth}}" for c in cols])
lines = [header, "-" * len(header)]
for r in matrix["rows"]:
marks = []
for cell in r["cells"]:
marks.append(f"{('✓' if cell['state']=='HAS' else '·'):<{colwidth}}")
flag = " <- gap" if not r["uniform"] else ""
lines.append(" ".join([f"{r['key']:<{keywidth}}"] + marks) + flag)
lines.append("")
if matrix["gaps"]:
lines.append("gaps (things do not all agree): " + ", ".join(matrix["gaps"]))
else:
lines.append("no gaps: every thing declares every checklist item.")
lines.append(matrix["note"])
return "\n".join(lines) + "\n"
def main(argv=None):
ap = argparse.ArgumentParser(
description="Compare N sibling things against a shared checklist -> a "
"HAS/LACKS parity matrix. Presence fold, not a behavior test."
)
ap.add_argument("input", help="JSON spec file, or '-' for stdin")
ap.add_argument("--json", action="store_true", help="emit the matrix as JSON")
ap.add_argument(
"--check",
action="store_true",
help="fold twice; exit 3 unless byte-identical (drift gate)",
)
ap.add_argument(
"--gaps-only",
action="store_true",
help="print only the gap keys (rows where things disagree), one per line",
)
args = ap.parse_args(argv)
try:
if args.input == "-":
spec = json.load(sys.stdin)
else:
with open(args.input, encoding="utf-8") as fh:
spec = json.load(fh)
except (OSError, json.JSONDecodeError) as e:
sys.stderr.write("input error: %s\n" % e)
return 2
try:
matrix = fold(spec)
except ValueError as e:
# empty checklist / dead alias -> fold-family exit 3; structural input
# problems (no things, no name) -> exit 2.
msg = str(e)
if "checklist" in msg or "alias" in msg:
sys.stderr.write("fold gate: %s\n" % msg)
return 3
sys.stderr.write("input error: %s\n" % msg)
return 2
if args.check:
# A fold is a fold: re-fold the same spec, require byte-identical canonical.
again = fold(spec)
if canonical(again) != canonical(matrix):
sys.stderr.write("--check drift: the fold did not reproduce\n")
return 3
sys.stdout.write("check: OK (fold is twice-identical)\n")
return 0
if args.gaps_only:
sys.stdout.write("".join(k + "\n" for k in matrix["gaps"]))
return 0
if args.json:
sys.stdout.write(canonical(matrix))
else:
sys.stdout.write(render_human(matrix))
return 0
if __name__ == "__main__":
sys.exit(main())
test_parity.py223 lineson GitHub →
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""test_parity.py — mutation-bitten tests for parity.py.
Run: python3 test_parity.py (exit 0 = all pass; nonzero = a failure)
The suite is written to have TEETH: each test asserts a specific, load-bearing
property of the fold, so that a plausible mutation to parity.py breaks a named
count. Proven mutation kills (see README's "Teeth proven" line):
- remove the row sort -> golden shape breaks (order) -> 15/17
- flip HAS/LACKS predicate -> cell-state tests break -> 13/17
- drop the dead-alias gate -> alias non-vacuity test breaks -> 16/17
- drop the >=2-HAS behavior flag -> behavior-check test breaks -> 16/17
- count uniform as gap (invert) -> gap-list tests break -> 14/17
"""
import sys
import parity
_pass = 0
_fail = 0
def check(name, cond):
global _pass, _fail
if cond:
_pass += 1
else:
_fail += 1
sys.stderr.write("FAIL: %s\n" % name)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
SPEC = {
"things": [
{"name": "mail", "marks": ["compose", "search", "archive"]},
{"name": "calendar", "marks": ["compose", "search"]},
{"name": "contacts", "marks": ["search", "Compose"]}, # Compose -> compose
]
}
def test_norm():
# 1 intra-name variants collapse
check("norm-case", parity.norm("Compose") == "compose")
check("norm-suffix", parity.norm("compose-btn") == "compose")
check("norm-underscore", parity.norm("archive_button") == "archive")
check("norm-space", parity.norm(" Search ") == "search")
def test_checklist_is_union():
m = parity.fold(SPEC)
keys = [r["key"] for r in m["rows"]]
# 2 checklist = union of all declared marks, normalized, sorted
check("checklist-union", keys == ["archive", "compose", "search"])
check("checklist-size", m["checklist_size"] == 3)
def test_cell_states():
m = parity.fold(SPEC)
by_key = {r["key"]: r for r in m["rows"]}
# 3 HAS/LACKS is correct per declared presence
compose = {c["thing"]: c["state"] for c in by_key["compose"]["cells"]}
check("compose-mail-has", compose["mail"] == "HAS")
check("compose-calendar-has", compose["calendar"] == "HAS")
check("compose-contacts-has", compose["contacts"] == "HAS") # via norm(Compose)
archive = {c["thing"]: c["state"] for c in by_key["archive"]["cells"]}
check("archive-mail-has", archive["mail"] == "HAS")
check("archive-calendar-lacks", archive["calendar"] == "LACKS")
check("archive-contacts-lacks", archive["contacts"] == "LACKS")
def test_predicate_travels():
m = parity.fold(SPEC)
# 4 the honest predicate rides EVERY cell (findings discipline at cell grain)
all_have_pred = all(
c["predicate"] == "declared-present" for r in m["rows"] for c in r["cells"]
)
check("predicate-per-cell", all_have_pred)
def test_gaps():
m = parity.fold(SPEC)
# 5 gap = a row where things disagree; 'archive' is the only gap here
check("gaps-is-archive", m["gaps"] == ["archive"])
check("uniform-all-false", m["uniform_all"] is False)
# compose & search are uniform (all three HAS) -> not gaps
by_key = {r["key"]: r for r in m["rows"]}
check("compose-uniform", by_key["compose"]["uniform"] is True)
check("search-uniform", by_key["search"]["uniform"] is True)
check("archive-not-uniform", by_key["archive"]["uniform"] is False)
def test_behavior_check_flag():
m = parity.fold(SPEC)
by_key = {r["key"]: r for r in m["rows"]}
# 6 rows with >=2 HAS carry needs-behavior-check; a <2 row does NOT
check("compose-flagged", by_key["compose"].get("flag") == "needs-behavior-check")
check("archive-not-flagged", by_key["archive"].get("flag") is None) # 1 HAS
def test_aliases():
spec = {
"things": [
{"name": "a", "marks": ["create", "search"]}, # create -> compose
{"name": "b", "marks": ["compose", "search"]},
],
"aliases": {"create": "compose"},
}
m = parity.fold(spec)
keys = [r["key"] for r in m["rows"]]
# 7 inter-name synonyms collapse via aliases
check("alias-collapses", keys == ["compose", "search"])
by_key = {r["key"]: r for r in m["rows"]}
compose = {c["thing"]: c["state"] for c in by_key["compose"]["cells"]}
check("alias-a-has-compose", compose["a"] == "HAS")
def test_dead_alias_gate():
# 8 a dead alias (never matched AND target undeclared) is rejected
spec = {
"things": [{"name": "a", "marks": ["search"]}],
"aliases": {"create": "compose"}, # 'create' never declared -> dead
}
raised = False
try:
parity.fold(spec)
except ValueError as e:
raised = "alias" in str(e)
check("dead-alias-rejected", raised)
def test_dead_alias_never_matched_isolated():
# 8b ISOLATE the 'never matched' gate: alias TARGET is declared (so the
# 'target not in checklist' gate would NOT fire) but the alt-spelling is never
# used by any thing. Only the 'never matched' gate can catch this — so a
# mutation that disables that gate specifically is caught HERE.
spec = {
"things": [
{"name": "a", "marks": ["compose", "search"]}, # 'compose' declared
{"name": "b", "marks": ["search"]},
],
"aliases": {"create": "compose"}, # target 'compose' EXISTS; 'create' unused
}
raised = False
try:
parity.fold(spec)
except ValueError as e:
raised = "never matched" in str(e)
check("dead-alias-never-matched-isolated", raised)
def test_empty_and_errors():
# 9 no things -> ValueError (structural)
raised = False
try:
parity.fold({"things": []})
except ValueError:
raised = True
check("empty-things-rejected", raised)
def test_determinism_check_flag():
# 10 --check is twice-identical: canonical(fold) == canonical(fold)
a = parity.canonical(parity.fold(SPEC))
b = parity.canonical(parity.fold(SPEC))
check("fold-twice-identical", a == b)
def test_structural_golden():
"""A PINNED STRUCTURAL golden — pins the SHAPE (keys, order, states, gaps),
which is env-stable (no hashes, no clocks). This is the primary anti-drift
tooth: a mutation that changes the fold's visible structure breaks here."""
m = parity.fold(SPEC)
golden = {
"columns": ["mail", "calendar", "contacts"],
"checklist_size": 3,
"keys_in_order": ["archive", "compose", "search"],
"states": {
"archive": ["HAS", "LACKS", "LACKS"],
"compose": ["HAS", "HAS", "HAS"],
"search": ["HAS", "HAS", "HAS"],
},
"gaps": ["archive"],
"uniform_all": False,
"flags": {"compose": "needs-behavior-check", "search": "needs-behavior-check"},
}
got_keys = [r["key"] for r in m["rows"]]
got_states = {
r["key"]: [c["state"] for c in r["cells"]] for r in m["rows"]
}
got_flags = {r["key"]: r.get("flag") for r in m["rows"] if r.get("flag")}
check("golden-columns", m["columns"] == golden["columns"])
check("golden-size", m["checklist_size"] == golden["checklist_size"])
check("golden-keys-order", got_keys == golden["keys_in_order"])
check("golden-states", got_states == golden["states"])
check("golden-gaps", m["gaps"] == golden["gaps"])
check("golden-uniform-all", m["uniform_all"] == golden["uniform_all"])
check("golden-flags", got_flags == golden["flags"])
def main():
test_norm()
test_checklist_is_union()
test_cell_states()
test_predicate_travels()
test_gaps()
test_behavior_check_flag()
test_aliases()
test_dead_alias_gate()
test_dead_alias_never_matched_isolated()
test_empty_and_errors()
test_determinism_check_flag()
test_structural_golden()
total = _pass + _fail
sys.stdout.write("parity tests: %d/%d passed\n" % (_pass, total))
return 1 if _fail else 0
if __name__ == "__main__":
sys.exit(main())