Skip to content

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.json
  • build/parity/runs/audit-20260909-macos-comparison.json
  • build/parity/runs/audit-20260909-ios-candidate/complete.json
  • build/parity/runs/audit-20260909-ios-comparison.json
  • build/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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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 pass depends 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 pins e5a00ef8. 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.lua loads a game environment but mutates shared package.path, package.zilpath, and on UIKit replaces global io.open with a read-only adapter. That needs an isolation and storage design before adding concurrent sessions or saves.
  • session:start() is outside showSession()'s pcall; 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:

  1. 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.
  2. Port the exact catalog/reviews and pin the intended game/runtime content. Keep app state in Model.lua with focused catalog, session, storage and preferences modules. Keep platform file access scoped to the backend.
  3. Implement save/new/continue/replay/delete and typed commands before claiming a playable port. Transfer source behavioral contracts into fast Lua tests.
  4. Move each screen and reusable card/rating/composer into views/ components returning view trees; use cross-platform etlua with injected ns and ForEach for repeated siblings. Controller wires actions and model state; only its app entry creates the window. Use retained descriptions for structural changes.
  5. 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 ★★★★★.
  6. 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.