Shop / List-Screen Contract

The key words MUST, MUST NOT, and MAY are used as in RFC 2119.

This is the one hierarchy that is deliberately not a class hierarchy: a cross-script base class on a lazily-loaded screen fails to resolve at runtime in the Android export (Could not resolve class "ShopBase" — every shop became unopenable; see docs/shop-dedup.md). The grouping goal is identical — shared behavior in one place, screens only owning what's theirs — but the mechanism is preloaded static helpers. Screens preload() and delegate.

What the helpers own

HelperOwns
ShopNav.handle(screen, event, opts)The input skeleton: modal guard → cancel → tab keys → up/down selection wrap → accept → screen-specific tail. Uniform SFX and input-as-handled.
ShopNav.switch_shop_tabThe buy/sell tab switch (wrap, selection reset, sell-list regen).
ShopNav.selected_item / confirm / infoThe selected-row bounds guard and the ConfirmDialog lifecycle — building the modal, owning _active_modal set/release, callback ordering.
ShopUI.setup_portraitThe shared two-column panel layout.

What a screen MUST provide

Invariants

Autopilot coupling (a real constraint, not a footnote)

The autopilot drives these screens by reading their privates via node.get("_selected_index")-style access. Renaming or relocating a screen's state variables MUST be paired with a grep of autopilot.gdget() on a missing property returns null silently and the sanity gate times out (the #335 incident).

Testing

The helper contract is pinned by test_shop_nav / test_shop_confirm (synthesized input events against a stub screen). New screens get a logic-level test plus coverage in the shop-smoke / shops matrix phase.