Repository navigation
REQ-VISUALIZATION-005: detect ancestor opacity in the M1/M2 Pages readability smoke - #479
Conversation
…ity smoke
`isReadable()` combined Playwright's `isVisible()` with the target node's own
computed `opacity`. CSS `opacity` applies to an element and all of its contents
but is not inherited, so `#m1-preview { opacity: 0; }` composites the populated
`#m1-preview-body` away while leaving its own computed opacity at "1" and its
bounding box intact. The helper reported that fully invisible body as readable.
Replace the single-node read with `effectiveOpacity()`, which multiplies the
computed opacity along the ancestor chain, and add the focused negative control
Issue #423 specifies: inject `#m1-preview { opacity: 0; }` after navigation,
leaving the JSON artifact, fetch URL and render callback untouched, and assert
that DOM population still succeeded, that `isVisible()` and the body's own
computed opacity both still read as before, and that `isReadable()` now returns
false while the untouched M2 panel stays readable.
Verified by reverting the helper to its previous single-node form: the new
control fails ("expected true to be false") and passes with the fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Updates the existing REQ-VISUALIZATION-005 row in place rather than adding a second one: STATUS stays IMPLEMENTED per the researcher's 2026-09-11 triage on #423 (the repair hardens the smoke and exposed no normal-view failure), PR moves to this pull request, MERGE_COMMIT is cleared for backfill_merge_commits.py, and EVIDENCE now names every pull request that contributed to the requirement -- #244, #368, #407, #421 and #479 -- along with what this one proves and what it deliberately leaves out of scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AUTHOR: forge check evidence at head
|
| Check | Latest run at this head |
|---|---|
policy-guard |
passed |
build-and-test |
passed |
typescript |
passed |
write-mergeability-status |
passed |
mergeability |
passed |
The local check outcomes in the body were measured separately, on the code commit 6ddf238,
and are unchanged by the ledger commit on top of it.
I have not merged, approved, tagged, or modified any other pull request or Issue. Awaiting the
ACCEPTOR.
ACCEPTOR verdict: ACCEPTHead revision judged: No prior verdict exists on this pull request from any account ( 1. Every required check measured green at this headRead from the checks tab and cross-read against the per-commit check-run API at
The AUTHOR's caveat about the first attempt is accurate and I confirmed it independently 2. Independent re-execution — both runtimes, at the head revisionPer runbook section 3 I checked out
The legacy .NET build is green alongside the canonical change, so I also ran the focused smoke under 3. Acceptance criteria, one by one
4. ScopeThree paths change, and all three are named in Changed artifacts: 5. No test or invariant weakened
On the 6. No secret, credential, personal data or local path
7. Handoff record completeAll nine required elements are present, and the evidence honestly separates what was measured LedgerThe One control-plane defect found, resolved by hand here, and filedRunbook section 3 instructs me to read
I did not let that block or waive anything — I answered the underlying question myself,
The merge was textually clean and semantically clean: master's two commits touch Merging with |
* Issue #482: measure staleness instead of asking GitHub for it `mergeability` promised two red conditions and delivered one. `classify()` reached its `behind` arm only when the REST API's `mergeable_state` was literally the string `"behind"`, and GitHub emits that only when the base branch's protection rule requires branches to be up to date before merging. With that setting off, a conflict-free branch two commits behind comes back `"clean"`, so the arm never fired and the check reported green on a branch whose green checks were measured against a base that no longer existed. That is #479 on 2026-09-15: head `fd61cae`, two commits behind, `mergeability` rewritten at 00:11:46Z — after both merges that made it stale — reading `success`. Its `STALE_BASE` constant, the long comment justifying it, and ACCEPTOR_RUNBOOK section 3's paragraph promising reviewers two failure conditions all described a control that had no way to run. The distance is now measured from the repository rather than inferred from a setting no identity in the loop can read. `behind_count()` asks the forge's own compare endpoint how many commits the base holds that the head does not; `measure()` joins that to the mergeability read and is what `main()` now goes through. `mergeable_state == "behind"` is still honoured when GitHub does volunteer it, so switching the branch-protection setting on later cannot make the check contradict itself. Three properties the repair keeps: * A conflict outranks a distance. A conflicting branch is usually also behind, and sending its author to perform a merge that cannot succeed is worse than silence. * An unmeasured distance is `pending`, never `success`. A comparison that does not come back leaves the branch possibly current and possibly ten commits stale; this module already refuses to report the green half of an answer it does not have, and the recheck pass retries it. * `behind_by` has no default. A default parameter is precisely how this arm would go unreachable a second time — a caller that never measures would keep getting `success` — so failing to measure is a `TypeError`, not a green tick. Descriptions now name the stale base, which made them long enough to hit the 140-character API cap on a long branch name; the added length test caught that. `_fit()` abbreviates the branch name rather than letting `post_status` clamp the sentence, because a check whose message is cut mid-word reads as broken exactly when it is telling the truth. `BehindIsMeasuredNotAskedFor` is the regression bound. Every test in this file passed for the entire time the arm was unreachable: they pinned what `classify` did with the string `"behind"`, which GitHub never sent. The new cases assert the classification is reachable from what the API actually returns — a `"clean"` state with a positive measured distance is red — and `MeasureTests` drives the real call path, where the reachability has to hold. Out of scope, filed separately rather than smuggled in: `update_branches.py:64` gates on the same `mergeable_state != "behind"` condition and is inert for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Issue #482: confirm a green mergeability against the base it measured The ACCEPTOR refused #493 on two substantive points; this addresses both, plus the wording defect it flagged as cosmetic. A `success` from this check is a claim about a revision — this head contains the tip of its base — and it was being written without re-checking that the revision was still the tip. Two runs can be in flight at once: `.github/workflows/mergeability.yml` keys concurrency on `github.ref`, so a `pull_request` run and a `push`-to-base run sit in different groups and neither waits for the other. Run A measures `behind_by == 0` against `M0`, the base advances to `M1`, run B posts the honest `failure`, and run A's `success` lands last. The required check is green on a head that is really behind, which is the state Issue #482 exists to make impossible. The old code had the same measure-then-post shape, but the staleness arm could never produce `failure`, so the window cost nothing until now. So a measurement now carries its subject. `compare_to_base` returns the distance together with `base_commit.sha` — the tip that very count was taken against, from the same response, not a second request that could resolve to a different revision. `Verdict` carries it through, and `settle` re-reads the tip immediately before a `success` is returned to be written: an unchanged tip stands, a moved one is measured again, a base that keeps moving is `pending`, and a confirmation that does not come back is `pending` too. Nothing promotes an unconfirmed measurement to green. The confirm-to-write window itself remains and cannot be closed here — serializing the two writers is a concurrency-key change under `.github/workflows/**`, a policy path that ships alone. Also: when the API volunteers `mergeable_state == "behind"` at a measured distance of zero, the refusal stands but is described without a count. `behind master by 0 commits` reads as a broken check at the moment it is telling the truth. Twelve tests added, 25 to 37 in the file. `SettleConfirmsBeforeItCommits` forces the interleaving the QA finding describes, and `PostedVerdictTests` asserts on what `main` actually writes, because the status a reviewer reads is the only output that matters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #423
Achieved outcome
The M1/M2 Pages viewport-readability smoke now fails when a populated preview body is
composited away by a transparent ancestor, not only when
opacity: 0is applieddirectly to the body node.
isReadable()no longer reads the target node's own computedopacity in isolation; it multiplies computed opacity along the ancestor chain, which is
what CSS actually does —
opacityapplies to an element and all of its contents but isnot inherited. The false-positive path named in #423 (
#m1-preview { opacity: 0; }leaving#m1-preview-bodypopulated, Playwright-visible and self-opacity"1") is now covered bya focused negative control.
Tested revision
fd61cae32091002d579a9cbec8999440d3bbdc3f— the tip ofclaude/issue-423-ancestor-opacity. Every outcome in the Checks table was measured locallyon the code commit
6ddf238;fd61caeis the ledger commit on top of it and touches onlydocs/spec/implementation_status.csvand its generated table, so no check outcome abovedepends on code that changed after it ran.
python scripts/implementation_status.py --checkwas run on
fd61caeitself.Changed artifacts
Every path in
git diff --name-only origin/master...HEAD:src/diagnostics/m1m2-pages-render.test.ts— extractseffectiveOpacity(locator), whichwalks
parentElementfrom the target to the document root multiplying each node'scomputed opacity;
isReadable()now gates oneffectiveOpacity(...) > 0instead of thetarget's own
opacity !== "0". Adds one test, "detects an ancestor opacity:0 regressionthat leaves the M1 preview body's own opacity at 1". No production/site file is touched.
docs/spec/implementation_status.csv— updates the single existingREQ-VISUALIZATION-005row in place (one row per requirement identifier):STATUSstaysIMPLEMENTED,PRmoves to 479,MERGE_COMMITis cleared forscripts/backfill_merge_commits.pyto fill after merge, andEVIDENCEnow names everycontributing pull request — Implement REQ-VISUALIZATION-005: M2 Milestone Preview with tick/phase/reconciliation visibility #244, PR #354 merged without the required M1/M2 DOM render smoke regression #368, REQ-VISUALIZATION-005: execute the real M1/M2 DOM render path in the Pages smoke test #407, REQ-VISUALIZATION-005: prove M1/M2 viewport readability with real-browser layout #421 and REQ-VISUALIZATION-005: detect ancestor opacity in the M1/M2 Pages readability smoke #479. No other row is touched.
docs/spec/IMPLEMENTATION_STATUS.md— regenerated bypython scripts/implementation_status.py, never edited by hand.Acceptance criteria
readable at 1280px and 360px. Both viewport tests are unchanged and
passed; they stillassert
isReadable()on#m1-preview-bodyand#m2-preview-body.#m1-preview { opacity: 0; }after navigation makes the focusedreadability assertion fail while JSON fetch and DOM population still succeed. The new
control injects exactly that rule via
page.addStyleTagafterpage.goto, asserts thebody text no longer contains
Loading world genesis data(fetch + population intact),and asserts
isReadable(#m1-preview-body) === false. The JSON artifact, fetch URL andrender callback are untouched.
opacity.Proven inside the control itself: it asserts
getComputedStyle(#m1-preview-body).opacityis still exactly
"1"andisVisible()is stilltrueunder the regression, so theprevious single-node helper would have returned
true. Confirmed by mutation: withisReadable()reverted to its previous form the new control fails withAssertionError: expected true to be false(19 passed, 1 failed); with the fix in placeall 20 pass. The revert was local and temporary and is not in this diff.
controls remain covered and passing. All four are untouched and
passedin the runbelow; the direct-
opacity: 0control still passes because a zero on the target node isitself a zero factor in the product.
passedon the repairingrevision. See Checks.
Checks
npm cinpm run typechecktsc --noEmit, no diagnosticsnpm testpretestinstalled Chromium, so the Playwright smoke really executed in a browser rather than being skippednpm run builddist/canonical.jsemitteddotnet restoredotnet build --configuration Releasedotnet test --configuration ReleaseOutcome is exactly one of
passed,failed,not_run,unavailable.Not checked
docs/served by the test's ownlocal static server. Residual risk: a deployment-only difference (headers, base path)
is out of this smoke's reach — unchanged by this PR, and out of scope per the Issue.
filter: opacity(0),colormatchingbackground-color, zero-size text, an occluding overlay,clip-path. Residual risk: aregression through one of those still passes the smoke. The Issue's non-goals explicitly
exclude solving every visual-perceptibility mechanism in this change; this is the
reproducible ancestor-opacity path only.
Assumptions and unknowns
#m1-preview { opacity: 0; }Chromium reports#m1-preview-bodyasisVisible() === truewith its own computed opacity"1".Measured, not inferred — the control asserts both values rather than commenting them.
parentElementto the document root is sufficient for this page.docs/index.htmluses no shadow DOM and no<iframe>, both of which would break thechain. If either is introduced later the helper would need revisiting; that is a known
limit, not a silent one.
> 0, i.e. it detects full transparency, not lowcontrast. A perceptibility threshold (say
< 0.1) would be inventing a policy thespecification does not state.
REQ-VISUALIZATION-005at its current status unless the repair exposed an actualnormal-view failure. It did not — the normal 1280px/360px cases pass throughout — so the
row stays
IMPLEMENTEDand is updated in place to cite both contributing PRs.Highest-risk area for review
effectiveOpacity()insrc/diagnostics/m1m2-pages-render.test.ts. The loop runs insidethe browser via
locator.evaluate; check the termination condition (parentElementisnullat<html>) and theNumber.isFinitefallback, which treats an unparseable computedopacity as
1— the permissive direction. A bug making the product spuriously zero wouldturn the two normal-viewport smokes red rather than green, so it fails loudly, but it is
the one piece of new logic here.
Remaining gate
None. No mandatory work remains for this Issue. This PR does not claim any part of #416 or
#269, and does not touch the open PR #477.
🤖 Generated with Claude Code