9 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 5.5 1857c6405e fix(tooling): body_definition_parser no longer configures logging on import
A module-level logging.basicConfig(level=INFO) ran whenever any reach verb
imported the parser. That configured root logging process-wide and wrote plain
text to stderr, breaking reach's contract that stderr carries only JSONL events
and one verdict. A dry run of bake-biome emitted 205 KB this way, most of it a
line per body of every parsed system.

Its eight log calls now go through console.event. Per-body and per-system lines
become debug, visible only with verbose output. The four warnings stay warn, as
structured events: 66 of them across the bake scope, each an authored orbit
that contradicts the body's planet class ("too hot for temperate").
Those are real data findings and stay visible. A dry run is now two lines
plus those warnings.

Also: tooling/core/command.py carried the same two-line comment twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:55:53 +02:00
jpmschweitzerandClaude Opus 5.5 76e3f3ec96 feat(assets): biome and terrain as two maps, and the bake that writes them (T-1295)
D-258 rulings 5 and 6: the pre-seed biome input is class ids only, on two
maps, because compute_biome was already computing them apart and throwing
one away. It gave every land pixel its Whittaker (climate) class, then painted
terrain over it (ocean bands, ice, altitude snow, mountain rock, lava and ash,
the dry, lunar and ferric ground).

planet_simulation:
- compute_biome_layers returns (biome, terrain). Each rule declares the layer
  it writes, by RULE not by class: ice from the cold climate box is biome,
  altitude snow is terrain with the lowland biome kept beneath. Rules that
  place life (thermophiles, mats, crust, the oasis rings) write the biome and
  clear the terrain under them.
- 255 means "nothing on this layer", not 0, because class 0 is ocean_deep,
  itself terrain. Ids stay literal.
- compute_biome is now compose_layers() of the pair, so the renderers are
  untouched. Proven byte-identical, dtype included, on 65 real bodies: every
  8th standard-atmosphere body plus every thin, thick, reducing and trace one,
  captured before the edit and compared after it.
- simulate() carries both maps as biome_layer / terrain_layer.

reach atlas planet bake-biome [--body --limit --dry-run --check]:
- Writes biomemap.png + terrainmap.png (8-bit, 1024x512, tEXt: layer, none,
  classes, decision) beside each heightmap. Scope is the heightmap bake set
  with an atmosphere (257 bodies; airless are deferred, per D-258).
- Pre-seed by construction: simulate() is keyed on the body frontmatter's
  seed, and no world seed is accepted anywhere.
- --check re-simulates and compares DECODED PIXELS, never bytes, since PNG
  encoding drifts across Pillow/zlib versions (T-1291) and a check that trips
  on that gets muted. Mutation-proven: one flipped pixel exits 1 and names
  the file; the restored file exits 0.
- import_heightmaps' body lookup is extracted as body_def_for and shared,
  not copied.

Tests (make test-tooling): rock keeps its biome beneath, thin-atmosphere
ground is terrain with only scattered life, water is terrain only, no pixel is
empty on both maps, and stacking equals the rendered grid. Routing rock
through the biome map fails two of them by name. The router drift test covers
the new verb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:55:47 +02:00
jpmschweitzerandClaude Opus 5.5 3cfec9025d fix(assets): mountain rock could never fire — kelvin compared to 0.35 (T-1295)
compute_biome receives absolute kelvin (the Whittaker table is kelvin; the
0-1 normalisation happens afterwards in simulate(), for the renderer only),
but two terrain rules compared it to 0.35. That was the case from the
generator's first commit (f84275edf):

- mountain rock, `temperature < 0.35`: never true, so the class never
  appeared through this rule on any body;
- sulfuric scrub, `temperature > 0.35`: always true.

0.35 can only mean a fraction of the world's own range, the normalisation
compute_moisture already uses, so both rules now compare against that. A
world with no temperature range reads 0 (the cold end).

tooling/test_planet_biome_rules.py pins the rule both ways on a synthetic
body: cold high ground turns to rock (>90%, since anomaly scatter repaints
~3%), and warm high ground and low ground do not. Mutation-proven: restoring
the kelvin comparison fails it at a rock share of exactly 0.000. Wired into
make test-tooling.

Sulfuric scrub remains unreachable for a different reason: it converts rock
only at elev_norm 0.25-0.65, and rock exists only above 0.65. It is left as
found, since making it reachable is a design call and belongs to the balancing
pass, so it is not pinned.

Found while shaping the D-258 biome bake. The committed reliefmaps and globes
were rendered with the bug, and the coming bake reflects the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:03:37 +02:00
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
jpmschweitzerandClaude Opus 5.5 6fb0ba0e3d chore(tooling): T-1272 + T-1274 — close E3: no hyphens left, and the lint ignores come off
T-1272 (a verification, as rescoped). No directory Python imports carries a
hyphen any more. The hyphenated script trees were emptied by the per-domain
moves, not renamed. What still has a hyphen is never imported: the three Rust
crates, and the provenance under tooling/archive/, which has no __init__.py.
CLAUDE.md and DEVOPS still pointed at tooling/db/, and pyproject still
predicted the rename; all three fixed.

T-1274. E402, E702 and F841 were ignored for the whole tree from T-1066 on
(43 / 41 / 21 violations). All three are back on:

- E402: the planet modules' imports only sat below their path constants
  because they used to follow a sys.path insert, gone since T-1288. Hoisted.
  The Blender payloads keep a per-file exception, because they extend
  sys.path under Blender's own Python.
- E702: the paired component assignments in three planet maths files are
  deliberate, so they get a per-file exception scoped to those files.
- F841: 10 dead locals removed from live code, each checked for side effects
  first; logo_uv keeps its call, which creates the UV layer.
- tooling/archive/ is excluded: it is provenance, and "fixing" a one-shot
  falsifies the record of what actually ran.

Evidence the lint is real: violations fed through stdin fire in a domain
module, and E402 stays quiet only on a payload path. Evidence nothing moved:
globe renders are pixel-identical before and after for an oceanic, a frozen
and a gas-giant body, and the ledger edit was regenerated (stamp fresh,
generated_brands.toml unchanged).

One finding, noted in the code rather than fixed: planet_renderer computed an
oblate-spheroid ray scale and never used it, so `oblateness` shapes no globe.
Wiring it in would change every globe render; that is a decision to make
deliberately, not a lint fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:05:07 +02:00
jpmschweitzerandClaude Opus 5.5 59b3fc4caa refactor(tooling): T-1293 — atlas map, and the generator that must not run
The star-map family was the last unported part of the tree, and it never had
a ticket. Two of its scripts become `reach atlas map` verbs, nested under
atlas like planet (D-243: the Reach map is the ladder's top rung):

- `reach atlas map data [--check]` regenerates client/data/star_map_data.json.
  The regenerated file differs by one line: `_meta.note`, which named the
  old script's path.
- `reach atlas map svg` renders the concentric SVG (+ PNG), byte-identical
  to the old script's output on the same data.

make check-star-map and star-map-data stay as one-line delegates, because
pre-pr-client and pre-pr-validate depend on check-star-map.

generate-star-map.py, its seed, sculpt-star-map.py and tune-star-map-topology.py
are archived, not ported. The generator rewrites docs/design/star-map.json
unconditionally from an S-NNN-keyed seed, so re-running it would erase the GJ
migration and every hand edit since; sculpt and tune only understand S-NNN
edges. .claude/rules/diagrams.md was telling agents to "edit the generator
and re-run it". It now distinguishes the live concentric render, the seven
frozen S-keyed sector .d2 files (T-1294), and the two SVGs that never had a
generator in the repo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:00:05 +02:00
jpmschweitzerandClaude Opus 5.5 668772075c refactor(tooling): T-1288 — planet-gen becomes reach atlas planet
The 30-file tree moves under atlas as its third rung (D-243), ten verbs
fronting it. Each verb restates its module's options so `--help` describes
something; tooling/test_planet_router.py hands every declared option to the
module's own argparse and fails on drift, and now runs in make test-tooling.

The 2026-09-02 half of this move had converted the top-level imports and the
repo roots. Finishing it found what the half-move left:

- Lazy in-function imports, and all of sol_data/, still named siblings bare.
  They resolved only through sys.path.insert hacks, so under reach the first
  globe render in generate, batch or sol-import would have raised
  ModuleNotFoundError. Qualified; the hacks are gone.
- 247 print() calls and a stdout progress writer that fired once per 8 KB
  block. Report verbs (audit, quality) write through console.out, progress
  through console.event, and download progress is throttled to 10% steps
  so a job log is not tens of thousands of lines.
- Every error exit raises ReachError with a fix.

Two checks that could not fail:

- batch --verify-determinism printed a warning and exited 0 on a mismatch.
- import-provinces exited 0 with errors > 0.

Both now raise. The 271-body bake is only safe to re-run because the first
one holds.

sol-import --body is action="append" in the module but the router took one
value, so --body GJ0d --body GJ0e kept one. Now repeatable, and _flags repeats
list options.

test_conformance walked one level, so a nested group was reported as a verb
missing @command and its ten verbs were never checked. It recurses now;
proven by stripping @command from `planet quality` and watching it fail.

Stray PNGs from the 2026-09-03 runaway router-test run are parked in
.cache/t1288-stray-pngs/, not committed. Their reliefmaps differ from HEAD
while the heightmap regenerated byte-identical — filed as T-1291.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:08:02 +02:00
jpmschweitzerandClaude Opus 5 338644b409 refactor(tooling): T-1286 — generate, pr and dev become reach domains
Twelve scripts retired, three domains registered. `reach` now covers nine.

generate: `generate-brands` and `generate-corporations` were the second and
third copies of the same 24-line build-if-missing-then-exec bash `tooling/atlas`
carried, so they collapsed into `core.process.cargo_binary` rather than being
ported. `import_economics` shelled out to the first of those, so it now calls
that helper — `generated_brands.toml` comes back byte-identical, and the stamp
registry swaps the retired wrapper for `core/process.py`.

pr: `watchlist-diff` derives its watched set from `generator_sources.py` instead
of restating it, so it cannot drift from the stamp check.

dev: the environment scripts split decision from performing, per D-263's
guarded-exec rule. `godot_plan()` and `worktree_plan()` decide what would
happen; `install_godot()`, `install_rust()` and `setup_worktree()` do it.
`tooling/test_environment.py` pins the version pin, both override precedences,
the already-current skip, the platform refusal and both worktree refusals —
none of them performed. `make setup` now installs reach first, since the
targets that install rust and godot are reach verbs.

Two live bugs found while porting:

- The clerk read its decision index from `decisions/README.md`, a path that
  stopped existing when the DQR tree moved to `governance/`. Every clerk agent
  has been grepping blind; its prompt pointed at the same dead directory.
- The conformance exec-check matched any `x.system()` regardless of receiver,
  so `platform.system()` read as `os.system()`. Narrowed and re-proved against
  a real mutant.

`process.run` gains `input=`, `timeout=` and a `ProcessTimeout` subclass so a
killed run stays distinguishable from a verdict. The pre-push hook no longer
merges the clerk's stderr into its stdout — under streaming the last merged
line is a JSONL event, which would read as an unrecognised verdict and block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:00:46 +02:00
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