chore(re): stop committing game assets; captures stay local

Issue #49: the repos carry code, tooling and docs only.

Untracks 143 screenshots, 3 savegame blobs and `tools/re-capture/ob_digits.png`
(a digit-template sheet cut from game frames) -- 146 files, 76.4 MB. They stay
in the working tree and are gitignored, so the pages' relative links still
resolve where the captures exist and nothing ships.

Derived measurements (csv/tsv/txt/log/json/jsonl/npy) are our own numbers, not
game content, and stay tracked -- they are what most claims rest on.

`check-capture-citations` had its contract inverted, and it is the half worth
reading:

  * presence now comes from the WORKING TREE, not `git ls-files`. The assets are
    deliberately untracked, so asking the index would report every screenshot as
    missing and fail all 203 citations.
  * a NEW failure: a game asset that IS tracked. A screenshot that sneaks back
    in is invisible in review -- a binary shows as "Bin 0 -> 1234567 bytes" --
    and is permanent once merged, since removing it later needs a history
    rewrite. So that half has to be loud.

Verified:
  * selftest 8/8, including the new rule
  * scan: 212 present, 212 cited, 0 dangling, 0 tracked  -> exit 0
  * force-add one PNG -> "game assets TRACKED: 1", exit 1, selftest red

⚠️ This does NOT remove the blobs from history; a clone still fetches them.
That needs a filter-repo rewrite and a force-push, which is a separate,
human-run step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-19 21:03:46 +02:00
parent 4c1e15b818
commit d96b225e47
4 changed files with 99 additions and 11 deletions

View File

@@ -18,6 +18,22 @@ A wrong-but-confident note is worse than no note: someone builds on it and the b
for weeks. Every entry therefore carries an explicit **confidence** and its **evidence**. for weeks. Every entry therefore carries an explicit **confidence** and its **evidence**.
This mirrors the project method — *measure the oracle, never infer; refute before believing.* This mirrors the project method — *measure the oracle, never infer; refute before believing.*
### Captures are local-only (issue #49)
The repository carries **code, tooling and docs**. Screenshots and savegame
blobs are game-derived, so since 2026-09-19 they live in `docs/re/captures/`
on disk and are **gitignored** — the pages' relative links still resolve on a
machine that has them, and nothing ships.
Derived measurements (`csv`, `tsv`, `txt`, `log`, `json`, `jsonl`, `npy`) are
our own numbers rather than game content, and stay tracked — they are what most
claims here actually rest on.
`tools/re/check-capture-citations` enforces both halves: a cited capture must be
**present**, and a game asset must **not be tracked**. ⚠️ A fresh clone has no
captures, so its citations will not resolve until the captures are copied in;
that is expected, and the checker is a local gate rather than a CI one.
### Clean-room firewall ### Clean-room firewall
- ✅ Allowed: behaviour descriptions, field offsets/types, formulas, state machines, - ✅ Allowed: behaviour descriptions, field offsets/types, formulas, state machines,

13
docs/re/captures/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Game-derived assets stay on disk and ship nowhere (issue #49).
#
# The pages' relative links still resolve on a machine that has the captures,
# so the evidence stays followable where it exists — it is simply not committed.
# `tools/re/check-capture-citations` enforces both halves: a cited capture must
# be PRESENT here, and an asset must NOT be tracked.
#
# Derived measurements (csv, tsv, txt, log, json, jsonl, npy) are our own
# numbers rather than game content, and remain tracked.
*.png
*.jpg
*.jpeg
*.bin

3
tools/re-capture/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Game-derived digit templates for ob_read.py — pixels from the game (issue #49).
ob_digits.png

View File

@@ -10,10 +10,17 @@
largest thing in the repository, and the one place a file can be added, never largest thing in the repository, and the one place a file can be added, never
cited, and never noticed. cited, and never noticed.
Two failures, which are opposites and must not be conflated: ⚠️ 2026-09-19, issue #49: **game assets are no longer committed.** Screenshots
and savegame blobs live in the working tree and are gitignored, so the pages'
relative links still resolve on a machine that has them while nothing ships.
That inverts half of this check — "is it committed?" became "is it PRESENT?",
and a NEW failure appeared: an asset that IS tracked. Both are below.
* a page cites a capture that **is not committed** — a reader following it Three failures, which are opposites and must not be conflated:
* a page cites a capture that **is not present** — a reader following it
gets nothing. That is an error, exactly as in `check-citations`. gets nothing. That is an error, exactly as in `check-citations`.
* a game asset that **is tracked by git** — issue #49 says it must not be.
* a capture that **no page cites** — not an error. It may be evidence a page * a capture that **no page cites** — not an error. It may be evidence a page
should have cited, and deleting on that basis would silently ratify the should have cited, and deleting on that basis would silently ratify the
omission. Reported, counted, never failed on. omission. Reported, counted, never failed on.
@@ -65,7 +72,16 @@ def committed() -> tuple[set[str], set[str]]:
proposed. `tools/port/check-citations` uses the working tree for exactly proposed. `tools/port/check-citations` uses the working tree for exactly
this reason; so does this. this reason; so does this.
""" """
files = {l for l in git("ls-files", ROOT).splitlines() if l} # 🔴 NOT `git ls-files`: since #49 the assets are deliberately untracked, so
# the index no longer knows they exist. Presence is a question about the
# working tree, and asking git would report every screenshot as missing and
# fail on all 203 citations.
files = set()
for dirpath, _dirnames, filenames in os.walk(ROOT):
for fn in filenames:
if fn == ".gitignore":
continue
files.add(os.path.join(dirpath, fn).replace(os.sep, "/"))
dirs = set() dirs = set()
for f in files: for f in files:
parts = f.split("/") parts = f.split("/")
@@ -74,6 +90,24 @@ def committed() -> tuple[set[str], set[str]]:
return files, dirs return files, dirs
ASSET_SUFFIXES = (".png", ".jpg", ".jpeg", ".bin")
def tracked_assets() -> list[str]:
"""Game assets that are committed — forbidden since issue #49.
The repo carries code, tooling and docs. A screenshot that sneaks back in
is invisible in review (a binary shows as "Bin 0 -> 1234567 bytes") and is
permanent once merged, because removing it later needs a history rewrite.
So this is the half of the check that has to be loud.
"""
out = []
for l in git("ls-files").splitlines():
if l.lower().endswith(ASSET_SUFFIXES):
out.append(l)
return out
def cited() -> set[str]: def cited() -> set[str]:
raw = git("grep", "-rhoE", CITE_ERE, "--", *SEARCH, SELF).splitlines() raw = git("grep", "-rhoE", CITE_ERE, "--", *SEARCH, SELF).splitlines()
out = set() out = set()
@@ -141,7 +175,7 @@ def selftest() -> int:
real_dir = next(iter(dirs), None) real_dir = next(iter(dirs), None)
real_file = next(iter(files), None) real_file = next(iter(files), None)
if not real_dir or not real_file: if not real_dir or not real_file:
print("selftest: 🔴 no captures committed — nothing to test against") print("selftest: 🔴 no captures present — nothing to test against")
return 2 return 2
cases = [ cases = [
("planted dangling caught", "docs/re/captures/no-such-file-anywhere.png", False), ("planted dangling caught", "docs/re/captures/no-such-file-anywhere.png", False),
@@ -168,7 +202,15 @@ def selftest() -> int:
print(" %-34s %s" print(" %-34s %s"
% ("own fixtures not counted", "🔴 FAILED" if fixture_counted else "ok")) % ("own fixtures not counted", "🔴 FAILED" if fixture_counted else "ok"))
ok = gathering_ok and not fixture_counted # 🔴 THE #49 RULE NEEDS ITS OWN TICK. Presence now comes from the working
# tree, so a checkout with the captures present looks identical whether or
# not they are tracked — only this assertion can tell the difference.
stowaways = tracked_assets()
print(" %-34s %s%s"
% ("no game asset tracked", "ok" if not stowaways else "🔴 FAILED",
"" if not stowaways else " (%d tracked)" % len(stowaways)))
ok = gathering_ok and not fixture_counted and not stowaways
for name, path, want in cases: for name, path, want in cases:
got = resolves(path.rstrip(".,;:)`"), files, dirs) got = resolves(path.rstrip(".,;:)`"), files, dirs)
mark = "ok" if got == want else "🔴 FAILED" mark = "ok" if got == want else "🔴 FAILED"
@@ -188,19 +230,33 @@ def main() -> int:
print(f) print(f)
return 0 return 0
print("captures committed : %d" % len(files)) stowaways = tracked_assets()
print("captures present on disk : %d" % len(files))
print(" cited by a page or a tool : %d" % (len(files) - len(orphans))) print(" cited by a page or a tool : %d" % (len(files) - len(orphans)))
print(" cited by nothing : %d (reported, not failed —" % len(orphans)) print(" cited by nothing : %d (reported, not failed —" % len(orphans))
print(" an orphan may be evidence a page owes)") print(" an orphan may be evidence a page owes)")
rc = 0
if dangling: if dangling:
print(" 🔴 cited but NOT committed: %d" % len(dangling)) print(" 🔴 cited but NOT present : %d" % len(dangling))
for d in dangling: for d in dangling:
print(" %s" % d) print(" %s" % d)
print("\n🔴 a reader following those gets nothing. Commit the capture, fix the") print("\n🔴 a reader following those gets nothing. Restore the capture, fix the")
print(" path, or drop the citation.") print(" path, or drop the citation.")
return 1 rc = 1
print(" 🔴 cited but NOT committed: 0") else:
return 0 print(" 🔴 cited but NOT present : 0")
if stowaways:
print(" 🔴 game assets TRACKED : %d (issue #49 — code, tooling, docs only)" % len(stowaways))
for a in stowaways[:10]:
print(" %s" % a)
if len(stowaways) > 10:
print(" … and %d more" % (len(stowaways) - 10))
print("\n🔴 run `git rm --cached` on those; they stay on disk and stay ignored.")
rc = 1
else:
print(" 🔴 game assets TRACKED : 0")
return rc
if __name__ == "__main__": if __name__ == "__main__":