Files
settled-reach/.pql/changelog/tickets/2026-09.sql
T
jpmschweitzerandClaude Opus 5 6daaa2235f docs(governance): D-263 — domains mirror the implant apps, and atlas is one ladder
reach's domain names should not be a fresh taxonomy. Where the game already
presents something to the player, the CLI takes that name and that shape: what
you browse in-game is what you generate and inspect from the terminal.

That splits domains in two. atlas, ledger and wiki mirror implant apps and
follow their structure. check, validate, godot, visual, jobs and dev mirror
nothing — no app exists for a lint gate, and inventing a player-facing framing
for one would be worse than having none.

The first consequence corrects a contradiction rather than a preference. D-191
already says "Atlas is the star map extended downward, not a separate app —
implant/map at different zoom levels", four rungs from Reach map to regional.
The domain map had atlas, starmap and planet as peers, which would have
presented as three unrelated things what the game presents as one descent.
Generation now nests by rung; authoring and inspection verbs stay flat on
atlas, because they act on the whole thing rather than a rung.

The second is a rename with the same reasoning: db becomes ledger, after the UI
component that will aggregate economics — markets, wealth, transactions, the
economic counterpart to what the Atlas offers for topography. db named a
storage layer nobody looks at.

One caution recorded because the words collide. D-191's MVP criterion 7 says
"Atlas is read-only (no verbs execute from map)". That governs the app. The
atlas tooling writes — it commits proposals, mutates fields, syncs the wiki —
and a later reader must not take the app's constraint as licence to delete the
authoring verbs.

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

78 lines
28 KiB
SQL

INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-01 15:22:13.360', NULL, 'd8cc608ee1be87634289397ebae7fd61', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-01 15:22:27.856', NULL, '89c48e98147081cb5465e7373234ec9f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.
DONE 2026-09-02. reach validate content / checklist / ron / name-collisions. All three old scripts retired, make targets retired, permission entry dropped.
PRINTS GO THROUGH THE LOGGING SINK, per Jeroen mid-ticket: "print statements go through the logging decorator" and "it already is a collector, the logging sink". My first pass added a per-module _lines list; that was redundant, because console IS the collector. The 48 sites in content.py and 24 in checklist.py now emit console events, message strings and order unchanged. Consequence beyond tidiness: a long content validation STREAMS as it runs rather than going quiet and dumping at the end, and every line carries the invocation''s job id.
validate-ron WAS THREE LANGUAGES DEEP — bash dispatching on a flag, a Python heredoc doing collision detection, cargo run for schema validation. Logic embedded in a shell string cannot be imported, tested, or found by anything that indexes Python; that is the clearest case yet for the rewrite decision. The heredoc became Python, the cargo call became a guarded exec through core/process.run with missing_fix naming `make setup-rust`.
It also SPLIT INTO TWO VERBS. --check-name-collisions answered a different question from the default path — whether the SET of cultures is coherent versus whether ONE file is well-formed — and the old script had to branch on the flag before doing anything. Two verbs, no branch.
THE MOVE BROKE SOMETHING, QUIETLY, which is the argument for doing these one domain at a time. validate-checklist computed ROOT as Path(__file__).resolve().parent.parent — the repo root while the file lived at tooling/validate-checklist, and tooling/domains once moved. Its schema and gauntlet paths silently repointed at nothing, the gauntlet directory "did not exist", find_checklists returned [], and it reported "skipped (no content yet)" with exit 0. Caught only by running it beside the original: old exit 1 (schema not found), new exit 0. Fixed to config.repo_root(). Also converted load_schema''s sys.exit(1) to a ReachError — a service must not end the process, and the caller now gets a remedy instead of a bare 1.
PARITY on the live tree: content reproduces the original byte for byte including its counts (0 validated, 7 skipped, 13 errors); name-collisions likewise (OK across 3 cultures). tooling/test_validate.py pins what those runs cannot reach — collision DETECTION (the repo currently has none), the comment-stripping rule, the empty and missing directory cases, and the two argument errors. Proven to fail by removing comment stripping, which tripped both the decoy assertion and the no-collision case.
TWO FINDINGS LEFT ALONE, deliberately:
- validate-content FAILS on the live tree: 13 MISSING SCHEMA errors under server/content/_schema/. Pre-existing, unrelated to this port, and it means `make pre-pr-validate` has been failing. Not fixed here because writing 13 JSON schemas is content work, not a port. Worth its own ticket.
- THIS TICKET''S DESCRIPTION IS WRONG about validate-content being in the pre-commit hook. That hook runs only check-fact-ids (ported in T-1281) and pql decisions validate. There was no shared hook edit to coordinate.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-02 10:50:19.803', NULL, '137939508f1a89fb45ebfa2fe7580a8e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.
DONE 2026-09-02. reach validate content / checklist / ron / name-collisions. All three old scripts retired, make targets retired, permission entry dropped.
PRINTS GO THROUGH THE LOGGING SINK, per Jeroen mid-ticket: "print statements go through the logging decorator" and "it already is a collector, the logging sink". My first pass added a per-module _lines list; that was redundant, because console IS the collector. The 48 sites in content.py and 24 in checklist.py now emit console events, message strings and order unchanged. Consequence beyond tidiness: a long content validation STREAMS as it runs rather than going quiet and dumping at the end, and every line carries the invocation''s job id.
validate-ron WAS THREE LANGUAGES DEEP — bash dispatching on a flag, a Python heredoc doing collision detection, cargo run for schema validation. Logic embedded in a shell string cannot be imported, tested, or found by anything that indexes Python; that is the clearest case yet for the rewrite decision. The heredoc became Python, the cargo call became a guarded exec through core/process.run with missing_fix naming `make setup-rust`.
It also SPLIT INTO TWO VERBS. --check-name-collisions answered a different question from the default path — whether the SET of cultures is coherent versus whether ONE file is well-formed — and the old script had to branch on the flag before doing anything. Two verbs, no branch.
THE MOVE BROKE SOMETHING, QUIETLY, which is the argument for doing these one domain at a time. validate-checklist computed ROOT as Path(__file__).resolve().parent.parent — the repo root while the file lived at tooling/validate-checklist, and tooling/domains once moved. Its schema and gauntlet paths silently repointed at nothing, the gauntlet directory "did not exist", find_checklists returned [], and it reported "skipped (no content yet)" with exit 0. Caught only by running it beside the original: old exit 1 (schema not found), new exit 0. Fixed to config.repo_root(). Also converted load_schema''s sys.exit(1) to a ReachError — a service must not end the process, and the caller now gets a remedy instead of a bare 1.
PARITY on the live tree: content reproduces the original byte for byte including its counts (0 validated, 7 skipped, 13 errors); name-collisions likewise (OK across 3 cultures). tooling/test_validate.py pins what those runs cannot reach — collision DETECTION (the repo currently has none), the comment-stripping rule, the empty and missing directory cases, and the two argument errors. Proven to fail by removing comment stripping, which tripped both the decoy assertion and the no-collision case.
TWO FINDINGS LEFT ALONE, deliberately:
- validate-content FAILS on the live tree: 13 MISSING SCHEMA errors under server/content/_schema/. Pre-existing, unrelated to this port, and it means `make pre-pr-validate` has been failing. Not fixed here because writing 13 JSON schemas is content work, not a port. Worth its own ticket.
- THIS TICKET''S DESCRIPTION IS WRONG about validate-content being in the pre-commit hook. That hook runs only check-fact-ids (ported in T-1281) and pql decisions validate. There was no shared hook edit to coordinate.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-02 10:50:19.824', NULL, 'e6eed1166d37970c10376ba6ad9b2270', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G63FEA51J4JKE3A7473D3QZ0', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Write the 13 missing content JSON schemas', 'reach validate content currently FAILS on the live tree with 13 MISSING SCHEMA errors: the campaigns tree references schemas that do not exist under server/content/_schema/ — system.schema.json among them. Found 2026-09-02 while porting the validator (T-1282); PRE-EXISTING and unrelated to that port, which reproduces the failure byte for byte because it reproduces the original''s behaviour. Consequence: make pre-pr-validate has been failing, so that chain has not been a working gate. Writing the schemas is content work, not tooling work, which is why it was not folded into the port. Run reach validate content for the current list.', 'backlog', 'medium', NULL, NULL, NULL, '2026-09-02 10:50:23.144', '2026-09-02 10:50:23.144', NULL, '64e70334d534198b9b07160eced5d909', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5GWEC9P1HNABW6S0BSNTZ2W', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the godot and visual domains', 'Two small domains, filed together because each is two or three files and neither has surprises. godot: godot-parse-sweep and godot-cold-parse into domains/godot/. visual: visual-diff, visual-thumbnail and visual-blank-check into domains/visual/. Standard port acceptance as defined on T-1281. Worth knowing before starting: both domains shell out to external binaries — godot4 and image tooling respectively — so their services need a launcher, and core/process.py already has the spawn primitive from T-1277. Do NOT let each service grow its own subprocess handling; if what is needed is broader than what core/process.py offers, extend it there rather than twice locally. That is the difference between a shared substrate and two copies that drift. Also: these are the first ports whose commands could genuinely be slow (a parse sweep over the whole client), so they are the first real candidates for progress events via console.event — the streaming machinery exists and this is where it starts earning.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:48.013', '2026-09-02 10:53:39.240', NULL, '9a437c8d09c93b7edbf080000399952a', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5GWEC9P1HNABW6S0BSNTZ2W', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the godot and visual domains', 'Two small domains, filed together because each is two or three files and neither has surprises. godot: godot-parse-sweep and godot-cold-parse into domains/godot/. visual: visual-diff, visual-thumbnail and visual-blank-check into domains/visual/. Standard port acceptance as defined on T-1281. Worth knowing before starting: both domains shell out to external binaries — godot4 and image tooling respectively — so their services need a launcher, and core/process.py already has the spawn primitive from T-1277. Do NOT let each service grow its own subprocess handling; if what is needed is broader than what core/process.py offers, extend it there rather than twice locally. That is the difference between a shared substrate and two copies that drift. Also: these are the first ports whose commands could genuinely be slow (a parse sweep over the whole client), so they are the first real candidates for progress events via console.event — the streaming machinery exists and this is where it starts earning.
DONE 2026-09-02. reach godot parse-sweep / cold-parse and reach visual diff / blank-check / thumbnail. Five old scripts retired, callers rewired.
GODOT — two bash scripts whose logic was grep pipelines encoding five separate hard-won lessons, each a comment nobody could test. Now Python filters with the reasons attached, and the engine invocation as a guarded exec. Verified on the real client: "godot-parse-sweep: clean — opened 229 scripts (0 unreadable dirs)".
Three not-ok states kept DISTINCT, because only one of them is a verdict about the code: engine_failed (godot crashed or is missing — reporting that as a parse failure blames the tree for a broken toolchain), did_not_run (the sweep emitted no completion marker, so it checked nothing — zero errors from a check that never ran reads as clean, the false-green this tool exists to close), and genuine parse errors.
The asymmetry between the two checks is preserved and documented: cold-parse filters "Cannot infer the type", the sweep does NOT. That suppression is why cold-parse stayed silent about a test helper that genuinely does not parse.
VISUAL — three Python scripts with argparse mains. argparse removed: a parser inside a service is a second transport layer, and the router declares the options now.
THE SAME ROOT BUG AS T-1282, in all three files. Every one computed Path(__file__).resolve().parent.parent — the repo root at tooling/, and tooling/domains/visual once moved, two levels too deep. Fixed to config.repo_root() during the move rather than after, having learned from validate-checklist that this fails SILENTLY: paths resolve to nothing, the work appears to have nothing to do, and the tool reports success. That is now three domains where a __file__-relative root would have shipped a false pass.
TWO BUGS MY OWN TRANSFORMATION INTRODUCED, both caught by running rather than reading:
- Multi-line print(..., file=sys.stderr) became console.event(..., file=sys.stderr). console.event puts unknown kwargs into the event payload, so a file OBJECT would have gone to json.dumps — a crash at the moment something was already being reported as an error. Removed, level="error" instead.
- The escaping in my replacement script wrote level=\"error\" into three files, which ruff caught as a syntax error.
Neither was visible by inspection of the diff; both surfaced on the first real run. Mechanical transformations need mechanical verification.
sys.exit REMOVED from diff.py and thumbnail.py — four sites. A service must not end the process; they raise ReachError with a remedy now.
CALLERS REWIRED, and this is the wider blast radius than the make targets: tests/run-visual invoked visual-blank-check, visual-thumbnail and visual-diff by path at four sites, and the pre-push hook invoked godot-parse-sweep. All now call reach --no-input. .claude/settings.json loses the godot-cold-parse entry; Bash(reach *) covers it.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:48.013', '2026-09-02 11:59:14.517', NULL, '9789ab592ea31d9474bca2166d1fb79e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G5GWEC9P1HNABW6S0BSNTZ2W', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the godot and visual domains', 'Two small domains, filed together because each is two or three files and neither has surprises. godot: godot-parse-sweep and godot-cold-parse into domains/godot/. visual: visual-diff, visual-thumbnail and visual-blank-check into domains/visual/. Standard port acceptance as defined on T-1281. Worth knowing before starting: both domains shell out to external binaries — godot4 and image tooling respectively — so their services need a launcher, and core/process.py already has the spawn primitive from T-1277. Do NOT let each service grow its own subprocess handling; if what is needed is broader than what core/process.py offers, extend it there rather than twice locally. That is the difference between a shared substrate and two copies that drift. Also: these are the first ports whose commands could genuinely be slow (a parse sweep over the whole client), so they are the first real candidates for progress events via console.event — the streaming machinery exists and this is where it starts earning.
DONE 2026-09-02. reach godot parse-sweep / cold-parse and reach visual diff / blank-check / thumbnail. Five old scripts retired, callers rewired.
GODOT — two bash scripts whose logic was grep pipelines encoding five separate hard-won lessons, each a comment nobody could test. Now Python filters with the reasons attached, and the engine invocation as a guarded exec. Verified on the real client: "godot-parse-sweep: clean — opened 229 scripts (0 unreadable dirs)".
Three not-ok states kept DISTINCT, because only one of them is a verdict about the code: engine_failed (godot crashed or is missing — reporting that as a parse failure blames the tree for a broken toolchain), did_not_run (the sweep emitted no completion marker, so it checked nothing — zero errors from a check that never ran reads as clean, the false-green this tool exists to close), and genuine parse errors.
The asymmetry between the two checks is preserved and documented: cold-parse filters "Cannot infer the type", the sweep does NOT. That suppression is why cold-parse stayed silent about a test helper that genuinely does not parse.
VISUAL — three Python scripts with argparse mains. argparse removed: a parser inside a service is a second transport layer, and the router declares the options now.
THE SAME ROOT BUG AS T-1282, in all three files. Every one computed Path(__file__).resolve().parent.parent — the repo root at tooling/, and tooling/domains/visual once moved, two levels too deep. Fixed to config.repo_root() during the move rather than after, having learned from validate-checklist that this fails SILENTLY: paths resolve to nothing, the work appears to have nothing to do, and the tool reports success. That is now three domains where a __file__-relative root would have shipped a false pass.
TWO BUGS MY OWN TRANSFORMATION INTRODUCED, both caught by running rather than reading:
- Multi-line print(..., file=sys.stderr) became console.event(..., file=sys.stderr). console.event puts unknown kwargs into the event payload, so a file OBJECT would have gone to json.dumps — a crash at the moment something was already being reported as an error. Removed, level="error" instead.
- The escaping in my replacement script wrote level=\"error\" into three files, which ruff caught as a syntax error.
Neither was visible by inspection of the diff; both surfaced on the first real run. Mechanical transformations need mechanical verification.
sys.exit REMOVED from diff.py and thumbnail.py — four sites. A service must not end the process; they raise ReachError with a remedy now.
CALLERS REWIRED, and this is the wider blast radius than the make targets: tests/run-visual invoked visual-blank-check, visual-thumbnail and visual-diff by path at four sites, and the pre-push hook invoked godot-parse-sweep. All now call reach --no-input. .claude/settings.json loses the godot-cold-parse entry; Bash(reach *) covers it.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:48.013', '2026-09-02 11:59:14.536', NULL, 'a3a0b27a427faca6f2c47ae6a9a6f9a5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G642RKW8PNW7AP9BY9203SPG', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the atlas domain — the authoring and inspection verbs', 'The eight top-level atlas-* scripts become FLAT verbs on reach atlas: the Rust binary wrapper (stats, show-system, list-bodies, populate), atlas-check, atlas-verify, atlas-names, atlas-systems-done, atlas-update-field, atlas-commit-and-sync, atlas-flatness. Flat rather than nested because these act on the atlas as a whole; the nested groups are for GENERATION per rung (reach atlas map, reach atlas planet), which are separate tickets. Five of the eight are BASH, so those are rewrites: atlas 24, atlas-names 17, atlas-systems-done 9, atlas-update-field 59, atlas-commit-and-sync 60. Standard port acceptance as on T-1281. THREE THINGS NEEDING A DECISION OR CARE, each recorded rather than assumed: (1) the Rust binary — tooling/atlas builds server/target/debug/atlas if missing then execs it with all args, so its verbs live in Rust not in the shell; declare the four known verbs in the router so reach --help stays a complete index, and keep a passthrough for anything the binary gains later. (2) atlas-commit-and-sync MAKES GIT COMMITS, which is a risk class nothing else in reach has; port it to stage and report by default with committing behind an explicit flag, and say so in the verb help rather than changing behaviour silently. (3) atlas-update-field MUTATES systems.db directly, which .claude/rules/asset-pipeline.md forbids outside a migration — establish whether it predates that rule or is a sanctioned exception BEFORE porting, because the port must not launder a violation into a first-class verb.', 'backlog', 'high', NULL, NULL, 'D-263', '2026-09-02 12:14:48.290', '2026-09-02 12:14:48.290', NULL, 'cf4a2315117474f165f1e689d6271300', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;