From 70583b9b2f6486127f5b811f407e995997ba4444 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Fri, 25 Sep 2026 04:32:16 -0400 Subject: [PATCH] Fail CI when the manual shows a picture no scene makes The traceability job now runs `tools/manual/record.sh --check`: every picture docs/manual/README.md shows must be made by a scene in tools/manual/scenes.py, and every picture a scene makes must be shown. It reads the two files and nothing else, so it needs no app, display or LFS pull. --changed now dates a scene by the newest commit among its pictures rather than each picture alone. A scene that also makes a picture which re-records byte for byte (panorama-aligned beside panorama.gif) no longer stays listed for ever. A scene all of whose pictures come out identical (launch) stays listed until one differs, which costs one harmless re-run. --- .gitea/workflows/traceability-check.yml | 6 +++++ tools/manual/scenes.py | 34 ++++++++++++------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/.gitea/workflows/traceability-check.yml b/.gitea/workflows/traceability-check.yml index 2ee35b1..9fe4cbc 100644 --- a/.gitea/workflows/traceability-check.yml +++ b/.gitea/workflows/traceability-check.yml @@ -67,6 +67,12 @@ jobs: # threshold: zero requirements parsed, zero files scanned, a ratio above # 100%, or any orphan tag all fail the build. A misconfigured run must not # report a plausible-looking 0%. + # Every picture the manual shows is made by a scene in + # tools/manual/scenes.py, and every picture a scene makes is shown. + # Two files read; no app, no display. + - name: Manual pictures have scenes + run: tools/manual/record.sh --check + - name: Traceability gate run: cargo run -q -p traceability -- check diff --git a/tools/manual/scenes.py b/tools/manual/scenes.py index d7467d7..b848265 100755 --- a/tools/manual/scenes.py +++ b/tools/manual/scenes.py @@ -1197,28 +1197,28 @@ def own_code_changed(s, since): def changed(): - """The scenes whose pictures are older than their sources: for each - picture, the commit that last touched it, and whether any source — or + """The scenes whose pictures are older than their sources: the commit + that last touched any of a scene's pictures, and whether a source — or the scene's own code — has changed since (the working tree included). - A picture never committed is new, and so is its scene.""" + A scene none of whose pictures was ever committed is new. + + A re-recording that comes out byte-identical touches no picture, so its + scene stays listed until one of its pictures really changes; re-running + it is harmless.""" out = [] for s in SCENES.values(): - pics = [m for m in s.media if '.' in m] + pics = [f'{MEDIA}/{m}' for m in s.media if '.' in m] if not pics: continue - reasons = [] - for m in pics: - since = git('log', '-1', '--format=%H', '--', f'{MEDIA}/{m}').strip() - if not since: - reasons.append(f'media/{m} was never committed') - continue - paths = git('diff', '--name-only', since, '--', *[f':(glob){g}' for g in s.sources]).split() - if paths: - reasons.append(f'{", ".join(sorted(set(paths))[:3])} changed since media/{m}') - elif own_code_changed(s, since): - reasons.append(f'its scene changed since media/{m}') - if reasons: - out.append((s.name, reasons[0])) + since = git('log', '-1', '--format=%H', '--', *pics).strip() + if not since: + out.append((s.name, 'its pictures were never committed')) + continue + paths = git('diff', '--name-only', since, '--', *[f':(glob){g}' for g in s.sources]).split() + if paths: + out.append((s.name, f'{", ".join(sorted(set(paths))[:3])} changed since {since[:9]}')) + elif own_code_changed(s, since): + out.append((s.name, f'its scene changed since {since[:9]}')) return out