Revert Safety Postmortem

Updated: 2026-08-29 · AB#2608 (incident) · AB#2610 (mitigation, closed)

TL;DR: git revert, git rebase, and git cherry-pick all resolve a replayed diff with the same three-way comparison (ours vs. base vs. theirs) — a conflict is only raised when ours has diverged from base for the same lines. Content nobody touches passes that check for free, silently, regardless of which of the three commands replays the diff over it. PR 2240 dropped six unrelated CSS rules this way. PR 2245 added scripts/revert-safety-check.sh to PR validation — a narrow, notify-only detector for the pattern — and it was proven live against a real pipeline run before AB#2610 closed.

1. What Happened

PR 2204 ("Dynamic Feed Work") merged into imp/blazor-hybrid at 42499b9e. It was judged premature and backed out via PR 2213 (commit e9dfe1a3, landing as a20e73a6) — a standard git revert, which restored whatever PR 2204 had removed or changed.

~30 unrelated real commits then landed on imp/blazor-hybrid normally — bug fixes, CI fixes, the AB#2602 signed-out home UI among them — up to 6101fb0b, the branch tip immediately before PR 2240 merged. None of them touched the content the original revert had restored — it simply sat there, unchanged.

PR 2240 ("Reintroducing Dynamic Feed Logic and Architecture w/ Fallback to Mocks") then reverted the revert — Revert "revert: back out premature Dynamic Feed merge (PR 2204) AB#2510", commit 7a63d96a — literally re-applying PR 2204's original diff on top of 6101fb0b. This silently erased six CSS rule blocks in ALPAMobile.Presentation/wwwroot/css/alpa-components.css, none related to Dynamic Feed — they simply happened to live in a file PR 2204 also touched:

TicketWhat was droppedImpact
AB#2528 / AB#2536 / AB#2391Bottom-nav clustering (reduced slot count + tablet idiom)Signed-out / tablet nav stretches instead of clustering
AB#2527Landscape side-bar inset paddingContent runs under the side system bar/cutout in landscape
AB#2570 / AB#2526 / AB#2529Inline-PDF retry placement, iframe/canvas toggle, per-page canvas sizingDocumentOpenPage.razor/alpa-pdf.js still emit these classes with no CSS to style them
AB#2545 / AB#2391Large-text-band hero-card image hiding, shortcut glyph-icon scalingAccessibility regression at the largest OS text size
AB#2528Login CTA full-width inside LoginModalCTA renders content-width instead of full-width in the modal path
AB#2538Immersive-landscape chrome-hiding media query, deleted outrightFeature silently does nothing — the triggering JS still fires

A developer's own fixup — commit 4fcc5923, "fixup: repair silent regressions from the revert-the-revert auto-merge" — caught and repaired a similar class of drop in Main.razor.cs and a handful of other files before PR 2240 merged. It did not catch this one. A second merge in the same PR branch (a3a2a4ca, merging a stale feat/DynamicFeedWork branch) was independently audited and found to be a real, conflict-resolved merge with no leftover damage — the CSS drops trace to the revert-of-revert alone.

Tracked as AB#2608. A restoration covering all six blocks was opened as PR 2242 — since abandoned in favor of one focused PR per dropped block, so each can be reviewed and documented on its own rather than as one bundled change. PR 2244 restored the bottom-nav clustering rule (the first row in the table above, AB#2608's original finding) and has merged; the other five blocks (AB#2527, AB#2570/2526/2529, AB#2545/2391, the login CTA fix, and AB#2538) are not covered by any active PR as of this writing.

PR 2244 verification — bottom-nav clustering restored

Logged-out 3-tab bar (HOME/JUMPSEAT/KCM), cropped to the tab strip, same device and app state across all three builds:

Pre-merge bottom nav, tabs clustered at center
6101fb0b (pre-merge) — clustered, expected
Post-merge bottom nav before the fix, tabs stretched to the edges
Tip before PR 2244 — bug, edge-stretched
Bottom nav after PR 2244, tabs clustered at center again
PR 2244 — clustering restored

AB#2536 — bottom-nav padding, a separate drop found afterward

Same revert-of-revert mechanism as the six AB#2608 blocks above, but a different rule discovered independently while verifying PR 2244 (not one of the six, and not caught by the automated check's initial sweep): .alpa-bottom-nav's padding shorthand. git log -S on the line traces both halves to a single commit, 7e7defc3 "fix: bring the bottom tab bar to the design's 88pt AB#2536" — the vertical value stopped double-counting the safe-area inset (bar was 114pt against an 88pt design), and the horizontal value moved 16px → 28px to match the design's 337pt content slot. PR 2240 reverted both back to the pre-fix formula in one silent drop. Fixed in two PRs since the second half (horizontal) wasn't recognized as part of the same ticket until after the first shipped:

PR 2247 — vertical inset (88pt vs. 114pt)

Pre-merge bottom nav, 88pt bar height
6101fb0b (pre-merge) — 88pt, design-exact
Post-merge bottom nav before the fix, 114pt bar height with extra navy below the labels
Tip before PR 2247 — bug, 114pt
Bottom nav after PR 2247, 88pt bar height restored
PR 2247 — 88pt restored

Horizontal side inset (28px vs. 16px)

Pre-merge bottom nav with 28px side inset guideline
6101fb0b (pre-merge) — 28px side inset
Post-merge bottom nav before the fix, cluster extends past the 28px guideline
Tip before this fix — bug, 16px side inset
Bottom nav after the fix, 28px side inset restored
28px restored (this fix)

The 12px delta is hard to see at native resolution in the three shots above. Zoomed 3x on the bar's left edge, with the design-correct 28px inset marked as a yellow dashed line and the actual content edge marked solid (green = correct, red = wrong). The shaded red band on the bug frame is the exact 12px the icon encroaches into space that should be empty padding:

Zoomed callout comparing the left-edge inset across pre-merge, bug, and fixed builds, with the 12px delta shaded
Left edge, 3x zoom — pre-merge vs. bug vs. fixed, 12px delta shaded

AB#2611 + AB#2616 — chrome title bar (not revert-drops)

Found in the same investigation window as AB#2536/AB#2608, but different root causes — both fixed together in PR 2250 since they land in the exact same CSS rule. AB#2611 is added code in PR 2240, not a dropped rule: AlpaPageTitleBar is app-wide chrome (AlpaScreen's shared title row), not orchestrator-rendered feed content, but PR 2240 wired TypographyTokenStyle.ClassFor(TitleToken) onto its title span. .alpa-typo-* rules are deliberately placed last in alpa-components.css specifically to win same-specificity battles against every component rule — this let a feed-supplied MEC typography preset override the title bar's font-family/size/weight/transform/color, defeating the documented --alpa-shell-font pin that keeps chrome on ALPA's Futura PT regardless of the active MEC theme. Fixed by dropping the class binding; TitleToken stays as an available-but-unused parameter.

AB#2616 was found while verifying AB#2611's screenshots: the same rule also hardcoded text-transform: uppercase — a plain literal, not a token, so no theme mechanism (leaked or otherwise) can explain it. git blame traces it to the same original commit as the (correct) --alpa-shell-font fix, 03bef3f8 "Implement typography token style tests and document typography pipeline" (Bryon Bauer) — landed with no Figma/ticket reference, unlike every other deliberate rule in this file. Checked directly against the Figma source (KCM Airports, node 4562:8250): the title reads "Airports" in title case, not uppercase — confirming this was a deviation from spec, not intentional design work. Removed the line.

Reproduced on all three builds (/kcm/airports, signed-out reachable) by injecting the class the old code emits when TitleToken is populated (current feed/mock data never sets it, so the AB#2611 leak isn't reachable through real content yet, but the mechanism is live) plus a simulated MEC theme matching UAL's real PageTitle preset — cropped to just the title-bar chrome row:

Pre-merge title bar, no typography mechanism exists
6101fb0b (pre-merge) — no typography mechanism
Post-merge title bar before the fix, leaked League Spartan bold blue uppercase typography
Tip before PR 2250 — bug, leaked + uppercase
Title bar after PR 2250, pinned navy Futura PT title-case chrome style restored
PR 2250 — pinned chrome + title case restored

AB#2622 — HomeLoggedOut LOGIN button weight (one real drop, two unrelated pre-existing gaps)

Found investigating AB#2621 (a separate, non-revert theme.ALPA.json data defect). Three discrepancies against Figma node 21139:18737 landed on the same two selectors and were bundled into one PR (AB#2622) for that reason, but only one of the three is actually a revert-drop.

The drop: .alpa-btn's LOGIN button renders at font-weight: 700; the pre-merge build (6101fb0b) has font-weight: 400 at the identical rule, confirmed live via computed style. git blame traces the 700 line to commit 7a63d96a3 — PR 2240's own revert-of-revert commit — the same silent-drop mechanism as AB#2536/AB#2608 above: PR 2204's old diff carried 700; something later, deliberately, corrected it to 400; PR 2240's blind revert-of-revert re-applied the old value with no conflict raised either way.

Not drops — pre-existing, confirmed identical on the pre-merge build too: the hero title and shortcut-card titles render at weight 700 where the established Lora-degradation convention (.alpa-policy-detail-name, "Bold reads far heavier than the design") calls for 400; and the LOGIN button uses Futura PT where Figma specifies League Spartan (fully licensed, unlike Lora, simply never wired for this button). Both predate PR 2240 entirely.

Pre-merge HomeLoggedOut, correct fonts and weights
6101fb0b (pre-merge)
Post-merge HomeLoggedOut before any fix, bare FuturaPT font plus wrong weights
Tip before AB#2621/AB#2622 — bug
HomeLoggedOut after AB#2621's theme.ALPA.json fix, correct font but still wrong weights and button font
AB#2621 only — font restored, weights/button font still wrong
HomeLoggedOut after AB#2622, correct weights and League Spartan LOGIN button
AB#2621 + AB#2622 — fully matches Figma

2. Why No Git Command Alone Could Have Caught It

git revert, git rebase, and git cherry-pick are three different commands, but underneath they resolve a replayed diff identically — a three-way comparison of base (the diff's own starting point), theirs (what the diff changes to), and ours (the current branch). A tool raises a conflict only when ours has diverged from base for the same lines. If nobody touched those lines, ours == base, and theirs applies with zero conflict — full stop, regardless of which command orchestrates it.

This is not theoretical for this incident — it was verified directly. git diff between PR 2204's own pre-merge base (42499b9e^1) and 6101fb0b (the tip immediately before PR 2240) shows zero difference for the dropped CSS hunks — byte-identical from before PR 2204 through the moment PR 2240 merged. Three attempts, same outcome:

AttemptWhat it doesOutcome
Revert (what actually happened)git revert e9dfe1a3 on top of 6101fb0b0 conflicts. Silent drop.
Rebase (hypothetical)Replay PR 2204's original commits onto 6101fb0b via git rebase --ontoSame base, same incoming diff — still 0 conflicts, same drop.
Cherry-pick (hypothetical)Pick PR 2204's commits individually onto 6101fb0bSame three-way check per commit — still 0 conflicts on the commit touching this file.

The actual gap is a review gap, not a tooling gap. None of the three commands can flag "this diff deletes something nobody else touched" — that is a valid, conflict-free merge from git's point of view, not an error. Catching it requires reading the resulting diff against a known-good point before merging, specifically for content unrelated to the feature being reintroduced.

3. What Actually Caught It

Diffing the full branch against the pre-merge tip and scanning every removed hunk for a ticket reference unrelated to the feature landing:

git diff 6101fb0b origin/imp/blazor-hybrid -- '**/*.css' \
  | grep -B15 '^-' | grep -oE 'AB#[0-9]+|BUG-[0-9]+' | sort -u
# cross-reference each ticket found against AB#2510 / PR 2204 —
# anything unrelated is a drop candidate, not a supersede

Every ticket-tagged rationale comment removed by a real drop carries its own, unrelated ticket in its own text — the nth-last-child bottom-nav clustering rule's comment cited AB#2528/AB#2536, nothing to do with Dynamic Feed. That is the signature this doc's mitigation automates below.

4. Mitigation — Automated PR Check (AB#2610)

scripts/revert-safety-check.sh, added to AzureDevOps/azdo_pr_validation_ios.yml in PR 2245, runs the detection above automatically:

Validated against this incident directly: pointed at PR 2240's real diff, it surfaces all six confirmed AB#2608 drops among 18 total candidates flagged (the rest were legitimate supersessions from unrelated work landing in the same files).

Known blind spots, by design (narrow detector, not exhaustive): a drop with no ticket-tagged comment nearby is invisible to this check. A silent drop inside a plain merge or rebase that never used git revert is not caught either — the trigger gate is deliberately "this PR contains a revert commit," the highest-risk, cheapest-to-detect pattern, not the only way this class of bug can occur.

5. If You See a Revert-Safety Warning

  1. Open each flagged ticket in Azure Boards and read what it originally fixed.
  2. Check whether that fix is still present in your PR's resulting file — read the surrounding code, not just the ticket title.
  3. If it is present (moved, refactored, or independently re-implemented): no action, the warning is a false positive on this pass.
  4. If it is genuinely gone: restore it in this PR (or a follow-up) rather than merging past the warning — see §3 for the manual sweep method if you need to check a file the automated check did not flag.

6. Live Verification (AB#2610)

PR 2245's own diff never touches a path azdo_pr_validation_ios.yml watches (AzureDevOps/* and scripts/* aren't in that pipeline's paths.include list) — so merging it proved the YAML was well-formed, but not that the step actually runs and flags correctly on a real hosted agent. That took a second, disposable PR.

Method

Branched from the current imp/blazor-hybrid tip and made one commit: git revert -m 1 18c218cf — a clean revert of PR 2244, the real, already-merged fix for this incident's original finding (the bottom-nav clustering rule, §1 above). Real content already on the branch, not a synthetic fixture — a same-PR add-then-revert nets to zero diff against merge-base and would prove nothing (the check deliberately diffs merge-base→tip, the net effect, exactly so a same-PR fixup isn't flagged; see §4). Opened as PR 2248, titled TEST — DO NOT MERGE, targeting imp/blazor-hybrid — required, since that's the only branch the iOS pipeline triggers on.

Result — build 4955

ALPA Mobile PR Validation - iOS, run 4955, step "Check revert safety (non-blocking)":

revert-safety-check: PR contains 1 revert commit(s) -- scanning full diff for orphaned tickets
##[warning]revert-safety-check: this PR reverts something and its diff removes 3 ticket(s) this PR doesn't own -- may be a silent drop (docs/detail/revert-safety-postmortem.html). Verify each is a deliberate supersession, not a regression:
  AB#2391: ALPAMobile.Presentation/wwwroot/css/alpa-components.css
  AB#2528: ALPAMobile.Presentation/wwwroot/css/alpa-components.css
  AB#2536: ALPAMobile.Presentation/wwwroot/css/alpa-components.css

Matched the prediction exactly: flagged AB#2391/AB#2528/AB#2536 — the tickets cited in the removed CSS comments — and correctly did not flag AB#2608, since that ticket appears in the revert commit's own auto-generated subject (Revert "Merged PR 2244: ... (AB#2608)") and is therefore treated as a ticket this PR already owns. The step itself reported succeeded, and the overall pipeline stayed green — the notify-only posture held under a real failure case, not just a clean one.

Cleanup: PR 2248 was abandoned immediately after this log was captured — never approved, auto-complete never set. The remote branch (test/AB2610-revert-safety-check-proof-01) and local worktrees were deleted the same session. Nothing from this test landed on imp/blazor-hybrid. Full trail on AB#2610, closed the same day.