src/switcher/state/TabGroupResolverSpecs.md
TabGroupResolver holds the pure decisions behind macOS OS-tab detection — deciding which windows are
inactive tabs of one tabbed window, given the facts AltTab can read. macOS exposes no public API that maps
an inactive tab to its window: _AXUIElementGetWindow on an AXTabButton returns the parent window's WID,
not the tab's own, so tabs must be matched to windows by title (full investigation:
TabbedWindowDetection.swift). Inactive tabs also appear in no CGS list, so the WindowServer-driven
discovery never sees them until the user focuses one. Extracted as a pure kernel from TabGroup (which
keeps the Windows.list reads/writes and the AX/CGS side effects) so the brittle matching is unit-testable
without the Window graph. Operates on the flat TabWindow record — the tab analogue of WindowState,
carrying the pid / wid / size / position / tabbedSiblingWids that grouping needs and WindowState
omits.
Two independent signals locate tabs, used at different times:
matchSiblings) — authoritative but only available for the active tab (an inactive tab
reports no AXTabGroup), read at discovery and on each show. Matches by title, so it's fragile when titles
are dynamic (Terminal renames tabs by cwd/command) or drift between the tab-button title and the window's
own title. That drift can be TOTAL, not marginal: an app may compose its WINDOW title from components its
TAB title has no setting for, which is what stock Terminal does ("~/Documents — -zsh ▸ -zsh — 80×23" as a
window, "~/Documents" as a tab, #5785). Then nothing matches, ever, and this signal is simply absent._AXUIElementGetWindow, AXWindow, AXParent and AXTopLevelUIElement on an AXTabButton all
return the PARENT window's wid — re-probed on macOS 26, don't re-litigate), but the AXTabGroup's presence
means tabbed and its button count is exact. So the count confirms a geometry cluster when titles can't,
and bounds it.geometryGroups) — reactive, AX-free: within one app, windows sharing an exact size where
one holds a Space and the others are Space-less are a tabbed window and its background tabs. Catches the
tab switch that AX missed (the "pop-in"), and fullscreen tabs that expose no readable AXTabGroup. Geometry
alone does not create a group: it only fires when the visible window is fullscreen or was already
AX-confirmed as tabbed (tabbedSiblingWids != nil). Otherwise separate windows of one app that share a
default size and go briefly Space-less (a Space transition, a flaky CGS read) get collapsed into a phantom
tab group, hiding real windows (#5830).geometryGroups(windows, newlyDiscovered: nil) -> [GeometryGroup] — group same-app, same-frame
(size, then split by POSITION — framePartitions) candidates where the visible tab holds a Space and
≥ 1 sibling is Space-less. Position was once excluded because a background tab's position goes stale while
ordered out — but size comes from the same frame and goes stale with it, so the size half of the key already
fails then (terminalFullscreenBackgroundTabs: 757×543 vs. their fullscreen active's 1440×864). All it
bought was merging distinct windows: macOS cascades by 29px, so two Finder windows at the default 920×436
were one cluster and one annexed every tab of the other mid-burst, vanishing its tile (rec12). A cluster with
ANY fullscreen member is left whole — a fullscreen window's tabs can't share its frame, and a new tab's
isFullscreen LAGS, so the flag is only trustworthy cluster-wide; two fullscreen windows are separated by
the per-Space partition instead. Minimized / size-less windows are excluded. A separate real window is never
Space-less, so two visible same-size windows are not collapsed. The cluster must be CONFIRMED — the
visible was AX-confirmed tabbed (tabbedSiblingWids != nil), OR any member is fullscreen (its tabs expose
no readable AXTabGroup and a new active's own fullscreen flag lags discovery), OR a background candidate is
already grouped and its group has lost its visible (no other member of it still holds a Space — a new tab
taking over that group) — so geometry never fabricates a group from unconfirmed same-size windows (#5830).
The visible's own AXTabGroup COUNT (tabCount > 1) is a fourth confirmation, for the app whose titles
never match: tabbedSiblingWids is written only by the title path, so without it that gate could never
open and the tabs churned as separate tiles forever (#5785). It can only be ADDITIONALLY true where no
group was formed — an app whose titles match already has tabbedSiblingWids — so it cannot change those
apps' behaviour. A fold confirmed ONLY this way is BOUNDED by the count: more background candidates than
tabCount - 1 means the cluster holds a non-tab (a separate same-frame window gone transiently
Space-less, a second group at the same frame), which one is undecidable, and guessing would hide a real
window — so the whole fold is refused. The bound is scoped to that clause because the model legitimately
holds MORE candidates than the window has tabs while a wid-minting tab switch is in flight (retired wids
aren't swept yet, generator seed 163).
A group whose own active tab is still on-screen is LIVE and never adopted: geometry is a guess and must not
overrule the AX read that linked it. Dropping that clause let an unrelated same-size window annex a tab that
had merely backgrounded inside another window, hiding a real window (rec10). The test is Space-holding, not
newlyDiscovered — that flag means newly TRACKED, and at launch everything is. The visible parent is a BRAND-NEW active (on-screen, no link yet — a
just-created tab that took over) else the ESTABLISHED visible (!isTabbed and holding a Space) — not
any Space-holder: updateState backfills the active's Space onto every background tab, so several members
hold a Space, and picking an arbitrary one flipped which wid the group shows (thumbnail bouncing to the app
icon and back). A HELD member never wins the pick while an un-held candidate exists (held = the OUTGOING
tab, and its Space is borrowed — rec14), but stays the last fallback (hysteresis for a mid-gap group whose
only Space-holder is its held rep). A held member also counts as BACKGROUND despite its borrowed Space —
the same fact as matchSiblings' held leg, patched on both claim paths. If no member is currently the
visible tab, the group is skipped — the AX paths re-establish it. newlyDiscovered (the wid this pass discovered) overrides the pick: in fullscreen AX names nothing
AND a brand-new tab is often momentarily Space-less — indistinguishable from an ungrouped background tab by
facts alone — so only the caller can say which member just took over. Without it geometry picked the held
old tab and the switcher showed the PREVIOUS tab, then flipped to two tiles.
Fullscreen: resolve per SPACE. Every fullscreen window is SCREEN-sized, so one size-cluster can hold
several fullscreen windows plus all their tabs. The invariant is about each Space, not the cluster: when a
cluster is fullscreen and spans several Spaces it is partitioned, and each Space resolved on its own —
one window and its tabs. (Asking "does the CLUSTER span ≤1 Space" instead made a second window's tabs each
get a tile.) A Space-less member can't be attributed to any of them, so it's left to a later pass — which is
why the partition is skipped when the cluster spans ≤1 Space, the case where attribution IS decidable (a
brand-new visible is momentarily Space-less and must still fold its old active in).
foldEveryMember is the "one Space ⇒ one window" side: every non-visible member is a background tab,
even one still holding the Space because its 1326 hasn't landed. Off for normal windows (a normal Space is
shared by many), where only Space-less members are background, so a same-size standalone isn't swept in.
Output sorted by visibleWid.matchSiblings(active, axTitles, sameAppWindows, activeIsNewlyDiscovered = false) -> SiblingMatch —
resolve the active tab's AXTabGroup titles to tracked windows. The active title is removed once (duplicates
allowed); each remaining title matches the first compatible, not-yet-matched same-app window that is
PLAUSIBLY an inactive tab: already isTabbed, Space-less, or holding a SYNTHETIC Space — isHeld (a
backgrounding tab mid-swap, rec14's ghost 4th tile) or spaceIsBorrowed (a Space COPIED from a sibling by
the backfill / the representative borrow; rec20's ex-representative, orphaned with the Space we lent it,
stood as a permanent stray tile). A held/borrowed Space is our annotation, not CGS evidence, so it must not
count as on-screen protection; both legs are size-gated so a held/borrowed tab of a DIFFERENT same-position
window isn't claimable across groups. A genuinely on-Space window is never claimed (it's on-screen, so by
definition not an inactive tab; without this a new same-title window filled a title whose real tab has no
window, Finder cmd-N). The borrow OUTLIVES the membership that justified it: leaving a group used to strip
the lent Space, which states "CGS places this window nowhere" — the strong phantom signal — about a window
nobody had asked CGS about, and hid live ones (QA T-05). The marker alone keeps an orphaned ex-member from
looking on-screen, and a genuinely-gone one is turned phantom by the next spacesSynced. A GENUINELY-fullscreen candidate is
ALSO never claimed (the fullscreen Space invariant: it is its own window on its own Space) — switching
Spaces TO a fullscreen window makes it transiently Space-less, and positionsCompatible's fullscreen
waiver (there to claim a fullscreen window's frozen NON-fullscreen tabs) would otherwise let a windowed
group annex the fullscreen WINDOW, whose links geometry's union then bridges into a mega-group hiding real
windows (rec24c: 10 members, 9 hidden). Second pass, only when activeIsNewlyDiscovered (this active is
a window the user JUST CREATED — a new tab that took over its group; newly CREATED, not merely newly
tracked: at launch every window is newly tracked, and firing there had two real windows sharing a tab title
swallow each other): fill any still-unmatched title with a
same-app, same-size on-screen sibling too. That sibling is the previous active tab, backgrounding as
this new tab takes over, whose "removed from Space" event hasn't landed yet — claiming it now groups the
pair atomically so it doesn't flash as a 2nd tile. Safe because a genuinely separate new window carries its
OWN AXTabGroup as the active element and never reaches this path (testOnScreenWindowNeverClaimedAsTab runs
with the flag off); still title-gated (coexisting same-app groups keep distinct titles) and bounded by the
tab count. A window still tabbed into THIS group (isTabbed + tabbedSiblingWids ∋ active) is then
kept even if no title named it, so a duplicate or renamed title can't flap an inactive tab out (#5830);
each kept sibling also cancels one untrackedTitle. ALSO kept: an on-screen un-tabbed member of this group
that is MORE RECENTLY FOCUSED than the reading active — AX reads are queued, so a read can land right after
the user switched to another member, and treating the reader as the active then ejected the REAL active
from its own group, stranding it as a stray tile that successive stale reads fought over (rec18). Strict
<, so a genuinely-departed window still reaches toUntabWids. Returns the group's wids (active first), the
matched+kept wids, untrackedTitles (titles with no window → inactive tabs to discover), and toUntabWids
(windows that were in this group but are no longer tabbed).
Merge All Windows: the tabs never converge on a frame. "Tabs of one window share its frame" — the premise
every position rule here rests on — is simply false after Window ▸ Merge All Windows. The merged window is a
BRAND-NEW wid one cascade step past the last of the windows it absorbed, and those keep the positions they
had, frozen (no geometry event reaches an ordered-out tab). Measured live in Finder and Terminal alike
(2026-07-30 QA, T-03/T-04; the capture is terminalMerge4Tabs): four tabs, one shared SIZE, four positions
29px apart. So framePartitions gave each tab a partition of one, no cluster survived count > 1, and no
merged group could form in any app, ever — the tabs stayed Space-less and un-isTabbed, hence PHANTOM, so
"separate window for each tab" showed 1 tile instead of 4 and three real windows sat exposed to the
dead-window sweep. That cascade is also exactly the offset the split exists to catch (rec11/rec12), so the two
requirements genuinely collide on position and are settled with the fact that isn't position:
tabCountAccountsForEveryMember leaves a cluster WHOLE when the visible's AXTabGroup COUNT equals the cluster
size, because AX has then accounted for every member and no second window can be hiding in it. Two conditions,
each with teeth:
<= would have waved
the whole recording through. Fewer members than tabs is the ordinary mid-discovery state and stays split:
an unclaimed background tab is hidden either way, and the next read re-decides.tabCount goes
stale and a background tab's is stale by construction: only an ACTIVE tab reports an AXTabGroup, and the
count is deliberately not retired while its window is still in a group (a nil read is transient, and
retiring on it tore live groups apart). So a window that WAS a 3-tab active keeps tabCount 3 after a tab
is dragged out of it. Live T-05: after Move Tab to New Window, with the drag-out already correctly
confirmed, that stale 3 "accounted" for a 3-member cluster and geometry folded the torn-out window at
(290,712) straight back into the group at (1116,683) — group form g13 members=[72914, 72915, 72910] reason=geometry. Holding a Space did not protect it: its Space was OUR annotation by then (normalize lends
a group's members one), which is why the test is hasGenuineSpace on both counts.positionsCompatible(a, b) -> Bool — tabs share their parent's frame. An existing tab link wins (a
stale position can't split an already-grouped pair). Unknown position or either fullscreen → title-only
fallback (true). Otherwise positions must match EXACTLY (rounded): macOS cascades new windows by 29px, so
any tolerance is precisely where a SECOND window lives — a 50px tolerance let two cascaded Finder windows
swallow each other (rec11). Exact is the safe side of the asymmetry: a missed claim is invisible (an
unclaimed background tab is hidden anyway), a false claim hides a real window. The link bypass is what
makes exact affordable.groupRepresentative(members) -> CGWindowID? — which member a group shows: the most recently FOCUSED
presentable member, full stop. Focusing a tab makes it the active tab by definition, so focus
(lastFocusOrder) is the one authoritative signal; every earlier heuristic (un-tabbed Space-holder, then
the just-backgrounded visible, then focus) approximated it through the derived isTabbed and backfilled
Spaces — circular, and read-order-sensitive exactly when several members look on-screen mid-switch
(rec18/rec19). Presentable-only (a new tab is 0×0 for ~640ms; handing it the group showed empty pixels,
rec11 — the outgoing tab keeps the tile until the new one is sized), full-set fallback when nothing is
presentable. nil ONLY for an incoherent group (members disagreeing on their links).
TabGroup.updateState also asks this kernel (over the pre-mutation facts) which member to pass to
TabGroups.form as representative, so a queued stale read can't displace the member the user is on.dragOutVerdict(joiner, previousRepresentative) -> Bool? — a group member that joined a Space is
EITHER the group's new active tab (a switch — a pure representative move, no membership churn; the old
leave-and-rediscover here oscillated the open switcher through several layouts per switch, rec19) or a tab
DRAGGED OUT to stand alone. Indistinguishable at event time (a dragged tab starts at the parent's frame),
so the adapter assumes a switch and re-checks this verdict on an interval: nil = joiner's frame not
settled/known yet (0×0 beat; re-check), false = same frame as the previous representative (switch
confirmed; stop), true = settled at another frame (it left; remove from the group, which re-picks its
representative by focus). Judged against the PREVIOUS representative — live on-screen at the join — not
the other members, whose stored frames go stale (a moved window leaves its background tabs frozen at the
old frame). Fullscreen excluded: frames are unreliable there and membersThatLeftGroup owns fullscreen
departures. The adapter refreshes both frames from the WindowServer before the first check (the joiner's
stored position is ~215ms stale after a switch, and a stale frame reads as a drag-out).
Only a PRESENTABLE member (a usable, non-degenerate size) may represent the group — the OS publishes a new tab
at 0×0 and sizes it ~640ms later, and that un-renderable tab is otherwise exactly the un-tabbed Space-holder
the rules prefer, which showed a tile of empty pixels (rec11). Skipping it keeps the outgoing tab — still
sized and captured — as the tile until the new one can be drawn, so a tab creation never blinks the window it
spawned on. Falls back to the full set when nothing is presentable (a stale tile beats none).
nil ONLY for an INCOHERENT group — members DISAGREEING on tabbedSiblingWids (a wid re-homed into a new
group while the old group's members still list it); forcing one of those visible spuriously un-hid a
background tab as a 2nd tile. Note "every member tabbed" is NOT that test: a coherent group whose visible
got lost must recover (via the focused member), not be abandoned — treating it as stale stranded it hidden.shouldHoldVisibleThroughDiscovery(isTabbed, becomesSpaceless, hadRecentWindowCreate, hadRecentUntrackedSpaceJoin) -> Bool — hold a backgrounding tab visible while the incoming tab is
discovered (~640ms: the OS creates it at 0×0 and sizes it later), so the group never shows ZERO tiles.
Gated on a REPLACEMENT SIGNAL, which separates "something is coming to take over" from a plain background
(nothing is → hide normally). Two signals qualify: a recent create (811 — a new tab being made), or a
recent UNTRACKED window joining the visible Space (rec24: Finder mints a brand-new wid per tab switch and
emits no create, only that 1325, one beat before the outgoing tab's 1326). A switch between TRACKED tabs
has neither signal and needs no hold — the representative swap covers it. The timer lives in the adapter;
this is the decision.shouldReleaseHold(isTabbed, hasPresentableReplacement, attemptsExhausted) -> Bool — the hold's only job
is "never ZERO tiles for a window mid tab-swap", so it ends once something else can show: the incoming tab
CLAIMED it (isTabbed ⇒ hidden by the tab filter anyway), a replacement is drawable, or the safety cap.
It once asked discoveryPending (!recentlyCreatedWindows.isEmpty), which was wrong twice: that set is
GLOBAL (a Chrome create pinned a Finder tab) and it LEAKED — a burst outruns discovery, so each tab's window
was created/added/removed while never discovered or focused, and Finder's destroy event never fires, so the
flag lived forever. The set was then never empty and NO hold released on its own for the rest of the session;
every one ran the 20s cap, pinning a stale tab as an extra tile (rec13). Observable state can't leak.hasPresentableReplacement(for:among:) -> Bool — is another window ready to take this tile? Same app,
on-screen, un-tabbed, drawable, at the held tab's own FRAME. Frame, not just size: same-size windows of one
app are the norm (29px cascade), and releasing because a DIFFERENT window has a tile re-creates the vanish.
Self-timing, no delay to tune: the OS publishes a new tab at 0×0 and sizes it to the parent's frame ~640ms
later, so "at my frame and drawable" IS "ready to show" — and a lagging fullscreen tab just isn't ready yet.membersThatLeftGroup(visible, members) -> [CGWindowID] — members that are separate windows still
carrying a stale tabbedSiblingWids, to unlink. FULLSCREEN-only: two fullscreen windows can never share a
Space, so a fullscreen member holding a Space the fullscreen visible doesn't is its own window (a tab dragged
out of a fullscreen group — macOS puts it on a brand-new fullscreen Space), and AX exposes no tab titles for
a fullscreen window to retire the link any other way. Requires the visible to HOLD a Space: a Space-less
visible proves nothing (against an empty visible Space every member looks disjoint), and it is Space-less
exactly while mid-transition — judging then unlinked a live group's own tabs and exploded it into a tile per
tab. Deliberately NOT generalized to "holds a Space the
visible doesn't": a real background tab's spaceIds can be non-empty AND disjoint from its visible's
(captured live), and un-grouping on that exploded a live Terminal group into one tile per tab. Space-less
members are never departed — that's exactly what a background tab looks like.
Group SHRINK/DISSOLVE is no longer here. It was dissolution(siblingWids, leaving, presentWids), a resolver
function reading a sibling array plus the set of wids still tracked. The group REGISTRY made both inputs
redundant — membership is the table — so TabGroupsTable.remove / shrinkAfterDetach owns it: ≤ 1 surviving
member ⇒ dissolve (a single window can't be a tab group), otherwise shrink and re-pick the representative if
it left with the detached members. Section F below still pins the rule, against its new owner.Mirrors TabGroupResolverTests.swift 1:1. Helpers build an all-default TabWindow and flip only the knobs
each test exercises.
replacedByWid / replacedWid carry the Space handover: a leave and a join on ONE Space, by two wids,
paired within recentPairingWindow. It is the same kind of fact as lastLeftSpaceId — the events say it
outright, so the projection forwards it unmasked — but strictly stronger where it matters. lastLeftSpaceId
names the SPACE left, which identifies a WINDOW only where one window owns a Space: true in fullscreen, false
everywhere else. Two windowed windows of one app SHARE a Space, and Terminal stacks them at the same frame
with the same title, so every Space-based test is true of both at once. The handover names a wid.
Recorded by WindowEventReducer.recordHandover from whichever half lands first (the delivery order of two
WindowServer datagrams is not pinned — see the HandoverOrder fuzz axis), and deliberately narrow: both wids
tracked and of the same app, not during a Space transition, within the pairing window. A wrong edge would
hide a real window, which is the expensive direction. The cost of "both tracked" is that a MINTED switch
(Finder's brand-new wid, no create event) records no edge — that case keeps its own machinery in
pendingGroupInheritance, and rec26 measured the REUSED switch as the normal one.
Written RED before the change, so "it worked" had a definition that predated it:
belongsToTheWindowThatReplacedIt refuses a candidate whose
successor is present in the cluster and is not the visible.dragOutVerdict read as a tab switch, and finally ("same frame ⇒ stop
checking"): the window stayed in the group, hidden as a tab of it. Now a join that REPLACED someone is a
switch (decided from the pair alone, no frame, no waiting), and a join that replaced nobody is a drag-out.
The second leg needs the pairing window to have ELAPSED, since a 1326 in flight also looks like no 1326 —
the caller owns that clock (dragOutCheck passes pairingWindowElapsed: attempt > 0).The recording itself is pinned separately in TestReducerRunnerTests (testHandoverIsRecorded…), both
arrival orders plus the three negatives — cross-app, mid-transition, and outside the pairing window — because
a kernel guard reading a field nothing writes is decoration.
A fullscreen Space holds one window and its tabs is the invariant every Space-based rule here rests on, and macOS breaks it on purpose in exactly one place: two windows tiled side by side share a single fullscreen Space. Measured live (macOS 26, two TextEdit windows via Window ▸ Full-Screen Tile):
wid 12542 fullscreen=true frame=(0,36 1022x1116) spaces=[2267]
wid 36133 fullscreen=true frame=(1034,0 1014x1152) spaces=[2267]
Both hold the Space, so the Space-less rule makes neither background — but the fullscreen fold deliberately absorbs a member SETTLED on the fold Space, to catch the visible's outgoing tab whose 1326 has not landed. That is what would swallow a split-view partner and hide a real window. The measured pair happens to differ in size (one had a toolbar showing) so it never reaches one cluster, but two halves of equal size — two Finder windows — do.
isSplitViewPartner gates it on the ORIGIN: a member genuinely holding the Space at a DIFFERENT origin than
the visible is a separate window. A lagging outgoing tab wears the visible's own frame; split halves sit side
by side and cannot share an origin. Both positions must be known — a brand-new tab is frameless for a beat,
and silence is not a verdict.
tabbedSiblingWids (AX-confirmed), one sibling Space-less → grouped. The tab-switch re-link.tabbedSiblingWids, since
AX can't read a fullscreen AXTabGroup) + one Space-less sibling → grouped via the fullscreen exemption.tabbedSiblingWids nil forever; the AXTabGroup's COUNT confirms the cluster instead and the tab is grouped.<= here would hide a real window.tabCount 0 (the reducer
retires it on a nil read of an ungrouped window), so it can't keep the gate open.tabbedSiblingWids) whose group has no other Space-holding
member → grouped. A new tab joining an existing group whose visible isn't AX-confirmed yet (fullscreen).RealWorldScenariosTests) — a background linked only to ITSELF
([own wid]: its AXTabGroup was read but its tabs aren't tracked yet) confirms nothing → NOT grouped.
TabWindow.tabbedSiblingWids non-nil means a real group of ≥ 2; the TabGroup adapter never writes a
self-only link, and the kernel re-checks.!isTabbed member is chosen visible, not the leading tabbed
one. The hysteresis that stops the group's representative flipping (and its thumbnail flickering).siblingWids = [active, lwouis].activeIsNewlyDiscovered) + a same-app, same-size sibling still on a (stale) Space → the sibling IS
claimed (its 1326 hasn't landed), grouping the pair atomically so it doesn't flash as a 2nd tile.positionsCompatible's fullscreen waiver stops the frame test from saving it. The fullscreen Space
invariant forbids the claim outright: a fullscreen window is its own window on its own Space.untrackedTitles (to brute-force
discover), not matched.tabbedSiblingWids ∋ active) with no matching AX title → kept (the #5830 flap fix), not un-tabbed.isTabbed (it went
standalone), the next match un-tabs it (toUntabWids), clearing the stale link.tabbedSiblingWids lacks this
active) is neither kept nor un-tabbed; coexisting groups of one app don't churn each other.toUntabWids (only windows carrying stale tab state are cleared).b already linked to a (its tabbedSiblingWids contains a.wid)
→ compatible even with far-apart positions.!isTabbed member holding a Space is the group's tile.finderFocusedWindowWronglyTabbed).RealWorldScenariosTests)
— a 0×0 incoming tab can't be drawn; the outgoing, still-captured tab keeps the tile.Its own subject and its own // MARK: in the test file, though it shares section D's question ("which member
does this group show?"): a Space-join is either the group's new active tab or a tab torn out of it.
membersThatLeftGroup owns fullscreen departures.isTabbed ⇒ release (it's a real background tab now).lastLeftSpaceId, not spaceIds: a held tab is Space-LESS by definition (its 1326 just landed), so the
first version of the guard compared against an empty list, never fired, and the tile went on vanishing.TabGroupsTable.remove, not the resolver)isTabbed to misread.lastLeftSpaceId — history as evidence (2026-07-18)Every other fact the kernel reads is a snapshot of NOW. That is not always enough: a tab which just
backgrounded inside fullscreen window A and a brand-new tab of fullscreen window B are, at the instant
geometry looks, indistinguishable — both Space-less, same app, both screen-sized, neither linked yet. The
1326 that backgrounded the first one names the Space it left, so lastLeftSpaceId (set on a leave, cleared
on a join) carries exactly that, and settledOnAnotherWindowsSpace rejects a candidate whose last-left Space
is not one the visible is on.
It is genuine CGS evidence, not an annotation, so — unlike isHeld and spaceIsBorrowed — the projection
forwards it unmasked. Found by the scenario generator, not by a live capture.
It has since answered two more questions the code had written off as undecidable:
geometryGroups used to drop those members from every partition ("we can't tell which window it
backgrounded from"). They go to the Space they just left.spaceIds is empty by
definition — its 1326 just landed — so the comparison has to be against the Space it left. Getting this
wrong made the guard a no-op in precisely the case it existed for (rec27).Both were live-visible: the first as the tile flashing the app icon, the second as the tile vanishing.
It also guards the TITLE claim path (belongsToAWindowOnAnotherSpace): an already-tabbed candidate passes the
plausibility test whatever its Space says — right for this window's own tabs, which carry a borrowed Space
equal to the active's, and wrong for another window's. With duplicate titles a fullscreen active claimed a
windowed window's tab and the membership union merged the two windows. A rule fixed on one claim path has to
be fixed on both; that is now the third time this file has learned it.
applySpaceMembershipDelta used to UNION an incoming 1325 with whatever spaceIds the window already had —
including a BORROWED one — and then clear the borrow marker, laundering our own guess into CGS evidence. A
tab that had been lent the windowed Space ended up genuinely on both it and its window's fullscreen Space, so
it read as on-screen and its own group could no longer claim it. A borrowed membership is replaced by the
event, not merged with it.
Claiming a wid already linked to a group the visible is not in means MERGING TWO GROUPS, and geometry is a guess that must not overrule the AX or causal link that built them. This is reachable in ordinary use: when a window's members are all transiently Space-less — a Spaces re-query issued before that window's new active was tracked lists none of them — its whole group folded into an unrelated fullscreen window sharing the size cluster, and the window lost its tile.
The exception is the sanctioned takeover: a tab that just became active adopting the group it belongs to.
It is identified by FOCUS, which is already the authoritative "which member is the active tab" signal — the
visible must be the cluster's most recently focused member. newlyDiscovered is too narrow a test: one of
the two interleavings in testFullscreenNewTabRaceBothInterleavingsGroupTheOldActive has it nil.
Membership can come from geometry, from AX titles, or from the minted-switch handover, but only geometry used
to backfill the Space onto the members it saw. Members linked by the other two paths stayed Space-less until
some later pass happened to re-run geometry over them — a state that is not a reconcile fixed point, which
the convergence invariant reports. normalizeGroupVisibility now backfills every member from the
representative, marked borrowed because it is our inference and not CGS evidence.