`reach character {logo, strip-glb, qa, qa-analyze}` replaces make_logo.py,
glb_strip_utility_nodes.py, analyze_captures.py and the run-garment-qa bash
driver. The QA configs and method doc move beside the domain (qa_configs/,
GARMENT_QA.md), and `qa` takes a config name (`reach character qa hoodie_modern`)
or a path.
Parity, from baselines taken before anything moved:
- the logo PNG is byte-identical
- a synthetic GLB with three real utility nodes strips to identical bytes
(the committed bodies strip 0 nodes, so they proved nothing)
- re-analyzing a cached capture set gives a byte-identical report.json and
summary
run-garment-qa is rewritten, not wrapped (D-263). Its decisions — which config,
which Godot ($GODOT, then ~/bin/godot4, then PATH), and whether xvfb-run is
needed — are capture_plan(), pinned by tooling/test_character.py without
launching Godot. The bash exit codes are kept: 2 for a missing config, 3 for
no Godot.
The T-1271 domain map was wrong about this domain. Six of its ten files import
bpy: convert_outfit, inspect_glb, check_hair_symmetry, check_icosphere,
render_quaternius_test and test_quaternius_raw. They are Blender payloads and
joined the carve-out as blender_* (41 payloads now). The 22 existing payloads'
docstrings still cited tooling/garment-fit/ from before T-1273; fixed.
Archived, with reasons in tooling/archive/README.md:
- setup_clothing_metadata.py wrote coverage data for five garments that no
longer exist in the 24-garment wardrobe
- wipe-bodies.sh ran raw DELETEs on systems.db
segment_reference_distribution.md moved to docs/assets/visual/.
Behaviour changes:
- The QA analyzer exited 0 whatever it found, though its own README says
clip-through "is the real defect and it gates". qa and qa-analyze now exit 1
on clip-through, and the remedy names --min-pixels (Wave 1/2 were accepted
at 150). The cached peasant set has 33 failures at the default 8 px.
- glb strip re-reported the same nodes as stripped on every re-run and
rewrote an unchanged file: it left them as orphans and then found them
again. Only nodes still linked into the graph count now, and a first pass
writes the same bytes as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 KiB
The domain map (T-1271, D-263)
Every Python file and every executable in tooling/, assigned to a domain
before anything moves. This is the artefact the per-domain port tickets are
written from — without it their boundaries would be guesses, renegotiated
halfway through the move.
Status: the map was written before anything moved; the ✅ rows below record which domains have since been ported, and by which ticket.
What the survey changed
Two corrections to the T-1250 description, both found by counting rather than reading:
- Three Rust crates, not one.
econ-sim,line-previewerandtest-clientare all Cargo projects with zero.pyfiles. None is renamed and none moves; a hyphen only matters for something Python must import. - The "28 singleton prefixes" were an artefact of my own measurement.
Splitting filenames on the first token scattered coherent families:
sculpt-star-map,tune-star-map-topologyandgenerate-star-map*are one group, not three orphans. Counting families instead of prefixes, the genuinely ambiguous set is small and is listed under Judgment calls below.
And two found while reading the tree:
garment-fit/is mostly a Blender payload directory. 22 of its 23 files areblender_author_*.py, leaving one real module. So the Blender carve-out is 35 files, not the 13 visible at top level — the T-1250 description was right about 35 and an intermediate survey of mine was wrong, because it counted only the top level. The consequence is not cosmetic:characteris a much smaller domain than the directory sizes imply, and the carve-out is much larger.tooling/db/is misnamed. It holds the audio/image/Trellis connectors andwiki_sync.py— the actual database work is ineconomy-db/. Naming a domaindbafter that directory would carry the misnomer forward, so its contents split betweenassetsandwikiinstead.
Half the executables are bash
Found 2026-08-31, after this map was first written and while starting the first port. 17 of the 33 extensionless executables are bash, ~900 lines. This map originally assigned them as though they were Python files to be moved; they are rewrites.
Decided: rewrite all of them in Python (D-263). Wrapping would achieve one
door while leaving half the surface outside the contract, so reach --help
would list verbs that behave differently from the ones beside them — worse than
two doors, because the inconsistency only shows up at a failure.
| shape | scripts | cost |
|---|---|---|
| thin wrappers | atlas-systems-done 9, atlas-names 17, generate-brands 23, atlas 24, generate-corporations 23, blender 28 |
near-free; the router calls what the script called |
| logic | check-fact-ids 89, validate-ron 137, godot-parse-sweep 64, godot-cold-parse 96, pr-watchlist-diff 37, atlas-update-field 59, atlas-commit-and-sync 60, tea-comment 41 |
real rewrite; gains the most — testable, failures that teach |
| environment | install-godot 108, install-rust 33, worktree-setup 49 |
gains least, riskiest — port the decision logic, keep the external calls behind core/process |
A bash source turns a port from mechanical into a rewrite that needs its own parity evidence. Each per-domain ticket must say which of its sources are bash.
Revised 2026-09-02: domains mirror the implant apps
The first version of this map invented a taxonomy. It should have read one off the game, and D-263 now says so: where the game presents something to the player, the CLI uses that name and that shape.
Two corrections follow, and the first was a contradiction of an existing record rather than a matter of taste:
atlasabsorbsstarmapandplanet. D-191: "Atlas is the star map extended downward, not a separate app —implant/mapat different zoom levels", four rungs Reach map → system → planetary → regional. This map had them as three peer domains, presenting as unrelated what the game presents as one descent. Generation now nests by rung; authoring and inspection verbs stay flat onatlas, because they act on the whole thing.dbbecomesledger. Named for the UI component that aggregates economics — markets, wealth, transactions — the economic counterpart to what the Atlas offers for topography.dbnamed a storage layer nobody looks at.
Domains that mirror nothing — check, validate, godot, visual, jobs,
dev — are unaffected. No app exists for a lint gate and none should be
invented.
Domains
| domain | what it is | sources |
|---|---|---|
check |
repo consistency gates the push hook runs | check-client-version ✅, check-canvas-version, check-systems-db-stamp, check-fact-ids, check-dataflow-graph.py |
validate |
content and schema validation | validate-content, validate-checklist, validate-ron |
atlas |
the whole spatial ladder (D-191). Flat verbs for authoring and inspection; nested groups per rung for generation | flat: atlas (Rust binary), atlas-check, atlas-names, atlas-commit-and-sync, atlas-systems-done, atlas-update-field, atlas-verify, atlas-flatness · atlas map: the 5 star-map files · atlas planet: ✅ ported (T-1288) from planet-gen/ (30) — ten verbs, each restating its module's options for real --help; test_planet_router.py fails if a router option drifts from the module parser |
starmap |
folded into atlas map — the top rung of the same ladder |
— |
planet |
folded into atlas planet — the third rung of the same ladder |
— |
ledger |
the economics pipeline, named for the UI component that will aggregate it | ✅ ported (T-1289). economy-db/ → domains/ledger/ (economy_import/ kept by name; the entrypoint became service.py), schema_version.py with it. reach ledger import; make regen-db survives as a one-line delegate, make economy-db retired. generated_brands.toml byte-identical across the move |
wiki |
wiki sync and content maintenance | ✅ ported (T-1290): reach wiki stats, reach wiki gttr-hook (both output-identical to the originals). wiki_sync.py moved whole, but its renderer and importer are NOT verbs — re-rendering deletes ~10,700 lines of committed pages (T-1292). The seven one-shots (assign-astro-ids, migrate-s-to-gj, patch-core-sector, fill-missing-globes, generate-stubs/find-stubs, backfill_cultural_corridor) and the destructive process-wiki-system-changes went to archive/wiki-bootstrap/ |
assets |
connectors to the tower-of-joy generators | ✅ ported (T-1290): reach assets {audio,image,trellis} … + synth-ui. The four audio bash wrappers and trellis-batch.sh retired; connectors call each other instead of spawning each other; tooling/db/ is gone. Synth WAVs byte-identical, ffmpeg pipeline decode-identical; network paths pinned by test_assets.py against a fake Gradio. Unreachable reads as OFF (VRAM, D-17), never "restart it" |
character |
bodies, garments, GLB handling | ✅ ported (T-1290): reach character {logo,strip-glb,qa,qa-analyze} — logo PNG, stripped GLB and QA report byte-identical; run-garment-qa (bash) rewritten with its decisions pinned by test_character.py. Map correction: six of the ten files assigned here (convert_outfit, inspect_glb, check_hair_symmetry, check_icosphere, render_quaternius_test, test_quaternius_raw) import bpy — they joined the carve-out as blender_* payloads. setup_clothing_metadata and wipe-bodies.sh archived; segment_reference_distribution.md moved to docs/assets/visual/ |
visual |
screenshot and render comparison | visual-diff, visual-thumbnail, visual-blank-check |
godot |
Godot parse and cold-start checks | ✅ ported (T-1283); the two bash originals were left beside the port and retired in T-1290 |
generate |
content generators not owned elsewhere | ✅ ported (T-1286). generate-brands + generate-corporations collapsed into core.process.cargo_binary — they were the same 24 lines of bash a third time |
dev |
developer environment and workflow | ✅ ported (T-1286). The three environment scripts split decision from performing — godot_plan/worktree_plan are pure and pinned by test_environment.py |
pr |
the PR/review loop | ✅ ported (T-1286). watchlist-diff now reads the watched set from generator_sources.py instead of restating it |
blender |
carve-out — payloads run by Blender | ✅ done (T-1273). 35 payloads in tooling/scripts/blender/ (41 after T-1290 found six more among the character files), excluded from package scope and from importability; the bash wrapper's install-resolution and path-absolutising became domains/blender/service.py, pinned by test_blender.py |
Judgment calls, with reasons
Each of these sets a precedent, so the reasoning matters more than the answer.
Registries stay module-level data, not commands. canvas_sources.py and
generator_sources.py are lists of paths consumed by gates — nothing types them.
They move beside the domain that reads them (check and db respectively) as
plain modules, not verbs. Making them commands would put something in
reach --help that answers no question a person has.
schema_version.py belongs to ledger (named db when this map was
drawn), and check reads it. It is consumed by the economics importer and
by the stamp gate. Shared, but not equally owned: the importer defines the
version, the gate reads it. It goes where it is defined. As ported (T-1289)
the gate never imports the module at all — it reads the version back out of
the meta table and checks it is semver — so the cross-domain import this
ruling allowed for turned out not to be needed.
test_*.py files do NOT become a domain. They are gate tests run by
make test-tooling, not commands anyone types. reach test … would imply a
test runner that does not exist. They stay standalone scripts.
pql-migrate/ is provenance, not tooling. One-shot scripts from a completed
migration, already documented as such in .claude/rules/project-structure.md.
They are not live tools and must not become verbs. Move to tooling/archive/,
excluded from package discovery — deleting them would destroy migration
provenance, and keeping them importable would imply they still run.
pr is a domain the epic did not list. tea-comment and pr-watchlist-diff
are the PR/review loop, which is neither dev (environment setup) nor anything
else on the original list. Folding them into dev would make dev the drawer
everything ambiguous goes into — which is how core/ rots, and the same
argument applies here.
atlas is overloaded, and the port must not merge the three uses. The word
appears in three unrelated places: the top-level atlas-* executables (the map
data surface), planet-gen/atlas_*.py (atlas_cohesion_audit,
atlas_common, atlas_quality_analysis — quality analysis of generated
terrain), and economy-db/atlas.py (the atlas index tables in systems.db).
These are three concerns sharing a noun, not one domain in three places. Each
stays with its owner — atlas, atlas planet and ledger respectively — and
the port should resist the pull to collect them, which would produce a domain
whose only common thread is a word. Held through both ports (T-1288,
T-1289): domains/atlas/, domains/atlas/planet/atlas_*.py and
domains/ledger/economy_import/atlas.py are still three separate files.
economy-db/errors.py predates core/errors.py and is not the same thing.
Domain-local error types are fine; what must not happen is a silent merge, or a
second ReachError with different semantics. Reconciled in T-1289:
ImportAborted stays, as internal control flow only — a step raises it to
request a rollback. It never reaches a caller: ledger/service.py rolls back
and converts it to a ReachError carrying the remedy.
character rather than garment. D-263's sketch says garment, but the
files cover bodies, hair, GLB utilities and Quaternius imports as well as
clothing. Naming it for one of its five concerns would leave the other four
looking misfiled.
Not moving
| what | why |
|---|---|
econ-sim/, line-previewer/, test-client/ |
Rust crates. Excluded from package discovery; hyphens are harmless. |
scripts/blender/ (after T-1273) |
Run under Blender's bundled Python; cannot import tooling.core. Excluded from the conformance scope for the same reason. |
archive/ (was pql-migrate/) |
Completed-migration provenance. |
test_*.py |
Gate tests, not commands. |
Naming, applied on the way in
- Modules
snake_case:check-dataflow-graph.py→dataflow_graph.py. - Verbs
kebab-case:reach check dataflow-graph. - No prefix-as-namespace. The domain directory supplies what the
blender_,atlas-andcheck-prefixes were doing by hand.atlas-verifybecomesreach atlas verify, notreach atlas atlas-verify. - Layering is a vocabulary, not a quota. A domain with only a router and a
service gets exactly those two files. Empty
schemas.pyanddependencies.pyare worse than absent ones — they suggest a shape the domain does not have.