SwiftUI and Adventure Arena parity audit
This is the pre-implementation audit. See the subsequent implementation and verification report for resolved findings and current results.
Verified 2026-09-09 against lua-objc d508f721 and the clean Swift app checkout
at /Users/igor/Developer/adventure-arena, revision e7805e8729fb7239398635a2cf18258a71da4834.
The pre-existing .gitignore edit is unrelated. This is an audit, not an
implementation of the missing features.
Neither SwiftUI delivery gate is complete. Adventure Arena is a partial prototype, not a 1:1 port. There is useful native control and measurement infrastructure, but construction tests, adapter geometry, and product parity must be counted separately.
1. Fresh verification
| Check | Result | Meaning |
|---|---|---|
make test parity-check |
52/52 Lua test files pass; 13 manifest cases validate | Existing regression contracts pass; not SwiftUI parity |
python3 -m unittest discover -s tests/parity/batch -p 'test_*.py' |
8/8 pass | Evidence protocol negative controls pass |
make ios-host |
Build succeeds | Current UIKit host compiles |
| macOS batch, rebuilt candidate, cached validated SwiftUI oracle | 158 geometry pass / 66 fail | Adapter-enabled stack/text subset only |
| iOS batch, rebuilt and installed candidate, cached validated SwiftUI oracle | 0 geometry pass / 224 fail | UIKit geometry remains failing |
| macOS diagnostic with adapter disabled | 0 geometry pass / 224 fail | Production defaults differ from the oracle |
| Gallery manifest | 13/13 implemented-unverified, all evidence arrays empty |
No gallery case closes a parity gate |
| Existing gallery reports | 13 fail, including 3 with no compared probes; no reference PNGs supplied | Historical evidence, not fresh paired visual verification |
The fresh runs reused reference-final and ios-reference-final. The batch
comparator validated source/binary/specification hashes, capture identities,
record hashes and matching environments. No oracle values or tolerances were
changed. Comparison commands correctly exit 1 for geometry failures.
macOS: 26.6.2 / 25G83, scale 1, candidate capture 1.29 seconds.
iOS: iPhone 17, UDID BBCF85EC-D271-4623-9E3A-6D577C91B170, iOS 26.5 / 23F77,
scale 3, candidate capture 89.57 seconds including Simulator launch/collection.
Both use en_DE, light appearance, large text size and LTR. This is not the
plan's full environment matrix. No fresh Swift app screenshot pair or live
interaction/accessibility sweep is claimed.
Local machine-readable evidence (generated, not committed):
build/parity/runs/audit-20260909-macos-candidate/complete.jsonbuild/parity/runs/audit-20260909-macos-comparison.jsonbuild/parity/runs/audit-20260909-ios-candidate/complete.jsonbuild/parity/runs/audit-20260909-ios-comparison.jsonbuild/parity/runs/audit-20260909-macos-raw-comparison.json
The last file is a diagnostic: it compares the same cases using
require("parity.batch").run(spec, output, false). Individual identities and
environments were validated, but this temporary invocation does not have the
coordinator's complete provenance manifest. Do not mix it with approved batch
evidence or infer unsupported-modifier equivalence from it.
2. SwiftUI plan findings
- The 158 macOS passes do not verify ordinary production defaults.
The candidate adapter disables stack growth,
shrinking and fill, subtracts four points from unconstrained text widths,
and represents padding with an extra wrapper.
runEnvironment()always enables this path. Some modifier translation is necessary, but these differences need an explicit public mapping and production/XML tests. The adapter's own comments say it preserves different production sizing. Resolve the intended public contract before treating these as core passes. - Layout is still failing inside the adapter. Of the macOS cases,
HStack has 66 pass / 30 fail, VStack 69 / 27, and ZStack 23 / 9.
In
hstack.w120.s0.p0.mixed, SwiftUI wraps “Longer label” to 67×48 points; the candidate measures 108×24 and exceeds the available width. Nested spacer cases also differ in height and placement. Fix proposal handling, wrapping and flexible descendants before tuning full-screen offsets. - The batch is a small subset of the contract. Its grammar supports only text, HStack, VStack, ZStack and Spacer. It does not measure controls, images, grids, navigation, lists, sheets, input actions, XML rendering, accessibility, dark appearance or text-size/RTL variants. Candidate batch PNG capture is explicitly unimplemented. 224 is a parameter matrix, not 224 UI families.
- Inventory and coverage accounting remain incomplete. coverage.md is a historical family snapshot, not the required property/style/platform inventory with fixture links. It still describes three seed cases and omits newer XML tags in places. The manifest has one status per case rather than independent Lua, XML, geometry, appearance, behavior and accessibility statuses. Its validator checks the shape of a small set of fields; it does not enforce the full acceptance contract or require evidence for a passing claim.
- Gate B has missing surfaces, not just unverified ones. No public implementations were found for LazyV/HStack, LazyV/HGrid or ViewThatFits in either exported Lua layer. Grid currently composes eager stacks and assigns column widths; that is not evidence for SwiftUI grid sizing, spanning or lazy realization. Other required semantics such as ordered modifiers, fixedSize and layoutPriority still need explicit mappings.
- The gallery reporter is not a complete acceptance gate.
report_case.py compares native
parent-local frames directly with reference frames, unlike the batch's
common-root coordinate contract. Its overall
passdepends on geometry; missing/different images and unverified input do not prevent it. It records reference action counts without asserting matching candidate actions. The current reports are all failing, but a future green report would still need these independent gates. Fix this before approving visual baselines.
The September 8 “0/224 on both platforms” handoff remains historical evidence. Today's adapter-enabled macOS result supersedes that number, not the plan's requirements. Neither core parity nor common SwiftUI coverage is nearly closed on the evidence available.
3. Adventure Arena feature comparison
Swift paths below are relative to
/Users/igor/Developer/adventure-arena/AdventureArena/; Lua paths are relative
to apps/adventure-arena/.
| Surface | Swift implementation | Lua implementation / gap |
|---|---|---|
| Catalog | Controllers/CatalogModel.swift loads bundled game.json, sorts by title, refreshes saves; view handles loading/error/empty and refresh |
Model.lua hardcodes nine records and a different order, has no loading/empty/error/refresh or saved-session state |
| Featured games | Views/Adventures/AdventuresView.swift: first three sorted games in native page-style TabView, page indicators, whole-card navigation |
Controller:home() chooses only Model.featured()[2]; no carousel or paging |
| Hero | HeroCard.swift: gradient, bottom padding 56, rating symbols, saved-session Play/Continue |
Template omits gradient/paging, uses bottom padding 8, text stars and unconditional Play |
| Catalog hit targets | NavigationLink wraps cover, title and rating | Only the cover has an invisible button; flow test explicitly enforces non-navigating titles |
| Game info | Views/GameInfo/GameInfoView.swift: cover, genre chips, rating distribution/count, description, reviews, How to Play, translated description |
Controller:detail() has cover/title/plain metadata/rating/description/play only |
| Play/continue/restart | PlayButton.swift: saved-state Continue, start-new action with confirmation, bottom safe-area action bar |
Always Play Adventure inside scroll content; no saved-state actions or confirmation |
| Navigation | Independent navigation stacks; detail → session push; native back; session hides tab bar | Details push only on UIKit. Session/error/end actions call install() and replace UIKit's root controller; AppKit opens extra windows |
| Session input | Views/GameSession/GameSessionView.swift: text composer, submit/focus, autoscroll, keyboard insets, loading/waiting state, transcript message model |
A single 20-line text label and four fixed buttons: look, inventory, north, south; no arbitrary input or message model |
| Gameplay assistance | Verb/noun completions, current exits/compass, score/moves, companion cards and choices | None connected in Lua |
| Saves and replay | GameSessionStore.swift persists seed, commands and companion choices; GameSessionModel.swift supports new/continue, replay and deletion |
currentEngine is the only session state; no session store, deterministic replay, continue or deletion |
| Ongoing | Actual saved sessions with resume/delete and an empty state | One hardcoded Limehouse row saying “12 commands played”; no actions |
| Create Game | Name/setting inputs, tone/length pickers, live draft, trimmed-field validation and enabled/disabled Create | Heading and explanatory text only |
| Settings | Language picker, How to Play, version/engine and Delete All Saves | Heading and explanatory text only |
| Reading preferences | Session sheet: font, size 14–24, theme and preview, persisted settings | Absent |
| Translation | Persistent language selection, native translation of description/transcript/companion content | Absent |
| How to Play | Dedicated guide covering parser, movement, objects, essential commands, tips and Infocom | Absent |
Scope qualification: the Swift Create button itself has a TODO/no-op action. 1:1 parity requires its existing form, preview and validation; it does not require inventing a game-generation backend. The Swift catalog currently swallows bundle-load errors into an empty catalog, so its error presentation exists but should not be described as a functioning error-propagation path.
Data and runtime differences
All nine IDs and all nine copied cover images match (SHA-256 byte comparison). That is the strongest completed part of the content port. However:
- All nine full descriptions differ; four short descriptions differ.
- All 21 individual review records are absent from the Lua model.
- Review counts differ: Spellbreaker 891 → 942, Zork II 1872 → 1180, Zork III 1567 → 934, Limehouse 342 → 214 (Swift → Lua).
- Limehouse's genre changes from Victorian Mystery to Mystery Adventure.
- The Swift sorted featured set is Planetfall, Sanitarium, Spellbreaker. Lua's featured set is Wondertown, Planetfall, Sanitarium, and only Planetfall is displayed as a hero.
- Swift pins zilscript
e80d5321; lua-objc pinse5a00ef8. Game/runtime behavior cannot be assumed identical across these revisions. - The Swift backend loads companion files, manages a per-backend Lua state,
and exposes progress/exits/suggestions/replay. Lua
ZIL.lualoads a game environment but mutates sharedpackage.path,package.zilpath, and on UIKit replaces globalio.openwith a read-only adapter. That needs an isolation and storage design before adding concurrent sessions or saves. session:start()is outsideshowSession()'spcall; startup failures do not take the intended error-view path. The current ZIL regression only verifies Wondertown loads and produces opening text; it does not prove all nine games, replay or companion behavior.
Observed macOS startup defect
Fresh native layout dump at 640×720 shows NSTabView at 400×200, with
Settings selected. This follows directly from
AppKit.TabView constructing a 400×200 tab
view and addTab selecting every added tab;
the app does not supply initial selection or flexible sizing. Swift
ContentView defaults to Adventures.
Inspected internal captures at 420×360 light, 640×720 light and 1000×900 dark show the same small Settings pane and unused space. No clipping flags appeared in the default layout dump: absence of clipping is insufficient to verify correct screen selection and space consumption. The internal renderer produces an opaque black tab-chrome region, so these images do not establish live native tab-chrome appearance.
Evidence: /tmp/adventure-arena-audit-default.xml,
/tmp/adventure-arena-audit-light.png,
/tmp/adventure-arena-audit-default-light.png,
/tmp/adventure-arena-audit-large-dark.png.
4. Architecture and completion order
The thin class-returning init.lua is correct. Most other screens are built
inside Controller.lua, and install() creates/replaces a window from screen
actions. The requested architecture needs further work:
- Establish one app root/window, model-owned selected tab/routes, and native navigation that retains tab and session state. Fix initial Adventures selection and flexible tab content first, with actual headless assertions.
- Port the exact catalog/reviews and pin the intended game/runtime content.
Keep app state in
Model.luawith focused catalog, session, storage and preferences modules. Keep platform file access scoped to the backend. - Implement save/new/continue/replay/delete and typed commands before claiming a playable port. Transfer source behavioral contracts into fast Lua tests.
- Move each screen and reusable card/rating/composer into
views/components returning view trees; use cross-platform etlua with injectednsand ForEach for repeated siblings. Controller wires actions and model state; only its app entry creates the window. Use retained descriptions for structural changes. - Complete carousel, details/reviews/help, Ongoing, Create, Settings, reading,
translation and companion UI. Track missing native framework support
separately from missing app code. Use SF Symbols for ratings, not
★★★★★. - Verify the same deterministic catalog/save/transcript states against the Swift app on the same iOS device/runtime. Record platform-appropriate macOS behavior separately; this Swift app is UIKit-oriented. Exercise empty, loading, error, saved, disabled and long-text states, keyboard/back/focus, resizing, light/dark and accessibility before calling it 1:1.
Do not retain temporary placeholder tabs, old navigation paths or compatibility shims once their replacements exist. The current three app test files have 14 assertions total; five are source-string navigation checks. They must grow into behavior/state/layout coverage, and the tests that enforce cover-only navigation must change with the implementation.