Merge remote-tracking branch 'origin/main' into test-hardening

This commit is contained in:
2026-07-25 19:36:39 +02:00
18 changed files with 1740 additions and 27 deletions
+52
View File
@@ -1999,3 +1999,55 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRMPWFRCD3M8282XJ01YWGX0', 'status', 'review', 'done', NULL, '2026-07-25 16:20:22', '2026-07-25 16:20:22.122', '2026-07-25 16:20:22.122', NULL, '8535ec9c01bcf0887849790d05bab399', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRV2EV5QK4BCZ1P9H85JH2YM', 'status', 'review', 'done', NULL, '2026-07-25 16:20:22', '2026-07-25 16:20:22.129', '2026-07-25 16:20:22.129', NULL, '8ed7643be68460f63e0d3555d9502718', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRMPTY517G7ZBMPAPMMCCMK8', 'status', 'review', 'done', NULL, '2026-07-25 16:20:22', '2026-07-25 16:20:22.131', '2026-07-25 16:20:22.131', NULL, 'cd5e54f6e1f1066e344ca2ab6740fade', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRZ9Q7NHVDRT6ARMCWW', 'description', 'D-210 defines SubBiomeVariant for fine-grained biome classification within SettingType::Wilderness districts. Currently deferred — SettingType::Wilderness carries a plain String biome field. Implement SubBiomeVariant enum and wire it into the atlas skeleton generator when the biome taxonomy is finalized.', 'D-210 defines SubBiomeVariant for fine-grained biome classification within SettingType::Wilderness districts. Currently deferred — SettingType::Wilderness carries a plain String biome field. Implement SubBiomeVariant enum and wire it into the atlas skeleton generator when the biome taxonomy is finalized.
Refinement review (2026-07-25, Si): OBSOLETE D-210''s design was fully implemented and merged in PR #953/#963 (commit d418eba8b, 2026-06-03), a month after this ticket was filed, and the ticket was never closed out. Evidence: server/src/atlas/subbiome.rs (classify/classify_variant/base_cost, all 11 variants + terrain_modification_cost from the four D-210 signals, unit-tested); SubBiomeVariant enum + GeographicAttractor.sub_biome (generator.rs:539/:567); wired into the Layer-1 orchestrator (layer1.rs:209-215, Alpine branch tested); consumed downstream (attractor_matching.rs, cascade.rs); client overlay live (atlas_overlay_colors.gd:203 sub_biome_color). The only literal residue SettingType::Wilderness carrying the cosmetic `Biome = String` alias is unrelated to D-210''s classification/cost system; tighten it via a separate small ticket if ever wanted. Closing as delivered-by-#953.', NULL, '2026-07-25 16:25:46', '2026-07-25 16:25:46.604', '2026-07-25 16:25:46.604', NULL, '93092399b4a74d80678fc0b0d3d82bd7', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRZ9Q7NHVDRT6ARMCWW', 'status', 'backlog', 'done', NULL, '2026-07-25 16:25:46', '2026-07-25 16:25:46.760', '2026-07-25 16:25:46.760', NULL, 'c269eb7751dfb286413b3a4d826889e4', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRZZ5XKET93HPHKQHKW', 'description', 'Follow-ups from the Hoshe/Tyre pre-bake review (the BLOCKER/MAJOR items were fixed inline; these are the deferred MINOR/coverage items):
TESTS:
- features.rs: full per-type attractor reachability fixtures craft heightmaps that guarantee LakeShore (enclosed depression), PassEntrance (saddle), PlainCenter (flat), RiverCrossing (confluence). Currently only RiverMouth/CoastalAccess/ValleyFloor are exercised on the slope/sine fixtures.
- heightmap.rs: 8-bit grayscale decode path; sea_level passthrough assertion; downsample identity/upsample early-return.
- drainage.rs: basin area_pct bit-for-bit determinism (only river_cells + basin count checked today).
- layer1.rs: attach_feature_names mountain branch with a non-empty mountain pool + 1 Alpine attractor.
- features.rs: thin_by_spacing behavior (spacing collisions, equirectangular column wrap).
- import_economics.py: automated idempotency test two runs on a scratch DB yield identical atlas_city_names count (not doubled), and 0 rows for system ''GJ 0'' (Sol exemption). Wire alongside check-systems-db / pre-push.
- Wire test_sim_determinism.py into pre-push or a make target.
MINOR CODE NITS:
- drainage.rs ~480-488: comment the intentional isolated-basin fallback divergence from the old merge-to-basin-0 behavior (Tyre N1).
- planet_simulation.py ~787-788: oasis_water binary_dilation iterations are fixed-pixel scale by GRID_W/512 for biome-display consistency at 1024 (cosmetic, Atlas only, not cascade; Tyre N2).
D-256/T-1174 finding (2026-07-25): the believability harness golden is INSENSITIVE to a total basin_direction regression an all-North basin collapse (every survey cell defaulting) produced a byte-identical believability.json at current sample density (64/2048 districts; basin only reaches voxel meander phase/channel width, too localized for the coarse aggregate stats). The cross-namespace keying seam is now covered by the restored derive_all_districts_threads_supplied_basin_directions test (production-shaped survey keys), but the harness itself has no basin-sensitive metric. Candidate hardening: a basin-direction distribution stat (distinct directions >= 2 on a drained body) or a voxel-transect metric that moves with meander phase.', 'Follow-ups from the Hoshe/Tyre pre-bake review (the BLOCKER/MAJOR items were fixed inline; these are the deferred MINOR/coverage items):
TESTS:
- features.rs: full per-type attractor reachability fixtures craft heightmaps that guarantee LakeShore (enclosed depression), PassEntrance (saddle), PlainCenter (flat), RiverCrossing (confluence). Currently only RiverMouth/CoastalAccess/ValleyFloor are exercised on the slope/sine fixtures.
- heightmap.rs: 8-bit grayscale decode path; sea_level passthrough assertion; downsample identity/upsample early-return.
- drainage.rs: basin area_pct bit-for-bit determinism (only river_cells + basin count checked today).
- layer1.rs: attach_feature_names mountain branch with a non-empty mountain pool + 1 Alpine attractor.
- features.rs: thin_by_spacing behavior (spacing collisions, equirectangular column wrap).
- import_economics.py: automated idempotency test two runs on a scratch DB yield identical atlas_city_names count (not doubled), and 0 rows for system ''GJ 0'' (Sol exemption). Wire alongside check-systems-db / pre-push.
- Wire test_sim_determinism.py into pre-push or a make target.
MINOR CODE NITS:
- drainage.rs ~480-488: comment the intentional isolated-basin fallback divergence from the old merge-to-basin-0 behavior (Tyre N1).
- planet_simulation.py ~787-788: oasis_water binary_dilation iterations are fixed-pixel scale by GRID_W/512 for biome-display consistency at 1024 (cosmetic, Atlas only, not cascade; Tyre N2).
D-256/T-1174 finding (2026-07-25): the believability harness golden is INSENSITIVE to a total basin_direction regression an all-North basin collapse (every survey cell defaulting) produced a byte-identical believability.json at current sample density (64/2048 districts; basin only reaches voxel meander phase/channel width, too localized for the coarse aggregate stats). The cross-namespace keying seam is now covered by the restored derive_all_districts_threads_supplied_basin_directions test (production-shaped survey keys), but the harness itself has no basin-sensitive metric. Candidate hardening: a basin-direction distribution stat (distinct directions >= 2 on a drained body) or a voxel-transect metric that moves with meander phase.
Refinement trim (2026-07-25, Si every listed gap re-verified against current code before this batch): (1) DROP the test_sim_determinism.py wiring item already done, Makefile:256 wires it into make test-tooling which the push gate runs on tooling changes. (2) The drainage.rs isolated-basin comment nit moved to ~line 654 (file grew; the old ~480-488 cite is stale) still open, just relocate. (3) The layer1.rs attach_feature_names mountain-branch item: implicit coverage may already exist via run_layer1_* tests check before adding a redundant test. (4) The D-256/T-1174 addendum''s "basin-direction distribution stat" candidate hardening is a genuine open design question (which stat, what threshold) PROPOSE the stat to the lead before coding it, don''t invent silently. All other items (features.rs per-type reachability fixtures + thin_by_spacing, heightmap.rs 8-bit/sea-level-passthrough/upsample-identity, drainage.rs area_pct bit-determinism, import_economics.py idempotency test, oasis_water binary_dilation nit at planet_simulation.py:784-788) verified still open as written.', NULL, '2026-07-25 16:25:56', '2026-07-25 16:25:56.990', '2026-07-25 16:25:56.990', NULL, '99464deb0f973bb04ffdc943badd4d10', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRWTP633SM8XB302MBM', 'description', 'D-226 layer 4 (apex). A client-side AtlasAgentInterface exposing a JSON control channel so an automated agent (terminal/curl, headless Godot) can navigate the REAL Atlas UI: observe -> JSON of (a) current data state shown and (b) a walkable UI affordance tree (toggle controls: id/label/state/locked); act -> named SEMANTIC intents (select_system/open_system/select_body/open_regional/select_city/set_overlay/reset_view/back/close_atlas) backed by the same handlers a click calls (the map uses _gui_input, NOT pixel-clicks), plus get_layer_data_summary (attractor/river/basin counts as JSON without rendering). Single AtlasAgentInterface autoload/static class for a stable contract; generic Control-tree walk so economics/saves plug in later. Turns human-eyeball review into an agent QA sweep. Depends on #960 (the viewer) + the auto-pause substrate. Screenshot capture is a separate ticket (kept off the critical path).', 'D-226 layer 4 (apex). A client-side AtlasAgentInterface exposing a JSON control channel so an automated agent (terminal/curl, headless Godot) can navigate the REAL Atlas UI: observe -> JSON of (a) current data state shown and (b) a walkable UI affordance tree (toggle controls: id/label/state/locked); act -> named SEMANTIC intents (select_system/open_system/select_body/open_regional/select_city/set_overlay/reset_view/back/close_atlas) backed by the same handlers a click calls (the map uses _gui_input, NOT pixel-clicks), plus get_layer_data_summary (attractor/river/basin counts as JSON without rendering). Single AtlasAgentInterface autoload/static class for a stable contract; generic Control-tree walk so economics/saves plug in later. Turns human-eyeball review into an agent QA sweep. Depends on #960 (the viewer) + the auto-pause substrate. Screenshot capture is a separate ticket (kept off the critical path).
Re-scope (2026-07-25, Si refinement + lead ruling): both cited blockers are satisfied (T-960, T-970 done) and the D-255(d) deep-rung cache cap is already built defensively for this exact consumer (step_canvas_disk_cache.gd:61 names the AtlasAgentInterface QA sweep). But D-226''s named-intent list was designed against the retired pre-D-255 continuous-zoom Atlas the ticket''s intent surface is STALE. Implementer instruction: derive the actual intent list from the CURRENT stepped-rung API before building the wrapper, and post the derived list to the team lead for confirmation before implementation. Known mapping start (verify, don''t trust): select_system atlas_app._on_system_selected(system_id); select_body _on_body_selected(body); reset_view StepCanvasViewer._reset_to_global(); set_overlay set_overlay_visible(); back _on_regional_back(); zoom step _scroll_rung(direction, cursor_local); open_regional/select_city/open_system need tracing through screens/regional_screen.gd, district_screen.gd, system_screen.gd. SEQUENCING: T-1157 (stepped capture-harness redesign) needs the same walk-the-Atlas-headlessly primitive its harness-techniques inventory (appended 2026-07-25) documents five proven eyeball-driver patterns (InputSwallower, in-process viewer-method navigation, fixed-center revisit, view-transform logging) that are the PROTOTYPE for this channel; build the T-971 intent layer as the productionization of that pattern so T-1157''s harness can consume it rather than the two designing overlapping scaffolding independently.', NULL, '2026-07-25 16:26:05', '2026-07-25 16:26:05.492', '2026-07-25 16:26:05.492', NULL, 'f7eb781029e013fcecfceffbf28932d8', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRZZ5XKET93HPHKQHKW', 'status', 'backlog', 'in_progress', NULL, '2026-07-25 16:26:10', '2026-07-25 16:26:10.181', '2026-07-25 16:26:10.181', NULL, 'f352b98435a6a00018e45a8e2dd46675', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRWTP633SM8XB302MBM', 'status', 'backlog', 'in_progress', NULL, '2026-07-25 16:26:10', '2026-07-25 16:26:10.188', '2026-07-25 16:26:10.188', NULL, '27e775276ffa828ab316d2f4e3034c1e', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRZZ5XKET93HPHKQHKW', 'assigned_to', NULL, 'dudley', NULL, '2026-07-25 16:26:11', '2026-07-25 16:26:11.164', '2026-07-25 16:26:11.164', NULL, '63073f8ecec393e1eefde9b98ea63e1c', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRWTP633SM8XB302MBM', 'assigned_to', NULL, 'stig', NULL, '2026-07-25 16:26:11', '2026-07-25 16:26:11.308', '2026-07-25 16:26:11.308', NULL, 'd0016aa197af14b8daab5e496d884b9b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRWTP633SM8XB302MBM', 'description', 'D-226 layer 4 (apex). A client-side AtlasAgentInterface exposing a JSON control channel so an automated agent (terminal/curl, headless Godot) can navigate the REAL Atlas UI: observe -> JSON of (a) current data state shown and (b) a walkable UI affordance tree (toggle controls: id/label/state/locked); act -> named SEMANTIC intents (select_system/open_system/select_body/open_regional/select_city/set_overlay/reset_view/back/close_atlas) backed by the same handlers a click calls (the map uses _gui_input, NOT pixel-clicks), plus get_layer_data_summary (attractor/river/basin counts as JSON without rendering). Single AtlasAgentInterface autoload/static class for a stable contract; generic Control-tree walk so economics/saves plug in later. Turns human-eyeball review into an agent QA sweep. Depends on #960 (the viewer) + the auto-pause substrate. Screenshot capture is a separate ticket (kept off the critical path).
Re-scope (2026-07-25, Si refinement + lead ruling): both cited blockers are satisfied (T-960, T-970 done) and the D-255(d) deep-rung cache cap is already built defensively for this exact consumer (step_canvas_disk_cache.gd:61 names the AtlasAgentInterface QA sweep). But D-226''s named-intent list was designed against the retired pre-D-255 continuous-zoom Atlas the ticket''s intent surface is STALE. Implementer instruction: derive the actual intent list from the CURRENT stepped-rung API before building the wrapper, and post the derived list to the team lead for confirmation before implementation. Known mapping start (verify, don''t trust): select_system atlas_app._on_system_selected(system_id); select_body _on_body_selected(body); reset_view StepCanvasViewer._reset_to_global(); set_overlay set_overlay_visible(); back _on_regional_back(); zoom step _scroll_rung(direction, cursor_local); open_regional/select_city/open_system need tracing through screens/regional_screen.gd, district_screen.gd, system_screen.gd. SEQUENCING: T-1157 (stepped capture-harness redesign) needs the same walk-the-Atlas-headlessly primitive its harness-techniques inventory (appended 2026-07-25) documents five proven eyeball-driver patterns (InputSwallower, in-process viewer-method navigation, fixed-center revisit, view-transform logging) that are the PROTOTYPE for this channel; build the T-971 intent layer as the productionization of that pattern so T-1157''s harness can consume it rather than the two designing overlapping scaffolding independently.', 'D-226 layer 4 (apex). A client-side AtlasAgentInterface exposing a JSON control channel so an automated agent (terminal/curl, headless Godot) can navigate the REAL Atlas UI: observe -> JSON of (a) current data state shown and (b) a walkable UI affordance tree (toggle controls: id/label/state/locked); act -> named SEMANTIC intents (select_system/open_system/select_body/open_regional/select_city/set_overlay/reset_view/back/close_atlas) backed by the same handlers a click calls (the map uses _gui_input, NOT pixel-clicks), plus get_layer_data_summary (attractor/river/basin counts as JSON without rendering). Single AtlasAgentInterface autoload/static class for a stable contract; generic Control-tree walk so economics/saves plug in later. Turns human-eyeball review into an agent QA sweep. Depends on #960 (the viewer) + the auto-pause substrate. Screenshot capture is a separate ticket (kept off the critical path).
Re-scope (2026-07-25, Si refinement + lead ruling): both cited blockers are satisfied (T-960, T-970 done) and the D-255(d) deep-rung cache cap is already built defensively for this exact consumer (step_canvas_disk_cache.gd:61 names the AtlasAgentInterface QA sweep). But D-226''s named-intent list was designed against the retired pre-D-255 continuous-zoom Atlas the ticket''s intent surface is STALE. Implementer instruction: derive the actual intent list from the CURRENT stepped-rung API before building the wrapper, and post the derived list to the team lead for confirmation before implementation. Known mapping start (verify, don''t trust): select_system atlas_app._on_system_selected(system_id); select_body _on_body_selected(body); reset_view StepCanvasViewer._reset_to_global(); set_overlay set_overlay_visible(); back _on_regional_back(); zoom step _scroll_rung(direction, cursor_local); open_regional/select_city/open_system need tracing through screens/regional_screen.gd, district_screen.gd, system_screen.gd. SEQUENCING: T-1157 (stepped capture-harness redesign) needs the same walk-the-Atlas-headlessly primitive its harness-techniques inventory (appended 2026-07-25) documents five proven eyeball-driver patterns (InputSwallower, in-process viewer-method navigation, fixed-center revisit, view-transform logging) that are the PROTOTYPE for this channel; build the T-971 intent layer as the productionization of that pattern so T-1157''s harness can consume it rather than the two designing overlapping scaffolding independently.
Intent-vocabulary diff vs D-226''s original list (recorded at implementation, 2026-07-25): select_city and open_regional DROPPED post-D-255, settlements are settlement_id canvas cell values with no hit-test affordance (StepCanvasAnnotationLayer._draw_settlements() draws markers from cell data; no server get-settlement-by-id affordance, no client hit-test). A settlement-hit-test intent becomes its own ticket when a real consumer needs it not filed speculatively. ADDED beyond D-226: open_atlas (a driver must be able to start a session), jump_to_center (the fixed-center revisit pattern proven by five T-1157-inventory eyeball drivers, now first-class via the new StepCanvasViewer.jump_to seam), and open_body replacing open_regional (Region..Chunk is one screen; descent is scroll_rung, not screen pushes). get_layer_data_summary reports the real EncodedStepCanvas fields (courses/cliffs/settlements) D-226''s attractor/river/basin-count phrasing was retired-AtlasViewer vocabulary. Transport: in-process only this ticket; remote JSON transport is a follow-up when a remote consumer exists.', NULL, '2026-07-25 16:50:40', '2026-07-25 16:50:40.290', '2026-07-25 16:50:40.290', NULL, 'e6dfa5f5986853952eef1eac98269926', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRWTP633SM8XB302MBM', 'status', 'in_progress', 'review', NULL, '2026-07-25 16:57:34', '2026-07-25 16:57:34.623', '2026-07-25 16:57:34.623', NULL, '7b2b60bf4e2ba234c7fbf9fc2fafecc9', 2) ON CONFLICT(hash) DO NOTHING;
+79
View File
@@ -2880,3 +2880,82 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
Scope adjudication (2026-07-25, PR round, lead ruling): the "client label draw" clause is RE-SCOPED OUT of this ticket into T-1195, for rivers as well as mountains. Implementation found the ticket''s cheap-anchor premise false: the FeatureNamesRequest pool proxy is position-free (correctly mirroring CityNamesRequest per this ticket''s own instruction), and the client''s course polylines (river_course::InventedCourse) are not correlated with layer1::GeographicAttractor positions by construction so anchoring a pool name at a mouth ring has no matching basis without a wire-carried position, the identical dependency mountains have. Rather than two ad-hoc wire changes, the one design decision (name rides the course array vs a separate sparse feature-position list mirroring courses/cliffs) lands once in T-1195 for both feature types. This ticket''s delivered scope: atlas_feature_names populated at regen (17,891 rows), the FeatureNamesRequest/Response proxy end-to-end, attach_feature_names wired into the cascade with assignments stored on Layer1Output/BodyWorldState the three dormant pieces connected.', 'done', 'low', 'dudley', 'server', 'D-226', '2026-07-23 05:54:09.837', '2026-07-25 16:20:22.129', NULL, '9c327527100e094d1a6e34180ffec6e6', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FRMPTY517G7ZBMPAPMMCCMK8', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Retire the legacy window_granularity u32 wire field', 'PR #192 review (Tyre): window_granularity_v2 (the WindowGranularity enum) fully shadows the legacy u32 field — the server always echoes both, and the only conceivable legacy readers are pre-T-1152 clients, which do not exist (single-repo client/server pair, no external clients). Retire the u32 field + the WINDOW_GRANULARITY_REGION_KEY sentinel machinery from AtlasLayerRequest/DistrictWindowLayer once confirmed nothing reads it: server resolve_window_granularity legacy path, client encode path, the sentinel echo, and the five-touch-point key components that still carry the legacy value alongside the enum. Scheduled in the D-226 amendment refinement-semantics note (PR #192). Do AFTER T-1153''s follow-ups settle — no urgency, the dual-echo is cheap; the win is contract simplicity.', 'done', 'low', 'dudley', 'server', NULL, '2026-07-22 15:04:31.528', '2026-07-25 16:20:22.131', NULL, 'f6bfff77f8268ad8002aad9f799493f0', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRZ9Q7NHVDRT6ARMCWW', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'D-210 SubBiomeVariant implementation', 'D-210 defines SubBiomeVariant for fine-grained biome classification within SettingType::Wilderness districts. Currently deferred — SettingType::Wilderness carries a plain String biome field. Implement SubBiomeVariant enum and wire it into the atlas skeleton generator when the biome taxonomy is finalized.
Refinement review (2026-07-25, Si): OBSOLETE D-210''s design was fully implemented and merged in PR #953/#963 (commit d418eba8b, 2026-06-03), a month after this ticket was filed, and the ticket was never closed out. Evidence: server/src/atlas/subbiome.rs (classify/classify_variant/base_cost, all 11 variants + terrain_modification_cost from the four D-210 signals, unit-tested); SubBiomeVariant enum + GeographicAttractor.sub_biome (generator.rs:539/:567); wired into the Layer-1 orchestrator (layer1.rs:209-215, Alpine branch tested); consumed downstream (attractor_matching.rs, cascade.rs); client overlay live (atlas_overlay_colors.gd:203 sub_biome_color). The only literal residue SettingType::Wilderness carrying the cosmetic `Biome = String` alias is unrelated to D-210''s classification/cost system; tighten it via a separate small ticket if ever wanted. Closing as delivered-by-#953.', 'backlog', 'low', NULL, 'server', NULL, '2026-05-03 10:30:14', '2026-07-25 16:25:46.604', NULL, 'b7a8c5833ac90ea380b52b32d48580b1', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRZ9Q7NHVDRT6ARMCWW', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'D-210 SubBiomeVariant implementation', 'D-210 defines SubBiomeVariant for fine-grained biome classification within SettingType::Wilderness districts. Currently deferred — SettingType::Wilderness carries a plain String biome field. Implement SubBiomeVariant enum and wire it into the atlas skeleton generator when the biome taxonomy is finalized.
Refinement review (2026-07-25, Si): OBSOLETE D-210''s design was fully implemented and merged in PR #953/#963 (commit d418eba8b, 2026-06-03), a month after this ticket was filed, and the ticket was never closed out. Evidence: server/src/atlas/subbiome.rs (classify/classify_variant/base_cost, all 11 variants + terrain_modification_cost from the four D-210 signals, unit-tested); SubBiomeVariant enum + GeographicAttractor.sub_biome (generator.rs:539/:567); wired into the Layer-1 orchestrator (layer1.rs:209-215, Alpine branch tested); consumed downstream (attractor_matching.rs, cascade.rs); client overlay live (atlas_overlay_colors.gd:203 sub_biome_color). The only literal residue SettingType::Wilderness carrying the cosmetic `Biome = String` alias is unrelated to D-210''s classification/cost system; tighten it via a separate small ticket if ever wanted. Closing as delivered-by-#953.', 'done', 'low', NULL, 'server', NULL, '2026-05-03 10:30:14', '2026-07-25 16:25:46.760', NULL, 'e6aa010c5e0067704611e73147fbffed', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRZZ5XKET93HPHKQHKW', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Phase-4 test hardening — deferred review gaps (#953/#963 review)', 'Follow-ups from the Hoshe/Tyre pre-bake review (the BLOCKER/MAJOR items were fixed inline; these are the deferred MINOR/coverage items):
TESTS:
- features.rs: full per-type attractor reachability fixtures craft heightmaps that guarantee LakeShore (enclosed depression), PassEntrance (saddle), PlainCenter (flat), RiverCrossing (confluence). Currently only RiverMouth/CoastalAccess/ValleyFloor are exercised on the slope/sine fixtures.
- heightmap.rs: 8-bit grayscale decode path; sea_level passthrough assertion; downsample identity/upsample early-return.
- drainage.rs: basin area_pct bit-for-bit determinism (only river_cells + basin count checked today).
- layer1.rs: attach_feature_names mountain branch with a non-empty mountain pool + 1 Alpine attractor.
- features.rs: thin_by_spacing behavior (spacing collisions, equirectangular column wrap).
- import_economics.py: automated idempotency test two runs on a scratch DB yield identical atlas_city_names count (not doubled), and 0 rows for system ''GJ 0'' (Sol exemption). Wire alongside check-systems-db / pre-push.
- Wire test_sim_determinism.py into pre-push or a make target.
MINOR CODE NITS:
- drainage.rs ~480-488: comment the intentional isolated-basin fallback divergence from the old merge-to-basin-0 behavior (Tyre N1).
- planet_simulation.py ~787-788: oasis_water binary_dilation iterations are fixed-pixel scale by GRID_W/512 for biome-display consistency at 1024 (cosmetic, Atlas only, not cascade; Tyre N2).
D-256/T-1174 finding (2026-07-25): the believability harness golden is INSENSITIVE to a total basin_direction regression an all-North basin collapse (every survey cell defaulting) produced a byte-identical believability.json at current sample density (64/2048 districts; basin only reaches voxel meander phase/channel width, too localized for the coarse aggregate stats). The cross-namespace keying seam is now covered by the restored derive_all_districts_threads_supplied_basin_directions test (production-shaped survey keys), but the harness itself has no basin-sensitive metric. Candidate hardening: a basin-direction distribution stat (distinct directions >= 2 on a drained body) or a voxel-transect metric that moves with meander phase.
Refinement trim (2026-07-25, Si every listed gap re-verified against current code before this batch): (1) DROP the test_sim_determinism.py wiring item already done, Makefile:256 wires it into make test-tooling which the push gate runs on tooling changes. (2) The drainage.rs isolated-basin comment nit moved to ~line 654 (file grew; the old ~480-488 cite is stale) still open, just relocate. (3) The layer1.rs attach_feature_names mountain-branch item: implicit coverage may already exist via run_layer1_* tests check before adding a redundant test. (4) The D-256/T-1174 addendum''s "basin-direction distribution stat" candidate hardening is a genuine open design question (which stat, what threshold) PROPOSE the stat to the lead before coding it, don''t invent silently. All other items (features.rs per-type reachability fixtures + thin_by_spacing, heightmap.rs 8-bit/sea-level-passthrough/upsample-identity, drainage.rs area_pct bit-determinism, import_economics.py idempotency test, oasis_water binary_dilation nit at planet_simulation.py:784-788) verified still open as written.', 'backlog', 'medium', NULL, 'server', NULL, '2026-05-23 06:09:45', '2026-07-25 16:25:56.990', NULL, 'e89f228c33aefc802abd1b95dabe078e', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRWTP633SM8XB302MBM', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas agent control channel — observe/act named-intent navigation (D-226)', 'D-226 layer 4 (apex). A client-side AtlasAgentInterface exposing a JSON control channel so an automated agent (terminal/curl, headless Godot) can navigate the REAL Atlas UI: observe -> JSON of (a) current data state shown and (b) a walkable UI affordance tree (toggle controls: id/label/state/locked); act -> named SEMANTIC intents (select_system/open_system/select_body/open_regional/select_city/set_overlay/reset_view/back/close_atlas) backed by the same handlers a click calls (the map uses _gui_input, NOT pixel-clicks), plus get_layer_data_summary (attractor/river/basin counts as JSON without rendering). Single AtlasAgentInterface autoload/static class for a stable contract; generic Control-tree walk so economics/saves plug in later. Turns human-eyeball review into an agent QA sweep. Depends on #960 (the viewer) + the auto-pause substrate. Screenshot capture is a separate ticket (kept off the critical path).
Re-scope (2026-07-25, Si refinement + lead ruling): both cited blockers are satisfied (T-960, T-970 done) and the D-255(d) deep-rung cache cap is already built defensively for this exact consumer (step_canvas_disk_cache.gd:61 names the AtlasAgentInterface QA sweep). But D-226''s named-intent list was designed against the retired pre-D-255 continuous-zoom Atlas the ticket''s intent surface is STALE. Implementer instruction: derive the actual intent list from the CURRENT stepped-rung API before building the wrapper, and post the derived list to the team lead for confirmation before implementation. Known mapping start (verify, don''t trust): select_system atlas_app._on_system_selected(system_id); select_body _on_body_selected(body); reset_view StepCanvasViewer._reset_to_global(); set_overlay set_overlay_visible(); back _on_regional_back(); zoom step _scroll_rung(direction, cursor_local); open_regional/select_city/open_system need tracing through screens/regional_screen.gd, district_screen.gd, system_screen.gd. SEQUENCING: T-1157 (stepped capture-harness redesign) needs the same walk-the-Atlas-headlessly primitive its harness-techniques inventory (appended 2026-07-25) documents five proven eyeball-driver patterns (InputSwallower, in-process viewer-method navigation, fixed-center revisit, view-transform logging) that are the PROTOTYPE for this channel; build the T-971 intent layer as the productionization of that pattern so T-1157''s harness can consume it rather than the two designing overlapping scaffolding independently.', 'backlog', 'medium', NULL, 'client', 'D-226', '2026-05-24 09:22:38', '2026-07-25 16:26:05.492', NULL, 'ebc75290cb615984581a87c04a0d88b5', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRZZ5XKET93HPHKQHKW', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Phase-4 test hardening — deferred review gaps (#953/#963 review)', 'Follow-ups from the Hoshe/Tyre pre-bake review (the BLOCKER/MAJOR items were fixed inline; these are the deferred MINOR/coverage items):
TESTS:
- features.rs: full per-type attractor reachability fixtures craft heightmaps that guarantee LakeShore (enclosed depression), PassEntrance (saddle), PlainCenter (flat), RiverCrossing (confluence). Currently only RiverMouth/CoastalAccess/ValleyFloor are exercised on the slope/sine fixtures.
- heightmap.rs: 8-bit grayscale decode path; sea_level passthrough assertion; downsample identity/upsample early-return.
- drainage.rs: basin area_pct bit-for-bit determinism (only river_cells + basin count checked today).
- layer1.rs: attach_feature_names mountain branch with a non-empty mountain pool + 1 Alpine attractor.
- features.rs: thin_by_spacing behavior (spacing collisions, equirectangular column wrap).
- import_economics.py: automated idempotency test two runs on a scratch DB yield identical atlas_city_names count (not doubled), and 0 rows for system ''GJ 0'' (Sol exemption). Wire alongside check-systems-db / pre-push.
- Wire test_sim_determinism.py into pre-push or a make target.
MINOR CODE NITS:
- drainage.rs ~480-488: comment the intentional isolated-basin fallback divergence from the old merge-to-basin-0 behavior (Tyre N1).
- planet_simulation.py ~787-788: oasis_water binary_dilation iterations are fixed-pixel scale by GRID_W/512 for biome-display consistency at 1024 (cosmetic, Atlas only, not cascade; Tyre N2).
D-256/T-1174 finding (2026-07-25): the believability harness golden is INSENSITIVE to a total basin_direction regression an all-North basin collapse (every survey cell defaulting) produced a byte-identical believability.json at current sample density (64/2048 districts; basin only reaches voxel meander phase/channel width, too localized for the coarse aggregate stats). The cross-namespace keying seam is now covered by the restored derive_all_districts_threads_supplied_basin_directions test (production-shaped survey keys), but the harness itself has no basin-sensitive metric. Candidate hardening: a basin-direction distribution stat (distinct directions >= 2 on a drained body) or a voxel-transect metric that moves with meander phase.
Refinement trim (2026-07-25, Si every listed gap re-verified against current code before this batch): (1) DROP the test_sim_determinism.py wiring item already done, Makefile:256 wires it into make test-tooling which the push gate runs on tooling changes. (2) The drainage.rs isolated-basin comment nit moved to ~line 654 (file grew; the old ~480-488 cite is stale) still open, just relocate. (3) The layer1.rs attach_feature_names mountain-branch item: implicit coverage may already exist via run_layer1_* tests check before adding a redundant test. (4) The D-256/T-1174 addendum''s "basin-direction distribution stat" candidate hardening is a genuine open design question (which stat, what threshold) PROPOSE the stat to the lead before coding it, don''t invent silently. All other items (features.rs per-type reachability fixtures + thin_by_spacing, heightmap.rs 8-bit/sea-level-passthrough/upsample-identity, drainage.rs area_pct bit-determinism, import_economics.py idempotency test, oasis_water binary_dilation nit at planet_simulation.py:784-788) verified still open as written.', 'in_progress', 'medium', NULL, 'server', NULL, '2026-05-23 06:09:45', '2026-07-25 16:26:10.181', NULL, '1330970c9d7ec160af9d9b6f116152a4', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRWTP633SM8XB302MBM', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas agent control channel — observe/act named-intent navigation (D-226)', 'D-226 layer 4 (apex). A client-side AtlasAgentInterface exposing a JSON control channel so an automated agent (terminal/curl, headless Godot) can navigate the REAL Atlas UI: observe -> JSON of (a) current data state shown and (b) a walkable UI affordance tree (toggle controls: id/label/state/locked); act -> named SEMANTIC intents (select_system/open_system/select_body/open_regional/select_city/set_overlay/reset_view/back/close_atlas) backed by the same handlers a click calls (the map uses _gui_input, NOT pixel-clicks), plus get_layer_data_summary (attractor/river/basin counts as JSON without rendering). Single AtlasAgentInterface autoload/static class for a stable contract; generic Control-tree walk so economics/saves plug in later. Turns human-eyeball review into an agent QA sweep. Depends on #960 (the viewer) + the auto-pause substrate. Screenshot capture is a separate ticket (kept off the critical path).
Re-scope (2026-07-25, Si refinement + lead ruling): both cited blockers are satisfied (T-960, T-970 done) and the D-255(d) deep-rung cache cap is already built defensively for this exact consumer (step_canvas_disk_cache.gd:61 names the AtlasAgentInterface QA sweep). But D-226''s named-intent list was designed against the retired pre-D-255 continuous-zoom Atlas the ticket''s intent surface is STALE. Implementer instruction: derive the actual intent list from the CURRENT stepped-rung API before building the wrapper, and post the derived list to the team lead for confirmation before implementation. Known mapping start (verify, don''t trust): select_system atlas_app._on_system_selected(system_id); select_body _on_body_selected(body); reset_view StepCanvasViewer._reset_to_global(); set_overlay set_overlay_visible(); back _on_regional_back(); zoom step _scroll_rung(direction, cursor_local); open_regional/select_city/open_system need tracing through screens/regional_screen.gd, district_screen.gd, system_screen.gd. SEQUENCING: T-1157 (stepped capture-harness redesign) needs the same walk-the-Atlas-headlessly primitive its harness-techniques inventory (appended 2026-07-25) documents five proven eyeball-driver patterns (InputSwallower, in-process viewer-method navigation, fixed-center revisit, view-transform logging) that are the PROTOTYPE for this channel; build the T-971 intent layer as the productionization of that pattern so T-1157''s harness can consume it rather than the two designing overlapping scaffolding independently.', 'in_progress', 'medium', NULL, 'client', 'D-226', '2026-05-24 09:22:38', '2026-07-25 16:26:10.188', NULL, 'e6121a05892f9e4e72c7974d4b0faf96', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRZZ5XKET93HPHKQHKW', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Phase-4 test hardening — deferred review gaps (#953/#963 review)', 'Follow-ups from the Hoshe/Tyre pre-bake review (the BLOCKER/MAJOR items were fixed inline; these are the deferred MINOR/coverage items):
TESTS:
- features.rs: full per-type attractor reachability fixtures craft heightmaps that guarantee LakeShore (enclosed depression), PassEntrance (saddle), PlainCenter (flat), RiverCrossing (confluence). Currently only RiverMouth/CoastalAccess/ValleyFloor are exercised on the slope/sine fixtures.
- heightmap.rs: 8-bit grayscale decode path; sea_level passthrough assertion; downsample identity/upsample early-return.
- drainage.rs: basin area_pct bit-for-bit determinism (only river_cells + basin count checked today).
- layer1.rs: attach_feature_names mountain branch with a non-empty mountain pool + 1 Alpine attractor.
- features.rs: thin_by_spacing behavior (spacing collisions, equirectangular column wrap).
- import_economics.py: automated idempotency test two runs on a scratch DB yield identical atlas_city_names count (not doubled), and 0 rows for system ''GJ 0'' (Sol exemption). Wire alongside check-systems-db / pre-push.
- Wire test_sim_determinism.py into pre-push or a make target.
MINOR CODE NITS:
- drainage.rs ~480-488: comment the intentional isolated-basin fallback divergence from the old merge-to-basin-0 behavior (Tyre N1).
- planet_simulation.py ~787-788: oasis_water binary_dilation iterations are fixed-pixel scale by GRID_W/512 for biome-display consistency at 1024 (cosmetic, Atlas only, not cascade; Tyre N2).
D-256/T-1174 finding (2026-07-25): the believability harness golden is INSENSITIVE to a total basin_direction regression an all-North basin collapse (every survey cell defaulting) produced a byte-identical believability.json at current sample density (64/2048 districts; basin only reaches voxel meander phase/channel width, too localized for the coarse aggregate stats). The cross-namespace keying seam is now covered by the restored derive_all_districts_threads_supplied_basin_directions test (production-shaped survey keys), but the harness itself has no basin-sensitive metric. Candidate hardening: a basin-direction distribution stat (distinct directions >= 2 on a drained body) or a voxel-transect metric that moves with meander phase.
Refinement trim (2026-07-25, Si every listed gap re-verified against current code before this batch): (1) DROP the test_sim_determinism.py wiring item already done, Makefile:256 wires it into make test-tooling which the push gate runs on tooling changes. (2) The drainage.rs isolated-basin comment nit moved to ~line 654 (file grew; the old ~480-488 cite is stale) still open, just relocate. (3) The layer1.rs attach_feature_names mountain-branch item: implicit coverage may already exist via run_layer1_* tests check before adding a redundant test. (4) The D-256/T-1174 addendum''s "basin-direction distribution stat" candidate hardening is a genuine open design question (which stat, what threshold) PROPOSE the stat to the lead before coding it, don''t invent silently. All other items (features.rs per-type reachability fixtures + thin_by_spacing, heightmap.rs 8-bit/sea-level-passthrough/upsample-identity, drainage.rs area_pct bit-determinism, import_economics.py idempotency test, oasis_water binary_dilation nit at planet_simulation.py:784-788) verified still open as written.', 'in_progress', 'medium', 'dudley', 'server', NULL, '2026-05-23 06:09:45', '2026-07-25 16:26:11.159', NULL, 'd2bc3750b255e45527ac695220f93c8d', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRWTP633SM8XB302MBM', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas agent control channel — observe/act named-intent navigation (D-226)', 'D-226 layer 4 (apex). A client-side AtlasAgentInterface exposing a JSON control channel so an automated agent (terminal/curl, headless Godot) can navigate the REAL Atlas UI: observe -> JSON of (a) current data state shown and (b) a walkable UI affordance tree (toggle controls: id/label/state/locked); act -> named SEMANTIC intents (select_system/open_system/select_body/open_regional/select_city/set_overlay/reset_view/back/close_atlas) backed by the same handlers a click calls (the map uses _gui_input, NOT pixel-clicks), plus get_layer_data_summary (attractor/river/basin counts as JSON without rendering). Single AtlasAgentInterface autoload/static class for a stable contract; generic Control-tree walk so economics/saves plug in later. Turns human-eyeball review into an agent QA sweep. Depends on #960 (the viewer) + the auto-pause substrate. Screenshot capture is a separate ticket (kept off the critical path).
Re-scope (2026-07-25, Si refinement + lead ruling): both cited blockers are satisfied (T-960, T-970 done) and the D-255(d) deep-rung cache cap is already built defensively for this exact consumer (step_canvas_disk_cache.gd:61 names the AtlasAgentInterface QA sweep). But D-226''s named-intent list was designed against the retired pre-D-255 continuous-zoom Atlas the ticket''s intent surface is STALE. Implementer instruction: derive the actual intent list from the CURRENT stepped-rung API before building the wrapper, and post the derived list to the team lead for confirmation before implementation. Known mapping start (verify, don''t trust): select_system atlas_app._on_system_selected(system_id); select_body _on_body_selected(body); reset_view StepCanvasViewer._reset_to_global(); set_overlay set_overlay_visible(); back _on_regional_back(); zoom step _scroll_rung(direction, cursor_local); open_regional/select_city/open_system need tracing through screens/regional_screen.gd, district_screen.gd, system_screen.gd. SEQUENCING: T-1157 (stepped capture-harness redesign) needs the same walk-the-Atlas-headlessly primitive its harness-techniques inventory (appended 2026-07-25) documents five proven eyeball-driver patterns (InputSwallower, in-process viewer-method navigation, fixed-center revisit, view-transform logging) that are the PROTOTYPE for this channel; build the T-971 intent layer as the productionization of that pattern so T-1157''s harness can consume it rather than the two designing overlapping scaffolding independently.', 'in_progress', 'medium', 'stig', 'client', 'D-226', '2026-05-24 09:22:38', '2026-07-25 16:26:11.308', NULL, 'c61776c96a1110fae0686f2eb4927fcd', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRWTP633SM8XB302MBM', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas agent control channel — observe/act named-intent navigation (D-226)', 'D-226 layer 4 (apex). A client-side AtlasAgentInterface exposing a JSON control channel so an automated agent (terminal/curl, headless Godot) can navigate the REAL Atlas UI: observe -> JSON of (a) current data state shown and (b) a walkable UI affordance tree (toggle controls: id/label/state/locked); act -> named SEMANTIC intents (select_system/open_system/select_body/open_regional/select_city/set_overlay/reset_view/back/close_atlas) backed by the same handlers a click calls (the map uses _gui_input, NOT pixel-clicks), plus get_layer_data_summary (attractor/river/basin counts as JSON without rendering). Single AtlasAgentInterface autoload/static class for a stable contract; generic Control-tree walk so economics/saves plug in later. Turns human-eyeball review into an agent QA sweep. Depends on #960 (the viewer) + the auto-pause substrate. Screenshot capture is a separate ticket (kept off the critical path).
Re-scope (2026-07-25, Si refinement + lead ruling): both cited blockers are satisfied (T-960, T-970 done) and the D-255(d) deep-rung cache cap is already built defensively for this exact consumer (step_canvas_disk_cache.gd:61 names the AtlasAgentInterface QA sweep). But D-226''s named-intent list was designed against the retired pre-D-255 continuous-zoom Atlas the ticket''s intent surface is STALE. Implementer instruction: derive the actual intent list from the CURRENT stepped-rung API before building the wrapper, and post the derived list to the team lead for confirmation before implementation. Known mapping start (verify, don''t trust): select_system atlas_app._on_system_selected(system_id); select_body _on_body_selected(body); reset_view StepCanvasViewer._reset_to_global(); set_overlay set_overlay_visible(); back _on_regional_back(); zoom step _scroll_rung(direction, cursor_local); open_regional/select_city/open_system need tracing through screens/regional_screen.gd, district_screen.gd, system_screen.gd. SEQUENCING: T-1157 (stepped capture-harness redesign) needs the same walk-the-Atlas-headlessly primitive its harness-techniques inventory (appended 2026-07-25) documents five proven eyeball-driver patterns (InputSwallower, in-process viewer-method navigation, fixed-center revisit, view-transform logging) that are the PROTOTYPE for this channel; build the T-971 intent layer as the productionization of that pattern so T-1157''s harness can consume it rather than the two designing overlapping scaffolding independently.
Intent-vocabulary diff vs D-226''s original list (recorded at implementation, 2026-07-25): select_city and open_regional DROPPED post-D-255, settlements are settlement_id canvas cell values with no hit-test affordance (StepCanvasAnnotationLayer._draw_settlements() draws markers from cell data; no server get-settlement-by-id affordance, no client hit-test). A settlement-hit-test intent becomes its own ticket when a real consumer needs it not filed speculatively. ADDED beyond D-226: open_atlas (a driver must be able to start a session), jump_to_center (the fixed-center revisit pattern proven by five T-1157-inventory eyeball drivers, now first-class via the new StepCanvasViewer.jump_to seam), and open_body replacing open_regional (Region..Chunk is one screen; descent is scroll_rung, not screen pushes). get_layer_data_summary reports the real EncodedStepCanvas fields (courses/cliffs/settlements) D-226''s attractor/river/basin-count phrasing was retired-AtlasViewer vocabulary. Transport: in-process only this ticket; remote JSON transport is a follow-up when a remote consumer exists.', 'in_progress', 'medium', 'stig', 'client', 'D-226', '2026-05-24 09:22:38', '2026-07-25 16:50:40.290', NULL, '5889e54d53d1d2a8f1457cfb56c2f699', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
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 ('06FB0TNSRWTP633SM8XB302MBM', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas agent control channel — observe/act named-intent navigation (D-226)', 'D-226 layer 4 (apex). A client-side AtlasAgentInterface exposing a JSON control channel so an automated agent (terminal/curl, headless Godot) can navigate the REAL Atlas UI: observe -> JSON of (a) current data state shown and (b) a walkable UI affordance tree (toggle controls: id/label/state/locked); act -> named SEMANTIC intents (select_system/open_system/select_body/open_regional/select_city/set_overlay/reset_view/back/close_atlas) backed by the same handlers a click calls (the map uses _gui_input, NOT pixel-clicks), plus get_layer_data_summary (attractor/river/basin counts as JSON without rendering). Single AtlasAgentInterface autoload/static class for a stable contract; generic Control-tree walk so economics/saves plug in later. Turns human-eyeball review into an agent QA sweep. Depends on #960 (the viewer) + the auto-pause substrate. Screenshot capture is a separate ticket (kept off the critical path).
Re-scope (2026-07-25, Si refinement + lead ruling): both cited blockers are satisfied (T-960, T-970 done) and the D-255(d) deep-rung cache cap is already built defensively for this exact consumer (step_canvas_disk_cache.gd:61 names the AtlasAgentInterface QA sweep). But D-226''s named-intent list was designed against the retired pre-D-255 continuous-zoom Atlas the ticket''s intent surface is STALE. Implementer instruction: derive the actual intent list from the CURRENT stepped-rung API before building the wrapper, and post the derived list to the team lead for confirmation before implementation. Known mapping start (verify, don''t trust): select_system atlas_app._on_system_selected(system_id); select_body _on_body_selected(body); reset_view StepCanvasViewer._reset_to_global(); set_overlay set_overlay_visible(); back _on_regional_back(); zoom step _scroll_rung(direction, cursor_local); open_regional/select_city/open_system need tracing through screens/regional_screen.gd, district_screen.gd, system_screen.gd. SEQUENCING: T-1157 (stepped capture-harness redesign) needs the same walk-the-Atlas-headlessly primitive its harness-techniques inventory (appended 2026-07-25) documents five proven eyeball-driver patterns (InputSwallower, in-process viewer-method navigation, fixed-center revisit, view-transform logging) that are the PROTOTYPE for this channel; build the T-971 intent layer as the productionization of that pattern so T-1157''s harness can consume it rather than the two designing overlapping scaffolding independently.
Intent-vocabulary diff vs D-226''s original list (recorded at implementation, 2026-07-25): select_city and open_regional DROPPED post-D-255, settlements are settlement_id canvas cell values with no hit-test affordance (StepCanvasAnnotationLayer._draw_settlements() draws markers from cell data; no server get-settlement-by-id affordance, no client hit-test). A settlement-hit-test intent becomes its own ticket when a real consumer needs it not filed speculatively. ADDED beyond D-226: open_atlas (a driver must be able to start a session), jump_to_center (the fixed-center revisit pattern proven by five T-1157-inventory eyeball drivers, now first-class via the new StepCanvasViewer.jump_to seam), and open_body replacing open_regional (Region..Chunk is one screen; descent is scroll_rung, not screen pushes). get_layer_data_summary reports the real EncodedStepCanvas fields (courses/cliffs/settlements) D-226''s attractor/river/basin-count phrasing was retired-AtlasViewer vocabulary. Transport: in-process only this ticket; remote JSON transport is a follow-up when a remote consumer exists.', 'review', 'medium', 'stig', 'client', 'D-226', '2026-05-24 09:22:38', '2026-07-25 16:57:34.622', NULL, 'e938b340830f851c004b4271226430ec', 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 OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+3
View File
@@ -6,6 +6,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Added
- **The Atlas can be driven by agents** (D-226, T-971) — a named-intent control channel (AtlasAgentInterface) lets automated QA agents navigate the real Atlas UI through the exact same handlers a click calls: open/select/descend, jump straight to any world coordinate at any zoom step, toggle overlays, and read back a summary of what's on screen — turning the hand-scripted verification drives into a first-class, repeatable QA capability. In-process consumers only for now; a shipped reference driver replaces the ad-hoc scripts
### Changed
- **Rivers read like rivers** (T-1175) — map courses now taper to a point at their upstream source instead of starting at full width (the classic cartographic river grammar), and the stream/tributary/trunk width ladder was retuned so a tributary joining a trunk visibly reads as a join. Part of the map-fluency polish pass benchmarked against the best-in-class world maps
- **Rivers and mountain ranges now have names behind the map** (T-1169) — every body's rivers and peaks are assigned names from the curated per-system pools (17,891 names across the Reach) during world generation, queryable over the wire. The labels that will draw them on the Atlas come in a follow-up; the naming layer underneath is live
+1
View File
@@ -32,6 +32,7 @@ HudGroups="*res://scripts/autoloads/hud_groups.gd"
MetaStack="*res://ui/meta/meta_stack.gd"
ImplantRegistry="*res://ui/implant/implant_registry.gd"
HardwareDetector="*res://ui/hardware_detector.gd"
AtlasAgentBridge="*res://scripts/autoloads/atlas_agent_bridge.gd"
[audio]
@@ -0,0 +1,29 @@
extends Node
## AtlasAgentBridge (T-971, D-226 item 4) — the ONE stable, always-reachable
## handle onto the live AtlasApp instance, so an in-process agent driver (a
## `-s` SceneTree script, a gdUnit suite, the T-1157 capture harness) has a
## fixed autoload address to start from instead of walking the scene tree
## itself.
##
## Deliberately a DUMB holder, per CLAUDE.md's autoload parse-order rule: an
## autoload script compiles before global `class_name` scripts are
## registered, so referencing a `class_name` type (AtlasApp, AtlasAgent
## Interface) at this file's top level or in _ready() would fail to
## resolve. This file therefore declares its field UNTYPED (`var
## current_app = null`) and does nothing else — every actual observe()/act()
## call is a STATIC method on AtlasAgentInterface
## (res://ui/implant/apps/atlas/atlas_agent_interface.gd), loaded via
## `load()` inline at call time by whatever script needs it (the same
## deferred-load pattern game_state.gd/sim_bridge.gd already use for their
## own class_name-typed fields). This autoload carries no logic of its own
## to keep that boundary clean — "dumb holder delegating to a static class",
## per the ruling.
##
## `current_app` is set by AtlasApp.on_install() (the same lifecycle point
## ImplantApp guarantees runs once, before any HudGroups open event) and
## cleared by nothing — an installed app instance lives for the process
## lifetime, matching every other ImplantApp subclass's own singleton-per-
## process posture (HudGroups.register() itself assumes one instance per
## app_path).
var current_app = null # AtlasApp, set by atlas_app.gd's on_install()
+254
View File
@@ -0,0 +1,254 @@
## Reference in-process Atlas agent driver (T-971, D-226 item 4) — the
## SANCTIONED way to drive the Atlas headlessly via AtlasAgentInterface,
## replacing the five hand-rolled scratch eyeball drivers the T-1183/T-1157
## harness rounds produced ad hoc. Every real navigation call below goes
## through AtlasAgentInterface.act()/observe() — this file does not touch
## `_gui_input`, does not synthesize pixel events, and does not reach into
## any screen's private fields.
##
## Folds in two of the T-1157 harness-techniques inventory patterns
## (pql ticket show T-1157):
## 1. InputSwallower — a root-level Node, added first, low process_priority,
## whose _input()/_unhandled_input() both call
## get_viewport().set_input_as_handled() unconditionally. Neutralizes
## real desktop input reaching this `-s` SceneTree driver's window
## (X11/XWayland can deliver pointer events to an unfocused window under
## the cursor on a shared desktop) — this driver runs real production
## code via method calls, but the WINDOW still exists and can still
## receive stray input, so the guard applies here too.
## 4. Full-run-restart-on-anomaly (documented, not code-enforced — see
## _run() header note below): any assertion failure here should discard
## the whole run rather than retrying mid-sequence, matching the
## inventory's "partial retries silently corrupt the state the
## comparison depends on" finding. This driver's own job list is short
## and idempotent per invocation (fresh process each run), so the
## discipline is "don't loop-retry a failed step in place" rather than a
## literal restart mechanism.
##
## Technique 5 (fixed-center revisit) is what `jump_to_center` (this file's
## own `_run_job("jump_to_center", ...)` dispatch) exists to exercise —
## AtlasAgentInterface's production seam for it, not reimplemented here.
##
## PR #209 review (Hoshe finding 2) — settle discipline after a mutating
## intent: a FIXED `for i in range(4)` frame count (the pre-fix shape) is a
## regression against the proven is_pending()-aware pattern every eyeball
## driver round already established (visual_capture.gd's own
## _wait_for_atlas_layers_ready()) — a slow live server round-trip leaves the
## NEXT job's observe() reading a mid-fetch viewer, exactly the class of bug
## the whole T-1157 "settle-until-ready" technique exists to prevent.
## _settle_after_intent() below polls StepCanvasRequest.is_pending() (via the
## regional viewer's own get_request()) when "regional" is the current screen
## — the only screen a step-canvas fetch could be in flight for — with a
## bounded max-frame fallback (SETTLE_MAX_FRAMES, matching how the eyeball
## drivers waited: a generous multi-second bound, never an unbounded await).
## Every OTHER screen-targeted intent (reach/system) has no request to wait
## on, so it keeps the small fixed settle (SETTLE_FIXED_FRAMES) for its own
## panel-rebuild/queue_redraw() to apply.
##
## Usage (JSON job list on stdin path, matching visual_capture.gd's own
## `-- --flag value` CLI convention):
## godot --rendering-driver opengl3 --path client \
## -s res://tests/atlas_agent_driver.gd -- \
## --jobs res://tests/fixtures/atlas_agent_smoke_jobs.json \
## --output /abs/path/observe_log.json
##
## Each job is {"intent": "...", "params": {...}}; "observe" is a
## pseudo-intent this driver special-cases to call
## AtlasAgentInterface.observe() instead of act() (observe takes no params).
## The full sequence of act()/observe() results is written to --output as a
## JSON array — the smoke test (test_atlas_agent_driver_smoke.gd) reads this
## back to assert the reference driver actually completes a real session.
extends SceneTree
## Fixed settle for a mutating intent with no in-flight request to poll
## (reach/system screen intents — a panel rebuild/queue_redraw() needs at
## most a couple of frames, never a network round-trip).
const SETTLE_FIXED_FRAMES: int = 4
## Bounded fallback for the is_pending()-aware settle on "regional" —
## generous, matching how the T-1157 eyeball drivers waited (multi-second
## bound at 60fps), never an unbounded await. A request that's STILL pending
## after this many frames is a real timeout, not "give it a bit longer" —
## the driver logs it and moves on rather than hanging the whole job list.
const SETTLE_MAX_FRAMES: int = 600
var _jobs_path: String = ""
var _output_path: String = ""
var _results: Array = []
func _init():
_run.call_deferred()
func _run() -> void:
_parse_args()
if _jobs_path.is_empty():
push_error("atlas_agent_driver: --jobs PATH is required")
quit(1)
return
if _output_path.is_empty():
push_error("atlas_agent_driver: --output PATH is required")
quit(1)
return
var jobs: Array = _load_jobs(_jobs_path)
if jobs.is_empty():
push_error("atlas_agent_driver: no jobs loaded from %s" % _jobs_path)
quit(1)
return
# SR_PORT wiring (PR #209 eyeball finding) — mirror visual_capture.gd's
# live-mode convention exactly: every sibling real-render driver
# (visual_capture, atlas_standalone, locomotion_sandbox) reads SR_PORT
# rather than silently relying on SimBridge's hardcoded default port;
# without this, a caller who started the server on a chosen port (the
# run-visual --port pattern) gets 20 silent connect retries against the
# wrong port and a hollow session whose act() results still have valid
# SHAPES but no data behind them. SR_LIVE=1 without SR_PORT is a hard
# error, same as visual_capture.gd.
if OS.get_environment("SR_LIVE") == "1":
var port_env := OS.get_environment("SR_PORT")
if port_env.is_empty():
push_error("atlas_agent_driver: SR_LIVE=1 but SR_PORT not set")
quit(1)
return
var sim_bridge := root.get_node("/root/SimBridge")
sim_bridge.server_port = int(port_env)
print("atlas_agent_driver: live mode — server port %d" % sim_bridge.server_port)
var main_scene = load("res://scenes/main.tscn")
var main_node = main_scene.instantiate()
# Technique 1 (T-1157 inventory) — InputSwallower, added FIRST so its low
# process_priority still runs before anything that might otherwise react
# to stray desktop input reaching this unfocused/off-screen window.
var swallower := _InputSwallower.new()
swallower.process_priority = -1000
root.add_child(swallower)
root.add_child(main_node)
# Settle frames — camera smoothing, fog uniform init, UI layout (same
# rationale as visual_capture.gd's own settle-frame pass).
for i in range(10):
await process_frame
var atlas_agent_interface := load("res://ui/implant/apps/atlas/atlas_agent_interface.gd")
var atlas_agent_bridge: Node = root.get_node("/root/AtlasAgentBridge")
for job: Dictionary in jobs:
var intent: String = str(job.get("intent", ""))
var params: Dictionary = job.get("params", {})
var result: Dictionary
if intent == "observe":
result = atlas_agent_interface.observe(atlas_agent_bridge.current_app)
else:
result = atlas_agent_interface.act(atlas_agent_bridge.current_app, intent, params)
await _settle_after_intent(atlas_agent_bridge.current_app)
_results.append({"intent": intent, "params": params, "result": result})
_write_output(_output_path, _results)
quit()
## PR #209 review (Hoshe finding 2) — see this file's own header doc for the
## full rationale. `app` may be null (e.g. after close_atlas) — a null app
## has nothing to poll, so this falls through to the fixed settle only. The
## bounded-fallback DECISION (keep waiting vs stop) is split out into the
## pure, no-await should_keep_waiting() below specifically so it's testable
## without a real SceneTree frame loop (test_atlas_agent_driver.gd drives it
## directly against a fake pending-state + frame counter).
func _settle_after_intent(app: Variant) -> void:
var viewer: Variant = _regional_viewer_if_current(app)
if viewer == null:
for i in range(SETTLE_FIXED_FRAMES):
await process_frame
return
var request: Variant = viewer.get_request()
var waited := 0
while should_keep_waiting(request.is_pending(), waited, SETTLE_MAX_FRAMES):
await process_frame
waited += 1
if request.is_pending():
print(
"atlas_agent_driver: settle timed out after %d frames — request still pending"
% SETTLE_MAX_FRAMES
)
## The pure bounded-fallback decision: keep waiting only while the request is
## STILL pending AND the frame budget isn't exhausted. Static + no SceneTree
## dependency — `is_pending`/`waited`/`max_frames` are plain values a test can
## supply directly, exercising the exact boundary conditions (pending forever
## past the bound stops; resolving early stops immediately) without needing
## a real request object or a real frame loop.
static func should_keep_waiting(is_pending: bool, waited: int, max_frames: int) -> bool:
return is_pending and waited < max_frames
## `app` may be null; "regional" may not even be registered yet this early in
## a session (e.g. before the first open_body). Returns null in either case
## rather than erroring — the caller's fixed-settle fallback covers it.
func _regional_viewer_if_current(app: Variant) -> Variant:
if app == null:
return null
if app.current_screen_id() != "regional":
return null
var regional_screen: Variant = app.get_screen("regional")
if regional_screen == null:
return null
return regional_screen.get_viewer()
func _parse_args() -> void:
var args := OS.get_cmdline_user_args()
var i := 0
while i < args.size():
match args[i]:
"--jobs":
i += 1
if i < args.size():
_jobs_path = args[i]
"--output":
i += 1
if i < args.size():
_output_path = args[i]
i += 1
static func _load_jobs(path: String) -> Array:
var abs_path: String = path
if path.begins_with("res://"):
abs_path = ProjectSettings.globalize_path(path)
var f := FileAccess.open(abs_path, FileAccess.READ)
if f == null:
return []
var text := f.get_as_text()
f.close()
var parsed: Variant = JSON.parse_string(text)
return parsed if parsed is Array else []
static func _write_output(path: String, results: Array) -> void:
var f := FileAccess.open(path, FileAccess.WRITE)
if f == null:
push_error("atlas_agent_driver: cannot open output path %s" % path)
return
f.store_string(JSON.stringify(results, " "))
f.close()
## T-1157 inventory item 1 — see this file's own header doc for the full
## rationale. A tiny inner class rather than a separate file: this pattern
## has exactly one consumer (this driver) today: promote to a shared
## `client/tests/` helper the moment a second `-s` driver needs it.
class _InputSwallower:
extends Node
func _input(_event: InputEvent) -> void:
get_viewport().set_input_as_handled()
func _unhandled_input(_event: InputEvent) -> void:
get_viewport().set_input_as_handled()
+6
View File
@@ -0,0 +1,6 @@
[
{"intent": "open_atlas", "params": {}},
{"intent": "observe", "params": {}},
{"intent": "close_atlas", "params": {}},
{"intent": "observe", "params": {}}
]
+106
View File
@@ -0,0 +1,106 @@
## T-971 smoke test for atlas_agent_driver.gd — the reference `-s` SceneTree
## driver for AtlasAgentInterface. A gdUnit suite cannot boot a SECOND full
## Godot process (the driver's real usage mode: `godot -s
## res://tests/atlas_agent_driver.gd -- --jobs ... --output ...`) without
## spawning a nested engine instance, which is slow/fragile and inconsistent
## with how this project already runs its OTHER `-s` drivers
## (visual_capture.gd/atlas_shots.json — manual `make` targets, never inside
## the gdUnit suite tests/run-godot invokes). The feasible headless smoke
## here is the driver's own PURE helper logic: job-file loading and
## result-array writing, both `static func`s with no SceneTree dependency —
## exercising the exact code path a real run's job-list parsing and
## output-writing go through, without booting a second engine.
class_name TestAtlasAgentDriver
extends GdUnitTestSuite
const DriverScript := preload("res://tests/atlas_agent_driver.gd")
var _tmp_dir: String = ""
func before_test() -> void:
_tmp_dir = "user://test_atlas_agent_driver/%d/" % Time.get_ticks_usec()
DirAccess.make_dir_recursive_absolute(_tmp_dir)
func after_test() -> void:
var d := DirAccess.open(_tmp_dir)
if d == null:
return
for f: String in d.get_files():
d.remove(f)
DirAccess.remove_absolute(_tmp_dir.rstrip("/"))
func test_load_jobs_reads_the_shipped_smoke_fixture() -> void:
var jobs: Array = DriverScript._load_jobs(
"res://tests/fixtures/atlas_agent_smoke_jobs.json"
)
assert_int(jobs.size()).is_equal(4)
assert_str((jobs[0] as Dictionary).get("intent", "")).is_equal("open_atlas")
assert_str((jobs[1] as Dictionary).get("intent", "")).is_equal("observe")
func test_load_jobs_returns_empty_array_for_a_missing_file() -> void:
var jobs: Array = DriverScript._load_jobs("res://tests/fixtures/does_not_exist.json")
assert_int(jobs.size()).is_equal(0)
func test_write_output_round_trips_through_json() -> void:
var out_path: String = _tmp_dir + "results.json"
var results: Array = [
{"intent": "observe", "params": {}, "result": {"screen": "reach"}},
{"intent": "select_system", "params": {"system_id": "GJ1"}, "result": {"ok": true}},
]
DriverScript._write_output(out_path, results)
var f := FileAccess.open(out_path, FileAccess.READ)
assert_object(f).is_not_null()
var parsed: Variant = JSON.parse_string(f.get_as_text())
f.close()
assert_that(parsed).is_equal(results)
# =============================================================================
# PR #209 review (Hoshe finding 2) — should_keep_waiting()'s bounded-fallback
# logic. Pure/static, no SceneTree dependency, so the boundary conditions are
# directly testable without a real frame loop or a real StepCanvasRequest.
# =============================================================================
func test_should_keep_waiting_true_while_pending_and_under_the_frame_budget() -> void:
assert_bool(DriverScript.should_keep_waiting(true, 0, 600)).is_true()
assert_bool(DriverScript.should_keep_waiting(true, 599, 600)).is_true()
func test_should_keep_waiting_false_once_no_longer_pending() -> void:
assert_bool(DriverScript.should_keep_waiting(false, 0, 600)).is_false()
func test_should_keep_waiting_false_once_the_frame_budget_is_exhausted() -> void:
# Still pending, but waited has reached max_frames — the bounded fallback
# must stop here rather than hanging indefinitely on a genuinely-stuck
# request (the whole reason SETTLE_MAX_FRAMES exists).
assert_bool(DriverScript.should_keep_waiting(true, 600, 600)).is_false()
assert_bool(DriverScript.should_keep_waiting(true, 601, 600)).is_false()
## The InputSwallower inner class (T-1157 inventory item 1) — a plain Node
## subclass with no SceneTree/window dependency for its OWN logic
## (set_input_as_handled() requires a live viewport to call meaningfully, but
## the class must at least instantiate and expose the two input hooks by
## name — this is the structural smoke check; the swallowing BEHAVIOR itself
## is exercised implicitly every time atlas_agent_driver.gd actually runs,
## per its own header doc).
func test_input_swallower_inner_class_instantiates_and_has_both_input_hooks() -> void:
# A nested `class X: extends Y` inside a script is exposed as a
# script-level constant on the OUTER GDScript resource — reachable via
# plain dot access on the preloaded DriverScript, same as any other
# nested-class reference in this codebase's own suites (e.g.
# GdUnitObjectAssertImplTest.gd's MyNode/MyExtendedNode pattern), just
# via a loaded resource instead of a same-file bare identifier.
var instance: Node = DriverScript._InputSwallower.new()
assert_bool(instance.has_method("_input")).is_true()
assert_bool(instance.has_method("_unhandled_input")).is_true()
instance.free()
+509
View File
@@ -0,0 +1,509 @@
## T-971 tests: AtlasAgentInterface — the observe/act named-intent channel.
## Every act() test asserts the intent reaches the SAME production state
## mutation the real click handler performs (spy at the handler seam: read
## the resulting screen/viewer state back, exactly as a real click's caller
## would observe it), never a synthesized _gui_input event. test_mode
## (SimBridge default outside SR_LIVE=1) means request_step_canvas() is a
## silent no-op — these tests exercise client-side state only, no live
## server needed, matching test_step_canvas_viewer.gd's own precedent.
class_name TestAtlasAgentInterface
extends GdUnitTestSuite
const AtlasAgentInterfaceScript := preload("res://ui/implant/apps/atlas/atlas_agent_interface.gd")
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
## AtlasApp._ready() unconditionally reloads the real app.tres manifest
## (`manifest = load("res://ui/implant/apps/atlas/app.tres")`), overwriting
## anything set before add_child() — so, unlike the generic ImplantApp
## lifecycle tests (which use a bare, manifest-injectable ImplantApp), a real
## AtlasApp instance ALWAYS registers under the production app_path. Tests
## drive HudGroups with THIS path, not a synthetic test-only one.
const TEST_APP_PATH := "implant/map"
const FIXTURE_SYSTEM := {
"system_id": "GJ1",
"proper_name": "Gliese J1",
"hop_distance": 0,
"orbit_bodies": [
{
"body_id": "GJ1b", "proper_name": "Barrenholt", "body_type": "planet",
"orbit_index": 0, "terrain_reference": "GJ1b_heightmap",
},
{
"body_id": "GJ1c", "proper_name": "Cindral", "body_type": "planet",
"orbit_index": 1, "terrain_reference": null,
},
],
"stations": [],
}
## Returns an AtlasApp instance (untyped — matches _make_app()'s own
## precedent in test_implant_app_lifecycle.gd, which keeps the return
## untyped there too for the same class_name parse-order reason). No manifest
## injection needed (unlike the generic ImplantApp lifecycle tests) —
## AtlasApp._ready() always loads the real production manifest itself.
func _make_app():
var AppClass := load("res://ui/implant/apps/atlas/atlas_app.gd")
var app = AppClass.new()
add_child(app) # fires _ready() -> on_install()
return app
func after_test() -> void:
HudGroups._active_app = ""
HudGroups._active_mode = HudGroups.Mode.GAMEPLAY
HudGroups._groups.erase(TEST_APP_PATH)
HudGroups._groups.erase("implant/map")
# PR #209 review (Tyre): every _make_app() call sets
# AtlasAgentBridge.current_app to the (now freed) instance via on_install()
# — clearing it here matches the HudGroups-reset discipline already above
# and closes the latent footgun of a later test/consumer reading a stale,
# freed-pending app reference.
AtlasAgentBridge.current_app = null
# =============================================================================
# observe()
# =============================================================================
func test_observe_reports_current_screen_id() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
var result: Dictionary = AtlasAgentInterfaceScript.observe(app)
assert_str(result.get("screen", "")).is_equal("reach")
app.queue_free()
func test_observe_returns_error_for_null_app() -> void:
var result: Dictionary = AtlasAgentInterfaceScript.observe(null)
assert_bool(result.has("error")).is_true()
func test_observe_affordance_tree_includes_overlay_bar_buttons_on_regional() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
app.nav.push("regional", {"body": {"body_id": "GJ1b"}, "system": FIXTURE_SYSTEM})
var result: Dictionary = AtlasAgentInterfaceScript.observe(app)
var affordances: Array = result.get("affordances", [])
assert_int(affordances.size()).is_greater(0)
app.queue_free()
func test_observe_is_side_effect_free() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
var before: String = app.current_screen_id()
AtlasAgentInterfaceScript.observe(app)
assert_str(app.current_screen_id()).is_equal(before)
app.queue_free()
# =============================================================================
# act() — reach screen intents
# =============================================================================
func test_select_system_reaches_reach_screen_selection_state() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
app.get_screen("reach").set_systems([FIXTURE_SYSTEM], {"GJ1": FIXTURE_SYSTEM})
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "select_system", {"system_id": "GJ1"})
assert_bool(result.get("ok", false)).is_true()
assert_bool(app.get_screen("reach").has_selection()).is_true()
app.queue_free()
func test_open_system_reaches_atlas_app_nav_push() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
app.get_screen("reach").set_systems([FIXTURE_SYSTEM], {"GJ1": FIXTURE_SYSTEM})
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "open_system", {"system_id": "GJ1"})
assert_bool(result.get("ok", false)).is_true()
assert_str(app.current_screen_id()).is_equal("system")
app.queue_free()
# =============================================================================
# act() — system screen intents
# =============================================================================
func _enter_orbital(app) -> void:
app.get_screen("system").set_systems([FIXTURE_SYSTEM])
app.nav.push("system", {"mode": "orbital", "system": FIXTURE_SYSTEM})
func test_select_body_reaches_system_screen_body_panel_state() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_orbital(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "select_body", {"body_id": "GJ1c"})
assert_bool(result.get("ok", false)).is_true()
assert_bool(app.get_screen("system").has_body_panel_open()).is_true()
app.queue_free()
func test_open_body_reaches_atlas_app_nav_push_to_regional() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_orbital(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "open_body", {"body_id": "GJ1b"})
assert_bool(result.get("ok", false)).is_true()
assert_str(app.current_screen_id()).is_equal("regional")
app.queue_free()
func test_open_body_returns_structured_error_for_unrecognized_id() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_orbital(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(
app, "open_body", {"body_id": "does_not_exist"}
)
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
assert_str(app.current_screen_id()).is_equal("system")
app.queue_free()
## PR #209 review (Hoshe finding 3, lead ruling): open_body must reject a
## RECOGNIZED body with no terrain_reference — this is the pre-existing
## click/Enter divergence the PR formalizes and fixes. Structured error, not
## a silent no-op, and no navigation must occur.
func test_open_body_rejects_a_body_with_no_terrain_reference() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_orbital(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "open_body", {"body_id": "GJ1c"})
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
assert_str(app.current_screen_id()).override_failure_message(
"open_body must not navigate for a body with no terrain_reference"
).is_equal("system")
app.queue_free()
## The positive case alongside the guard above: a body WITH a
## terrain_reference still opens normally (GJ1b in the fixture) — this is
## test_open_body_reaches_atlas_app_nav_push_to_regional() above, kept as its
## own assertion here too so the guard test and the "terrain body opens" test
## sit next to each other per the review's own test list.
func test_open_body_opens_a_body_with_a_terrain_reference() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_orbital(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "open_body", {"body_id": "GJ1b"})
assert_bool(result.get("ok", false)).is_true()
assert_str(app.current_screen_id()).is_equal("regional")
app.queue_free()
# =============================================================================
# act() — regional/step-canvas intents
# =============================================================================
func _enter_regional(app) -> void:
app.nav.push("regional", {"body": {"body_id": "GJ1b", "body_radius_km": 6371.0}, "system": FIXTURE_SYSTEM})
func test_scroll_rung_reaches_step_canvas_viewer_rung_transport() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_regional(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "scroll_rung", {"direction": 1})
assert_bool(result.get("ok", false)).is_true()
assert_str(result.get("rung", "")).is_equal(StepCanvasTransport.RUNG_REGION)
app.queue_free()
func test_reset_view_reaches_step_canvas_viewer_reset() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_regional(app)
AtlasAgentInterfaceScript.act(app, "scroll_rung", {"direction": 1})
AtlasAgentInterfaceScript.act(app, "scroll_rung", {"direction": 1})
AtlasAgentInterfaceScript.act(app, "reset_view", {})
var viewer: Variant = app.get_screen("regional").get_viewer()
assert_str(viewer.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
app.queue_free()
func test_set_overlay_reaches_step_canvas_viewer_overlay_state() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_regional(app)
AtlasAgentInterfaceScript.act(
app, "set_overlay", {"overlay_id": "gen_dw_temp", "visible": true}
)
var viewer: Variant = app.get_screen("regional").get_viewer()
assert_bool(viewer.is_overlay_visible("gen_dw_temp")).is_true()
app.queue_free()
func test_back_reaches_nav_pop() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_orbital(app)
AtlasAgentInterfaceScript.act(app, "back", {})
assert_str(app.current_screen_id()).is_equal("reach")
app.queue_free()
# =============================================================================
# jump_to_center — key-discipline test (T-971 highest-value intent)
# =============================================================================
## The core guarantee this intent exists for: jumping to a center and
## scrolling to the SAME center must produce the SAME request key (same
## body/rung/center/extent) — jump_to() must go through the identical
## _fire_request()/_request_extent() path, never a parallel one. Verified via
## the request object's own held state (StepCanvasRequest's private _center/
## _rung fields aren't public, so this compares the PUBLIC echo surface:
## held_rung + world_center, which is exactly what a cache-key comparison
## downstream would use).
func test_jump_to_center_produces_the_same_request_key_as_an_equivalent_scroll() -> void:
var app_a = _make_app()
app_a._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_regional(app_a)
var viewer_a: Variant = app_a.get_screen("regional").get_viewer()
viewer_a._scroll_rung(1, Vector2(400.0, 300.0))
var scrolled_center: Vector2 = viewer_a.get_world_center()
var scrolled_rung: String = viewer_a.get_held_rung()
app_a.queue_free()
var app_b = _make_app()
app_b._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_regional(app_b)
var result: Dictionary = AtlasAgentInterfaceScript.act(
app_b,
"jump_to_center",
{"world_center": [scrolled_center.x, scrolled_center.y], "rung": scrolled_rung}
)
assert_bool(result.get("ok", false)).is_true()
var viewer_b: Variant = app_b.get_screen("regional").get_viewer()
assert_str(viewer_b.get_held_rung()).is_equal(scrolled_rung)
assert_that(viewer_b.get_world_center()).is_equal(scrolled_center)
app_b.queue_free()
func test_jump_to_center_requires_world_center_param() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_regional(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "jump_to_center", {})
assert_bool(result.get("ok", false)).is_false()
app.queue_free()
func test_jump_to_center_keeps_current_rung_when_rung_param_omitted() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_regional(app)
var viewer: Variant = app.get_screen("regional").get_viewer()
viewer._scroll_rung(1, Vector2(400.0, 300.0)) # now at Region
AtlasAgentInterfaceScript.act(app, "jump_to_center", {"world_center": [1000.0, 2000.0]})
assert_str(viewer.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION)
app.queue_free()
# =============================================================================
# open_atlas / close_atlas
# =============================================================================
func test_open_atlas_reaches_hud_groups_open_app() -> void:
var app = _make_app()
# AtlasApp._ready() already registers itself under "implant/map" via
# HudGroups.register() (see ImplantApp._ready()) — no manual registration
# needed here, unlike a bare ImplantApp fixture.
AtlasAgentInterfaceScript.act(app, "open_atlas", {})
assert_bool(HudGroups.is_app_active("implant/map")).is_true()
app.queue_free()
func test_close_atlas_reaches_hud_groups_close_app() -> void:
var app = _make_app()
HudGroups.open_app("implant/map")
AtlasAgentInterfaceScript.act(app, "close_atlas", {})
assert_bool(HudGroups.is_app_active("implant/map")).is_false()
app.queue_free()
# =============================================================================
# PR #209 review (Hoshe finding 1) — off-screen intent dispatch. Every
# screen-targeted intent must reject when its expected screen is NOT the
# current one, with the structured {"ok": false, "error": ...} shape, and
# must NOT mutate the off-screen screen/viewer as a side effect.
# =============================================================================
## The explicitly-called-out case: scroll_rung while "reach" is showing must
## not silently mutate the (registered but off-screen) regional viewer — the
## exact bug this whole guard exists to close. Verifies both the structured
## error AND the absence of a side effect (the off-screen viewer's held rung
## is unchanged from its "regional" was never entered" default).
func test_scroll_rung_from_reach_screen_returns_structured_error_and_does_not_mutate_viewer() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
assert_str(app.current_screen_id()).override_failure_message(
"test setup: app must default to the reach screen"
).is_equal("reach")
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "scroll_rung", {"direction": 1})
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
var viewer: Variant = app.get_screen("regional").get_viewer()
assert_str(viewer.get_held_rung()).override_failure_message(
"scroll_rung from an off-screen 'reach' must not mutate the regional viewer"
).is_equal(StepCanvasTransport.RUNG_GLOBAL)
app.queue_free()
func test_jump_to_center_from_system_screen_returns_structured_error() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_orbital(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(
app, "jump_to_center", {"world_center": [1.0, 2.0]}
)
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
app.queue_free()
func test_reset_view_from_reach_screen_returns_structured_error() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "reset_view", {})
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
app.queue_free()
func test_set_overlay_from_system_screen_returns_structured_error_and_does_not_mutate_viewer() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_orbital(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(
app, "set_overlay", {"overlay_id": "gen_dw_temp", "visible": true}
)
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
var viewer: Variant = app.get_screen("regional").get_viewer()
assert_bool(viewer.is_overlay_visible("gen_dw_temp")).override_failure_message(
"set_overlay from an off-screen 'system' must not mutate the regional viewer"
).is_false()
app.queue_free()
func test_select_system_from_system_screen_returns_structured_error() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_orbital(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(
app, "select_system", {"system_id": "GJ1"}
)
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
app.queue_free()
func test_open_system_from_regional_screen_returns_structured_error() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
_enter_regional(app)
var result: Dictionary = AtlasAgentInterfaceScript.act(
app, "open_system", {"system_id": "GJ1"}
)
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
app.queue_free()
func test_select_body_from_reach_screen_returns_structured_error() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "select_body", {"body_id": "GJ1c"})
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
app.queue_free()
func test_open_body_from_reach_screen_returns_structured_error_and_does_not_navigate() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "open_body", {"body_id": "GJ1b"})
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
assert_str(app.current_screen_id()).override_failure_message(
"open_body from an off-screen 'reach' must not navigate"
).is_equal("reach")
app.queue_free()
# =============================================================================
# Unknown intent
# =============================================================================
func test_unknown_intent_returns_ok_false_with_error() -> void:
var app = _make_app()
app._internal_app_changed(TEST_APP_PATH, HudGroups.Mode.FULLSCREEN)
var result: Dictionary = AtlasAgentInterfaceScript.act(app, "not_a_real_intent", {})
assert_bool(result.get("ok", true)).is_false()
assert_bool(result.has("error")).is_true()
app.queue_free()
+113
View File
@@ -706,3 +706,116 @@ func test_global_canvas_scale_never_drops_below_native_on_a_narrow_viewport() ->
"the effective pixels-per-gridunit ratio must floor at 1 (native),"
+ " never a sub-1x downscale, even on a viewport this narrow"
).is_equal_approx(1.0, 0.001)
# =============================================================================
# T-971 (AtlasAgentInterface): jump_to() — the fixed-center revisit seam —
# and get_current_canvas_summary().
# =============================================================================
## jump_to() must set the SAME held rung/world_center a cursor-anchored
## scroll to that same spot would land on — this is the "same cache key"
## guarantee AtlasAgentInterface's jump_to_center intent depends on
## (verified end-to-end, through act(), in test_atlas_agent_interface.gd;
## this is the narrower unit-level check directly against the viewer).
func test_jump_to_sets_held_rung_and_world_center() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
v.jump_to(Vector2(500.0, -250.0), StepCanvasTransport.RUNG_QUARTER)
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_QUARTER)
assert_that(v.get_world_center()).is_equal(Vector2(500.0, -250.0))
## Omitting `rung` keeps whatever rung is currently held — the "revisit
## within the same rung" common case shouldn't require repeating it.
func test_jump_to_keeps_current_rung_when_omitted() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
v._scroll_rung(1, Vector2(400.0, 300.0)) # Region
v.jump_to(Vector2(10.0, 20.0))
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION)
## Jumping to Global always forces world_center to ZERO — Global has no
## panned-center concept (mirrors _scroll_rung()'s own Global-rung handling).
func test_jump_to_global_forces_world_center_to_zero() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
v._scroll_rung(1, Vector2(400.0, 300.0)) # Region
v.jump_to(Vector2(777.0, 888.0), StepCanvasTransport.RUNG_GLOBAL)
assert_that(v.get_world_center()).is_equal(Vector2.ZERO)
func test_jump_to_unrecognized_rung_is_a_no_op() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
var rung_before: String = v.get_held_rung()
var center_before: Vector2 = v.get_world_center()
v.jump_to(Vector2(1.0, 2.0), "NotARealRung")
assert_str(v.get_held_rung()).is_equal(rung_before)
assert_that(v.get_world_center()).is_equal(center_before)
func test_get_current_canvas_summary_before_any_canvas_arrives() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
var summary: Dictionary = v.get_current_canvas_summary()
assert_bool(summary.get("has_canvas", true)).is_false()
assert_str(summary.get("rung", "")).is_equal(StepCanvasTransport.RUNG_GLOBAL)
## The T-1157-inventory-relevant correctness check: course/cliff/settlement
## counts must match a fixture canvas exactly, including the per-class
## course histogram and settlement id dedup (mirrors
## StepCanvasAnnotationLayer._draw_settlements()'s own dedup discipline —
## covering the same cell id twice must not double-count).
func test_get_current_canvas_summary_counts_match_a_fixture_canvas() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
v._scroll_rung(1, Vector2(400.0, 300.0)) # Region
var canvas: Dictionary = _synthetic_canvas(4, 4)
canvas["courses"] = [
{"class": 0, "points": [[0.0, 0.0], [1.0, 1.0]]},
{"class": 0, "points": [[2.0, 2.0], [3.0, 3.0]]},
{"class": 2, "points": [[4.0, 4.0], [5.0, 5.0]]},
]
canvas["cliffs"] = [{"a": 1}, {"b": 2}]
# 4x4 grid; ids 5 and 5 (repeat, same settlement footprint) dedup to one,
# id 9 is a second distinct settlement, 0 is "no settlement" and ignored.
canvas["settlement_id"] = [
0, 5, 5, 0,
0, 0, 0, 0,
9, 0, 0, 0,
0, 0, 0, 0,
]
v._on_canvas_ready(canvas)
var summary: Dictionary = v.get_current_canvas_summary()
assert_bool(summary.get("has_canvas", false)).is_true()
assert_int(summary.get("canvas_width", 0)).is_equal(4)
assert_int(summary.get("canvas_height", 0)).is_equal(4)
assert_int(summary.get("course_count", 0)).is_equal(3)
assert_int(summary.get("cliff_count", 0)).is_equal(2)
assert_int(summary.get("settlement_count", 0)).is_equal(2)
var by_class: Dictionary = summary.get("course_count_by_class", {})
assert_int(int(by_class.get(0, 0))).is_equal(2)
assert_int(int(by_class.get(2, 0))).is_equal(1)
@@ -0,0 +1,338 @@
class_name AtlasAgentInterface
extends RefCounted
## Atlas agent control channel (T-971, D-226 item 4) — observe/act named-
## intent navigation over the REAL Atlas UI. Turns the five hand-rolled
## T-1183 eyeball-review scratch drivers into one production, testable
## contract: `observe()` returns a JSON-safe Dictionary of (a) the current
## data state and (b) a walkable UI affordance tree; `act(intent, params)`
## dispatches to a named semantic intent, each backed by the EXACT method a
## real click/keypress calls in production — never a synthesized
## `_gui_input`/pixel event. Every method here is `static` (no instance
## state) taking an `AtlasApp` reference explicitly — see
## atlas_agent_bridge.gd's own doc for why the stable entry point is a dumb
## autoload holding that reference rather than this class itself being an
## autoload (the CLAUDE.md autoload parse-order rule: this file has a
## `class_name`, so an autoload referencing it directly at top level/`_ready()`
## would fail to resolve before this script is registered).
##
## **Reconciled against the current stepped-rung API (T-971 Phase 1, this
## ticket's own confirmed re-scope) — NOT the original D-226 list, which was
## written against the retired continuous-zoom AtlasWindowViewer:**
##
## - **`select_city`/`open_regional` DROPPED.** Post-D-255, settlements are
## `settlement_id` cell values inside a fetched step-canvas array
## (StepCanvasAnnotationLayer._draw_settlements()), not a separate
## clickable list — there is no server- or client-side "select settlement
## X" affordance to wire this intent to today. Faking one (e.g. picking the
## nearest cell to a guessed screen position) would be worse than omitting
## it: it would exercise a code path no real click can reach. A
## settlement-hit-test intent is a new ticket, filed only once a real
## consumer needs it — not speculatively here.
## - **`open_atlas`/`close_atlas` cover opening the app itself** — D-226's
## original list implicitly assumed the Atlas was already open; an
## in-process driver needs to open it too (HudGroups.open_app/close_app).
## - **`jump_to_center` is NEW** — the T-1183 eyeball-driver "fixed-center
## revisit" pattern (record a prior run's actual derived world center,
## re-request it literally rather than repeating a cursor gesture that
## would re-derive a merely-similar one) made a first-class intent, backed
## by the new StepCanvasViewer.jump_to() production seam. This is the
## highest-value intent in the set for T-1157's future capture harness.
## - **`get_layer_data_summary`'s actual post-D-255 shape** is
## StepCanvasViewer.get_current_canvas_summary()'s own return —
## rung/world_center/held_extent/canvas dimensions/course/cliff/settlement
## counts. D-226's original "attractor/river/basin counts" phrasing is
## itself stale (attractors don't exist post-D-255; "basin" was never a
## step-canvas field) — courses/cliffs/settlements are what the wire
## actually carries.
##
## **TRANSPORT SCOPE (this ticket): in-process consumers only.** Every real
## consumer today — gdUnit suites, a `-s` SceneTree headless driver, the
## T-1157 capture harness — calls these static methods directly, in the same
## process as the running AtlasApp. There is no network/stdio listener here;
## D-226's original "terminal/curl" framing is deferred to a follow-up ticket
## when a genuinely remote consumer exists. This keeps the surface small and
## delivers the QA value (turning eyeball review into a scripted sweep)
## immediately. See client/tests/atlas_agent_driver.gd for the sanctioned
## reference `-s` driver this ticket ships alongside the interface itself.
##
## **observe() is side-effect-free** — no request firing, no state mutation,
## purely reads already-held view/screen state (mirrors get_layer_data_summary's
## own "without rendering" requirement one level up: observing never triggers
## a fetch).
# =============================================================================
# observe()
# =============================================================================
## Full observation snapshot: current screen id + its own data state, the
## walkable affordance tree (every reachable toggle/button under the current
## screen), and — when "regional" is current — the step-canvas layer data
## summary. `app` is the live AtlasApp (typically
## AtlasAgentBridge.current_app, loaded by the caller via `load()` per the
## autoload parse-order rule).
static func observe(app: Node) -> Dictionary:
if app == null:
return {"error": "no AtlasApp instance available"}
var screen_id: String = app.current_screen_id()
return {
"screen": screen_id,
"data": _observe_data_state(app, screen_id),
"affordances": _walk_affordances(app),
}
## The "current data state shown" half of observe() — per-screen, since each
## screen's own state shape differs (D-226's own split: "current data state
## shown" vs "the affordance tree" are two distinct halves of one observe()
## call, not folded together).
static func _observe_data_state(app: Node, screen_id: String) -> Dictionary:
match screen_id:
"reach":
return {"has_selection": app.get_screen("reach").has_selection()}
"system":
var system_screen: Node = app.get_screen("system")
return {
"in_orbital": system_screen.is_in_orbital(),
"current_system": system_screen.current_system(),
"has_body_panel_open": system_screen.has_body_panel_open(),
"has_station_panel_open": system_screen.has_station_panel_open(),
}
"regional":
var viewer: Variant = _get_viewer(app)
if viewer == null:
return {}
return viewer.get_current_canvas_summary()
_:
return {} # gdlint:ignore = max-returns
## Generic Control-tree walk (D-226's own requirement: "so economics/saves
## plug in later" — never a per-screen hand-written affordance list). Walks
## the CURRENT screen's own Control subtree (not the whole app — an
## inactive screen's controls aren't real affordances right now) collecting
## every Button/toggle-capable node: id (node name), label (text), state
## (pressed/disabled), locked (disabled). Buttons with no distinguishing text
## still get an entry (id falls back to the node's own scene path) so the
## tree is never silently incomplete.
static func _walk_affordances(app: Node) -> Array:
var screen: Node = app.get_screen(app.current_screen_id())
if screen == null:
return []
var out: Array = []
_walk_affordances_recursive(screen, out)
return out
static func _walk_affordances_recursive(node: Node, out: Array) -> void:
if node is Button:
var b: Button = node
out.append({
"id": String(b.name),
"label": b.text,
"pressed": b.button_pressed,
"locked": b.disabled,
})
for child in node.get_children():
_walk_affordances_recursive(child, out)
# =============================================================================
# act()
# =============================================================================
## Dispatch a named semantic intent. `params` is a JSON-safe Dictionary;
## returns a JSON-safe Dictionary result (at minimum {"ok": bool}, plus
## intent-specific fields). Unknown intents return {"ok": false,
## "error": "..."} rather than pushing an error/crashing — this channel is
## meant to be driven by a fallible external caller (a curl-style JSON body,
## a test fixture), and a malformed intent name is exactly the kind of input
## it must handle gracefully.
##
## **PR #209 review (Hoshe finding 1, the off-screen dispatch bug):**
## app.get_screen(id) is a REGISTRY lookup — every screen is registered (and
## therefore reachable) for the app's whole lifetime, regardless of which one
## is currently visible/current. Before this fix, every screen-targeted
## intent resolved its target via get_screen() alone, so e.g. scroll_rung
## while "reach" was showing silently mutated the off-screen "regional"
## viewer — including firing a real SimBridge.request_step_canvas() IPC
## call — and returned {"ok": true}, as if the player had actually been
## looking at the map. Every screen-targeted intent below now checks
## app.current_screen_id() against the screen it targets FIRST, returning
## the same structured {"ok": false, "error": ...} shape the null-app/
## unknown-intent paths already use. This is a per-intent expectation, not a
## single global gate, because different intents target different screens
## (select_system/open_system expect "reach"; select_body/open_body expect
## "system"; scroll_rung/jump_to_center/reset_view/set_overlay expect
## "regional") — see _require_current_screen()'s own doc.
static func act(app: Node, intent: String, params: Dictionary = {}) -> Dictionary:
if app == null and intent != "open_atlas":
return {"ok": false, "error": "no AtlasApp instance available"}
match intent:
"open_atlas":
HudGroups.open_app("implant/map")
return {"ok": true}
"close_atlas":
HudGroups.close_app()
return {"ok": true}
"select_system":
return _act_on_current_screen(
app, "reach", func(s: Node) -> void: s.select_system_by_id(str(params.get("system_id", "")))
)
"open_system":
return _act_on_current_screen(
app, "reach", func(s: Node) -> void: s.open_system_by_id(str(params.get("system_id", "")))
)
"select_body":
return _act_on_current_screen(
app, "system", func(s: Node) -> void: s.select_body_by_id(str(params.get("body_id", "")))
)
"open_body":
return _act_open_body(app, params)
"scroll_rung":
return _act_scroll_rung(app, params)
"jump_to_center":
return _act_jump_to_center(app, params)
"reset_view":
return _act_on_current_screen(
app, "regional", func(s: Node) -> void: s.get_viewer()._reset_to_global()
)
"set_overlay":
return _act_on_current_screen(
app,
"regional",
func(s: Node) -> void: s.get_viewer().set_overlay_visible(
str(params.get("overlay_id", "")), bool(params.get("visible", true))
)
)
"back":
app.nav.pop()
return {"ok": true}
_:
return {"ok": false, "error": "unknown intent '%s'" % intent} # gdlint:ignore = max-returns
## The shared current-screen guard (PR #209 review, Hoshe finding 1): returns
## the structured {"ok": false, "error": ...} shape if `expected_screen_id`
## isn't the CURRENT screen (app.current_screen_id()), otherwise runs
## `body` against the resolved screen instance and returns {"ok": true}.
## `body` is a Callable taking the screen Node — every screen-targeted intent
## that has no extra result fields to report (select_system/open_system/
## select_body/reset_view/set_overlay) routes through this single check
## rather than five copies of the same "is this screen current" branch.
## scroll_rung/jump_to_center/open_body still need their own wrappers (they
## report extra fields — rung/world_center — or a body-specific guard) but
## reuse _require_current_screen() for the identical check.
static func _act_on_current_screen(
app: Node, expected_screen_id: String, body: Callable
) -> Dictionary:
var screen: Variant = _require_current_screen(app, expected_screen_id)
if screen == null:
return _not_current_screen_error(app, expected_screen_id)
body.call(screen)
return {"ok": true}
## Returns the registered screen instance for `expected_screen_id` ONLY if it
## is also the CURRENTLY showing screen (app.current_screen_id() ==
## expected_screen_id) — null otherwise (either unregistered, per
## get_screen()'s own contract, or registered-but-not-current, the bug this
## whole guard exists to close). This is the ONE place "is this screen
## current" is checked — every act() branch above and every _act_* helper
## below calls this rather than checking current_screen_id() inline.
static func _require_current_screen(app: Node, expected_screen_id: String) -> Variant:
if app.current_screen_id() != expected_screen_id:
return null
return app.get_screen(expected_screen_id)
static func _not_current_screen_error(app: Node, expected_screen_id: String) -> Dictionary:
return {
"ok": false,
"error": (
"intent requires screen '%s' to be current, but '%s' is showing"
% [expected_screen_id, app.current_screen_id()]
),
}
## `open_body` — PR #209 review (Hoshe finding 3, lead ruling): the
## Enter-key path has always gated body entry on `terrain_reference != null`
## (AtlasApp._handle_enter()'s "system" branch); double-click (and therefore
## this intent, which drives the identical SystemScreen.body_selected signal)
## did not — a pre-existing click/Enter divergence this PR formalizes into a
## contract, so it fixes it. The guard itself now lives in the SHARED tail
## (AtlasApp._on_body_selected(), the signal handler both double-click and
## this intent funnel through) — a guarded body is a silent no-op there,
## matching what the Enter path always did. This intent does its OWN
## pre-check via SystemScreen.find_body() so it can report a STRUCTURED
## error instead of masking "nothing happened" as {"ok": true} the way a bare
## click has no way to report either way.
static func _act_open_body(app: Node, params: Dictionary) -> Dictionary:
var screen: Variant = _require_current_screen(app, "system")
if screen == null:
return _not_current_screen_error(app, "system")
var body_id: String = str(params.get("body_id", ""))
var body: Dictionary = screen.find_body(body_id)
if body.is_empty():
return {"ok": false, "error": "unrecognized body_id '%s'" % body_id}
if body.get("terrain_reference") == null:
return {"ok": false, "error": "body '%s' has no terrain reference" % body_id}
screen.open_body_by_id(body_id)
return {"ok": true}
## `scroll_rung` — direction is required; cursor_local defaults to a
## reasonable canvas-center guess (Vector2(400, 300), matching this cluster's
## own gdUnit test fixtures' convention, see test_step_canvas_viewer.gd) since
## an agent driver has no real cursor position to anchor on.
static func _act_scroll_rung(app: Node, params: Dictionary) -> Dictionary:
var screen: Variant = _require_current_screen(app, "regional")
if screen == null:
return _not_current_screen_error(app, "regional")
var viewer: Variant = screen.get_viewer()
var direction: int = int(params.get("direction", 1))
var cursor_raw: Variant = params.get("cursor_local")
var cursor_local: Vector2 = (
Vector2(cursor_raw[0], cursor_raw[1])
if cursor_raw is Array and (cursor_raw as Array).size() >= 2
else Vector2(400.0, 300.0)
)
viewer._scroll_rung(direction, cursor_local)
return {"ok": true, "rung": viewer.get_held_rung()}
## `jump_to_center` — the fixed-center revisit intent (T-1183 pattern, made
## first-class). `world_center` is required (`[x, y]` world metres);
## `rung` is optional (keeps the currently-held rung when omitted, matching
## StepCanvasViewer.jump_to()'s own default). Goes through jump_to(), which
## itself falls through to the SAME _fire_request()/_request_extent() path
## every other navigation intent uses — no parallel request-building code
## exists in this file.
static func _act_jump_to_center(app: Node, params: Dictionary) -> Dictionary:
var screen: Variant = _require_current_screen(app, "regional")
if screen == null:
return _not_current_screen_error(app, "regional")
var center_raw: Variant = params.get("world_center")
if not (center_raw is Array and (center_raw as Array).size() >= 2):
return {"ok": false, "error": "jump_to_center requires world_center: [x, y]"}
var viewer: Variant = screen.get_viewer()
var world_center := Vector2(float(center_raw[0]), float(center_raw[1]))
var rung: String = str(params.get("rung", ""))
viewer.jump_to(world_center, rung)
return {"ok": true, "rung": viewer.get_held_rung(), "world_center": [world_center.x, world_center.y]}
## Shared "regional" screen -> StepCanvasViewer accessor, used ONLY by
## observe() (which already knows "regional" is current — it matched on
## screen_id itself — so it doesn't need _require_current_screen()'s guard,
## just a null-safe read of a screen that may not even be registered yet
## early in app lifecycle).
static func _get_viewer(app: Node) -> Variant:
var regional_screen: Node = app.get_screen("regional")
if regional_screen == null:
return null
return regional_screen.get_viewer()
+28 -5
View File
@@ -33,6 +33,12 @@ func _ready() -> void:
func on_install() -> void:
# T-971: publish this instance to the dumb AtlasAgentBridge autoload holder
# so AtlasAgentInterface (a static class, loaded on demand by whatever
# in-process driver calls it) has a stable address to start from — see
# atlas_agent_bridge.gd's own doc for why this is a plain field set, not a
# class_name-typed autoload reference.
AtlasAgentBridge.current_app = self
_load_system_data()
# T-949: the star map now arrives over the bridge, possibly after this app
# is already installed (or before the handshake completes at all) — refresh
@@ -121,11 +127,7 @@ func _handle_enter() -> void:
nav.replace("system", {"mode": "orbital", "system": sys})
elif _system_screen and _system_screen.has_body_panel_open():
var body: Dictionary = _system_screen.get_selected_body()
if body.get("terrain_reference") != null:
nav.push("regional", {
"body": body,
"system": nav.current_payload().get("system", {}),
})
_on_body_selected(body)
# =============================================================================
@@ -141,7 +143,28 @@ func _on_system_selected(system_id: String) -> void:
nav.push("system", {"mode": "orbital", "system": system})
## PR #209 review (Hoshe finding 3, lead ruling): the SHARED tail every path
## that "opens" a body funnels through — double-click (SystemScreen.
## body_selected), the Enter-key path (_handle_enter()'s "system" branch,
## which used to gate on terrain_reference itself and now just calls this),
## and AtlasAgentInterface's open_body/open_body_by_id intent (also via this
## same signal). Gating HERE, once, closes a pre-existing divergence: the
## Enter-key path already required `terrain_reference != null` before this
## PR, but double-click (and therefore the new open_body intent) did not —
## both silently pushed "regional" for a body with no heightmap. Formalizing
## the intent contract is what surfaced it, so this PR fixes it for both
## callers at once rather than reintroducing the same split.
##
## A guarded body (no terrain_reference) is a silent no-op here — same
## behavior the Enter path always had (it simply never called nav.push() for
## that case). AtlasAgentInterface's open_body/open_body_by_id wrap this at
## the intent layer with a STRUCTURED error instead of a silent no-op (see
## atlas_agent_interface.gd's own _act_open_body()) — an agent driver must be
## able to tell "nothing happened" from "call succeeded", which a bare click
## has no way to report either way.
func _on_body_selected(body: Dictionary) -> void:
if body.get("terrain_reference") == null:
return
nav.push("regional", {
"body": body,
"system": nav.current_payload().get("system", {}),
@@ -474,16 +474,36 @@ func _gui_input(event: InputEvent) -> void:
func _handle_reach_click(pos: Vector2) -> void:
var sid := _find_nearest_reach_system(pos)
_reach_selected = sid
_rebuild_reach_info_panel()
_dirty = true
select_system_by_id(sid)
func _handle_reach_double_click(pos: Vector2) -> void:
var sid := _find_nearest_reach_system(pos)
if not sid.is_empty():
_reach_selected = sid
system_selected.emit(sid)
open_system_by_id(sid)
## T-971 (AtlasAgentInterface `select_system` intent) — the SAME state
## mutation _handle_reach_click() performs (single-click equivalent: sets the
## selection + rebuilds the info panel, does not navigate), reachable by id
## directly rather than a screen position. An empty/unknown id clears the
## selection (mirrors what a click on empty space already does via
## `_find_nearest_reach_system`'s own "" fallback).
func select_system_by_id(system_id: String) -> void:
_reach_selected = system_id
_rebuild_reach_info_panel()
_dirty = true
## T-971 (AtlasAgentInterface `open_system` intent) — the SAME state mutation
## _handle_reach_double_click() performs (double-click/Enter equivalent:
## selects AND emits system_selected, which AtlasApp._on_system_selected()
## turns into a nav.push("system", ...)). A no-op for an unrecognized id,
## matching the double-click handler's own empty-id guard.
func open_system_by_id(system_id: String) -> void:
if system_id.is_empty():
return
_reach_selected = system_id
system_selected.emit(system_id)
func _update_reach_hover(pos: Vector2) -> void:
@@ -47,5 +47,15 @@ func leave() -> void:
pass
## T-971 test/agent seam: exposes the owned StepCanvasViewer so
## AtlasAgentInterface can drive rung-transport intents (scroll_rung,
## jump_to, set_overlay, get_current_canvas_summary) without this screen
## having to grow a forwarding method per viewer capability — matches the
## get_request()/get_disk_cache() accessor-chain precedent StepCanvasViewer
## itself already established for the same reason.
func get_viewer() -> Variant:
return _viewer
func _on_viewer_back() -> void:
back_requested.emit()
@@ -377,27 +377,14 @@ func _gui_input(event: InputEvent) -> void:
func _handle_orbital_double_click(pos: Vector2) -> void:
var bid: String = _find_nearest_body(pos)
if not bid.is_empty():
for b: Dictionary in _orbital_bodies:
if str(b.get("body_id", "")) == bid:
body_selected.emit(b)
return
open_body_by_id(bid)
func _handle_orbital_click(pos: Vector2) -> void:
var bid: String = _find_nearest_body(pos)
if not bid.is_empty():
for b: Dictionary in _orbital_bodies:
if str(b.get("body_id", "")) == bid:
_selected_body = b
_selected_station = {}
if _station_panel:
_station_panel.visible = false
_rebuild_body_panel()
if _body_panel:
_body_panel.visible = true
_dirty = true
return
select_body_by_id(bid)
return
var sid: String = _find_nearest_station(pos)
if not sid.is_empty():
@@ -416,6 +403,59 @@ func _handle_orbital_click(pos: Vector2) -> void:
_dirty = true
## T-971 (AtlasAgentInterface `select_body` intent) — the SAME state
## mutation the body branch of _handle_orbital_click() performs (opens the
## body info panel, does not navigate), reachable by id directly. A no-op for
## an unrecognized id (mirrors the click handler's own "no match under the
## cursor" fall-through to _close_detail_panels(), except an agent-driven
## select never had a "click landed on empty space" case to fall through to —
## an unrecognized id is a caller error, not a deselect gesture, so it is
## simply ignored here rather than clearing an unrelated selection).
func select_body_by_id(body_id: String) -> void:
for b: Dictionary in _orbital_bodies:
if str(b.get("body_id", "")) == body_id:
_selected_body = b
_selected_station = {}
if _station_panel:
_station_panel.visible = false
_rebuild_body_panel()
if _body_panel:
_body_panel.visible = true
_dirty = true
return
## T-971 (AtlasAgentInterface `open_body` intent) — the SAME state mutation
## _handle_orbital_double_click() performs (double-click equivalent: emits
## body_selected, which AtlasApp._on_body_selected() now gates on
## terrain_reference — PR #209 review, Hoshe finding 3 — before pushing
## "regional"). A no-op for an unrecognized id. Emits unconditionally for a
## RECOGNIZED body regardless of terrain_reference, same as the double-click
## handler always has — the guard lives in the shared _on_body_selected()
## tail, not here, so this stays a pure "found it, emit" lookup identical to
## the click path (AtlasAgentInterface does its OWN pre-check via
## find_body() to report a structured error instead of a silent no-op).
func open_body_by_id(body_id: String) -> void:
for b: Dictionary in _orbital_bodies:
if str(b.get("body_id", "")) == body_id:
body_selected.emit(b)
return
## Generic body lookup by id — {} if not found. Shared read used by both
## select_body_by_id()/open_body_by_id()'s own linear scans (kept inline in
## each for now, matching this file's existing per-method scan style) and by
## AtlasAgentInterface's open_body pre-check (PR #209 review, Hoshe finding
## 3) so the intent layer can build a structured "no terrain reference"
## error without re-deriving the emit-vs-no-op logic already owned by
## open_body_by_id() above.
func find_body(body_id: String) -> Dictionary:
for b: Dictionary in _orbital_bodies:
if str(b.get("body_id", "")) == body_id:
return b
return {}
func _close_detail_panels() -> void:
_selected_body = {}
_selected_station = {}
@@ -133,6 +133,13 @@ var _canvas_scale: float = 1.0
# ── Overlay visibility ─────────────────────────────────────────────────────
var _overlay_visibility: Dictionary = {}
## The last-adopted (Ready-response OR cache-hit) canvas Dictionary — held
## here (not read from `_terrain_layer._canvas_ref`, which stays private to
## that node) so get_current_canvas_summary() has its own reference. Set in
## _on_canvas_ready() alongside the terrain/annotation layer handoff, same
## single adoption point every other per-canvas field on this class uses.
var _current_canvas_data: Variant = null # decoded EncodedStepCanvas Dictionary, or null pre-arrival
# ── Child nodes ────────────────────────────────────────────────────────────
var _canvas: Node2D = null
var _terrain_layer: StepCanvasTerrainLayer = null
@@ -209,6 +216,7 @@ func enter(body: Dictionary, system: Dictionary) -> void:
_view_offset = Vector2.ZERO
_request.reset()
_annotation_layer.clear_frame()
_current_canvas_data = null
_fire_request()
_refresh_screen_header()
if _legend_panel:
@@ -274,6 +282,73 @@ func get_overlay_defs() -> Array:
return OVERLAY_DEFS
## T-971 (D-226 item 4, `get_layer_data_summary`) — a public, allocation-light
## read of the currently-held canvas's shape, with NO rendering/PNG-decode
## triggered (every field read here comes straight off the raw decoded
## EncodedStepCanvas dict — step_canvas_protocol.gd's own shape — before
## StepCanvasTerrainLayer ever touches Image.load_png_from_buffer()). A
## proper public accessor, not a reach into `_terrain_layer._canvas_ref`
## (which stays private to that node) — see `_current_canvas_data`'s own doc
## for why the viewer holds its own reference instead.
##
## D-226's original "attractor/river/basin counts" phrasing is itself stale
## post-D-255: attractors don't exist on this wire, and "basin" was never a
## step-canvas field — the real countable features are courses (rivers,
## `class`-tagged), cliffs, and settlements (dedup'd exactly like
## StepCanvasAnnotationLayer._draw_settlements() dedups them, so the count
## always matches what's actually drawn). Called potentially once per step in
## a sweep loop (T-1157), so this is a single linear walk of already-small
## sparse lists — no per-cell work beyond the settlement dedup scan
## `_draw_settlements()` already pays for anyway.
func get_current_canvas_summary() -> Dictionary:
if not _current_canvas_data is Dictionary:
return {
"has_canvas": false,
"rung": _held_rung,
"world_center": [_world_center.x, _world_center.y],
"held_extent": [_held_extent.x, _held_extent.y],
}
var d: Dictionary = _current_canvas_data
var courses: Array = d.get("courses", [])
var course_count_by_class: Dictionary = {}
for course_raw: Variant in courses:
if not course_raw is Dictionary:
continue
var cls: int = int((course_raw as Dictionary).get("class", -1))
course_count_by_class[cls] = int(course_count_by_class.get(cls, 0)) + 1
return {
"has_canvas": true,
"rung": _held_rung,
"world_center": [_world_center.x, _world_center.y],
"held_extent": [_held_extent.x, _held_extent.y],
"canvas_width": int(d.get("width", 0)),
"canvas_height": int(d.get("height", 0)),
"course_count": courses.size(),
"course_count_by_class": course_count_by_class,
"cliff_count": (d.get("cliffs", []) as Array).size(),
"settlement_count": _count_distinct_settlements(d),
}
## Mirrors StepCanvasAnnotationLayer._draw_settlements()'s own dedup
## discipline exactly (distinct non-zero ids only) so this count always
## matches what the player actually sees drawn — never a raw non-zero CELL
## count, which would overcount every settlement whose footprint covers more
## than one gridunit.
static func _count_distinct_settlements(canvas: Dictionary) -> int:
var settlement_id: Variant = canvas.get("settlement_id")
if not (settlement_id is Array or settlement_id is PackedByteArray):
return 0
var seen: Dictionary = {}
var ids: Array = settlement_id
for i in range(ids.size()):
var sid: int = int(ids[i])
if sid == 0:
continue
seen[sid] = true
return seen.size()
# =============================================================================
# Request lifecycle
# =============================================================================
@@ -324,6 +399,7 @@ func _on_canvas_ready(canvas: Dictionary) -> void:
_held_extent = _request.get_held_extent()
if _held_rung == StepCanvasTransport.RUNG_GLOBAL:
_global_body_extent = _held_extent
_current_canvas_data = canvas
_rebuild_terrain_texture(canvas)
_annotation_layer.set_frame(canvas, _world_center, _held_rung, _held_extent)
_recompute_canvas_transform()
@@ -451,6 +527,49 @@ func _is_global_view_drifted() -> bool:
return _world_center != Vector2.ZERO or _view_offset != _centered_view_offset()
## Fixed-center revisit (T-971, D-226 item 4 — the AtlasAgentInterface
## `jump_to_center` intent; also the T-1183 eyeball-driver "record a prior
## run's actual derived centers and re-request them literally" pattern, made
## a first-class production seam rather than five hand-rolled scratch
## drivers). Directly sets rung + center — no cursor-anchored derivation,
## since a caller here is handing us the ALREADY-DERIVED world point to land
## on (the whole reason this exists: a comparison run must hit run 1's exact
## cache key, not repeat a gesture that re-derives a merely-similar one).
##
## Constraint honored (Jeroen's ruling): this selects center+rung ONLY, then
## falls through to the SAME tail every other navigation uses —
## `_fire_request()` (which reads extent from `_request_extent()`, the same
## T-1189 body-cap/viewport-fit math every other request goes through),
## `_refresh_screen_header()`, `queue_redraw()`. No parallel request-building
## code exists here; a `jump_to` request is indistinguishable, cache-key-wise,
## from a `_scroll_rung`/`enter` request that happened to land on the same
## rung/center.
##
## `rung` defaults to the empty string, meaning "keep the currently held
## rung" — a caller jumping within the same rung (the common revisit case)
## need not repeat it. An unrecognized rung name is rejected (push_warning,
## no-op) rather than silently coerced to Global — same defensive posture
## StepCanvasTransport.index_for_rung() already documents for its own
## out-of-range read.
func jump_to(world_center: Vector2, rung: String = "") -> void:
var target_rung: String = rung if not rung.is_empty() else _held_rung
var target_index: int = StepCanvasTransport.index_for_rung(target_rung)
if target_index < 0:
push_warning("StepCanvasViewer: jump_to unrecognized rung '%s'" % rung)
return
_rung_index = target_index
_held_rung = target_rung
_world_center = world_center if _held_rung != StepCanvasTransport.RUNG_GLOBAL else Vector2.ZERO
_view_offset = Vector2.ZERO
_fire_request()
_refresh_screen_header()
queue_redraw()
func get_world_center() -> Vector2:
return _world_center
## The letterbox-centered `_view_offset` for the CURRENTLY held canvas
## footprint/scale — the "no pan drift" baseline _is_global_view_drifted()
## compares against, what a hard reset restores, and what
+10
View File
@@ -130,6 +130,16 @@ func current_screen_id() -> String:
return _current_screen_id
## T-971 (AtlasAgentInterface, D-226 item 4): a generic, read-only screen
## lookup by id — the same `_screens` dict register_screen() already
## maintains, exposed so an agent-control channel can reach a specific
## screen instance (StepCanvasViewer via RegionalScreen.get_viewer(), etc.)
## without every ImplantApp subclass growing its own bespoke getters. Returns
## null for an unregistered id, matching Dictionary.get()'s own default.
func get_screen(screen_id: String) -> Variant:
return _screens.get(screen_id, null)
func _on_screen_changed(new_id: String) -> void:
if _current_screen_id != new_id:
var old: Control = _screens.get(_current_screen_id, null)
+1
View File
@@ -1688,6 +1688,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Raised by:** Jeroen + Claude (design), with Tyre (channel/pause/headless architecture) + Araminta (overlay encoding + affordance UX), 2026-05-24.
- **Cross-reference:** [D-225](#d-225) (layer-stream proxy — the data path), [D-166](#d-166) (per-layer Atlas progress viewer), [D-191](#d-191) (Atlas viewer), [D-169](#d-169) / [D-170](#d-170) (implant components / HUD occlusion — `gameplay_occluded` trigger), [D-200](#d-200) / [D-203](#d-203) (execution tiers / LRU cache), Q-099 (mod content catalog), `tests/run-visual` (capture primitive), `save_state.rs` (save-inspection consumer). **T-1112 amendment additionally:** [D-222](#d-222) (Quarter terminology — the 512m unit this layer surfaces), [D-234](#d-234) (footprint geometry — the block-subdivision source the aggregates summarize), [D-243](#d-243) (quarter = 512m rung, and the containment ladder that makes a quarter sub-pixel at planetary projection — Araminta's no-outline rationale), [D-010](#d-010) (determinism — integer-only aggregates, `BTreeMap` keying). **T-1124 amendment additionally:** [D-227](#d-227) (derive-don't-store + the "invented deterministically" clause this design is the first to surface on a screen; determinism is what makes the echoed-center staleness guard and the client-side window cache both sound), [D-243](#d-243) (district = 2,048m rung — this is precisely the D-226(d) ceiling's floor, the regional altitude above quarter-skeleton and below the never-mapped chunk/voxel tier; edge-fuzz discipline — the temperature quantization-consistency rationale), [D-239](#d-239) §6 (the frozen 17-zone `MorphologyZone` vocabulary this layer serves unchanged) / §8 (vegetation climate law, amended by T-1126's `Marine`), D-225 extension / T-1131 / PR #184 (the five-map-shape demux ceiling — why the window rides on `AtlasLayerRequest` rather than a new inbound shape), [D-010](#d-010) (wire-integer discipline — all six per-cell fields are integer/quantized, no `f32` on the wire). §5 (screen) additionally leans on [D-191](#d-191) (the `AtlasViewer` whose `_view_zoom` LOD vocabulary and `SETTLEMENT_LABEL_MIN_ZOOM` threshold the district window extends in place), [D-169](#d-169) / [D-170](#d-170) (implant chrome / theme accent-role discipline — map palettes stay out of `ACCENT_ACTIVE` gold the settlement marker owns), and [D-013](#d-013) (diegetic navigation — the zoom gesture owns spatial descent, so it is not overloaded onto the city-click sidebar). Tickets: T-1123 (the `derive_district` window-render precedent this design promotes to a served layer), T-1127 (glaciation_grade derivation + probe render pattern, wire half deferred here and now accepted), T-1126 (`VegetationClass::Marine`), T-1118 (region-grid climate overlay — §5's temperature/moisture overlays reuse its ramp so one colorizer spans both zoom levels), T-1119 (concurrent `quarter_footprints` wiring — the sibling whole-body layer this design's §2 distinguishes itself from).
- **Dissent:** None
- **Amendment (2026-07-25, T-971 / PR #209 — agent-channel vocabulary reconciled to the stepped Atlas):** the layer-4 `AtlasAgentInterface` ships with its intent vocabulary re-derived against the post-[D-255](#d-255) stepped Atlas, superseding this record's original list. **Dropped:** `select_city` and `open_regional` — post-D-255, settlements are `settlement_id` canvas cell values with no hit-test affordance (no server get-settlement-by-id, no client hit-test; a settlement-selection intent becomes its own ticket when a real consumer exists), and Region→Chunk is one screen (descent is `scroll_rung`, not a screen push — `open_body` replaces `open_regional`). **Added:** `open_atlas` (a driver must be able to start the session), and `jump_to_center` — the fixed-center revisit pattern proven by the T-1157 eyeball drivers, made first-class via `StepCanvasViewer.jump_to` (same request tail, extent cap, and cache keys as cursor navigation, so an agent cannot request a state a player couldn't). `get_layer_data_summary` reports the real `EncodedStepCanvas` fields (courses-by-class, cliffs, draw-matched settlement counts) — the original attractor/river/basin-count phrasing was retired-AtlasViewer vocabulary. **Transport narrowed:** in-process consumers only (gdUnit, `-s` drivers, the T-1157 capture harness — the committed reference driver replaces the scratch eyeball drivers); the original curl/terminal remote transport is a follow-up when a remote consumer exists. Full handler-by-handler mapping on T-971.
---