CB-WP-0028 T01/T02: the cards say what they do
ADR-0015 and the import. F18's fix: "I don't understand the GROUND card" was never a design gap -- the card explains itself in the edition and we never imported the explanation. THE MEASUREMENT IS THE DECISION, and the gap is bigger than "one file of nineteen". Of the file we DID vendor, the engine reads 5 of 13 columns: title, problem_text, front_rules, reveal_effect and unresolved_effect were discarded at parse time. The cheapest part of this pass costs no new bytes and was sitting in the repo for eight days. And SCN_01 is hardcoded at lib.rs:1824 -- the edition ships FOUR scenarios and the engine has never dealt three of them. Nobody had said so. ADR-0011's revisit condition is measurably absent, so the dependency argument does not get re-run: across Actions, Solutions, Modes and Scenarios there are ZERO doubled quotes and ZERO embedded newlines. The hand reader's only job is comma-in-quoted-field, which it already did. Refusing csv on a measurement rather than on a preference. Vendored Actions, Solutions and Modes -- the text a player reads. Not the production artifacts (BOM, Print_Manifest, Back_Designs, Symbols). NOT Extensions.csv, which names content the designer placed outside the core; importing it would break the claim that this engine plays the edition as printed. It is now known to exist, which was the real risk. One Table reader with four callers, because a per-file copy is how a parser acquires four subtly different bugs. The GROUND card now shows "Regulate. Restore the frame. Decide." with its GR/OU/ND text on demand; Problems show their own titles where a priority number used to be. The load-bearing test asserts the text is a SUBSTRING OF THE VENDORED FILE rather than equal to a Rust literal -- a test comparing against a hardcoded expectation would pass for a hand-copied string, which is the drift this ends. `edition` came out from behind #[cfg(feature = "scenarios")]. It was gated because its only consumer was; the edition is the game's own data and the shipped runtime now reads it. Test machinery and game content are different things and only one of them is optional. And edition-check was written for a single-file world: it compared the first recorded digest against Problems.csv regardless of which file that digest described. It now checks every file both ways -- a vendored file with no digest fails, a digest naming an absent file fails -- and asserts ADR-0015 D3's falsifier directly rather than trusting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
efe9f8f1a9
commit
4b5b601ce0
11 changed files with 767 additions and 42 deletions
|
|
@ -16,9 +16,9 @@ import sys
|
|||
|
||||
from repo import ROOT, enter_root
|
||||
|
||||
VENDORED = "editions/ground-darvo-r0/Problems.csv"
|
||||
PROVENANCE = "editions/ground-darvo-r0/PROVENANCE.md"
|
||||
UPSTREAM = os.path.join(os.path.dirname(ROOT), "ground-game", VENDORED)
|
||||
EDITION = "editions/ground-darvo-r0"
|
||||
PROVENANCE = f"{EDITION}/PROVENANCE.md"
|
||||
UPSTREAM_DIR = os.path.join(os.path.dirname(ROOT), "ground-game", EDITION)
|
||||
|
||||
|
||||
def digest(path):
|
||||
|
|
@ -26,38 +26,90 @@ def digest(path):
|
|||
|
||||
|
||||
def recorded():
|
||||
"""Every recorded digest, by filename.
|
||||
|
||||
The first version matched ONE `sha256 <hex>` and assumed it described
|
||||
`Problems.csv`. ADR-0015 vendored three more files, and the check
|
||||
reported the first digest against the wrong file -- a gate written for
|
||||
a single-file world silently comparing across files.
|
||||
"""
|
||||
text = open(os.path.join(ROOT, PROVENANCE)).read()
|
||||
m = re.search(r"sha256\s+([0-9a-f]{64})", text)
|
||||
if not m:
|
||||
raise ValueError(f"{PROVENANCE} records no sha256 digest")
|
||||
return m.group(1)
|
||||
found = dict(
|
||||
(name, d)
|
||||
for d, name in re.findall(r"sha256\s+([0-9a-f]{64})\s+(\S+)", text)
|
||||
)
|
||||
if not found:
|
||||
raise ValueError(f"{PROVENANCE} records no `sha256 <hex> <file>` digests")
|
||||
return found
|
||||
|
||||
|
||||
def vendored_files():
|
||||
"""Every `.csv` actually present, so a file added without a digest is
|
||||
caught rather than skipped."""
|
||||
d = os.path.join(ROOT, EDITION)
|
||||
return sorted(f for f in os.listdir(d) if f.endswith(".csv"))
|
||||
|
||||
|
||||
def check():
|
||||
have = digest(os.path.join(ROOT, VENDORED))
|
||||
want = recorded()
|
||||
print("edition-check — vendored data against its provenance")
|
||||
if have != want:
|
||||
print(f" [FAIL] {VENDORED} does not match its recorded digest")
|
||||
print(f" recorded {want}\n actual {have}")
|
||||
return 1
|
||||
print(f" [ok ] vendored copy matches its recorded digest")
|
||||
rc = 0
|
||||
|
||||
if not os.path.exists(UPSTREAM):
|
||||
present = vendored_files()
|
||||
undocumented = [f for f in present if f not in want]
|
||||
if undocumented:
|
||||
print(f" [FAIL] vendored with no recorded digest: {', '.join(undocumented)}")
|
||||
rc = 1
|
||||
missing = [f for f in want if f not in present]
|
||||
if missing:
|
||||
print(f" [FAIL] a digest is recorded for a file that is not here: {', '.join(missing)}")
|
||||
rc = 1
|
||||
|
||||
for name in present:
|
||||
if name not in want:
|
||||
continue
|
||||
have = digest(os.path.join(ROOT, EDITION, name))
|
||||
if have != want[name]:
|
||||
print(f" [FAIL] {name} does not match its recorded digest")
|
||||
print(f" recorded {want[name]}\n actual {have}")
|
||||
rc = 1
|
||||
else:
|
||||
print(f" [ok ] {name} matches its recorded digest")
|
||||
|
||||
# ADR-0015 D3's falsifier, checked rather than asserted: the hand
|
||||
# reader handles commas inside quotes and NOTHING ELSE. A doubled
|
||||
# quote or an embedded newline means `csv` is the answer after all.
|
||||
for name in present:
|
||||
raw = open(os.path.join(ROOT, EDITION, name), encoding="utf-8-sig").read()
|
||||
if '""' in raw:
|
||||
print(f" [FAIL] {name} contains a doubled quote — ADR-0011's revisit")
|
||||
print(" condition has fired; the hand reader cannot parse it")
|
||||
rc = 1
|
||||
# An embedded newline shows up as an odd quote count on a line.
|
||||
for i, line in enumerate(raw.splitlines(), 1):
|
||||
if line.count('"') % 2:
|
||||
print(f" [FAIL] {name}:{i} has an unbalanced quote — embedded newline?")
|
||||
rc = 1
|
||||
break
|
||||
if rc == 0:
|
||||
print(" [ok ] no doubled quotes or embedded newlines (ADR-0015 D3)")
|
||||
|
||||
if not os.path.isdir(UPSTREAM_DIR):
|
||||
# NOT a pass and NOT a failure: the question could not be asked.
|
||||
print(" [----] upstream not checked out — freshness UNVERIFIED")
|
||||
print(f" expected {UPSTREAM}")
|
||||
return 0
|
||||
up = digest(UPSTREAM)
|
||||
if up != have:
|
||||
print(" [FAIL] upstream has changed since this copy was vendored")
|
||||
print(f" upstream {up}\n vendored {have}")
|
||||
print(" ground-game froze point_value and required_solution")
|
||||
print(" within r0 — a change here is a new revision, or a")
|
||||
print(" contract violation worth raising.")
|
||||
return 1
|
||||
print(" [ok ] vendored copy is current with ../ground-game")
|
||||
return 0
|
||||
print(f" expected {UPSTREAM_DIR}")
|
||||
return rc
|
||||
for name in present:
|
||||
up = os.path.join(UPSTREAM_DIR, name)
|
||||
if not os.path.exists(up):
|
||||
print(f" [FAIL] {name} is not in upstream — where did it come from?")
|
||||
rc = 1
|
||||
elif digest(up) != digest(os.path.join(ROOT, EDITION, name)):
|
||||
print(f" [FAIL] upstream {name} has changed since it was vendored")
|
||||
rc = 1
|
||||
if rc == 0:
|
||||
print(" [ok ] every vendored copy is current with ../ground-game")
|
||||
return rc
|
||||
|
||||
|
||||
def self_test():
|
||||
|
|
@ -67,19 +119,33 @@ def self_test():
|
|||
def chk(name, ok, detail=""):
|
||||
results.append((name, ok, detail))
|
||||
|
||||
chk("the vendored file exists", os.path.exists(os.path.join(ROOT, VENDORED)))
|
||||
chk("provenance records a digest", len(recorded()) == 64)
|
||||
chk("digest of the real file matches provenance",
|
||||
digest(os.path.join(ROOT, VENDORED)) == recorded())
|
||||
present = vendored_files()
|
||||
want = recorded()
|
||||
chk("vendored files exist", len(present) >= 4, ", ".join(present))
|
||||
chk("every vendored file has a recorded digest",
|
||||
all(f in want for f in present),
|
||||
"a file added without a digest must fail, not be skipped")
|
||||
chk("every digest names a file that is here",
|
||||
all(f in present for f in want),
|
||||
"a stale digest is a lie with a filename")
|
||||
chk("digests match the real files",
|
||||
all(digest(os.path.join(ROOT, EDITION, f)) == want[f] for f in present))
|
||||
|
||||
# The control that matters: a changed byte must be detected.
|
||||
import tempfile
|
||||
one = present[0]
|
||||
with tempfile.NamedTemporaryFile("wb", delete=False) as fh:
|
||||
fh.write(open(os.path.join(ROOT, VENDORED), "rb").read() + b"\n#tamper\n")
|
||||
fh.write(open(os.path.join(ROOT, EDITION, one), "rb").read() + b"\n#tamper\n")
|
||||
tampered = fh.name
|
||||
chk("a tampered copy has a different digest",
|
||||
digest(tampered) != recorded(), "otherwise the check is decoration")
|
||||
digest(tampered) != want[one], "otherwise the check is decoration")
|
||||
os.unlink(tampered)
|
||||
|
||||
# ADR-0015 D3's condition must be DETECTABLE, or asserting its absence
|
||||
# in `check()` proves nothing.
|
||||
chk("a doubled quote would be detected", '""' in 'a,""b""', "the pattern check itself")
|
||||
chk("an unbalanced quote would be detected", 'a,"b'.count('"') % 2 == 1)
|
||||
|
||||
print("edition-check self-test (positive control)")
|
||||
ok = True
|
||||
for name, passed, det in results:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue