Skip to content

feat(monetization): host the banner in tabViewBottomAccessory with a GameRoot-owned lease on an iOS 26.1 floor - #1095

Merged
wei18 merged 20 commits into
mainfrom
session/brisk-stork-isio
Sep 21, 2026
Merged

wei18 merged 20 commits into
mainfrom
session/brisk-stork-isio

Conversation

@wei18

@wei18 wei18 commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

What

The banner moves from per-screen inline slots into the shared tabViewBottomAccessory, on top of the #1062 banner session model. The shell now drives the accessory with tabViewBottomAccessory(isEnabled:), so no empty capsule is drawn when the ad gate denies. The iOS deployment floor rises to 26.1 for that API (owner decision on #1079, option 1).

The accessory path is what ships. #1029 B-6 PASSED: a real AdMob BannerView renders inside the accessory at non-zero size and survives placement changes. The tab-content-bottom fallback stays reachable and documented, but it is not wired.

Merge of main (post-#1062)

origin/main (728ef19) is merged into the #1024 branch following the #1080 rules.

iOS floor 26.1 and isEnabled

  • Project.swift: all four app targets iOS: "26.1". macOS stays 26.0.
  • Every package .iOS(.v26) becomes .iOS("26.1"). The macOS-only ASCRegisterKit is untouched.
  • RootShellView takes bottomAccessoryIsEnabled: Bool. The modifier is still attached unconditionally inside #if os(iOS). isEnabled is the SDK's own display switch, not a conditional attach.
  • GameRoot passes bannerSession.isVisible, read from body, so @Observable re-renders the shell when the gate changes.
  • Docs: design.md §2.4 as-built, foundations.md, SUBMISSION-CHECKLIST, both v2.6 review-information docs.

Built Debug simulator apps report:

App MinimumOSVersion UIDeviceFamily
Sudoku 26.1 1, 2
Minesweeper 26.1 1, 2

A workspace generated before the floor change fails to build with "compiling for iOS 26.0, but module … has a minimum deployment target of iOS 26.1". Run mise run tuist:generate after pulling this branch.

macOS ships no banner (design §2.4.1 option A)

The macOS build contains no tabViewBottomAccessory call, because the only call site is inside #if os(iOS). Checked on the committed tree:

$ git grep -n "tabViewBottomAccessory" 0a6739d3 -- '*.swift'
58 hits, classified:
- 52 are comments or doc comments
- 1 is the API call, inside #if os(iOS):
    Packages/GameShellKit/Sources/GameShellUI/RootShellView.swift
      185:  #if os(iOS)
      186:  .tabViewBottomAccessory(isEnabled: bottomAccessoryIsEnabled) { bottomAccessory() }
      187:  #endif
- 5 are string literals (suite name, Issue.record/#expect messages) in
  iOS-only test code:
    GameShellUITests/RootShellViewBottomAccessoryRenderTests.swift (whole file #if os(iOS))
    GameAppKitTests/BannerAccessoryPinTests.swift (the #if os(iOS) section)

On macOS, makeBottomAccessory() returns EmptyView. BannerAccessoryPinTests (c) pins this, and BannerAccessoryView itself is #if os(iOS). The §2.4.1 options B and C are not implemented.

Tests

Every pin below comes with a named mutation. Each mutation was run and turned the test red, then it was restored.

Pin What it guarantees Mutation that turns it red
BannerAccessoryPinTests (a), iOS, real GameRoot An open gate renders a loaded accessory slot with non-zero height through Observation; dismiss() collapses the capsule again (a1) BannerAccessoryView.body becomes EmptyView(); (d2) GameRoot passes bottomAccessoryIsEnabled: false
BannerAccessoryPinTests (b), iOS A denied gate draws no capsule and registers no slot (b) RootShellView uses isEnabled: true; (d1) GameRoot passes true
BannerAccessoryPinTests (c), macOS makeBottomAccessory() is structurally EmptyView Removing the #if os(iOS) fails the macOS build
BannerAccessoryReHostTests T2, iOS A push+pop re-host on the real GameRoot keeps the same slot id and one request BannerAccessoryView goes back to a self-owned lease (slot id changes, refresh 1→2)
BannerSlotExternalLeaseTests T1 Two sequential slots sharing one external lease give one register and one refresh Registration ignores the external lease
BannerAccessoryMissingLeaseTests T4, iOS No fallback. With no \.bannerAccessoryLease, over a started gate-open session, the hook fires, no slot registers or loads, and refreshCallCount == 0 The nil branch renders the self-owned BannerSlotView (a loaded slot appears and refresh becomes 1)
RootShellViewBottomAccessoryRenderTests, iOS Sentinel content renders; with isEnabled: false nothing is drawn (container absent, or zero height and empty) isEnabled: true

Test totals:

Suite Passed
swift test AppMonetizationKit 181
swift test GameAppKit (macOS) 96
swift test GameShellKit 124
GameAppKitTests on the iOS Simulator 97

Teardown has no pin: it is hygiene, see Known limits.

The four totals above were run on a7ae575, and each run exited 0 with 0 ✘ or "recorded an issue" lines. The two later commits touch none of those packages, so the results carry over:

  • c1d72fa restores five Minesweeper .txt baselines.
  • 0a6739d merges main, touching the app catalogs, mise-tasks/scan/l10n, and SudokuKit tests only.

Both apps' packages were run on 0a6739d. SudokuKit was run in batches, as the repo requires, because its full suite segfaults on this machine.

Suite or batch Result
swift test MinesweeperKit (full) 412 passed
SudokuKit --filter SudokuAppCompositionTests 69 passed
SudokuKit --filter SudokuPersistenceTests 24 passed
SudokuKit --filter SudokuUITests, skipping the six settings suites 376 passed
Settings half A: AudioSettingsSectionTests|ReminderSettingsSectionTests|SettingsIAPRowTests 14 passed
Settings half B, each suite on its own: SettingsViewAppStoreRowsTests, SettingsViewWindowSnapshotTests, SettingsViewTests (run 3 times, once with --no-parallel) 4 + 2 + 15 passed, no crash

Running the three half-B suites together in one invocation (--filter "SettingsViewAppStoreRowsTests|SettingsViewTests|SettingsViewWindowSnapshotTests") crashes swiftpm-testing-helper on this machine. The crash is already on main and does not come from this branch:

Tree Combined runs Helper crashes Tests passed before the crash
This branch, 0a6739d 3 3 21, 21, 22
Clean main c2959d0 (throwaway worktree) 3 3 18, 22, 22

It is the known local SudokuKit settings segfault. Running each suite on its own, as in the table above, is the effective verification. Logs: build/evidence-1080/final-tests/merged/settingsB-{split,attribution}/.

One unattributed macOS failure, not reproduced. During the pin rewrite, one full swift test run of GameAppKit ended "96 tests in 22 suites failed after 2.019 seconds with 1 issue". That output was piped through tail -10, so the failing test's name and assertion were never captured. The ten surviving lines are all passing tests. SwiftPM writes no per-test log, so the detail cannot be recovered. It is not attributed to this branch or to main.

Check Result
Consecutive full-suite reruns 15
Passed 15
Lines matching ✘, recorded an issue or failed 0

The rerun logs are local in build/evidence-1080/flake/gak-run-{1..15}.log.

Simulator evidence

Debug builds, Google's public test banner unit (forced by the LiveAdMobBridge DEBUG init), BGM muted, own simulators only. Screenshots and crops are local under build/evidence-1080/ and are not committed.

iPhone 17 Pro, both apps, on the lease-fix build f929c73 (app code identical at 0a6739d)

Evidence files are local, under build/evidence-1080/final/{sudoku,minesweeper}/. Each check was spot-verified by the Leader.

Check Sudoku Minesweeper
E1: loaded banner in the accessory (.expanded) PASS: creative leading-aligned, 320 pt PASS: slot (33,734,364×50) pt, ✕ (353,737,44×44)
E2: gate denied (sim has no iCloud, so the gate fails closed) PASS: 12/12 polls found no slot; the tab bar has no capsule above it. Before #1079 an empty 48 pt capsule was drawn here. PASS: same result
E3a: isEnabled flips to false with Settings pushed PASS: ✕ tap, Settings stays pushed, capsule gone in under 2 s PASS
E3b: isEnabled flips to false with a board presented PASS: board ✕ tap, fullScreenCover board still presented (pause toggle and grid present), banner gone PASS
E4: board banner geometry PASS: slot (16,480,370×50) pt, creative 16→336, ✕ (336,483,44×44) in the gutter, themed ground, no black sliver outside the creative PASS: slot (16,704,370×50) pt (sits lower, above the tap-mode row), ✕ (336,707,44×44), themed ground

Built apps report MinimumOSVersion 26.1.

Test-fill note. The ads shown are Google's rotating test creatives. Sudoku got the 468×60 letterboxed creative; Minesweeper got a different one with no letterbox.

Minesweeper mirror request probe (iPhone, f929c73)

The accessory is shared GameAppKit code, and the request-count probe had only run on Sudoku. So Minesweeper was run once more. Setup: a throwaway worktree at f929c73 with DEBUG os_log lines at lease attach, lease deinit and ad request (build/evidence-1080/ms-mirror/ms-mirror-instrumentation.patch), and a single continuous log (ms-iphone.log).

Step Accessory lease-attach Accessory lease-deinit ad-request
Cold launch 5A35E8FF… — 1
ATT "Not now" — — 0
Settings push — — 0
Pop to Today — — 0
Idle 30 s — — 0
  • Result: 1 request for the whole run, and the accessory lease never deinits (PASS).
  • Other deinits are expected: all 6 other lease-deinit ids in the log were never attached. They are the self-owned leases created and discarded on each re-host, as described in the lease note above.

iPad regular width (E5)

iPad Pro 13-inch (M5), iOS 26.5, portrait. iPadOS puts the tab bar at the top, so the accessory sits at the bottom of the tab content. That matches Apple's .expanded definition: "at the bottom of the tab's content view".

App Gate Banner slot frame (pt) ✕ frame (pt) Capsule
Sudoku open 46, 1306, 940×50 366, 1309, 44×44 drawn, about 33–994 pt wide
Minesweeper open 46, 1306, 940×50 366, 1309, 44×44 drawn
Sudoku denied no slot element none none (full-res crop of bottom 180 px)
Minesweeper denied no slot element none none (full-res crop of bottom 180 px)

In the open case the creative is 320 pt wide, leading-aligned at x = 46, and the ✕ sits in the gutter past its trailing edge. The denied state is the sim's natural one: no iCloud account, so the gate store is unreadable and the gate fails closed.

Test-fill observation, not a defect. On both iPad and iPhone, Sudoku runs received Google's "This is a 468x60 test ad" creative, which shows black letterbox bars above and below inside the 320×50 frame. The iPhone run is build/evidence-1080/remount-probe/ec-head-iphone-reparent-att-1.png. The Minesweeper iPad run received a house creative that fills the frame cleanly. The bars are inside the creative, so the bridge's clear background cannot reach them, and a real fill is needed to judge production. Crops: build/evidence-1080/sudoku-ipad/E5-open-behindprimer-creative-fullres.png, build/evidence-1080/minesweeper-ipad/E5-open-creative-fullres.png.

.inline is not applicable

The shipped shell never enters .inline on iPhone, so there is no .inline evidence (PM ruling on #1080).

  • Apple, tabViewBottomAccessory: "On iPhone, the placement of the bottom accessory depends on the tab bar size: when the tab bar is normal size, the accessory appears above it; when the tab bar is collapsed, the accessory displays inline."
  • Apple, TabBarMinimizeBehavior.automatic: "On iOS, iPadOS, tvOS, and watchOS, the tab bar does not minimize."
$ git grep -n "tabBarMinimizeBehavior" 0a6739d3 -- Packages
(no hits)

Known limits

  • fix(monetization): ATT primer does not gate the first ad request — maybePresentOnAdContext returns before the user decides #1091, the ATT primer does not gate the first ad request. maybePresentOnAdContext() presents the primer and returns without waiting for the user's choice, so the first load goes out while the primer is on screen. This is pre-existing from fix(monetization): restore cold-launch gate resolution + consent-before-request ordering #1062 and is tracked in fix(monetization): ATT primer does not gate the first ad request — maybePresentOnAdContext returns before the user decides #1091. Observation: build/evidence-1080/sudoku-ipad/remount-burst/stack-before.png (creative loaded behind the primer).
  • The accessory re-host re-requested ads. Fixed here.
    • Cause. UIKit re-hosts tabViewBottomAccessory's content on sheet dismissal, push and pop. That reset the slot's @StateObject lease, disposed the loaded ad and sent a new request.

    • Measured on the same scripted run:

      Build Accessory requests
      Unpatched branch 8 on each device
      Main's inline slots 2
      This PR (GameRoot-owned lease) 1, on both iPhone and iPad
    • Evidence. build/evidence-1080/remount-probe/ (E-a / E-c logs).

  • fix(monetization): accessory banner creative unpainted ~1–2 s after tab-accessory re-host #1094: creative unpainted after a re-host. The same BannerView survives each re-host with no new request, but its creative area stays empty for about 0.6–2.5 s before painting, measured from simulator video.
  • Teardown hygiene. The app is single-scene (no UIApplicationSceneManifest), so GameRoot, and the accessory lease it owns, lives for the whole process. Scene teardown is process death.
    • Harness result. In a hosted-window diagnostic, releasing the hosting controller leaves the GameRoot-owned lease alive. session.slots still holds that same lease id 5 s later, and nothing is disposed.

    • Evidence. build/evidence-1080/remount-probe/test-ec-teardown-v2.log.

    • Open question. What retains the lease after the host is torn down is not identified. There is no production teardown path for it.

    • No accumulation per re-host. Across each whole E-c script (ec-head-{iphone,ipad}.log), self-owned leases that were never attached deinit in step with their inits. The one left alive on each device is the live host's own lease.

      Device Never-attached inits Never-attached deinits Alive at end All unique lease ids, init / deinit
      iPhone 23 22 1 25 / 23
      iPad 16 15 1 18 / 16

      Counting rule: the first three columns leave out the two attached leases on each device. One is the accessory's GameRoot-owned lease, which stays alive by design. The other is the board slot's lease, which deinits on leave. Adding them back gives the last column. The evidence index comment attributes each ad request to its slot and explains where each log ends.

      So old hosted content is released on every re-host. The retention seen in the harness is specific to harness teardown.

    • Other slots. The board, Practice and Settings slots keep self-owned leases and deinit normally. Examples: the board slot lease deinits on leave in E-c, and in main S3.

    • Correction. An earlier v1 run reported the lease as deallocated. That run never actually captured the lease, so it is superseded by v2.

Review so far

This is a pre-PR Leader-side code review of the lease-ownership commit only (6d549fe7..f929c734). It is not the PM gate. Two independent reviewers read committed content through git show.

Reviewer Verdict Findings
sonnet APPROVE 1 NIT: BannerSlotBandLayout was moved to its own file. The move is byte-identical apart from private becoming internal, it stays under the 400-line limit, and it is documented in the file header. No change needed.
haiku APPROVE All contract items PASS: minimal public surface, existing init untouched, no fallback, dismantle comment only, GameShellKit untouched, no leftover probe code or production IDs.

Neither reviewer re-ran the T1/T2 red mutations. Both were run during implementation, and the evidence is recorded in impl-notes.

Gates

Run on 0a6739d, the final HEAD.

$ mise run scan:admob
WARN: canImport(GoogleMobileAds) outside the bridge (soft dependency):
Packages/AppMonetizationKit/Package.swift:47:            // `#if canImport(GoogleMobileAds)` / `canImport(UserMessagingPlatform)`
Packages/AppMonetizationKit/Sources/AdsAdMob/LiveAdMobAdProvider.swift:35:    #if canImport(GoogleMobileAds)
PASS: AdMob isolation intact (bridge: Packages/AppMonetizationKit/Sources/AdsAdMob/LiveAdMobBridge.swift).

$ mise run scan:hygiene   # prints only on failure
exit 0

$ mise run scan:l10n
L10n gate passed: all 2 catalogs 0 placeholders, 0 stale, all 7 locales complete.

The scan:admob warning is not from this PR. git grep -n "canImport(GoogleMobileAds)" shows the same two non-bridge hits on origin/main.

Cleanup note: stash 1f79ad3f

The tagged stash "1058-default-removal-moot-2026-09-11" only removes the default value of a bootSignal: MonetizationBootSignal parameter and adds that argument at call sites. That type no longer exists.

Check Result
Stash changed lines 215
Lines naming bootSignal / MonetizationBootSignal 117
Remaining lines old side of the same calls, plus import MonetizationCore for the type
MonetizationBootSignal|bootSignal in Packages/ at HEAD and origin/main 0 files

The drop is recorded in a comment on this PR.

Closes #1080
part of #1024
part of #1014

🤖 Generated with Claude Code

wei18 and others added 18 commits September 8, 2026 00:35
Running in-flight decision log per agent-impl-notes-log convention:
locked decisions from the dispatch, B-6 (#1029) gate evidence, scope
adjudications, and PM (sly-bunting) rulings incl. the static-modifier
suppression shape and ATT C-33 re-anchor conditions.

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RootShellView gains a third generic `Accessory: View` param + a
`bottomAccessory` ViewBuilder closure, attached via
`.tabViewBottomAccessory` inside `#if os(iOS)` only (macOS has no such
API — design.md §2.4.1 option A). The modifier attaches
UNCONDITIONALLY; visibility is decided inside the accessory's own
content, never by conditionally attaching/detaching the modifier
(PM ruling — see meetings/2026-09-07_1024-banner-accessory.impl-notes.md).

An `Accessory == EmptyView` convenience init keeps every existing call
site (GameRoot, RootShellViewGenericityTests) compiling unchanged.

GameShellKit stays zero-dependency — the closure is fully opaque to
this module; GameAppKit supplies the real (monetization) content next.

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add `BannerAccessoryView` (GameAppKit, iOS/iPadOS only): the same
`BannerSlotView` gate-then-provider flow, now wired as
`GameRoot`/`RootShellView`'s shared `tabViewBottomAccessory` content
instead of a per-screen inline slot. macOS gets `{ EmptyView() }`
(`makeBottomAccessory`, MakeGameApp+Helpers.swift) — no AdProvider/
AdGate/BannerSlotView type is even constructed there.

`TodayTabHost` drops its own banner slot and the adProvider/adGate/
attPrimer params that fed it — the accessory now covers Today,
Practice, and Settings (pushed onto a tab's stack, so it stays inside
the TabView) with ONE shared banner. The C-33 ATT primer anchor moves
with it: `bannerAccessoryFireOnAdContext` replaces the retired
`todayTabHostFireOnAdContext`, same ordering guarantee (unchanged
`BannerSlotView.resolveGateAndLoad`: onAdContext always precedes the
actual ad load).

TodayTabHostTests.swift (tested exactly this wiring) is replaced by
BannerAccessoryViewTests.swift on the new host; verified on the iOS
simulator (macOS `swift test` structurally excludes this iOS-only
suite, it never compiles into that build).

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… start)

Checkpoint of interrupted subagent work: Sudoku practice call site no
longer passes banner:; shared accessory covers the tab. Remaining phase-3
sites (sudoku settings, minesweeper, TodayTabHost/ATT) follow.

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both apps stop passing a `banner:` closure at their Practice/Settings
composition sites (Sudoku Live+TabRoots.swift + LiveRouteFactory.swift;
Minesweeper mirrors) — the shared `tabViewBottomAccessory` now covers
the whole tab shell. The shells' `banner:` params stay (default to
`EmptyView()`) as design.md §2.4's documented tab-content-bottom
fallback; `SudokuAppComposition.themedBanner` / `LiveRouteFactory
.bannerSlot` (MS) stay as that fallback's ready-made implementation,
now with zero production callers, doc-noted why.

Drops the now-orphaned `adProvider`/`adGate` parameters from both
apps' `makeTabRoot` (only the retired Practice banner call used them)
and updates every test call site: RouteFactoryTests / LiveRouteFactoryTests
(makeTabRoot signature), TodayTabViewTests / MinesweeperTodayTabViewTests
/ ASCScreenshotEmitTests (TodayTabHost's simplified init — no more
adProvider/adGate/attPrimer). The retired per-suite banner-visible
coverage (TodayTabHost had its own banner slot pre-#1024) is deleted
along with its now-dead snapshot fixtures — that surface is
BannerAccessoryViewTests + the pre-existing BannerSlotView coverage now.

All other TodayTabView/MinesweeperTodayTabView snapshot baselines are
UNCHANGED and still pass byte-for-byte: every surviving baseline seeds
the ad gate closed, so the banner region already rendered EmptyView
before this move — removing the slot's own subtree produces identical
pixels and structure dumps (verified via swift test, not re-recorded).

Verified: SudokuKit filtered batch (32/32) + MinesweeperKit full suite
(408/408) green; swiftlint --strict clean.

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…1024)

PM condition: prove the primer fires before any ad-context/ad-load
call, not just by reading source. `primerFiresBeforeAnyAdLoad` renders
the real `BannerAccessoryView` in a `UIHostingController` inside a key
`UIWindow` and records, into one shared actor log, the moment ATT's
`isNotDetermined()` check runs vs. the moment the reload coordinator's
`refreshBanner()` actually starts a load — asserting the log order
directly (no timing-race poll loop).

This only pumps on a real run-loop host: confirmed via `xcodebuild
test -destination 'platform=iOS Simulator,name=iPhone 17 Pro'
-only-testing:GameAppKitTests/BannerAccessoryViewTests` (4/4 pass).
`TodayTabHostTests`' documented headless-macOS-`NSWindow` harness
limitation (an actor-hop inside `.task` never resumes there) does not
apply to an iOS Simulator XCTest host, which boots a real
`UIApplication` run loop.

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…T contract (#1024)

design.md §2.4/§2.4.1: U-10 (AdMob-in-accessory) and U-13 (macOS banner
exclusion) both resolve — accessory path shipped, option A shipped as
final, degradation fallback documented but unused. §3.6.1: C-33's ATT
anchor moves a second time, from the Today tab's own banner slot to
the ONE shared accessory banner slot.

screen-contracts.md: the HOME-retirement banner + the ATT-PRIMER
section both re-anchored to `BannerAccessoryView`, with an explicit
before/after reachability table (Today-gated → tab-agnostic) since the
accessory's tab-agnostic mount genuinely changes when the primer can
first fire, not just where the code lives.

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ivers (#1024)

B-6 bonus finding (#1029): the fixed-size 320×50 test creative doesn't
cover the tabViewBottomAccessory capsule's rounded ends when the SDK
stretches its own view to fill the accessory. `LiveAdMobBridge` now
sets the native `BannerView.backgroundColor` to `.clear` so
`BannerAccessoryView`'s themed SwiftUI background shows through the
gap instead of the SDK's opaque fill.

Updates the #1024 impl-notes log with final phase status.

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…1024 CR)

CR round-1 cosmetic findings (sonnet reviewer): MakeGameApp.swift and
GameConfig.swift doc comments still described TodayTabHost as carrying
the banner slot + C-33 ATT anchor; both moved to BannerAccessoryView /
tabViewBottomAccessory in this branch. Comment-only change.

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PM ruling: the #1029 spike proved AdMob-in-tabViewBottomAccessory works
as an API; that isn't proof our own wiring (RootShellView's generic
bottomAccessory param -> .tabViewBottomAccessory { bottomAccessory() })
actually threads a caller's content through, and replacing coverage
removed elsewhere in #1024 is part of the change.

RootShellViewBottomAccessoryRenderTests (new, GameShellKit, #if
os(iOS)) renders the real shell with a sentinel accessory in a
UIHostingController/UIWindow and finds SwiftUI's private
"...BottomAccessory" container in the live hierarchy, asserting it has
real content. A second test with an EmptyView accessory proves the
container is genuinely empty in that case, ruling out "always has
stray children" as a false-positive explanation for the first test's
pass.

Accessibility-identifier lookup was tried first and is documented as a
dead end in the file: `.accessibilityIdentifier(_:)` on plain Text
does not surface through UIAccessibilityIdentification or
UIAccessibilityContainer's count/index methods when walked in-process
in a headless XCTest host.

Verified via `xcodebuild test -scheme GameShellKit -destination
'platform=iOS Simulator,name=iPhone 17 Pro'` — 2/2 new tests pass, full
suite 106/106 on iOS Sim, 116/116 unaffected on macOS (file is
`#if os(iOS)`-excluded there).

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PM-approved: lets the tabViewBottomAccessory banner be sim-verified
showing a REAL rendered ad, not just an empty/gated slot. New
UITestAlwaysOpenAdGateStateStore (UITestFakeSeams.swift) reports an
unconditionally-open AdGateState (ancient firstLaunchAt, no purchase,
no dismissal, no clock-tamper baseline); resolveAdGateStore swaps it
in when the new arg is present and -uitest-fake-ad-gate-repoll is not.

Strictly additive: the new check is inserted BEFORE the existing
repoll guard, whose lines stay byte-identical (git diff proves it).
resolveAdProvider is completely untouched, so the real live AdProvider
(AdMob SDK, Google's public DEBUG test creative per LiveAdMobBridge)
still loads for real -- only the gate's read path is faked.

Release-unreachable: swift build -c release succeeds, and `strings`
over every compiled .o in the Release GameAppKit.build directory
returns zero hits for any "uitest-" string.

Part of #1024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… model (#1080)

Merge origin/main (728ef19) into session/brisk-stork-isio. Resolution rules
from #1080:

- #1024's removal of the inline Today/Practice/Settings banner slots wins:
  every conflict hunk in MakeGameApp, TodayTabHost, both Live+TabRoots, both
  LiveRouteFactory(+Helpers) and both TodayTabViewTests takes the #1024 side;
  main's non-conflicting hunks in those files (the `BannerSlotView(
  isSuppressed:)` rewrite of the unused fallback helpers) are kept.
- The four `snapshotIPhoneLightBannerVisible` fixtures main re-added are
  dropped with the test that produced them (Today no longer hosts a slot).
- `TodayTabHostTests.swift` comes back from main: it pins the
  `makeBannerSession` ATT wiring, not TodayTabHost's retired slot.
- `BannerAccessoryView` is rewritten onto the session model: a plain
  `BannerSlotView(isSuppressed: false, …)` that reads `\.bannerSession` like
  every other slot — no provider / gate / primer parameters; C-33 lives on the
  session's `onAdContext` hook. `makeBottomAccessory()` loses its parameters
  and `BannerAccessoryViewTests` (which called the deleted init and a hook
  `makeBannerSession` already covers) is removed.
- `MarkerProvider` test fixture gains `awaitReady()` and the handle-returning
  `refreshBanner()` the #1062 protocol requires.
- No `MonetizationBootSignal` / `bootSignal` reintroduced (zero hits).
- No board file touched by hand; board slots auto-merged clean.

Verified: `swift test` AppMonetizationKit 180/180, GameAppKit 95/95.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…accessory with isEnabled (#1079, #1080)

Owner decision #1079 option 1: instead of an empty 48pt accessory capsule
whenever the ad gate denies (signed out, Remove Ads, dismissed today), the
shell now uses `tabViewBottomAccessory(isEnabled:content:)`, which is
`@available(iOS 26.1, *)` in the iOS 26 SDK — so the iOS deployment floor
moves from 26.0 to 26.1. macOS stays 26.0 and still has zero
`tabViewBottomAccessory` hits (design.md §2.4.1 option A).

- Project.swift: all four app targets `iOS: "26.1"`.
- Every package `.iOS(.v26)` → `.iOS("26.1")` (synced versioning; the
  macOS-only ASCRegisterKit is untouched).
- RootShellView (GameShellKit, still zero-dependency): new
  `bottomAccessoryIsEnabled: Bool` on the designated init, applied as
  `.tabViewBottomAccessory(isEnabled:)` inside the unchanged `#if os(iOS)`
  attach point. The modifier is still attached unconditionally — `isEnabled`
  is the SDK's own visibility switch, not a conditional attach. The
  `Accessory == EmptyView` convenience init passes `false`.
- GameRoot: passes `bannerSession.isVisible`, read inside `shellContent`
  (evaluated from `body`) so `@Observable` tracking re-renders the shell.
- RootShellViewBottomAccessoryRenderTests: `isEnabled: false` with real
  sentinel content draws no accessory (iOS-only suite).
- Docs: design.md §2.4 as-built addendum, foundations.md floor lines,
  SUBMISSION-CHECKLIST minimum-OS note, both v2.6 review-information docs,
  impl-notes follow-through section.

Verified: swift test GameShellKit 124/124, GameAppKit 95/95 (macOS host);
swiftlint --strict 0 violations on the touched Swift files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e ATT docs (#1080)

Replaces the deleted BannerAccessoryViewTests with a render-level pin
(PM condition on #1080; E2E-only pins do not count):

- BannerAccessoryPinTests (GameAppKitTests): (a) gate open — the real
  RootShellView + BannerAccessoryView over a started session renders a
  non-empty accessory container, the \.bannerSession injection reaches the
  slot (onMissingSession never fires) and exactly one slot registers;
  (b) gate denied — no container or a zero-height empty one, no slot state;
  (c) macOS — makeBottomAccessory() is EmptyView. (a)/(b) run on the iOS
  Simulator; (c) on the macOS host.
- makeBottomAccessory() returns `some View` instead of AnyView.
- RootShellViewBottomAccessoryRenderTests: the isEnabled:false case now also
  requires zero height, not just no subviews.
- screen-contracts ATT-PRIMER and three test headers stop naming the deleted
  BannerSlotView.resolveGateAndLoad / BannerAccessoryViewTests.

Verified: swift test GameAppKit 96/96, GameShellKit 124/124 (macOS);
xcodebuild iOS Simulator BannerAccessoryPinTests (a)(b) and
RootShellViewBottomAccessoryRenderTests 3/3 green; mutation `isEnabled: true`
turned both suites red and was restored; scan:design_contracts PASS;
swiftlint --strict 0 violations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…so re-hosts stop re-requesting ads (#1080)

`tabViewBottomAccessory` re-hosts its SwiftUI content natively on sheet
dismissal, NavigationStack push/pop and some layout passes, without
re-evaluating `GameRoot.body`. The accessory's `BannerSlotView` kept its
`BannerSlotLease` in a `@StateObject`, so every re-host created a new lease.
The old lease's `isolated deinit` disposed the loaded ad and the new one sent
a fresh request. A simulator probe with request logging measured 8 accessory
ad requests on one scripted run (cold launch, ATT dismiss, push, reminder
sheet, pop, idle) on both iPhone and iPad, against 2 on main's
never-re-hosted inline slots. With this change the same script sends 1.

AppMonetizationKit, minimal public surface:
- `BannerSlotLease` is public with `public init()`. `id` and `attach(to:)`
  stay internal.
- `BannerSlotRegistration` attaches `external ?? own`. The self-owned
  `@StateObject` lease still exists but is never attached when an external
  lease is supplied, and a comment explains why it must stay.
- One additional `BannerSlotView(lease:…)` init. The existing init and
  `BannerSessionModel` keying are unchanged: one lease per placement.
- `BannerSlotBandLayout` moved to its own file with no behaviour change,
  `private` becoming `internal`, to keep BannerSlotView.swift under the
  400-line limit.
- `LiveAdMobBridge`'s representable gets only an invariant comment. The new
  host adopts the retained `BannerView` before the old host is dismantled, so
  a future `dismantleUIView` must never `removeFromSuperview` it.

GameAppKit:
- `GameRoot` owns `@State accessoryLease` for the scene's lifetime and
  injects it via the new `\.bannerAccessoryLease` environment entry.
- `BannerAccessoryView` uses the external lease. A missing injection calls
  `onMissingAccessoryLease` (assertionFailure) and renders nothing. There is
  no silent fallback.
- GameShellKit is unchanged and still has zero dependencies.

Tests, each with an executed mutation that turned red:
- T1 `BannerSlotExternalLeaseTests`: two sequential slots sharing one lease
  make 1 register and 1 refresh. Mutation: registration ignores the
  external lease.
- T2 `BannerAccessoryReHostTests` (iOS): push+pop on the real `GameRoot`
  keeps the same slot id and refresh count 1. The mutation (self-owned
  lease) goes red, which shows the harness really re-hosts.
- T4 `BannerAccessoryMissingLeaseTests`: a missing lease fires the hook.
- Round-2 `BannerAccessoryPinTests` (a)/(b)/(c)/(d1)/(d2) are also included
  here. The (a)/(b) cases now host the real `GameRoot`, so the
  `isVisible`→`isEnabled` flip is pinned through Observation.
- T2 lives in its own suite. The two pin disjoint behaviour: lease identity
  across a re-host versus capsule visibility. Folding T2 into the pin file
  would have pushed it past 400 lines.

Docs:
- #1058 design doc: "Externally owned lease (#1080)" section.
- design.md §2.4: as-built note.
- screen-contracts ATT-PRIMER: the trigger is corrected to
  `BannerSessionModel.start()` from `GameRoot.onAppear`.
- impl-notes: probe summary, the PM ruling on the residual repaint gap
  (tracked in #1094), and teardown classified as hygiene (single-scene app;
  no per-re-host lease accumulation).

Verified:
- `swift test`: AppMonetizationKit 181, GameAppKit 96, GameShellKit 124,
  all pass.
- iOS Simulator GameAppKitTests: 97 pass.
- `swiftlint --strict`: 0 violations.
- `scan:admob` and `scan:design_contracts`: PASS.
- Sudoku and Minesweeper iOS Simulator builds succeed.

part of #1024

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…st the hook (#1080)

T4 previously asserted only that `onMissingAccessoryLease` fires when
`BannerAccessoryView` has no `\.bannerAccessoryLease`. A regression that
re-added a fallback to the self-owned `BannerSlotView` init next to that hook
call would have stayed green. That fallback would silently bring back the
per-re-host re-request this PR fixes. (PM request on #1080.)

The test now hosts `BannerAccessoryView` over a started, gate-open
`BannerSessionModel` backed by a fresh `FakeAdProvider`, with the session
injected and the lease left nil, and asserts all three:
- the hook fired;
- `session.slots` is empty (nothing registered or loaded);
- `refreshCallCount == 0`.

The suite is `.serialized` because it swaps a static.

Mutation executed on the iOS Simulator: rendering the self-owned
`BannerSlotView(isSuppressed:…)` in the nil branch turns it red.
`session.slots` holds a `.loaded` slot, and `refreshCallCount == 0` fails.
Restored and green again. Full GameAppKitTests iOS run: 97 tests pass.
`swiftlint --strict`: 0 violations.

part of #1024

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…inesweeper Today tab (#1080)

The main merge (68f840a) combined the wrong sides of one test.
`MinesweeperTodayTabViewTests.swift` came from #1024, where `TodayTabHost`
has no banner slot, so no `\.bannerSession` environment is written. The
view-structure `.txt` baselines came from main (#1062), whose version of the
test wraps the host in that extra environment modifier.

Result: all five `assertViewStructure` checks failed on line 1 (the root
type string) in `swift test --package-path Packages/MinesweeperKit`. Found
while running both apps' packages for #1080.

Evidence that this corrects a merge and does not re-record a behaviour
change:
- The failing assertions are `assertViewStructure` only; every `.image`
  snapshot passed.
- The five PNG baselines are the same blobs at 7b423da (#1024), 728ef19
  (main) and HEAD.
- The test's actual structure output matches the #1024 `.txt` baselines at
  7b423da byte-for-byte, and differs from the main-side baselines only on
  line 1.

So the five `.txt` files are restored from 7b423da.
`MinesweeperTodayTabViewTests` now passes. SudokuKit's `TodayTabViewTests`
already passed, since its test file and baselines agree.

part of #1024

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge origin/main (c2959d0) into session/brisk-stork-isio. Main advanced
by two commits after the first #1080 merge (68f840a):
- aeb80f0 test(sudoku): freeze the session clock in the Ready-window test (#1092)
- c2959d0 chore(l10n): remove proven-orphan keys and relabel live stale keys in both catalogs (#1093)

A read-only `git merge-tree` dry run was clean. The five files main touched
(both app catalogs, the SudokuAppCompositionTests L10n fixture,
BoardLeaveOrPauseStateTests.swift, mise-tasks/scan/l10n) do not overlap with
any file this branch changes.

L10n check: this branch adds no localized string literals. None of the 14
Sudoku keys #1093 removed is referenced by branch-added code.

part of #1024

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wei18

wei18 commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Stash 1f79ad3f moot check (#1080 step 8)

#1080 step 8 asks for the stash to be dropped only after confirming it is moot. The tagged stash is "1058-default-removal-moot-2026-09-11", made on fix/1058-bannerslot-task-lifecycle.

What it contains. Every change in it removes the default value of a bootSignal: MonetizationBootSignal parameter, or adds that argument at call sites. Its base commit is 85c65cb.

Check Result
Changed lines in the stash (git stash show -p) 215
Lines naming bootSignal or MonetizationBootSignal 117
The other 98 lines The old side of the same call sites, plus import MonetizationCore for the type
git grep -c -E 'MonetizationBootSignal|bootSignal' in Packages/ at this PR's HEAD 0 files
Same grep at origin/main 0 files

The type the stash edits was deleted by #1062, so nothing in it can apply. Moot. The stash is dropped right after this comment. The drop command and its result are in the Leader's report to the PM.

@wei18

wei18 commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner Author

Evidence index (#1080)

These are the key numbers from the PR body, with where each measurement lives. Evidence files are local and uncommitted, under build/evidence-1080/ in the #1080 worktree. Every row below was checked by the Leader against the logs, CSVs or crops, not taken from agent reports.

Claim Number Evidence
Accessory ad requests before the lease fix, same scripted run 8 on iPhone, 8 on iPad (main's inline slots: 2) remount-probe/ea-head-{iphone,ipad}.{log,marks}
Accessory ad requests with the GameRoot-owned lease 1 on iPhone, 1 on iPad (cold launch only). Each log's other request is the board slot's; see "E-c request attribution" below remount-probe/ec-head-{iphone,ipad}.{log,marks}
Minesweeper mirror, same probe 1 request; the accessory lease 5A35E8FF… never deinits ms-mirror/ms-iphone.log, ms-mirror-instrumentation.patch
Gate closed in-session (h2), 30 s idle 0 requests, external lease never deinits, both devices remount-probe/h2-{iphone,ipad}.{log,marks}
Re-host churn, per-transition accumulation Never-attached self-owned lease inits/deinits: iPhone 23/22, iPad 16/15 (1 = live host). All unique lease ids: iPhone 25/23, iPad 18/16. See the counting rule below remount-probe/ec-head-*.log
Repaint gap after re-host (#1094) iPad 644–1926 ms, iPhone 824–1341 ms; C1 control on main: 0 empty frames of 144 remount-probe/{ipad,iphone}-gap-*.csv, c1-ipad-main.*
E1–E4, both apps, iPhone 10/10 PASS final/{sudoku,minesweeper}/E{1,2,3a,3b,4}-*
E5, iPad regular width Capsule drawn when the gate is open; no capsule when denied; both apps sudoku-ipad/, minesweeper-ipad/
Practice and Settings wrapper removal (9799f96): before 0a6739d vs after, iPhone, both apps, gate denied and gate open 0 differing pixels in every top, bottom and edge band outside the clock, home-indicator and ad-creative exclusions, except P-top in both apps: about 1,000 px on the Liquid Glass tab-selection pill still settling (P-bottom, the same screen after it settles, is 0). Tab bar and banner slot frames identical wrapper-ab/{Sudoku,Minesweeper}/compare/*-{top,bottom,edges}.png; raw captures wrapper-ab/*/{before,after}/; residual zoom wrapper-ab/Sudoku/compare/P-top-residual-zoom.png
Settings half-B combined-run crash 3/3 on HEAD, 3/3 on clean main c2959d0, so it predates this PR; each suite alone passes final-tests/merged/settingsB-{split,attribution}/
Teardown hygiene (single-scene) Harness retains the lease 5 s after host release (v2) remount-probe/test-ec-teardown-v2.log
Unattributed macOS failure during the pin rewrite Not reproduced in 15/15 reruns flake/gak-run-{1..15}.log

The throwaway probe patches are archived in remount-probe/archive/. That covers the host-owned lease prototype, the open-ad-gate port, the repaint-nudge attempt and the analysis scripts.

E-c request attribution

The probe logs do not tag a request with its slot. Each request below is attributed from three things: the lease-attach line just before it, the step in the .marks file, and whether that lease id later deinits.

iPhone, ec-head-iphone.log

Request (line, time) Preceding lease-attach Step Slot Lease deinits
20, 19:10:25.366 line 14, C732562E… step1 cold launch Accessory Never
165, 19:14:05.815 line 164, B5811C37… h1-open-board, tap 19:14:05.026 Board Yes, line 199 at 19:14:56.142, after h1-leave

iPad, ec-head-ipad.log

Request (line, time) Preceding lease-attach Step Slot Lease deinits
24, 19:23:43.484 line 14, 812B3C8F… step1 cold launch Accessory Never
104, 19:27:30.685 line 103, D861415F… h1-open-board, tap 19:27:29.172 Board Yes, line 131 at 19:28:15.745, after h1-leave

Each device therefore made 1 accessory request, at cold launch, and 1 board request.

Lease counting rule. The "Re-host churn" row counts only self-owned leases that were never attached: iPhone 23 inits / 22 deinits, iPad 16 / 15. The raw unique lease ids in the same files are iPhone 25 / 23 and iPad 18 / 16. The difference is exactly the two attached leases in the tables above. The accessory's lease never deinits, by design. The board's lease deinits once.

End of each log. Both logs end with "Child process terminated with signal 15". That line is the script stopping its own log stream: run-ec.sh kills the stream just before it writes done. The log files were last written at 19:16:00 (iPhone) and 19:29:17 (iPad), 1–2 s before done. The last probe event is at 19:14:56.183 on iPhone and 19:28:15.842 on iPad. These later steps have no log lines:

  • h1-after-leave-idle15
  • h2-dismiss-ad, marked NOT-FOUND, so nothing was tapped
  • h2-idle30
  • done

None of these steps navigates, so no probe events are expected. The files alone cannot rule out output lost from a buffer when the stream was killed. No claim in this index depends on those steps. The h2 results come from separate continuous per-device logs, remount-probe/h2-{iphone,ipad}.{log,marks}.

…R gate part-1 findings (#1080)

PM gate on PR #1095, part 1.

Dead fallback removal (both apps, mirror principle):
- Delete Sudoku `LiveRouteFactory.themedBanner()` and Minesweeper
  `LiveRouteFactory+Helpers.bannerSlot()`. The accessory path shipped in
  #1024 and was confirmed stable in #1079, so the tab-content-bottom
  fallback is obsolete. This also removes the resurrected pre-#1062 comment
  in the Minesweeper helpers file (B1).
- Drop the `banner:` parameter and `Banner` generic from
  `SudokuUI.PracticeHubView`, `MinesweeperUI.MinesweeperPracticeHubView`,
  `GameAppKit.SettingsView` and `SettingsUI.SettingsScreen`; the per-app
  `SettingsView` typealiases become non-generic.
- The Minesweeper helpers file now imports UIKit explicitly under
  `canImport(UIKit)`; `UIApplication` had only resolved through the removed
  SwiftUI import, which only the iOS app build exposed.

Tests:
- `BannerSlotCollapsedHeightTests`: drop the two rows for the deleted
  helpers (Sudoku `themedBanner`, Minesweeper `bannerSlot`); the Today and
  Board rows are unchanged.
- `BannerSlotDarkBandRegressionTests`: doc comment only, notes the removal.
- No test was deleted and no snapshot baseline changed.

Other findings:
- M1: `mise-tasks/new_game/scaffold` generates iOS 26.1, matching
  Project.swift.
- m1: comments no longer point at private memory files; they cite
  `meetings/2026-09-11_1058-slot-model-design.md` "Externally owned lease
  (#1080)" or CLAUDE.md.
- m2: design.md 2.4.1 now says only that the macOS `#else` never builds
  `BannerAccessoryView`; `AdProvider`, `AdGate` and `BannerSlotView` are
  still constructed on macOS.
- m3: `EnvironmentValues.bannerAccessoryLease` is internal and
  `MonetizationUI` is an internal import in that file.
- design.md 2.4 and the #1024 impl notes record the fallback removal.

Verification: swift test AppMonetizationKit 181, GameAppKit 96,
GameShellKit 124, MinesweeperKit 412 (all pass); SudokuKit
SudokuAppCompositionTests 69, SudokuUITests without settings suites 376,
audio/reminder/IAP settings 14, SettingsViewAppStoreRowsTests 4,
SettingsViewWindowSnapshotTests 2, SettingsViewTests 15 (all pass, each
settings suite run alone); swiftlint --strict 0 violations on 19 files;
scan:hygiene, scan:design_contracts, scan:admob pass; Sudoku and
Minesweeper iOS simulator builds succeed.

Part of #1024

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tings shells (#1080)

PM gate on PR #1095, part 1 follow-up rulings.

- `PracticeHubShellView` (GameShellKit) and `SettingsShellView`
  (SettingsKit) lose their `banner:` parameter and `Banner` generic. The
  previous commit removed their only forwarders (both Practice hub views
  and `GameAppKit.SettingsView` / `SettingsScreen`), so nothing fed these
  slots. The single-child `VStack(spacing: 0)` wrapper that existed only to
  seam the content against the banner is gone too; the modifiers now sit on
  the `ScrollView` and the `Form` directly. GameShellKit stays
  zero-dependency.
- `BannerSlotCollapsedHeightTests`: delete the "TodayTabHost (Today tab,
  both apps)" row and its header bullet. `TodayTabHost` has constructed no
  `BannerSlotView` since #1024, so the row pinned a host that no longer
  exists. The table now has 4 cases.
- Both `Live+TabRoots.swift` Today-case comments no longer claim
  `TodayTabHost` carries its own slot and is the ATT anchor. They now say the
  shared `tabViewBottomAccessory` covers the tab, the accessory slot's
  registration is the ad context that triggers the ATT primer, and the
  Daily hub `banner:` param is dead and tracked in #1096.
- `DailyHubShellView`, `DailyHubView` and `MinesweeperDailyHubView` are
  untouched; they were dead before this PR and #1096 owns them.

No snapshot baseline changed and no test was deleted; the Practice hub and
Settings snapshot suites pass unchanged on both apps.

Verification: swift test AppMonetizationKit 181, GameAppKit 96,
GameShellKit 124, SettingsKit 51, MinesweeperKit 412 (all pass); SudokuKit
SudokuAppCompositionTests 69, SudokuUITests without settings suites 376,
audio/reminder/IAP settings 14, SettingsViewAppStoreRowsTests 4,
SettingsViewWindowSnapshotTests 2, SettingsViewTests 15 (all pass, each
settings suite run alone); swiftlint --strict 0 violations; scan:hygiene,
scan:design_contracts, scan:admob pass; Sudoku and Minesweeper iOS
simulator builds succeed.

Part of #1024

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wei18

wei18 commented Sep 21, 2026

Copy link
Copy Markdown
Owner Author

PM merge record (head 9799f960). Gate satisfied. Recording the T6 verdict here because a tooling outage on the PM side blocked posting it when it returned.

  • Dual code review at 0a6739d3. Opus (merge resolution + production correctness): no production defect; the two merges forward from the pre-fix(monetization): restore cold-launch gate resolution + consent-before-request ordering #1062 branch resurrected nothing but one stale comment; fix(monetization): restore cold-launch gate resolution + consent-before-request ordering #1062 invariants intact; the lease shape matches the approved conditions; iOS/macOS cold launch, purchase and repoll traced. Findings: 1 BLOCKER (resurrected pre-fix(monetization): restore cold-launch gate resolution + consent-before-request ordering #1062 comment), 1 MAJOR (new_game/scaffold still emitted iOS 26.0), 3 MINOR. Sonnet (suites, mutations, snapshots, mirror, evidence): all 11 suite counts matched, T1 mutation red, only 2 tests deleted with no orphan baselines, the 5 Minesweeper .txt files byte-identical to 7b423daf and their PNGs sha256-identical to main, mirror symmetric, 6 evidence spot-checks matched. Both verdicts MERGE after fixes.
  • Fixes 6d3b7401 + 9799f960 per the scope rulings recorded on feat(monetization): #1024 follow-through after #1062 merges — merge main, simulator evidence, open PR #1080: comment, scaffold, doc and visibility fixes; the obsolete fallback helpers and the banner: params they orphaned removed (both Practice hubs, SettingsView, SettingsScreen, PracticeHubShellView, SettingsShellView); the TodayTabHost row dropped from BannerSlotCollapsedHeightTests; both Today comments rewritten; the dead Daily hub chain deferred to chore(ui): remove dead banner ViewBuilder params from the Daily hubs (unused since #1020) #1096. The single-child wrapper removal was accepted only after a before/after simulator smoke of Practice and Settings on both apps came back equivalent (0 differing pixels in every band except a settling Liquid Glass pill; identical tab-bar and slot frames).
  • Fresh-context T6 at 9799f960: ACCEPT. It reviewed the post-CR delta item by item (every ruled fix present, nothing else changed, the three Daily hub files untouched) and executed the three iOS-only pin mutations that neither CR could run under the no-simulator rule: T2 re-host (green then red then green; slot id changed + refresh 2), T4 no-fallback (red: slots non-empty + refresh 1), and the GameShellKit isEnabled pin (red: container non-nil, non-zero height). It created and deleted its own simulator, and verified the evidence-index line numbers against the logs byte for byte.
  • One documentation gap, not blocking: the body's test-count table has no SettingsKit row, though the delta touches that package. T6 ran it: 51 tests, all pass.

Known limits carried forward: #1091 (the ATT primer does not gate the first request), #1094 (0.6-1.9 s repaint gap after an accessory re-host), and teardown retention seen only in the test harness (hygiene; no per-re-host accumulation: self-owned lease inits/deinits 23/22 iPhone, 16/15 iPad). Follow-up: #1096.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(monetization): #1024 follow-through after #1062 merges — merge main, simulator evidence, open PR

1 participant