Files
jpmschweitzerandClaude Opus 5 b1b57d603f feat(config): T-1285 — the atlas authoring verbs, and a verify nobody checked
reach atlas db / names / systems-done / check / verify / commit-and-sync /
update-field / flatness. Eight scripts retired, five of them bash. verify
reproduces the original exactly: 2 errors across 301 proposals, exit 1.

The binary has more verbs than its wrapper documented. The bash usage text
listed four; atlas-commit-and-sync calls four more it never mentioned. All
eight are declared so reach atlas --help is a complete index, and unknown verbs
are still forwarded — a hand-maintained list falls behind the binary it
describes, so rejecting on it would break the day someone adds a subcommand.

commit-and-sync now stages by default and commits only with --commit. Nothing
else in reach writes to git history, and committing as a side effect of "sync"
is a different risk class from writing a file; the default prints the message
it would use, leaving the decision where it was.

Two real bugs found in that script while porting it. It ran atlas-verify and
never checked the exit code, so a proposal that FAILED verification was still
wiped, committed and synced — bad data in systems.db is far harder to undo than
a failed command, and it now refuses. And it hardcoded a pinned
"Co-Authored-By: Claude Opus 4.6" into every atlas commit, which the git-commit
skill names as the root cause of attribution drift.

update-field gains two guards the original lacked. Its field→table map lived
inside a bash heredoc string where nothing could check it, and an unknown field
produced an UPDATE against a table of None; it now names the nine accepted
fields. And it checks rowcount, so a system_id that does not exist is a failure
rather than a silent no-op reported as success.

The three Python scripts moved with the usual treatment — prints to console
events, argparse replaced by typed functions, __file__ roots to
config.repo_root(). No root bug this time: checked before moving rather than
after, three domains running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 14:36:51 +02:00

121 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Measure how much STRUCTURE an Atlas capture carries, per rung.
"Flat" is the word T-1213/D-258 use for the defect the descent ladder exposes,
but a word cannot be an acceptance gate and an eyeball cannot be a regression
test. This turns the ladder into numbers.
WHY NOT JUST COUNT COLOURS
--------------------------
Because the count goes the wrong way. Measured on the 2026-08-16 cold ladder:
rung distinct lum p1-p99
Global 1581 145.69
Region 2923 33.59
District 53 13.72
Quarter 46 11.01
Region carries almost TWICE Global's distinct-colour count while holding a
quarter of its structure — that is the dither/stipple pass (T-1194) adding colour
noise, not information. A metric that rewards speckle would have called the
flattest rung the richest. So the headline number here is the 1st-99th percentile
luminance spread, which ignores per-pixel noise and measures the large-scale
variation a map is actually read for; distinct-count is reported alongside
precisely so the two can be seen disagreeing.
Usage:
tooling/atlas-flatness .cache/screenshots/atlas_GJ820Bc_land_Region.png [...]
tooling/atlas-flatness --ladder # the standard descent ladder
"""
import sys
from pathlib import Path
from tooling.core import config, console
from tooling.core.errors import ReachError
try:
from PIL import Image
except ImportError: # pragma: no cover - environment guard
console.event("atlas-flatness: Pillow not installed", level="error")
sys.exit(2)
REPO_ROOT = config.repo_root()
SHOTS = REPO_ROOT / ".cache" / "screenshots"
# The standard descent ladder: one body, one world point, once per rung.
LADDER = [
("Global", "atlas_GJ820Bc_Global.png"),
("Region", "atlas_GJ820Bc_land_Region.png"),
("District", "atlas_GJ820Bc_land_District.png"),
("Quarter", "atlas_GJ820Bc_land_Quarter.png"),
]
# Terrain-only crop. The header/legend panels sit top-left and the overlay chips
# top-right; both are flat UI fills that would drag every statistic toward
# whatever the panel colour happens to be, and they do not vary with the rung.
CROP_LEFT = 700
CROP_TOP = 120
def luminance(px) -> float:
r, g, b = px[:3]
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def measure(path: Path) -> dict:
img = Image.open(path).convert("RGB")
w, h = img.size
if w <= CROP_LEFT or h <= CROP_TOP:
raise ValueError(f"{path.name} is {w}x{h}, smaller than the UI crop")
img = img.crop((CROP_LEFT, CROP_TOP, w, h))
# get_flattened_data() is the Pillow 12+ name; getdata() is deprecated there
# and removed in 14, but is all that older Pillows have.
reader = getattr(img, "get_flattened_data", None) or img.getdata
pixels = list(reader())
stats = {}
for name, ch in zip("RGB", zip(*pixels)):
n = len(ch)
mean = sum(ch) / n
stats[name] = (sum((v - mean) ** 2 for v in ch) / n) ** 0.5
lums = sorted(luminance(p) for p in pixels)
n = len(lums)
return {
"distinct": len(set(pixels)),
"std": stats,
"spread": lums[int(n * 0.99)] - lums[int(n * 0.01)],
}
def report(images: list[Path], ladder: bool = False) -> int:
"""Measure capture structure per rung. Returns 0, or 1 if any file is missing."""
targets = []
if ladder:
targets = [(label, SHOTS / name) for label, name in LADDER]
targets += [(p.stem, p) for p in images]
if not targets:
raise ReachError(
"no captures given",
fix="pass image paths, or --ladder to measure the standard descent",
exit_code=2,
)
console.out(
f"{'rung':22} {'distinct':>9} {'R std':>7} {'G std':>7} {'B std':>7} {'lum p1-p99':>11}"
)
missing = 0
for label, path in targets:
if not path.exists():
console.out(f"{label:22} MISSING {path}")
missing += 1
continue
m = measure(path)
r, g, b = (m["std"][c] for c in "RGB")
console.out(
f"{label:22} {m['distinct']:>9} {r:>7.2f} {g:>7.2f} {b:>7.2f} {m['spread']:>11.2f}"
)
return 1 if missing else 0