A script scanned every tracked doc, rule, skill, agent, hook and source file for tooling/ paths that no longer exist, skipping historical records (sprints, discussions, workshops, governance, generated wiki pages). It found 62. The ones that tell a reader what to RUN now name the reach verb: - The atlas skill still sent agents to tooling/atlas, atlas-verify, atlas-update-field and atlas-commit-and-sync — about forty lines, all retired in T-1285. They now name the `reach atlas` verbs, and the skill records that commit-and-sync STAGES by default (--commit to commit) and takes --corridor as an option. - The clerk agent named tooling/clerk-review (now `reach dev clerk`). The Si and clerk briefings sent those agents to the retired tooling/db/decision and sqlite-query CLIs and to decisions/*.md paths that moved to governance/ in the pql migration. They now name pql. - The ticket-cli rule documented `pql decisions read`, which does not exist; `show` already includes the body. - The culture authoring guide and the RON sources name `reach validate ron`, with the same arguments as before. - The 41 Blender payloads' usage lines ran the retired tooling/blender wrapper, and the docstrings still cited pre-carve-out paths. They now read `reach blender run <payload>`. - Doc comments in server/, client/, wiki TOMLs and the domain modules. What is left is deliberate: "Formerly …" provenance, dated plans and findings docs, the retired-pipeline doc, and a build-artefact path. project.yaml 0.4.14 (mirrored to the client). Comment-only, but four touched files are in the canvas-version registry (trait_catalog_reader.rs, since T-1289, canvas_sources.py itself, and two client files). The gate is path-based and has no override. The previous push was rejected on exactly this. Three of the edits are stamped ledger sources, so systems.db is regenerated and the stamp is fresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
4.1 KiB
Python
Executable File
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:
|
|
reach atlas flatness .cache/screenshots/atlas_GJ820Bc_land_Region.png [...]
|
|
reach 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
|