Files
settled-reach/tooling/canvas_sources.py
T
jpmschweitzerandClaude Opus 5.5 c597ec9131 docs(tooling): T-1253 — sweep the live references to retired tool paths
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>
2026-09-23 20:13:58 +02:00

176 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""
canvas_sources — single source of truth for the Atlas canvas-generation path set.
`project.yaml`'s `version:` is the Atlas disk cache's ONLY invalidation signal
(client/scripts/build_version.gd, D-255). A change to how a canvas is GENERATED
is therefore only half a change; the other half is bumping that version, or every
warm cache keeps serving canvases built by code that no longer exists.
Nothing enforced that pairing, and it broke five times: project.yaml's own
comments record 0.4.2 (lake_margin_q semantics), 0.4.3 (coast_warp_px at orbital
sampling), 0.4.4 (the D-255 extent inversion), 0.4.5 (the Global sentinel), and
0.4.6 (T-1237 one-course-per-river) — every one of them bumped *after the fact*,
the last only after T-1239 spent eight days diagnosing a map drawn from a canvas
whose generating code had been replaced. The failure is invisible to its author:
it reproduces only where a warm cache exists, so a cold checkout looks fine.
This module is the registry `reach check canvas-version` intersects against,
kept here rather than inline in the hook for the same reason
`tooling/generator_sources.py` exists (T-1067): one list, one place, imported by
everything that needs it.
WHY THIS DELIBERATELY OVER-INCLUDES
-----------------------------------
The set is collected by GLOB, not hand-listed, and covers the whole atlas module
rather than a traced dependency closure.
`step_canvas.rs` directly imports ten sibling modules and those pull in more
(district_profile -> domain_warp/detail_scatter/coast_invention/..., layer1 ->
drainage/hydrology_equilibrium/features). A hand-maintained closure of that would
be wrong within a month, and being wrong here is silent — exactly the failure
this registry exists to stop. Globbing is self-maintaining: a module added in a
future split is covered the moment it exists.
The cost asymmetry is the whole argument, and it is the ticket's own ruling
(T-1242): a false positive costs one version bump and one round of cache misses;
a false negative costs another week of a wrong map. So when the choice is
"include a file that might not change canvas bytes" versus "risk missing one that
does", this includes it.
Fail closed on an empty glob, per generator_sources.py's precedent: an empty set
would silently pass every push.
Usage:
python3 tooling/canvas_sources.py --list
"""
import argparse
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
# ---------------------------------------------------------------------------
# Server: the code that PRODUCES canvas bytes
# ---------------------------------------------------------------------------
# The whole atlas module. Canvas bytes are produced by step_canvas.rs out of
# layer1/TerrainAnalysis/district_profile/river_course/hydrology and a long tail
# of invention modules; see this file's header for why the boundary is the
# module rather than a traced import closure.
ATLAS_DIR: Path = REPO_ROOT / "server" / "src" / "atlas"
# The seed chain feeds every deterministic decision the cascade makes
# (step_canvas.rs: `use crate::seed::SeedChain`), so a change to seed derivation
# changes canvas bytes without touching atlas/ at all.
SEED_RS: Path = REPO_ROOT / "server" / "src" / "seed.rs"
# ---------------------------------------------------------------------------
# Client: the code that KEYS, STORES and INTERPRETS those bytes
# ---------------------------------------------------------------------------
# The step_canvas cluster: the disk cache and its index format, the in-memory
# cache, the request/transport layer that derives extents and spacing (and
# therefore cache KEYS), and the layers that decode the payload.
#
# The render-only members (terrain/annotation layers) are included on purpose.
# A pure draw change cannot make cached bytes wrong, so including them can force
# an unnecessary re-derive — but drawing the line *inside* this directory means
# hand-judging which file is "really" wire-shaped, and T-1237 changed the server
# course shape and the client annotation layer in the same commit. That judgement
# is precisely where a false negative would come from.
CLIENT_STEP_CANVAS_DIR: Path = (
REPO_ROOT / "client" / "ui" / "implant" / "apps" / "atlas" / "step_canvas"
)
# The accessor the cache reads its invalidation tag through.
BUILD_VERSION_GD: Path = REPO_ROOT / "client" / "scripts" / "build_version.gd"
# The wire codec, which decides which planes EXIST client-side.
#
# Added 2026-08-16 after T-1213 proved the omission expensive: this file's decode
# dictionary was missing `relief_q`, so the plane never reached the renderer and
# the deep rungs rendered flat for weeks. The gate would not have flagged the fix,
# because the registry only covered ui/.../step_canvas/ and this lives under
# scripts/protocol/.
#
# It belongs here for a second, sharper reason: the disk cache stores the DECODED
# canvas, so a decode change alters what a cached entry contains. Entries written
# before that fix have no relief_q key at all and keep rendering flat until the
# version moves — exactly the stale-cache class this registry exists to catch.
STEP_CANVAS_PROTOCOL_GD: Path = (
REPO_ROOT / "client" / "scripts" / "protocol" / "step_canvas_protocol.gd"
)
def _rust_sources(directory: Path, label: str) -> tuple[Path, ...]:
"""Every .rs file in `directory`, collected by glob and fail-closed."""
sources = tuple(sorted(directory.rglob("*.rs")))
if not sources:
raise RuntimeError(
f"{label} sources not found at {directory} — the canvas-generation "
"path set would be incomplete, and this check would pass every push"
)
return sources
def _gdscript_sources(directory: Path, label: str) -> tuple[Path, ...]:
"""Every .gd file in `directory`, collected by glob and fail-closed.
`.uid` sidecars are Godot bookkeeping and carry no behaviour, so they are
excluded — a uid churn should not demand a version bump.
"""
sources = tuple(sorted(directory.rglob("*.gd")))
if not sources:
raise RuntimeError(
f"{label} sources not found at {directory} — the canvas-generation "
"path set would be incomplete, and this check would pass every push"
)
return sources
def canvas_sources() -> tuple[Path, ...]:
"""Every file whose change may alter canvas bytes or their interpretation.
This registry itself is a member: loosening the set must be as visible as
any other canvas-generation change (generator_sources.py makes the same
call for the same reason).
"""
return (
Path(__file__).resolve(),
*_rust_sources(ATLAS_DIR, "server atlas"),
SEED_RS,
*_gdscript_sources(CLIENT_STEP_CANVAS_DIR, "client step_canvas"),
BUILD_VERSION_GD,
STEP_CANVAS_PROTOCOL_GD,
)
def relative_paths() -> tuple[str, ...]:
"""The registry as repo-relative POSIX paths, for matching git output."""
return tuple(p.relative_to(REPO_ROOT).as_posix() for p in canvas_sources())
def main() -> None:
parser = argparse.ArgumentParser(
description="Single source of truth for the Atlas canvas-generation path set"
)
parser.add_argument(
"--list",
action="store_true",
help="Print the canvas-generation paths, one repo-relative path per line",
)
args = parser.parse_args()
if not args.list:
parser.print_help()
sys.exit(2)
for path in relative_paths():
print(path)
if __name__ == "__main__":
main()