Some checks failed
ci / check (push) Failing after 4s
vendor tool that covers what the gate checks
They ruled on all seven items the same day. Two were actionable here.
F28 RULED: points. Modes.csv MODE_COOP clarified upstream to say
"penalties apply to points, not card count"; mastery is now
total - blame - denied. A recorded scenario went red on it --
gr-e02-shared-ground pinned 0 (2 claimed CARDS - 1 - 1) and now expects
2 (4 POINTS - 1 - 1). The number moved because the rule was decided, not
because the engine drifted, and the scenario records both rulings; its
schema has no field for a second one, so both live in ruled_note with
`ruled` carrying the LATEST date.
F29 RULED not-intended and APPLIED upstream: SCN_02's suits re-tuned the
same day. The characterisation test is how we found out -- it pinned the
duplication, went red on the re-tune, and that red WAS the notification.
It now asserts every pair distinct, the stronger statement the
duplication had made unavailable. SCN_02 re-measures at 73 at 2p, not
67: its own board now.
F26/F30 ruled and recorded. F30's ruling incidentally confirms our
reading -- they name priority-2's suit as the first lever, which is the
difference we identified without having measured causation.
vendor-editions grew twice, both times because it covered less than the
gate it exists to satisfy:
- It refused to touch ground-darvo-r0/ on the reasoning that the
baseline is "a separate record". That was wrong within the hour:
ground-game clarified Modes.csv and `make vendor` reported a clean
sync while edition-check went red. A sync tool that covers less than
its check reports success into a red gate.
- Its two-block rewrite DETECTED which fence held which set and
preserved the arrangement -- faithfully preserving a swap an earlier
write had introduced, leaving each fence under a heading describing
the other. edition-check reads every sha256 line flat and passed
throughout: a document can be self-consistently wrong and green.
Order is now asserted, with a control that goes red on a swap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
314 lines
13 KiB
Python
314 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Re-vendor the edition mirror from ground-game and record its digests.
|
|
|
|
`edition-check` asks two questions: does every vendored file match its
|
|
recorded digest, and is every vendored copy current with the sibling
|
|
checkout. Answering "no" is routine — ground-game is edited by its own
|
|
maintainer and the mirror goes stale several times a day.
|
|
|
|
**This does the sync; it does not decide anything.** It copies what
|
|
upstream has, records what it copied, and refuses to invent either.
|
|
|
|
## Why this is a tool and not a habit
|
|
|
|
The mirror went stale three times in one session and was refreshed by
|
|
hand each time: copy the changed files, recompute a digest, edit
|
|
`PROVENANCE.md`. A hand-repeated sync is exactly the thing that drifts —
|
|
the second time, one file gets missed and its digest keeps certifying a
|
|
version nobody has.
|
|
|
|
The digest block is **generated by walking `editions/`**, never typed.
|
|
CB-REV-0002 #8 and CB-REV-0003 #8 both found hand-written file lists that
|
|
made their own controls vacuous, and a mirror that grows a *directory* is
|
|
precisely the case a maintained list loses.
|
|
|
|
## What it will not do
|
|
|
|
- It does not delete files upstream no longer has, and it does not add
|
|
files upstream does not have. `edition-check` reports both, and a sync
|
|
tool that silently resolved them would remove the only signal that the
|
|
mirror and upstream disagree about what exists.
|
|
- It does not delete or add; see above. It **does** refresh the baseline
|
|
package's CSVs, which the first version refused to do on the reasoning
|
|
that `ground-darvo-r0/` is "a separate record". That was wrong within
|
|
the hour: ground-game clarified `Modes.csv` and `make vendor` reported
|
|
a clean sync while `make edition-check` went red on it. **A sync tool
|
|
that covers less than the check it exists to satisfy is a tool that
|
|
reports success into a red gate.**
|
|
"""
|
|
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
EDITIONS = os.path.join(ROOT, "editions")
|
|
EDITION_DIR = os.path.join(EDITIONS, "ground-darvo-r0")
|
|
PROVENANCE = os.path.join(EDITION_DIR, "PROVENANCE.md")
|
|
UPSTREAM = os.path.join(ROOT, "..", "ground-game", "editions")
|
|
|
|
SKIP = {".DS_Store"}
|
|
|
|
|
|
def digest(path):
|
|
return hashlib.sha256(open(path, "rb").read()).hexdigest()
|
|
|
|
|
|
def edition_csvs():
|
|
"""The baseline package's own CSVs, as bare filenames.
|
|
|
|
Mirrors `edition-check`'s `vendored_files()`, including its limit:
|
|
only `.csv`. A non-CSV added to that directory is invisible to both,
|
|
which is recorded as a known hole rather than fixed here.
|
|
"""
|
|
return sorted(
|
|
(n, os.path.join(EDITION_DIR, n))
|
|
for n in os.listdir(EDITION_DIR)
|
|
if n.endswith(".csv")
|
|
)
|
|
|
|
|
|
def siblings():
|
|
"""Every file beside the edition directory, as `../`-relative paths.
|
|
|
|
Mirrors `edition-check`'s own walk. Two walks of the same tree is a
|
|
duplication worth naming: if they ever disagree the check fails
|
|
loudly, which is the direction the duplication should fail in."""
|
|
out = []
|
|
for dirpath, _dirs, files in os.walk(EDITIONS):
|
|
if os.path.abspath(dirpath).startswith(os.path.abspath(EDITION_DIR)):
|
|
continue
|
|
for name in sorted(files):
|
|
if name in SKIP:
|
|
continue
|
|
full = os.path.join(dirpath, name)
|
|
rel = os.path.relpath(full, EDITION_DIR).replace(os.sep, "/")
|
|
out.append((rel, full))
|
|
return sorted(out)
|
|
|
|
|
|
def refresh():
|
|
if not os.path.isdir(UPSTREAM):
|
|
print(f" upstream not checked out at {UPSTREAM}")
|
|
print(" NOT a failure: the sync could not be asked for.")
|
|
return 1
|
|
|
|
copied, missing, extra = [], [], []
|
|
# The baseline package first: it is the one `edition-check` checks
|
|
# by a different code path, and the one this tool used to skip.
|
|
for name, full in edition_csvs():
|
|
up = os.path.join(UPSTREAM, "ground-darvo-r0", name)
|
|
if not os.path.exists(up):
|
|
extra.append(f"ground-darvo-r0/{name}")
|
|
continue
|
|
if digest(up) != digest(full):
|
|
shutil.copy2(up, full)
|
|
copied.append(f"ground-darvo-r0/{name}")
|
|
for rel, full in siblings():
|
|
up = os.path.normpath(os.path.join(UPSTREAM, rel.replace("../", "", 1)))
|
|
if not os.path.exists(up):
|
|
# Ours, or gone upstream. Either way it is a question for a
|
|
# human -- see the module docstring.
|
|
extra.append(rel)
|
|
continue
|
|
if digest(up) != digest(full):
|
|
shutil.copy2(up, full)
|
|
copied.append(rel)
|
|
|
|
# Files upstream has that we do not. Reported, never copied blind:
|
|
# vendoring a new package is a decision (ADR-0011), not a sync.
|
|
for dirpath, _dirs, files in os.walk(UPSTREAM):
|
|
for name in files:
|
|
if name in SKIP:
|
|
continue
|
|
up = os.path.join(dirpath, name)
|
|
rel = os.path.relpath(up, UPSTREAM).replace(os.sep, "/")
|
|
here = os.path.join(EDITIONS, rel)
|
|
if rel.startswith("ground-darvo-r0/"):
|
|
continue
|
|
if not os.path.exists(here):
|
|
missing.append(rel)
|
|
|
|
# Two digest fences, and **which holds which is fixed by the prose
|
|
# above them, not sniffed from their contents.**
|
|
#
|
|
# The first version detected the current arrangement and preserved
|
|
# it — which faithfully preserved a swap an earlier buggy write had
|
|
# introduced, leaving each fence under a heading describing the other
|
|
# one. `edition-check` reads every `sha256` line and so passed
|
|
# throughout: a document can be self-consistently wrong and green.
|
|
#
|
|
# Order is now asserted. `SIBLING_FENCE` is the fence whose prose is
|
|
# about the packages beside the edition; it is the second in the file
|
|
# and this refuses to write if that stops being true.
|
|
text = open(PROVENANCE, encoding="utf-8").read()
|
|
fences = []
|
|
i = 0
|
|
while True:
|
|
j = text.find("```", i)
|
|
if j < 0:
|
|
break
|
|
fences.append(j)
|
|
i = j + 3
|
|
if len(fences) < 4:
|
|
raise SystemExit(
|
|
f"{PROVENANCE}: expected two digest fences, found {len(fences) // 2}"
|
|
)
|
|
edition_fence, sibling_fence = fences[0], fences[2]
|
|
# The heading immediately above the sibling fence must be about
|
|
# siblings, or the file has been reorganised and this tool would be
|
|
# writing digests under the wrong claim.
|
|
lead = text[max(0, sibling_fence - 400) : sibling_fence]
|
|
if "Digests" not in lead and "beside" not in lead and "outside" not in lead:
|
|
raise SystemExit(
|
|
f"{PROVENANCE}: the second fence is not the sibling digest block; "
|
|
"the file was reorganised and this tool will not guess"
|
|
)
|
|
|
|
rows_edition = sorted(
|
|
((digest(full), name) for name, full in edition_csvs()), key=lambda r: r[1]
|
|
)
|
|
rows_sibling = sorted(
|
|
((digest(full), rel) for rel, full in siblings()), key=lambda r: r[1]
|
|
)
|
|
rows = rows_edition + rows_sibling
|
|
|
|
out = text
|
|
# Last fence first, so earlier offsets stay valid.
|
|
for start, rowset in ((sibling_fence, rows_sibling), (edition_fence, rows_edition)):
|
|
end = out.index("```", start + 3) + 3
|
|
body = "\n".join(f"sha256 {h} {r}" for h, r in rowset)
|
|
out = out[:start] + "```\n" + body + "\n```" + out[end:]
|
|
open(PROVENANCE, "w", encoding="utf-8").write(out)
|
|
|
|
print("vendor-editions — the mirror, and what it now records")
|
|
print(f" refreshed from upstream {len(copied)}")
|
|
for r in copied:
|
|
print(f" {r}")
|
|
print(f" digests recorded {len(rows)}")
|
|
if extra:
|
|
print(f" here and NOT upstream {len(extra)} (not deleted — a human decides)")
|
|
for r in extra:
|
|
print(f" {r}")
|
|
if missing:
|
|
print(f" upstream and NOT here {len(missing)} (not copied — vendoring is a decision)")
|
|
for r in missing:
|
|
print(f" {r}")
|
|
return 0
|
|
|
|
|
|
def recorded_paths():
|
|
"""The SIBLING paths the digest block names.
|
|
|
|
**`../`-prefixed only.** `PROVENANCE.md` carries two digest blocks —
|
|
one for the edition's own CSVs and one for everything beside it — and
|
|
this tool owns the second. Reading both reported ten edition CSVs as
|
|
"recorded but absent" the first time the self-test ran, because they
|
|
are absent *from the sibling walk*, which is a different question
|
|
from absent.
|
|
|
|
The tool's own rewrite is likewise scoped: it replaces the first
|
|
`sha256` block, which is the sibling one, and never touches the
|
|
edition's.
|
|
"""
|
|
text = open(PROVENANCE, encoding="utf-8").read()
|
|
out = []
|
|
for line in text.splitlines():
|
|
parts = line.split()
|
|
if len(parts) == 3 and parts[0] == "sha256" and parts[2].startswith("../"):
|
|
out.append(parts[2])
|
|
return sorted(out)
|
|
|
|
|
|
def recorded_all():
|
|
"""Every path the digest blocks name, siblings and edition alike."""
|
|
text = open(PROVENANCE, encoding="utf-8").read()
|
|
return sorted(
|
|
line.split()[2]
|
|
for line in text.splitlines()
|
|
if len(line.split()) == 3 and line.split()[0] == "sha256"
|
|
)
|
|
|
|
|
|
def self_test():
|
|
"""A sync tool that cannot detect an unrecorded file is decoration.
|
|
|
|
**These are read-only.** A self-test that ran `refresh` would write
|
|
to `PROVENANCE.md` to prove that it can, which is a control that
|
|
changes the thing it measures.
|
|
"""
|
|
ok = True
|
|
|
|
def check(name, cond, detail=""):
|
|
nonlocal ok
|
|
ok = ok and bool(cond)
|
|
print(f" [{'ok ' if cond else 'FAIL'}] {name}" + (f" — {detail}" if detail else ""))
|
|
|
|
import tempfile
|
|
|
|
with tempfile.NamedTemporaryFile("wb", delete=False) as f:
|
|
f.write(b"clay-borg")
|
|
tmp = f.name
|
|
check("digest is sha256 of the bytes",
|
|
digest(tmp) == hashlib.sha256(b"clay-borg").hexdigest())
|
|
os.unlink(tmp)
|
|
|
|
found = [rel for rel, _ in siblings()]
|
|
check("the walk finds files, not just the top level", bool(found))
|
|
# CB-REV-0003 #8: the first edition-check listed three glob patterns
|
|
# and missed everything a directory deeper. The mirror is now three
|
|
# deep, so this asserts the walk actually descends.
|
|
deep = [r for r in found if r.count("/") >= 3]
|
|
check("the walk descends into module directories", bool(deep),
|
|
f"deepest: {max(found, key=lambda r: r.count('/')) if found else 'none'}")
|
|
|
|
# THE control. The recorded block must name exactly what is on disk:
|
|
# a file added to the mirror without re-running this tool is the
|
|
# failure it exists to prevent, and `edition-check` would then be
|
|
# certifying a set nobody chose.
|
|
rec = set(recorded_paths())
|
|
disk = set(found)
|
|
check("every vendored file has a recorded digest", not (disk - rec),
|
|
f"unrecorded: {sorted(disk - rec)}" if disk - rec else "")
|
|
check("the baseline package's CSVs are recorded too",
|
|
set(n for n, _ in edition_csvs()) <= set(recorded_all()),
|
|
"the first version skipped ground-darvo-r0/ and reported a clean "
|
|
"sync while edition-check went red on Modes.csv")
|
|
# **Each fence under its own heading** — the control that was
|
|
# missing. An earlier write swapped the two blocks and everything
|
|
# stayed green, because `edition-check` reads every `sha256` line and
|
|
# does not care which fence it came from. A document can be
|
|
# self-consistently wrong and pass every check that reads it flat.
|
|
text = open(PROVENANCE, encoding="utf-8").read()
|
|
fences = []
|
|
i = 0
|
|
while True:
|
|
j = text.find("```", i)
|
|
if j < 0:
|
|
break
|
|
fences.append(j)
|
|
i = j + 3
|
|
first_body = text[fences[0] : text.index("```", fences[0] + 3)]
|
|
second_body = text[fences[2] : text.index("```", fences[2] + 3)]
|
|
check("the first digest fence holds the edition's own CSVs",
|
|
" ../" not in first_body and "sha256" in first_body)
|
|
check("the second digest fence holds the siblings",
|
|
all(" ../" in ln for ln in second_body.splitlines() if "sha256" in ln))
|
|
|
|
check("no digest is recorded for a file that is not here", not (rec - disk),
|
|
f"recorded but absent: {sorted(rec - disk)}" if rec - disk else "")
|
|
|
|
# And the digests are the CURRENT bytes, not a stale record that
|
|
# happens to name the right files.
|
|
stale = [rel for rel, full in siblings()
|
|
if digest(full) not in open(PROVENANCE, encoding="utf-8").read()]
|
|
check("every recorded digest is the file's current bytes", not stale,
|
|
f"stale: {stale}" if stale else "")
|
|
|
|
print("vendor-editions self-test (positive control)")
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(self_test() if "--self-test" in sys.argv else refresh())
|