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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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>
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>
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>
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>
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.
## 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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Sprint 36 client — three shipped tickets plus a foundational implant-app architecture refactor.
Tickets:
implant/mapHudGroups 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.project.yaml) and protocol version, bottom-centre.sqlite-queryandsqlite-execrespond to--help/-hwith proper usage text (previously returned empty JSON).Foundational refactor —
ImplantApppattern: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—Resourceclass declaringapp_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, synchronousscreen_changedsignal).implant_app.gd— base class all implant apps extend. Absorbs anchoring, visibility,HudGroupsregistration,app_changedfiltering,set_insert_active. Lifecycle hooks:on_install,on_open,on_close,on_insert_deactivated.implant_registry.gd— new autoload. Scansres://ui/implant/apps/*/app.treslazily on firstget_manifests()call (avoids theclass_nameautoload parse-order trap).Apps migrated to the new pattern:
client/ui/implant/apps/atlas/— manifest, shell (atlas_app.gdextendingImplantApp), four screens (reach_screen,system_screen,planet_screen,regional_screen) as plainControls withenter/leaveinterface, 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 hardcodedKEY_M/KEY_Ndispatch replaced with a registry-driven loop that readsdefault_keyfrom each app's manifest. New apps drop intoapps/<name>/and bind automatically.Cleanup:
implant/map/starchartHudGroups path deleted (star_map.gd/star_map.tscnremoved).SnapshotConsumerscallson_insert_deactivated()uniformly instead of per-panel type-specific plumbing.atlas_panel.gd(1252 lines) andeconomics_panel.gdreplaced by theapps/shells.Architecture reference:
docs/architecture/implant-app-pattern.md(commitbe9cfa2f) documents the pattern, lifecycle contract, directory layout, nav-stack edge cases, deferred work (Intents dispatcher,DataChannelsautoload, mod SDK), and a#844review checklist.Key decisions
implant/mapis the unified HudGroups path (supersedesimplant/map/atlasandimplant/map/starchart).main.gdbranch.ImplantApp. New apps require zero core-code edits: drop a directory underapps/, ship anapp.tres, restart.ImplantNavStack. No more ad-hocLevelenums.SYSTEM_PICKER/ORBITAL_DIAGRAMlevel asymmetry fixed — forward and back paths now symmetric.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.grep -r "implant/map/starchart" client/→ no hits.grep -r "atlas_panel\|economics_panel" client/→ no hits outside comments/history.v0.1.35 · protocol 21.tooling/db/sqlite-query --helpandtooling/db/sqlite-exec --helpprint usage text.Follow-up tickets
economics_link_requestedwire with a genericintent_requested(target_app, action, params)routed through anImplantIntentssibling of the registry.DataChannelsautoload — manifest-declared snapshot subscriptions so modded apps can subscribe to simulation feeds without pokingGameStatedirectly.Ticket #852 (zombie starchart retirement) is closed-as-done-in-this-PR since the legacy HudGroups path was deleted.
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>feat(ui): Sprint 36 client — atlas reach map, version display, DB help flagsto feat(ui): sprint 36 client — ImplantApp pattern + atlas unification + toolingReview:
sprint-36/client→ main (type: code)Pre-review checks:
make gameverification — process gapHoshe (QA / bugs): REQUEST_CHANGES
Solid refactor with correctly-handled autoload parse-order trap, sound nav stack, and clean key routing. But
star_map.gd/.tscnwere 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_pathis declared but unused; apps are still direct-instanced inhud.tscn),display_name/icon_pathare 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
client/ui/star_map.gd,client/ui/star_map.tscnstar_map.gd:122still registers deadimplant/map/starchartHudGroups path and declaresclass_name StarMapRenderer. Delete both files per D-191.client/tests/test_sprint30.gd:568-590test_star_map_is_accessible_from_insert_uiassertshud.get_node_or_null("StarMap") != null— now returns null; test will fail on CI. Delete (D-191 supersedes) or rewrite to assertAtlasApp.client/ui/implant/implant_registry.gd:7-43scene_path. Apps are still direct-instanced inhud.tscn:4-5, 19-25. Contradicts the arch doc's moddability promise. Either consumescene_pathand lazy-instance, or documentscene_pathas reserved-for-future-use.client/ui/implant/implant_registry.gd:17-36_scan()withpush_warning; also warn on invaliddefault_mode.implant_nav_stack.gd,implant_registry.gd,implant_app.gdtest_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_stateboth values).client/ui/implant/implant_app_manifest.gd:6-12display_nameandicon_pathdeclared but consumed nowhere — dead public API; (b)default_keyembeds input concerns in app manifest (layering violation — call out with TODO); (c) no schema_version — add@export var schema_version: int = 1before mods ship against implicit v1.apps/atlas/atlas_app.gd:195-216+apps/economics/screens/overview_screen.gd:114-136STAR_MAP_DATA := "res://data/star_map_data.json"loader + sort-by-proper-name lambda. Extractclient/ui/implant/widgets/system_index.gd(arch doc reserves that dir).client/ui/implant/implant_app.gd:46-60on_insert_deactivated()auto-closes — correct for INSERT, wrong for FULLSCREEN. Gate onHudGroups.get_active_mode() == Mode.INSERT, or document that FULLSCREEN apps must override topass.client/ui/implant/implant_app.gd:36-60on_install→on_open→on_close) ordering vs nav-stack state is undocumented. Subclass puts "grab current screen" inon_install→ gets""; inon_open→ gets post-push id. Document the contract in the base class and arch doc.apps/atlas/atlas_app.gd:68-82+apps/economics/economics_app.gd:24-37_on_screen_changedare near-identical 14-line swap blocks — cargo-culted boilerplate the base class is meant to absorb. Move intoImplantAppvia_screens: Dictionary[String, Control]+register_screen(id, control).tooling/db/sqlite-query:4-14,tooling/db/sqlite-exec:4-14--helpaccidentally gets non-JSON → silent pipeline corruption. Emit help to stderr, or wrap as{"ok": false, "error": "...", "usage": "..."}.docs/architecture/implant-app-pattern.md:292vsclient/ui/star_map.gd:122implant/map/starchartis deleted, but the registration remains. Fix one side.client/ui/implant/implant_app.gd:14signal insert_deactivateddeclared but never emitted.on_insert_deactivated()is the actual hook. Remove the dead signal, or emit it from inside the hook.client/scripts/main.gd:198HudGroups.open_app → app_changed. Rewrite the comment accordingly.client/ui/implant/implant_nav_stack.gd:23-33pop()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.client/ui/implant/implant_nav_stack.gd:17-41screen_changedhandler callingnav.push/popsynchronously can leave_current_screen_idstale in subclasses. Add in-emit deferral, or document the hazard.client/scripts/main.gd:181-186manifest.get("default_key")/.get("app_path")as if manifest were a dictionary. Cast + validate (as String, reject empty).client/scripts/main.gd:187-193[/]keys for economics nav hardcoded with hand-wave comment — contradicts moddability. Either route viaImplantApp.handle_global_key(event), or document the exception in the arch doc.apps/economics/app.tres:11,apps/atlas/app.tres:11default_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.client/ui/loading_screen.gd:58-72project.yamland server version fromProtocol.PROTOCOL_VERSIONare 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.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>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.tscndeleted (637 + 18 lines), brokentest_star_map_is_accessible_from_insert_uiremoved, registry collision detection firing viapush_warning,main.gd:208-210comment causality corrected, typedImplantAppManifestiteration with validatedapp_path, version-mismatch loading screen documented,sqlite-query/sqlite-exechelp routed to stderr (JSON stdout preserved). 736 lines of new tests hit real contracts (push/pop/replace/reset/empty-pop,preserves_stateboth values, INSERT-gated deactivation vs FULLSCREEN no-op, re-entrancy guard, duplicate screen id first-wins) — not happy-path-only.Minor non-blocking observations:
default_keyto exercise the collision-warn code path (shipped manifests have distinct keys 77/78).test_real_scan_no_duplicate_keysasserts the invariant but not the warn branch. Low priority.implant_nav_stack.gd:68-72—reset_to_default()lacks the_mutatingguard thatpush/pop/replacehave. Calling it from inside ascreen_changedhandler 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-23has zero app ExtResources, only anAppsContainerplacement node;hud.gd:13callsImplantRegistry.instantiate_all($AppsContainer)as the single seam;main.gdlooks up apps viaImplantRegistry.get_app_instance(...). A modder'sapp.treswith a validscene_pathnow produces a running app with zero core-code edits — the moddability promise is real. Manifestschema_versionadded (implant_app_manifest.gd:6) with forward-tolerant warn-don't-reject policy atimplant_registry.gd:79-90. String-valueddefault_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.gdextracted, atlas_app + overview_screen both migrated (zero remainingSTAR_MAP_DATAduplication). Nav re-entrancy via_mutatingflag with escape-hatch error message pointing atcall_deferred.Minor non-blocking observations:
[/]hardcoded inmain.gd:199-204) — partially resolved with named follow-up in the arch doc for ahandle_global_keylifecycle 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_manifestuses duck-typed"app_path" in mcheck. Fine (forward-tolerant drop-with-warn is intended behavior), flagging for future awareness.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.
Pull request closed