feat(ui): sprint 36 client — ImplantApp pattern + atlas unification + tooling #131

Closed
jpmschweitzer wants to merge 0 commits from sprint-36/client into main
Owner

Summary

Sprint 36 client — three shipped tickets plus a foundational implant-app architecture refactor.

Tickets:

  • #844 — Unified star map and atlas into a single implant/map HudGroups app per D-191. Atlas opens at the hop-ring Reach view as the entry screen, navigates down through system → planet → heightmap in one continuous session. Single key binding (M); KEY_A removed.
  • #724 — Loading screen shows client version (from project.yaml) and protocol version, bottom-centre.
  • #722sqlite-query and sqlite-exec respond to --help / -h with proper usage text (previously returned empty JSON).

Foundational refactor — ImplantApp pattern:

The implant is now treated as a faux mobile OS, and implant apps as faux mobile apps. This is the foundational shape for future implant apps and the seam for modded content.

New classes (client/ui/implant/):

  • implant_app_manifest.gdResource class declaring app_path, display_name, scene_path, default_key, default_mode, preserves_state.
  • implant_nav_stack.gd — intra-app navigation helper (push/pop/replace/reset_to_default, synchronous screen_changed signal).
  • implant_app.gd — base class all implant apps extend. Absorbs anchoring, visibility, HudGroups registration, app_changed filtering, set_insert_active. Lifecycle hooks: on_install, on_open, on_close, on_insert_deactivated.
  • implant_registry.gd — new autoload. Scans res://ui/implant/apps/*/app.tres lazily on first get_manifests() call (avoids the class_name autoload parse-order trap).

Apps migrated to the new pattern:

  • client/ui/implant/apps/atlas/ — manifest, shell (atlas_app.gd extending ImplantApp), four screens (reach_screen, system_screen, planet_screen, regional_screen) as plain Controls with enter/leave interface, overlays, heightmap viewer.
  • client/ui/implant/apps/economics/ — manifest, shell (economics_app.gd), overview screen with preserved selection state.

Generic key routing:

  • main.gd's hardcoded KEY_M / KEY_N dispatch replaced with a registry-driven loop that reads default_key from each app's manifest. New apps drop into apps/<name>/ and bind automatically.

Cleanup:

  • Legacy implant/map/starchart HudGroups path deleted (star_map.gd/star_map.tscn removed).
  • SnapshotConsumers calls on_insert_deactivated() uniformly instead of per-panel type-specific plumbing.
  • Old atlas_panel.gd (1252 lines) and economics_panel.gd replaced by the apps/ shells.

Architecture reference: docs/architecture/implant-app-pattern.md (commit be9cfa2f) documents the pattern, lifecycle contract, directory layout, nav-stack edge cases, deferred work (Intents dispatcher, DataChannels autoload, mod SDK), and a #844 review checklist.

Key decisions

  • implant/map is the unified HudGroups path (supersedes implant/map/atlas and implant/map/starchart).
  • KEY_M toggles the atlas via the manifest, not via a hardcoded main.gd branch.
  • Every implant app extends ImplantApp. New apps require zero core-code edits: drop a directory under apps/, ship an app.tres, restart.
  • Intra-app navigation goes through ImplantNavStack. No more ad-hoc Level enums.
  • SYSTEM_PICKER / ORBITAL_DIAGRAM level asymmetry fixed — forward and back paths now symmetric.
  • Primitives in D-169 stay data-shape agnostic (no in-tree type coupling) to preserve modder compatibility.

Test plan

  • gdlint client/scripts/ client/ui/ — zero warnings.
  • godot --headless --path client --quit — no script errors.
  • make game → press M → atlas opens at the Reach hop-ring view. Click a system → orbital diagram. Click a body → body entry. Enter → heightmap viewer. ESC back through each to close.
  • Press N → economics opens in INSERT mode. Close. Reopen. Confirm selection state preserved.
  • grep -r "implant/map/starchart" client/ → no hits.
  • grep -r "atlas_panel\|economics_panel" client/ → no hits outside comments/history.
  • Loading screen shows v0.1.35 · protocol 21.
  • tooling/db/sqlite-query --help and tooling/db/sqlite-exec --help print usage text.

Follow-up tickets

  • Intents dispatcher — replace the ad-hoc economics_link_requested wire with a generic intent_requested(target_app, action, params) routed through an ImplantIntents sibling of the registry.
  • DataChannels autoload — manifest-declared snapshot subscriptions so modded apps can subscribe to simulation feeds without poking GameState directly.
  • Launcher UI — once a fourth implant app exists, a chooser becomes necessary.
  • Shipped-build mod discovery — pck mounting, manifest signing, trust model (Phase 6+).

Ticket #852 (zombie starchart retirement) is closed-as-done-in-this-PR since the legacy HudGroups path was deleted.

## Summary Sprint 36 client — three shipped tickets plus a foundational implant-app architecture refactor. **Tickets:** - **#844** — Unified star map and atlas into a single `implant/map` HudGroups app per D-191. Atlas opens at the hop-ring Reach view as the entry screen, navigates down through system → planet → heightmap in one continuous session. Single key binding (M); KEY_A removed. - **#724** — Loading screen shows client version (from `project.yaml`) and protocol version, bottom-centre. - **#722** — `sqlite-query` and `sqlite-exec` respond to `--help` / `-h` with proper usage text (previously returned empty JSON). **Foundational refactor — `ImplantApp` pattern:** The implant is now treated as a faux mobile OS, and implant apps as faux mobile apps. This is the foundational shape for future implant apps and the seam for modded content. New classes (`client/ui/implant/`): - `implant_app_manifest.gd` — `Resource` class declaring `app_path`, `display_name`, `scene_path`, `default_key`, `default_mode`, `preserves_state`. - `implant_nav_stack.gd` — intra-app navigation helper (`push`/`pop`/`replace`/`reset_to_default`, synchronous `screen_changed` signal). - `implant_app.gd` — base class all implant apps extend. Absorbs anchoring, visibility, `HudGroups` registration, `app_changed` filtering, `set_insert_active`. Lifecycle hooks: `on_install`, `on_open`, `on_close`, `on_insert_deactivated`. - `implant_registry.gd` — new autoload. Scans `res://ui/implant/apps/*/app.tres` lazily on first `get_manifests()` call (avoids the `class_name` autoload parse-order trap). Apps migrated to the new pattern: - `client/ui/implant/apps/atlas/` — manifest, shell (`atlas_app.gd` extending `ImplantApp`), four screens (`reach_screen`, `system_screen`, `planet_screen`, `regional_screen`) as plain `Control`s with `enter`/`leave` interface, overlays, heightmap viewer. - `client/ui/implant/apps/economics/` — manifest, shell (`economics_app.gd`), overview screen with preserved selection state. Generic key routing: - `main.gd`'s hardcoded `KEY_M` / `KEY_N` dispatch replaced with a registry-driven loop that reads `default_key` from each app's manifest. New apps drop into `apps/<name>/` and bind automatically. Cleanup: - Legacy `implant/map/starchart` HudGroups path deleted (`star_map.gd`/`star_map.tscn` removed). - `SnapshotConsumers` calls `on_insert_deactivated()` uniformly instead of per-panel type-specific plumbing. - Old `atlas_panel.gd` (1252 lines) and `economics_panel.gd` replaced by the `apps/` shells. **Architecture reference:** `docs/architecture/implant-app-pattern.md` (commit `be9cfa2f`) documents the pattern, lifecycle contract, directory layout, nav-stack edge cases, deferred work (Intents dispatcher, `DataChannels` autoload, mod SDK), and a `#844` review checklist. ## Key decisions - `implant/map` is the unified HudGroups path (supersedes `implant/map/atlas` and `implant/map/starchart`). - KEY_M toggles the atlas via the manifest, not via a hardcoded `main.gd` branch. - Every implant app extends `ImplantApp`. New apps require zero core-code edits: drop a directory under `apps/`, ship an `app.tres`, restart. - Intra-app navigation goes through `ImplantNavStack`. No more ad-hoc `Level` enums. - `SYSTEM_PICKER` / `ORBITAL_DIAGRAM` level asymmetry fixed — forward and back paths now symmetric. - Primitives in D-169 stay data-shape agnostic (no in-tree type coupling) to preserve modder compatibility. ## Test plan - [ ] `gdlint client/scripts/ client/ui/` — zero warnings. - [ ] `godot --headless --path client --quit` — no script errors. - [ ] `make game` → press **M** → atlas opens at the Reach hop-ring view. Click a system → orbital diagram. Click a body → body entry. Enter → heightmap viewer. ESC back through each to close. - [ ] Press **N** → economics opens in INSERT mode. Close. Reopen. Confirm selection state preserved. - [ ] `grep -r "implant/map/starchart" client/` → no hits. - [ ] `grep -r "atlas_panel\|economics_panel" client/` → no hits outside comments/history. - [ ] Loading screen shows `v0.1.35 · protocol 21`. - [ ] `tooling/db/sqlite-query --help` and `tooling/db/sqlite-exec --help` print usage text. ## Follow-up tickets - Intents dispatcher — replace the ad-hoc `economics_link_requested` wire with a generic `intent_requested(target_app, action, params)` routed through an `ImplantIntents` sibling of the registry. - `DataChannels` autoload — manifest-declared snapshot subscriptions so modded apps can subscribe to simulation feeds without poking `GameState` directly. - Launcher UI — once a fourth implant app exists, a chooser becomes necessary. - Shipped-build mod discovery — pck mounting, manifest signing, trust model (Phase 6+). Ticket #852 (zombie starchart retirement) is closed-as-done-in-this-PR since the legacy HudGroups path was deleted.
jpmschweitzer added 7 commits 2026-04-19 12:34:08 +02:00
Per D-191, the atlas is the star map extended downward — not a separate
app. AtlasPanel now owns the full Reach → system → planet → heightmap
zoom hierarchy as a single HudGroups app (implant/map).

- Add REACH_MAP as Level 0 of AtlasPanel's zoom hierarchy; renumber the
  enum so higher index = deeper zoom
- Port hop-ring rendering (pan/zoom, system markers, hover/info, sector
  layout) from star_map.gd into AtlasPanel methods
- Change AtlasPanel.APP_PATH from "implant/map/atlas" to "implant/map"
- main.gd: KEY_M toggles unified atlas; KEY_A binding removed
- Symmetric nav: ORBITAL_DIAGRAM back goes to REACH_MAP (not
  SYSTEM_PICKER), matching the forward skip
- hud_groups.gd docstring documents the unified path and flags the
  legacy starchart path as kept-for-compat (retirement tracked in #852)

star_map.gd's HudGroups registration stays live but inert — no key
binding reaches it. Full retirement follows in #852 after a sprint of
soak on the unified panel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both wrappers previously silently treated --help as a SQL comment and
returned empty result JSON. They now intercept --help/-h before
delegating to the Python connector and print proper usage text with
the correct JSON key names (affected_rows, not rows_affected).

ticket, sprint, and decision already supported --help — no change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Loading screen reads the client version from project.yaml (root version
field) and displays it alongside Protocol.PROTOCOL_VERSION at the bottom
of the overlay: "v0.1.35  ·  protocol 21".

Falls back to "?.?.?" if project.yaml is missing or unreadable (e.g.
when run from an exported pck where the relative path is unavailable).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Unreleased entries for Sprint 36 client: #844 atlas unification, #724
version on loading screen, #722 --help on DB wrappers, plus the enum
renumber and symmetric back-nav tweak.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces the "faux mobile OS" framing: ImplantApp base class,
ImplantNavStack, ImplantAppManifest (app.tres), and ImplantRegistry
autoload. Moddability is a first-class design driver — apps are
droppable directories discovered at startup, main.gd key routing is
manifest-driven, and D-169 primitives stay data-shape agnostic.

Phasing: full pattern lands in the Sprint 36 atlas refactor PR
(#844); Intents dispatcher and DataChannels seam are sketched but
deferred; shipped-build mod discovery stays Phase 6+.

Includes review checklist for #844 and nav-stack edge cases.
atlas_panel.gd exceeded gdlint's 1000-line limit after the REACH_MAP level
was added in #844. Split into:
  - atlas_panel.gd (298 lines)   — shell: HudGroups reg, level enum, nav, key handler, screen header
  - atlas_reach_map.gd (494)     — Level 0 REACH_MAP hop-ring view; emits enter_system
  - atlas_system_map.gd (495)    — Level 1/2 SYSTEM_PICKER + ORBITAL_DIAGRAM; emits enter_body
  - atlas_planet_map.gd (146)    — Level 3/4 BODY_ENTRY + HEIGHTMAP_VIEWER; emits back_to_viewer_body

Shell owns all level transitions. Sub-widgets emit signals, never call show_level().
All four pass gdlint with no warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jpmschweitzer added 1 commit 2026-04-19 13:17:20 +02:00
Establishes the ImplantApp faux-mobile-OS architecture (D-191):

Foundation:
- ImplantAppManifest (Resource): @export vars for app_path, scene_path, default_key, default_mode, preserves_state
- ImplantNavStack (Node): push/pop/replace/reset_to_default, synchronous screen_changed signal
- ImplantApp (Control base class): absorbs HudGroups boilerplate; on_install/on_open/on_close/on_insert_deactivated lifecycle hooks
- ImplantRegistry (autoload): lazy-scans res://ui/implant/apps/*/app.tres; avoids autoload parse-order trap

Atlas app (replaces atlas_panel + atlas_reach_map + atlas_system_map + atlas_planet_map + root viewer files):
- apps/atlas/app.tres — manifest (implant/map, FULLSCREEN, key=M)
- apps/atlas/atlas_app.gd — coordinator; 4-screen nav via ImplantNavStack
- apps/atlas/screens/{reach,system,planet,regional}_screen.gd — enter/leave interface
- apps/atlas/{atlas_viewer,atlas_marker_overlay,atlas_overlay_bar}.gd — moved from root

Economics app (replaces economics_panel):
- apps/economics/app.tres — manifest (implant/economics, INSERT, key=N)
- apps/economics/economics_app.gd — thin shell delegating to OverviewScreen
- apps/economics/screens/overview_screen.gd — full panel logic, enter/leave interface

Wiring:
- project.godot: add ImplantRegistry autoload after HudGroups
- main.gd: registry-driven key toggle loop; rename atlas_panel→atlas_app, economics_panel→economics_app
- hud.tscn: swap to new scene paths; remove legacy StarMap node
- snapshot_consumers.gd: on_insert_deactivated() uniformly; rename vars
- hud_groups.gd: remove stale starchart compat comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jpmschweitzer added 1 commit 2026-04-19 13:18:09 +02:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jpmschweitzer added 1 commit 2026-04-19 13:18:44 +02:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jpmschweitzer changed title from feat(ui): Sprint 36 client — atlas reach map, version display, DB help flags to feat(ui): sprint 36 client — ImplantApp pattern + atlas unification + tooling 2026-04-19 13:22:09 +02:00
Author
Owner

Review: sprint-36/client → main (type: code)

Pre-review checks:

  • gdlint: 0 issues
  • Godot headless parse check: inconclusive (cold worktree cache is not a reliable signal)
  • Runtime smoke test: PR description does not mention make game verification — process gap

Hoshe (QA / bugs): REQUEST_CHANGES

Solid refactor with correctly-handled autoload parse-order trap, sound nav stack, and clean key routing. But star_map.gd/.tscn were not deleted with the atlas unification, a sprint-30 test now fails silently, and the registry's documented "first-wins, warn on startup" key-collision policy is unimplemented.

Tyre (architecture): REQUEST_CHANGES

The ImplantApp pattern is a genuinely sound abstraction, but it's partial in ways that bake in moddability-blocking assumptions: the registry only drives key routing, not app lifecycle (scene_path is declared but unused; apps are still direct-instanced in hud.tscn), display_name/icon_path are dead public fields, key-collision detection isn't implemented, zero test coverage on the new primitives, and the arch doc already drifts from reality on day one.


Consolidated comments

# File Sev Issue
1 client/ui/star_map.gd, client/ui/star_map.tscn blocker Not deleted with atlas unification. star_map.gd:122 still registers dead implant/map/starchart HudGroups path and declares class_name StarMapRenderer. Delete both files per D-191.
2 client/tests/test_sprint30.gd:568-590 blocker test_star_map_is_accessible_from_insert_ui asserts hud.get_node_or_null("StarMap") != null — now returns null; test will fail on CI. Delete (D-191 supersedes) or rewrite to assert AtlasApp.
3 client/ui/implant/implant_registry.gd:7-43 blocker Registry discovers manifests but never consumes scene_path. Apps are still direct-instanced in hud.tscn:4-5, 19-25. Contradicts the arch doc's moddability promise. Either consume scene_path and lazy-instance, or document scene_path as reserved-for-future-use.
4 client/ui/implant/implant_registry.gd:17-36 blocker Arch doc (line 72) promises "first-wins, warn on startup" for key collisions. Unimplemented — duplicates dispatch to first filesystem-iteration-order match with no warning. Add collision check in _scan() with push_warning; also warn on invalid default_mode.
5 implant_nav_stack.gd, implant_registry.gd, implant_app.gd issue Zero test coverage on the foundational primitives. Add test_implant_nav_stack.gd (push/pop/replace/reset/push_default/empty-pop), test_implant_registry.gd (scan + collision), test_implant_app_lifecycle.gd (on_install → on_open → on_close order, preserves_state both values).
6 client/ui/implant/implant_app_manifest.gd:6-12 issue (a) display_name and icon_path declared but consumed nowhere — dead public API; (b) default_key embeds input concerns in app manifest (layering violation — call out with TODO); (c) no schema_version — add @export var schema_version: int = 1 before mods ship against implicit v1.
7 apps/atlas/atlas_app.gd:195-216 + apps/economics/screens/overview_screen.gd:114-136 issue Both files duplicate STAR_MAP_DATA := "res://data/star_map_data.json" loader + sort-by-proper-name lambda. Extract client/ui/implant/widgets/system_index.gd (arch doc reserves that dir).
8 client/ui/implant/implant_app.gd:46-60 issue Default on_insert_deactivated() auto-closes — correct for INSERT, wrong for FULLSCREEN. Gate on HudGroups.get_active_mode() == Mode.INSERT, or document that FULLSCREEN apps must override to pass.
9 client/ui/implant/implant_app.gd:36-60 issue Three-phase lifecycle (on_installon_openon_close) ordering vs nav-stack state is undocumented. Subclass puts "grab current screen" in on_install → gets ""; in on_open → gets post-push id. Document the contract in the base class and arch doc.
10 apps/atlas/atlas_app.gd:68-82 + apps/economics/economics_app.gd:24-37 issue Both _on_screen_changed are near-identical 14-line swap blocks — cargo-culted boilerplate the base class is meant to absorb. Move into ImplantApp via _screens: Dictionary[String, Control] + register_screen(id, control).
11 tooling/db/sqlite-query:4-14, tooling/db/sqlite-exec:4-14 issue Help branch emits plain text to stdout exit 0, while normal output is JSON. JSON-parsing caller that passes --help accidentally gets non-JSON → silent pipeline corruption. Emit help to stderr, or wrap as {"ok": false, "error": "...", "usage": "..."}.
12 docs/architecture/implant-app-pattern.md:292 vs client/ui/star_map.gd:122 issue Arch doc's "Lands this PR" checklist claims implant/map/starchart is deleted, but the registration remains. Fix one side.
13 client/ui/implant/implant_app.gd:14 nit signal insert_deactivated declared but never emitted. on_insert_deactivated() is the actual hook. Remove the dead signal, or emit it from inside the hook.
14 client/scripts/main.gd:198 nit Comment "AtlasApp closes itself before emitting this signal" is backwards. AtlasApp closes as a consequence of economics opening via HudGroups.open_app → app_changed. Rewrite the comment accordingly.
15 client/ui/implant/implant_nav_stack.gd:23-33 nit pop() on last item silently pushes default → while not nav.is_empty(): nav.pop() loops forever. Document "pop never empties below default", or let it truly empty.
16 client/ui/implant/implant_nav_stack.gd:17-41 nit No re-entrancy guard. screen_changed handler calling nav.push/pop synchronously can leave _current_screen_id stale in subclasses. Add in-emit deferral, or document the hazard.
17 client/scripts/main.gd:181-186 nit Registry loop uses manifest.get("default_key") / .get("app_path") as if manifest were a dictionary. Cast + validate (as String, reject empty).
18 client/scripts/main.gd:187-193 nit [ / ] keys for economics nav hardcoded with hand-wave comment — contradicts moddability. Either route via ImplantApp.handle_global_key(event), or document the exception in the arch doc.
19 apps/economics/app.tres:11, apps/atlas/app.tres:11 nit default_mode = 2 / = 1 — bare HudGroups.Mode enum ints. Mod author has no discovery path. Use a String field with registry parsing, or companion .gd constants.
20 client/ui/loading_screen.gd:58-72 nit Client version from project.yaml and server version from Protocol.PROTOCOL_VERSION are independent — mismatched builds display silently. Add intent-clarifying comment, or cross-check and surface mismatch.

Verdict: CHANGES REQUESTED

All 20 comments are actionable. Please address each (or push back with a concrete technical rationale per the review-disagreement protocol).

Process note: no runtime smoke test (make game) mentioned in the PR description or commits. For a refactor of this scope, launching the game and exercising atlas + economics end-to-end should happen before push — this is the Sprint 28 lesson.

## Review: `sprint-36/client` → main (type: code) **Pre-review checks:** - gdlint: 0 issues - Godot headless parse check: inconclusive (cold worktree cache is not a reliable signal) - Runtime smoke test: PR description does not mention `make game` verification — **process gap** --- ### Hoshe (QA / bugs): REQUEST_CHANGES Solid refactor with correctly-handled autoload parse-order trap, sound nav stack, and clean key routing. But `star_map.gd`/`.tscn` were not deleted with the atlas unification, a sprint-30 test now fails silently, and the registry's documented "first-wins, warn on startup" key-collision policy is unimplemented. ### Tyre (architecture): REQUEST_CHANGES The ImplantApp pattern is a genuinely sound abstraction, but it's partial in ways that bake in moddability-blocking assumptions: the registry only drives key routing, not app lifecycle (`scene_path` is declared but unused; apps are still direct-instanced in `hud.tscn`), `display_name`/`icon_path` are dead public fields, key-collision detection isn't implemented, zero test coverage on the new primitives, and the arch doc already drifts from reality on day one. --- ### Consolidated comments | # | File | Sev | Issue | |---|------|-----|-------| | 1 | `client/ui/star_map.gd`, `client/ui/star_map.tscn` | blocker | Not deleted with atlas unification. `star_map.gd:122` still registers dead `implant/map/starchart` HudGroups path and declares `class_name StarMapRenderer`. Delete both files per D-191. | | 2 | `client/tests/test_sprint30.gd:568-590` | blocker | `test_star_map_is_accessible_from_insert_ui` asserts `hud.get_node_or_null("StarMap") != null` — now returns null; test will fail on CI. Delete (D-191 supersedes) or rewrite to assert `AtlasApp`. | | 3 | `client/ui/implant/implant_registry.gd:7-43` | blocker | Registry discovers manifests but never consumes `scene_path`. Apps are still direct-instanced in `hud.tscn:4-5, 19-25`. Contradicts the arch doc's moddability promise. Either consume `scene_path` and lazy-instance, or document `scene_path` as reserved-for-future-use. | | 4 | `client/ui/implant/implant_registry.gd:17-36` | blocker | Arch doc (line 72) promises "first-wins, warn on startup" for key collisions. Unimplemented — duplicates dispatch to first filesystem-iteration-order match with no warning. Add collision check in `_scan()` with `push_warning`; also warn on invalid `default_mode`. | | 5 | `implant_nav_stack.gd`, `implant_registry.gd`, `implant_app.gd` | issue | Zero test coverage on the foundational primitives. Add `test_implant_nav_stack.gd` (push/pop/replace/reset/push_default/empty-pop), `test_implant_registry.gd` (scan + collision), `test_implant_app_lifecycle.gd` (on_install → on_open → on_close order, `preserves_state` both values). | | 6 | `client/ui/implant/implant_app_manifest.gd:6-12` | issue | (a) `display_name` and `icon_path` declared but consumed nowhere — dead public API; (b) `default_key` embeds input concerns in app manifest (layering violation — call out with TODO); (c) no schema_version — add `@export var schema_version: int = 1` before mods ship against implicit v1. | | 7 | `apps/atlas/atlas_app.gd:195-216` + `apps/economics/screens/overview_screen.gd:114-136` | issue | Both files duplicate `STAR_MAP_DATA := "res://data/star_map_data.json"` loader + sort-by-proper-name lambda. Extract `client/ui/implant/widgets/system_index.gd` (arch doc reserves that dir). | | 8 | `client/ui/implant/implant_app.gd:46-60` | issue | Default `on_insert_deactivated()` auto-closes — correct for INSERT, wrong for FULLSCREEN. Gate on `HudGroups.get_active_mode() == Mode.INSERT`, or document that FULLSCREEN apps must override to `pass`. | | 9 | `client/ui/implant/implant_app.gd:36-60` | issue | Three-phase lifecycle (`on_install` → `on_open` → `on_close`) ordering vs nav-stack state is undocumented. Subclass puts "grab current screen" in `on_install` → gets `""`; in `on_open` → gets post-push id. Document the contract in the base class and arch doc. | | 10 | `apps/atlas/atlas_app.gd:68-82` + `apps/economics/economics_app.gd:24-37` | issue | Both `_on_screen_changed` are near-identical 14-line swap blocks — cargo-culted boilerplate the base class is meant to absorb. Move into `ImplantApp` via `_screens: Dictionary[String, Control]` + `register_screen(id, control)`. | | 11 | `tooling/db/sqlite-query:4-14`, `tooling/db/sqlite-exec:4-14` | issue | Help branch emits plain text to stdout exit 0, while normal output is JSON. JSON-parsing caller that passes `--help` accidentally gets non-JSON → silent pipeline corruption. Emit help to stderr, or wrap as `{"ok": false, "error": "...", "usage": "..."}`. | | 12 | `docs/architecture/implant-app-pattern.md:292` vs `client/ui/star_map.gd:122` | issue | Arch doc's "Lands this PR" checklist claims `implant/map/starchart` is deleted, but the registration remains. Fix one side. | | 13 | `client/ui/implant/implant_app.gd:14` | nit | `signal insert_deactivated` declared but never emitted. `on_insert_deactivated()` is the actual hook. Remove the dead signal, or emit it from inside the hook. | | 14 | `client/scripts/main.gd:198` | nit | Comment "AtlasApp closes itself before emitting this signal" is backwards. AtlasApp closes as a *consequence* of economics opening via `HudGroups.open_app → app_changed`. Rewrite the comment accordingly. | | 15 | `client/ui/implant/implant_nav_stack.gd:23-33` | nit | `pop()` on last item silently pushes default → `while not nav.is_empty(): nav.pop()` loops forever. Document "pop never empties below default", or let it truly empty. | | 16 | `client/ui/implant/implant_nav_stack.gd:17-41` | nit | No re-entrancy guard. `screen_changed` handler calling `nav.push`/`pop` synchronously can leave `_current_screen_id` stale in subclasses. Add in-emit deferral, or document the hazard. | | 17 | `client/scripts/main.gd:181-186` | nit | Registry loop uses `manifest.get("default_key")` / `.get("app_path")` as if manifest were a dictionary. Cast + validate (`as String`, reject empty). | | 18 | `client/scripts/main.gd:187-193` | nit | `[` / `]` keys for economics nav hardcoded with hand-wave comment — contradicts moddability. Either route via `ImplantApp.handle_global_key(event)`, or document the exception in the arch doc. | | 19 | `apps/economics/app.tres:11`, `apps/atlas/app.tres:11` | nit | `default_mode = 2` / `= 1` — bare HudGroups.Mode enum ints. Mod author has no discovery path. Use a String field with registry parsing, or companion .gd constants. | | 20 | `client/ui/loading_screen.gd:58-72` | nit | Client version from `project.yaml` and server version from `Protocol.PROTOCOL_VERSION` are independent — mismatched builds display silently. Add intent-clarifying comment, or cross-check and surface mismatch. | --- ### Verdict: CHANGES REQUESTED All 20 comments are actionable. Please address each (or push back with a concrete technical rationale per the review-disagreement protocol). **Process note:** no runtime smoke test (`make game`) mentioned in the PR description or commits. For a refactor of this scope, launching the game and exercising atlas + economics end-to-end should happen before push — this is the Sprint 28 lesson.
jpmschweitzer added 4 commits 2026-04-19 15:11:26 +02:00
Addresses blocker comments from PR review:

- Delete star_map.gd and star_map.tscn — dead implant/map/starchart
  HudGroups registration that should have landed with the atlas
  unification (D-191 criterion 1)
- Remove test_star_map_is_accessible_from_insert_ui and
  test_star_map_scene_exists from test_sprint30.gd — D-191 supersedes
  the insert-UI accessibility pattern
- ImplantRegistry._scan() now detects default_key collisions
  (first-wins with push_warning) and validates default_mode against
  HudGroups.Mode enum (skip + warn on invalid)

star_map_data.json remains — still used by atlas_app and
economics_app for system index lookups.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses 6 mechanical issues from PR review:

- ImplantAppManifest: drop dead display_name and icon_path fields.
  default_key carries a TODO noting its future migration to a keybinds
  manifest (input concern in app manifest is a layering violation,
  tracked explicitly).
- default_mode wire format is now a String ("fullscreen" / "insert") for
  mod-author discovery. ImplantRegistry parses via _MODE_MAP, caches the
  resolved int in _resolved_modes, and exposes get_resolved_mode(app_path).
  main.gd reads the resolved int directly instead of re-parsing.
- Extract client/ui/implant/widgets/system_index.gd (class_name SystemIndex,
  static get_sorted_systems). Removes duplicated star_map_data.json loader +
  sort lambda from atlas_app and economics overview_screen.
- ImplantApp.on_insert_deactivated() default auto-closes only when the app
  is active in INSERT mode. FULLSCREEN apps no longer spuriously close on
  insert state changes.
- tooling/db/sqlite-query and sqlite-exec: --help output goes to stderr
  (exit 0). Keeps stdout reserved for JSON payloads so JSON-parsing
  callers can't get silently corrupted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Seven nit-level fixes from PR review:

- Remove dead signal insert_deactivated from ImplantApp; the hook method
  on_insert_deactivated() is the actual contract.
- Rewrite _on_atlas_economics_link comment in main.gd to reflect the
  actual flow (AtlasApp closes as a consequence of HudGroups single-
  active-app, not before emitting anything).
- Document the "pop never empties below default" invariant on
  ImplantNavStack.pop() with a pointer to reset_to_default.
- Add _mutating re-entrancy guard on ImplantNavStack mutation methods.
  push_error + early return if called during a screen_changed emission.
- main.gd registry loop now uses typed ImplantAppManifest property
  access (manifest.app_path, manifest.default_key) instead of
  dictionary-style .get() calls. Empty app_path triggers push_warning.
- Document the economics [/] hotkey exception in main.gd and reference
  the planned handle_global_key lifecycle hook. Arch doc Follow-up
  section gains a bullet for the new hook.
- Comment the independent-version-read rationale above client_ver and
  proto_ver in loading_screen.gd.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three architectural review comments implemented per Tyre's proposals:

#3 — scene_path consumption (full, Option A)
- ImplantRegistry.instantiate_all(parent) loads and instantiates all
  registered apps with a declared scene_path. Manifests with empty
  scene_path are treated as metadata-only (silently skipped).
- ImplantRegistry.get_app_instance(app_path) returns the live instance.
- hud.tscn no longer direct-instances AtlasApp or EconomicsApp — an
  AppsContainer Control holds the registry-managed children.
- hud.gd._ready() calls ImplantRegistry.instantiate_all($AppsContainer).
- main.gd drops @onready vars for atlas_app/economics_app; looks up
  both from the registry at the top of _ready().

#6c — schema_version on manifest
- ImplantAppManifest: @export var schema_version: int = 1 (first field).
- ImplantRegistry: const CURRENT_SCHEMA_VERSION := 1; tiered check in
  _scan() — older-than-current emits print_verbose, newer-than-current
  emits push_warning, both proceed best-effort.
- apps/atlas/app.tres + apps/economics/app.tres: schema_version = 1.

#10 — absorb _on_screen_changed boilerplate into ImplantApp
- Base class gains _screens: Dictionary, _current_screen_id: String,
  register_screen(id, screen), current_screen_id(), and a real default
  _on_screen_changed implementation that handles leave+hide+enter+show
  with has_method guards and same-screen-replace detection.
- atlas_app: deletes _current_screen_id, _get_screen(),
  _on_screen_changed() override; on_install() collapses to construct →
  setup → wire → register_screen(id, screen) per screen. Preserves
  direct screen refs for atlas-specific signal wiring and method calls.
- economics_app: deletes same scaffolding; on_install() reduces to three
  lines (construct overview_screen, register_screen, nav.set_default).

Also: replaced Resource.get(name, default) dict-style calls with direct
property access on typed ImplantAppManifest reads (2-arg get() is
Dictionary-only; causes "Too many arguments" parse errors on Resources).
_is_valid_manifest gained a Resource type guard and a property-exists
check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jpmschweitzer added 1 commit 2026-04-19 15:19:01 +02:00
Reflects the final shape of the ImplantApp pattern after PR #131
review rounds:

- Manifest: schema_version field, default_mode as String, drop
  display_name / icon_path (no callers). Documents the tiered
  schema_version behavior and the default_mode string-to-enum resolution.
- ImplantApp base class: add register_screen, current_screen_id,
  default _on_screen_changed with has_method tolerance + same-screen-
  replace detection. Drop the unused insert_deactivated signal;
  on_insert_deactivated() default closes if active in INSERT mode.
- Screens paragraph: rewrite to describe subclass-constructs-then-
  registers flow; base owns add_child, visibility, enter/leave dispatch.
- Phasing: promote "Lands this PR" to "Landed — Sprint 36 #844 and
  review rounds" with concrete surface of the full shipped API
  (instantiate_all, get_app_instance, get_resolved_mode, schema_version).
- Review checklist: rewrite as a per-app PR checklist for future apps
  entering the pattern (manifest shape, on_install contract, no direct
  instantiation in hud.tscn, no KEY_* literals in main.gd).

Draft by Tyre; committed by team lead per the team-lead-commits rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jpmschweitzer added 1 commit 2026-04-19 15:26:44 +02:00
Documents the ImplantApp lifecycle ordering and the nav-stack state
guarantee at each hook:

- Class-level docstring on implant_app.gd describes on_install,
  on_open, on_close, and on_insert_deactivated: when each fires,
  what nav state subclasses can rely on, and what is safe to do
  (construct + register_screen in on_install; data refresh + read
  nav.current() in on_open; pause timers in on_close; no close_app
  manual call in on_insert_deactivated — call super or replicate
  the guard).

- Arch doc gains a "Lifecycle hooks" subsection under ImplantApp
  base class with a four-row contract table plus explanatory notes
  on two load-bearing invariants: why on_install sees an empty
  stack (bottom-up _ready order, no open signal yet); why on_close
  must not push/pop (would destroy preserved position on reopen).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jpmschweitzer added 1 commit 2026-04-19 15:36:18 +02:00
Three test suites covering ImplantNavStack, ImplantRegistry, and
ImplantApp lifecycle:

- test_implant_nav_stack.gd: 26 tests — push/pop/replace/reset,
  push_default, is_empty, current/current_payload, signal emission,
  re-entrancy guard, stack-floor-to-default-on-last-pop.
- test_implant_registry.gd: 21 tests — _is_valid_manifest validation,
  lazy scan flag, get_manifests / get_resolved_mode both trigger scan,
  cache on second call, real scan finds atlas and economics, no
  duplicate keys, _MODE_MAP coverage, get_app_instance null before
  instantiate_all, CURRENT_SCHEMA_VERSION = 1.
- test_implant_app_lifecycle.gd: 18 tests — nav created in _ready,
  starts hidden, open FULLSCREEN/INSERT makes visible, GAMEPLAY/wrong
  path doesn't open, nav non-empty on open, close hides, app-switching
  closes active, preserves_state true/false, on_insert_deactivated
  gated on INSERT (closes) vs FULLSCREEN (no-op), register_screen
  adds hidden child, duplicate id does not overwrite (first-wins).

Two team-lead fix-ups before commit (Stig caught the class_name
parse-order issue but used the wrong gdUnit4 hook names):
- before_each/after_each renamed to before_test/after_test per
  gdUnit4 API. test_game_state.gd's use of before_each appears to
  work by coincidence (that test resets autoload state rather than
  constructing objects, so the never-called hook didn't matter);
  tests that rely on hook-driven setup need the correct names.
- test_register_screen_duplicate_id_does_not_overwrite rewritten to
  assert the actual contract (first-wins on _screens dict +
  duplicate screen is not reparented) instead of Control.visible
  default, which defaults to true regardless of registration.

Also removed a stray client/ui/implant/apps/collision_test/app.tres
fixture left over from Hoshe's earlier manual collision-warning
verification. It was untracked and would have blocked atlas from
registering at runtime (KEY_M collision, collision_test won the scan
order). Not committing it.

All three suites exit 0, totals 26/26, 42/42, 36/36 passed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

Review: sprint-36/client → main (round 2)

Round 1 recap: 4 blockers, 8 issues, 8 nits (20 comments).
Team response: 7 commits organized as 4 internal rounds + docs + tests.


Hoshe (QA): APPROVE

All 7 Hoshe-relevant items resolved. star_map.gd + star_map.tscn deleted (637 + 18 lines), broken test_star_map_is_accessible_from_insert_ui removed, registry collision detection firing via push_warning, main.gd:208-210 comment causality corrected, typed ImplantAppManifest iteration with validated app_path, version-mismatch loading screen documented, sqlite-query/sqlite-exec help routed to stderr (JSON stdout preserved). 736 lines of new tests hit real contracts (push/pop/replace/reset/empty-pop, preserves_state both values, INSERT-gated deactivation vs FULLSCREEN no-op, re-entrancy guard, duplicate screen id first-wins) — not happy-path-only.

Minor non-blocking observations:

  • No test injects two manifests with the same default_key to exercise the collision-warn code path (shipped manifests have distinct keys 77/78). test_real_scan_no_duplicate_keys asserts the invariant but not the warn branch. Low priority.
  • implant_nav_stack.gd:68-72reset_to_default() lacks the _mutating guard that push/pop/replace have. Calling it from inside a screen_changed handler is an anti-pattern the docs explicitly warn against, but the guard pattern elsewhere creates an expectation of protection this method doesn't share.

Tyre (architecture): APPROVE

Architectural rework is sound — the abstraction now matches the doc. hud.tscn:1-23 has zero app ExtResources, only an AppsContainer placement node; hud.gd:13 calls ImplantRegistry.instantiate_all($AppsContainer) as the single seam; main.gd looks up apps via ImplantRegistry.get_app_instance(...). A modder's app.tres with a valid scene_path now produces a running app with zero core-code edits — the moddability promise is real. Manifest schema_version added (implant_app_manifest.gd:6) with forward-tolerant warn-don't-reject policy at implant_registry.gd:79-90. String-valued default_mode ("fullscreen"/"insert"/"gameplay") replaces bare enum ints via _MODE_MAP. Three-phase lifecycle explicitly documented in both the class doc-comment (implant_app.gd:6-37) and the arch doc (implant-app-pattern.md:191-200), including the "on_install sees empty stack" gotcha. widgets/system_index.gd extracted, atlas_app + overview_screen both migrated (zero remaining STAR_MAP_DATA duplication). Nav re-entrancy via _mutating flag with escape-hatch error message pointing at call_deferred.

Minor non-blocking observations:

  • NIT #18 (economics [/] hardcoded in main.gd:199-204) — partially resolved with named follow-up in the arch doc for a handle_global_key lifecycle hook. Acceptable defer, but the next app needing intra-app keys will add more hardcodes before the hook lands.
  • implant_registry.gd:112-119_is_valid_manifest uses duck-typed "app_path" in m check. Fine (forward-tolerant drop-with-warn is intended behavior), flagging for future awareness.
  • Base class grew 88→149 lines (justified: doc block + screen management + lifecycle hooks). Still a seam, not a god class — both concrete apps demonstrate clean overrides.

Overall verdict: APPROVED

All 4 blockers, 8 issues, and 8 nits from round 1 are resolved (one nit partially, with a documented follow-up). No new blockers. 736 lines of contract-pinning tests. Arch doc is now an honest accounting of the branch state.

Recommend merge.

## Review: `sprint-36/client` → main (round 2) **Round 1 recap:** 4 blockers, 8 issues, 8 nits (20 comments). **Team response:** 7 commits organized as 4 internal rounds + docs + tests. --- ### Hoshe (QA): APPROVE All 7 Hoshe-relevant items resolved. `star_map.gd` + `star_map.tscn` deleted (637 + 18 lines), broken `test_star_map_is_accessible_from_insert_ui` removed, registry collision detection firing via `push_warning`, `main.gd:208-210` comment causality corrected, typed `ImplantAppManifest` iteration with validated `app_path`, version-mismatch loading screen documented, `sqlite-query`/`sqlite-exec` help routed to stderr (JSON stdout preserved). 736 lines of new tests hit real contracts (push/pop/replace/reset/empty-pop, `preserves_state` both values, INSERT-gated deactivation vs FULLSCREEN no-op, re-entrancy guard, duplicate screen id first-wins) — not happy-path-only. **Minor non-blocking observations:** - No test injects two manifests with the same `default_key` to exercise the collision-warn code path (shipped manifests have distinct keys 77/78). `test_real_scan_no_duplicate_keys` asserts the invariant but not the warn branch. Low priority. - `implant_nav_stack.gd:68-72` — `reset_to_default()` lacks the `_mutating` guard that `push/pop/replace` have. Calling it from inside a `screen_changed` handler is an anti-pattern the docs explicitly warn against, but the guard pattern elsewhere creates an expectation of protection this method doesn't share. ### Tyre (architecture): APPROVE Architectural rework is sound — the abstraction now matches the doc. `hud.tscn:1-23` has zero app ExtResources, only an `AppsContainer` placement node; `hud.gd:13` calls `ImplantRegistry.instantiate_all($AppsContainer)` as the single seam; `main.gd` looks up apps via `ImplantRegistry.get_app_instance(...)`. A modder's `app.tres` with a valid `scene_path` now produces a running app with **zero core-code edits** — the moddability promise is real. Manifest `schema_version` added (`implant_app_manifest.gd:6`) with forward-tolerant warn-don't-reject policy at `implant_registry.gd:79-90`. String-valued `default_mode` ("fullscreen"/"insert"/"gameplay") replaces bare enum ints via `_MODE_MAP`. Three-phase lifecycle explicitly documented in both the class doc-comment (`implant_app.gd:6-37`) and the arch doc (`implant-app-pattern.md:191-200`), including the "on_install sees empty stack" gotcha. `widgets/system_index.gd` extracted, atlas_app + overview_screen both migrated (zero remaining `STAR_MAP_DATA` duplication). Nav re-entrancy via `_mutating` flag with escape-hatch error message pointing at `call_deferred`. **Minor non-blocking observations:** - NIT #18 (economics `[`/`]` hardcoded in `main.gd:199-204`) — **partially resolved** with named follow-up in the arch doc for a `handle_global_key` lifecycle hook. Acceptable defer, but the next app needing intra-app keys will add more hardcodes before the hook lands. - `implant_registry.gd:112-119` — `_is_valid_manifest` uses duck-typed `"app_path" in m` check. Fine (forward-tolerant drop-with-warn is intended behavior), flagging for future awareness. - Base class grew 88→149 lines (justified: doc block + screen management + lifecycle hooks). Still a seam, not a god class — both concrete apps demonstrate clean overrides. --- ### Overall verdict: APPROVED All 4 blockers, 8 issues, and 8 nits from round 1 are resolved (one nit partially, with a documented follow-up). No new blockers. 736 lines of contract-pinning tests. Arch doc is now an honest accounting of the branch state. Recommend merge.
jpmschweitzer closed this pull request 2026-04-19 16:29:10 +02:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jpmschweitzer/settled-reach#131