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.
This commit is contained in:
2026-09-25 07:26:37 -04:00
parent f426bb903a
commit 70583b9b2f
2 changed files with 23 additions and 17 deletions
+6
View File
@@ -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
+17 -17
View File
@@ -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