Skip to content

Phase 4: Ads Live — AdSense placement, consent-gated loader, privacy disclosure, RPGLogs deploy decision - #17

Open
alexandermayes wants to merge 154 commits into
mainfrom
growth/phase-2-review-fixes
Open

alexandermayes wants to merge 154 commits into
mainfrom
growth/phase-2-review-fixes

Conversation

@alexandermayes

Copy link
Copy Markdown
Owner

Summary

This branch carries Phase 4 (Ads Live) plus the still-unmerged Phase 2.1 and Phase 3 work that
has accumulated on growth/phase-2-review-fixes since PR #16. Phase 4's own commits (04-01
through 04-07 Task 1) are the focus of this PR:

  • 04-01 — Captured the RPGLogs API Terms of Service and API docs with provenance; wrote the
    pre-ad Core Web Vitals baseline, D-10 rollback trigger, D-08 rollback runbook, and the D-11/D-14
    observation and escalation clocks into docs/OPS-01-SHIP-GATE.md Part 6.
  • 04-02 — Recorded real WCL rankings/rate-limit/guild fixtures (R0-2 tracer).
  • 04-03 — Shipped the phase tracer: one reserved, consent-gated AdSense slot end-to-end,
    report-only CSP entries for Google ad hosts, /ads.txt, and the ad-slot whitelist gate in
    scripts/protected-elements.mjs.
  • 04-04 — Rankings blob types, a pure parse-lens engine, and a Redis-backed WCL rate-budget
    gate — tested, unwired, imported by nothing yet (R0-3, by design).
  • 04-05 — Expanded to the full four-slot D-02 ad placement map across /tbc-audit and
    /analyze/[reportCode], rewrote /privacy's advertising and Warcraft Logs data-source
    disclosure, and wired the AdSense account (unit ids, env vars, Auto ads confirmed off).
  • 04-06 — Four-round preview-deployment defect-fix loop: found and fixed a reserved-box sizing
    bug and an occluding unfilled-ad-frame visual mismatch, both confirmed by the developer's
    first-hand review of the fourth preview.
  • 04-07 (Task 1 of 3, this dispatch) — Recorded the RPGLogs correspondence state honestly (the
    approval request drafted in 04-01 was never sent, so there is no reply) and the developer's
    explicit, twice-given, dated operator-override decision to proceed with the production ad
    deploy without that approval on record, after being told the real consequence (RPGLogs could
    revoke the WCL API client entirely). See docs/OPS-01-SHIP-GATE.md Part 6
    ### Deploy decision (Phase 4, 2026-09-22) for the full record.

Earlier commits on this branch (Phase 2.1's consent-gate hotfix, Phase 3's share loop) are
already deployed to production in prior sessions but have not yet been merged to main via a
PR — this PR brings main up to date with everything currently live plus Phase 4's work so far.

Test plan

  • npx tsc --noEmit clean
  • npm test — full vitest suite passing (285/285 as of 04-06)
  • npm run lint — no new findings (pre-existing debt in components/ui/meteors.tsx and
    lib/analysis-engine.ts left as-is)
  • npm run protected-elements — all rows passing
  • Production deploy + post-deploy route-contract evidence (04-07 Task 2, in progress)
  • Post-deploy OPS-01 live-traffic evidence (04-07 Task 3, not yet dispatched — needs 60+ min
    of live traffic to measure)

🤖 Generated with Claude Code

alexandermayes and others added 30 commits September 14, 2026 13:46
…sent-gate hotfix

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, no CMP dependency

- lib/geo.ts: CONSENT_REGIONS (32-code EEA+UK+CH set) + isConsentRegionCode, fail-closed on unknown/missing input
- app/api/geo/route.ts: sole reader of x-vercel-ip-country; returns { isConsentRegion } with Cache-Control: private, no-store, no cookie
- app/components/PostHogProvider.tsx: posthog.init moved to module scope (fixes child-effect-before-parent-effect pageview loss); consent-readiness gate fetches /api/geo once and applies the geo or TCF decision; PostHogPageView now waits for the gate before capturing $pageview

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… gate

- Item 7: post-deploy live-traffic check with both HogQL queries, inclusive
  thresholds (>=20 pageviews, >=2 countries, >=50% of Vercel Analytics), the
  zero-events-is-a-FAIL rule, and the bounded netlog command (D-08)
- Item 4 reworded: an event definition persisting is not evidence of current
  ingestion; item 4 alone can no longer close the PostHog half of the gate
- Fourth recording rule: an absent event count is a FAIL, never no-data
- Items 1-6 and the original three recording rules are unchanged

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- 01-VERIFICATION.md, 02-VERIFICATION.md: dated append-only addendum stating
  the OPS-01 PostHog criterion was not actually met at sign-off (cookieless_mode
  on_reject dropped PENDING-consent capture since the 2026-09-06 deploy), and
  was met for real by Phase 2.1. No existing text rewritten or removed.
- REQUIREMENTS.md: OPS-01 checklist line and traceability-table row both extended
  (not replaced) to cite the correction and point at Phase 2.1; MONY-01 untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…executor

- Add ConsentGatePath, ConsentGateOutcome and deriveConsentGateOutcome to
  lib/consent.ts (D-06/D-07): pure merge of the geo boolean and the TCF
  action into register/opt_in_capturing/startSessionRecording/capture calls
- Non-consent-region outcomes ignore tcfAction entirely (D-04); consent-region
  outcomes reproduce today's SDK calls exactly (D-05); opt-in-non-eea reached
  with isConsentRegion true is surfaced as a countable list/CMP mismatch
  (gdpr_applies false) rather than silently absorbed
- 10 new tests in lib/consent.test.ts covering every outcome row, the
  ConsentGatePath literal set, and the optIn/startReplay invariant
- PostHogProvider.tsx: replace the inline four-branch TCF switch with a
  single applyOutcome executor; both the geo path and the TCF path route
  through it, so there is exactly one call site each for
  posthog.register/opt_in_capturing/startSessionRecording

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Record Task 2 checkpoint resolution (user-selected option a: GI/IM/JE/GG
  stay outside CONSENT_REGIONS, no code change needed)
- Document Task 3's deriveConsentGateOutcome + single-executor rewiring
- Full suite green at 161 tests, typecheck clean, scoped lint clean

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…te hotfix

- Part 4 added to docs/OPS-01-SHIP-GATE.md: local gate output, preview
  deployment dpl_7JGkziQrpZrhKfvxHJNAmtuXfuFy, deployed /api/geo response,
  and the D-10 netlog proof (3 distinct /ingest/i/v0/e/ capture requests
  against a production baseline of zero)
- Answers RESEARCH Open Question 1: preview deployments receive real
  x-vercel-ip-country geo headers, same as production
- Documents a previously-unknown false-negative trap in the bare D-10
  command: posthog-js's built-in bot filter silently drops captures for
  any User-Agent containing "HeadlessChrome" (opt_out_useragent_filter
  defaults to false) - fixed for this run via a --user-agent override
  that does not change what is being tested
- Item 7 (live-traffic) and the phase sign-off are left explicitly open,
  pending the production deploy in 02.1-04
- Purely additive to Parts 1-3; no production deploy was run

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…osed on malformed body

A malformed-but-valid-JSON /api/geo response (e.g. a Vercel platform-level
failure payload lacking isConsentRegion) previously made
body.isConsentRegion undefined, and !undefined === true opted the visitor
in immediately with no CMP involvement — the opposite of the fail-closed
guarantee. Now the parsed body is runtime-checked for a boolean
isConsentRegion field; anything else (missing field, wrong type, null)
resolves to true (consent region), matching the existing .catch fail-closed
path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t block consent capture indefinitely

The TCF path already has CMP_TIMEOUT_MS as a fail-safe against a
blocked/absent CMP, but the geo fetch that gates the entire consent
decision had no analogous protection. A hung request (dropped connection,
stalled edge function) never hit .then or .catch, so applyDecision was
never called and consentReady never became true — reproducing the class
of bug this hotfix fixes via a different trigger. Bound the fetch with
AbortSignal.timeout(5000); the abort rejects the promise, which the
existing fail-closed .catch handles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…a fully missing one

isConsentRegionCode already treats a blank/whitespace-only country header
identically to a missing one (both fail closed to consent region), but the
logging condition only checked !country, which is false for a non-empty
whitespace string. That silently under-counted the blank-header case in
the geo_header_missing observability signal this hotfix relies on. Compare
against the trimmed value instead, and add a test covering a
whitespace-only header.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…T handler

.claude/CLAUDE.md's Function Design convention requires explicit return
types on all functions, and async functions to declare Promise<T>. This
route was the one exception among the files reviewed in this phase.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n a repeat TCF resolution

startConsentListener's hasResolved guard only covers its timeout branch;
the __tcfapi addEventListener callback calls resolve() unconditionally on
every non-pending event. Some CMPs (e.g. Google Funding Choices'
manage-consent re-confirmation flow) can emit a second resolved event for
the same visitor, which previously re-fired posthog.capture(consent_resolved,
...) and inflated the OPS-01 gate's consent_gate_path counts. Track
already-fired gate paths in a ref and skip the outbound event (but still
update consent state via posthog.register/opt_in_capturing/
startSessionRecording, which are idempotent) on a repeat of the same gate
path; a genuine decision change still gets its own event.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Record production deployment dpl_HY5319wSDVw3M4ibBU42JrSTgw4e (2026-09-14T22:16:59Z),
  the 60-minute measurement window, /api/geo check, and the D-10 netlog proof
  (16 lines / 3 capture requests vs a baseline of zero)
- Record both HogQL queries from Part 1 item 7 run against project 337485: pageview
  count 25 across 3 non-consent-region countries (PASS x2), consent_gate_path breakdown
  (geo-non-consent-region only, TCF path unobserved), and per-event ingestion status for
  the five previously-never-ingested events
- Record the Vercel Web Analytics ratio threshold as PENDING (not evaluated), with the
  reason the figure could not be read from this session
- Record the unintended preview-deployment deviation (production unaffected)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cs figure

- Record the post-deploy code-review follow-up (02.1-REVIEW.md / 02.1-REVIEW-FIX.md) as
  not yet in the measured production deployment, requiring a separate developer-approved
  deploy
- Record the Part 1 item 5 Search Console row as no-data (no GSC MCP tool available this
  session; no route/metadata surface changed this phase)
- Record Phase 2.1 as NOT SIGNED: two of item 7's three thresholds pass, the Vercel Web
  Analytics ratio is pending, so the phase-level sign-off is deliberately withheld per
  this document's own recording rule; list the remaining steps to close the gate
- Append an OPS-01 addendum to REQUIREMENTS.md noting the PostHog criterion remains
  not-yet-met for Phase 2.1 pending that same figure, without rewriting the existing
  traceability row (D-09)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… plan

Two of item 7's three thresholds pass with counted PostHog evidence (25 pageviews
across 3 non-consent-region countries); the Vercel Web Analytics ratio is recorded
PENDING because it could not be read from this session. Phase 2.1 is left
deliberately unsigned rather than fabricated as complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Four additive gap_closure plans from 02.1-VERIFICATION.md (gaps_found):
score the pending Vercel ratio and sign or FAIL (05), drive theme/Timeline
events on prod and re-query (06), approval-gated redeploy of the five review
fixes with a fresh item-7 window (07), EEA/UK TCF human observation (08).
Plan-checker: VERIFICATION PASSED. ROADMAP lists the new plans; STATE.md
records 8 plans, ready to execute.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Vercel Web Analytics figure (23 page views, upper bound) recorded in
  docs/OPS-01-SHIP-GATE.md Part 4 with URL, range, timezone, and reader
- All three item 7 thresholds scored on separate lines: PASS, PASS, PASS
  (25 pageviews >= 20; 3 countries >= 2; 25/23 ~= 108.7% >= 50%)
- Phase 2.1 Sign-off SIGNED (2026-09-15, gap-closure 02.1-05), appended
  below the untouched 2026-09-14 NOT SIGNED section
- .planning/REQUIREMENTS.md gains OPS-01 Addendum 2 recording the real
  met-date, superseding the 2026-09-14 addendum by addition

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eline events

- theme_changed, timeline_viewed, timeline_filter_used observed with count>=1 and consent_gate_path=geo-non-consent-region in window 2026-09-15T18:26:27Z-18:28:57Z
- timeline_error recorded not triggered by design (optional error step skipped)
- consent_resolved / consent_unavailable recorded structurally unobservable from non-consent-region egress, deferred to 02.1-08
- route: automation-performed (developer authorized 'you drive it'; orchestrator-driven headless Chrome scoped to parseforge.gg only)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
alexandermayes and others added 16 commits September 21, 2026 12:42
…, and AdSense account config plan

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…the ad build

Append Part 6 subsections recording every Part 1 item 1 command (tsc, lint,
284/284 tests, theme-parity, token-audit, protected-elements) green against
the ad build (HEAD 3a5be52), plus the pre-deploy grep proof that all four
ad_slot_* events have exactly one true capture call site in AdSlot.tsx with
only three low-cardinality properties (route/slot_id/consent_gate_path) and
zero report-code/player-name/URL leakage. Records, rather than silently
works around, the plan's own literal grep gate false-negative on the
ad_slot_filled/ad_slot_empty ternary — same pattern as Part 2's
consent_resolved precedent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… box-size defect found

Deploy dpl_689q6eqCETYSzy1Yfe9THKNpAJdP (preview only, no --prod) with
NEXT_PUBLIC_ADS_ENABLED/NEXT_PUBLIC_ADSENSE_PUB_ID baked in for the first
time, and record into Part 6: route contracts for /tbc-audit, the demo
analyze page and /ads.txt; a new CDP-based measurement technique (Node's
built-in fetch + --experimental-websocket, no package installed) proving
every reserved box actually renders at 300x250 on both desktop and phone
viewports; the two-direction netlog proof (adHostRequestCount=3 for an
admitted visitor, 0 for one whose consent decision fails closed via a
network-layer-blocked /api/geo); and the report-only CSP violation harvest
(ep1/ep2.adtrafficquality.google, www.google.com as new Google ad hosts not
yet in the CSP allowlist, left for the checkpoint decision; vercel.live and
wow.zamimg.com as non-ad, non-blocking findings).

The box measurement surfaces a real defect: AdSlot.tsx's reserved-box <div>
carries an inline style={{width: base.width, height: base.height}} that
outranks the responsive md: Tailwind classes at every viewport, so no slot
ever grows to its declared desktop size. Recorded as a failure per this
plan's own "a mismatch is a failure, not a rounding note" instruction, not
fixed here (app/ is out of this dispatch's scope) — surfaced at the Task 3
checkpoint for the developer's decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…new findings

Record the both-theme structural check (no border/background on the
reserved box in either theme, verified via computed style), a six-row phone
reachability table at 384px with distance-to-nearest-ad-box for every
protected element (smallest clearance 117px, no overlap), and an adjacency
read (no ad box inside a table or between a share control and its target).
Every judgment-only line is marked "awaiting the developer" rather than
folded into a pass, per this document's own recording rule.

Also logs two findings to WINDOWS.md: the AdSlot.tsx inline-style box-size
defect (already recorded in Part 6's measured-box section), and a
pre-existing, non-ad-related horizontal-overflow on / and /tbc-audit caused
by an unwrapped example-URL <code> string in ReportUrlForm.tsx, found while
measuring ad-box adjacency but outside Phase 4's scope to fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…illed AdSense frames

Orchestrator-authorized deviation (Rule 1 auto-fix) to the docs-only scope
of 04-06-PLAN.md: fixes the two real defects this plan's own preview
verification found, per the developer's checkpoint answer.

- AdSlot.tsx's reserved <div> no longer sets an inline
  style={{width, height}} pinned to the `base` size — that inline style
  outranked the md:w-[...]/md:h-[...] Tailwind classes at every viewport,
  so a box never reached its declared md size (Part 6 "Measured box
  dimensions" MISMATCH). Size is now controlled by boxClass alone.
- AD_SLOTS' boxClass gains a max-w-full safety net on every slot: the
  728px-wide -mid banner slots sit in a container narrower than 728px
  between the md breakpoint (768px) and ~776px (layout's sm:px-6 padding),
  so without a cap the box would overflow that gap instead of shrinking to
  fit it.
- AdSlot.tsx now hides the mounted <ins> (visibility: hidden) until a
  MutationObserver confirms data-ad-status="filled"; an "unfilled" status,
  or the 10s observe ceiling passing with no resolution, both leave it
  hidden. Fixes the white-rectangle defect the developer reported in dark
  mode: Google's adsbygoogle.js mounts a blank iframe inside the <ins> the
  instant it starts processing a pushed unit, well before fill status
  resolves, and our transparent wrapper could not restyle that iframe's
  interior. The reserved wrapper div keeps its declared box size
  regardless (D-04, zero layout shift) — only the <ins>'s own paint is
  gated.
- lib/ads.test.ts: asserts every slot's boxClass carries max-w-full.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y CSP

Orchestrator-authorized deviation to the docs-only scope of 04-06-PLAN.md,
per the plan's own Task 2 CSP violation harvest against the first Phase 4
preview (docs/OPS-01-SHIP-GATE.md Part 6): adds ep1.adtrafficquality.google
(observed under connect-src) and ep2.adtrafficquality.google (observed
under script-src and frame-src) to those exact directives. Both are
Google's "sodar" ad-viewability/anti-fraud beacon network for AdSense,
seen only on the admitted (ads-loading) netlog run.

www.google.com was also observed on that run but the harvest's own top-5
sample did not capture which directive it violated, so per the harvest's
stated disposition it is deliberately NOT added here.

Policy stays report-only; promotion to enforcing is Phase 7, unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records checkpoint round 2: the box-size and dark-mode-white-block fixes
(249058e) plus the CSP allowlist fix (349c2e9), a fresh preview deployment
(dpl_9SxD4PEAPUHv8aghVucyjqJ3LQoV) built from both, and an honest blocker
this session hit trying to re-run the plan's own Task 2 netlog/measured-box/
CSP-harvest procedure against that new preview.

The one documented method for sourcing the Vercel SSO automation-bypass
secret (`vercel project protection ... --format json`) was denied this
session by the coding tool's own Bash-permission auto-classifier
(Credential Materialization) — an environment-level guard, not a plan
decision — and this dispatch's own instructions separately forbid touching
project-protection settings or rotating the secret, so no alternative was
attempted. `vercel env pull` was retried and, as Parts 4/5 and this plan's
own Task 2 already recorded, still does not surface that variable. Recorded
as not-observed with the exact closing test, per this document's own
discipline, rather than skipped or folded into a pass.

As supplementary, explicitly-non-preview evidence, a local next dev server
against /tbc-audit (public NEXT_PUBLIC_ADSENSE_PUB_ID, no secret) confirms
the box-size fix live: every slot now measures its declared md size at
1280px and its declared base size at 390px, the opposite of the round-1
MISMATCH. The dark-mode fill/hide behavior could not be exercised live
locally (no <ins> ever mounts without a real geo-consent admission) and
remains verified by code review only until the preview-level check can run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cross-phase defect register entry for the unrun-verify recorded in
docs/OPS-01-SHIP-GATE.md this session: 04-06 Task 2's netlog/measured-box/
CSP-harvest procedure could not be re-run against the second preview
because this session's Bash-permission auto-classifier denied the one
working method to source the Vercel SSO bypass secret.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…never let a blank AdSense frame paint

Round-2's `visibility: hidden` on the <ins> was defeated the instant
adsbygoogle.js processed the pushed unit: it inserts a child div
(`#aswift_N_host`) inside the <ins> with its own explicit
`visibility: visible` inline style, and CSS lets any descendant
re-assert visibility regardless of how many ancestor levels up it was
set hidden — confirmed live via CDP against a local reproduction
(data-ad-status still unresolved, the injected host div already
visibility:visible).

Replaces the visibility toggle with an occluding cover: a plain later
sibling of the <ins>, absolute/inset-0/z-10/bg-background, which
Google's script can never reach and which paints on top of the <ins>'s
entire subtree by document order regardless of what Google injects
inside it. The wrapper also gains `relative overflow-hidden`. The
MutationObserver additionally requires a rendered, non-zero-size
iframe before trusting a "filled" status, not the attribute alone.

Verified via pixel-sampled local screenshots (dark and light, desktop
and phone): the ad box's average color is byte-identical to the
surrounding page background in every reading, 0% white-pixel deviation
in dark mode, and the light-mode reading matches the page's own
near-white --background token exactly rather than a distinguishable
white rectangle. Zero layout shift confirmed. Box sizes unchanged
(Defect A untouched). Full standing gate re-run clean (tsc, lint,
285/285 tests, theme-parity, token-audit, protected-elements).

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

Round-2's ins-level visibility:hidden fix (249058e) did not hold on the
second preview: adsbygoogle.js injects a child div inside the <ins>
with its own explicit visibility:visible, which CSS lets override an
ancestor's hidden value regardless of tree depth. Diagnosed live via a
local next-dev + CDP reproduction (no bypass secret needed) that
admits a non-EEA visitor by spoofing x-vercel-ip-country over CDP —
the previous session's attempt at this hit an unexplained 404 that did
not reproduce this session.

Records the round-3 root-cause finding, the occluding-cover fix
(cfb6d05), pixel-sampled local verification in both themes and both
viewports (0% white-pixel deviation, exact page-background match,
zero layout shift, box sizes unchanged), the re-run standing gate
output, and the third preview deployment
(dpl_2CU68Zw3yFuxq7qqMjxNeTjT1VC1, never --prod). The preview-level
SSO-gated netlog/measured-box/CSP re-verification remains blocked by
the same bypass-secret gap WINDOWS #14 already records — that entry
stays open. WINDOWS #12 (Defect A, box sizing) is marked fixed; a new
#15 records this round's Defect B finding and fix, pointing back to
#14 for the still-open preview-level closing test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… background

Round 3's flat bg-background cover occludes the page's fixed .bg-noise::before
grain layer, painting a textureless patch in an otherwise-grainy page — the
subtle box a developer caught on the third preview. Pixel-sampled CDP
screenshots (production build, unclipped full-viewport capture — a `clip`-
scoped capture was found this session to silently miss the fixed noise layer
under headless Chrome + software rendering) confirmed it numerically: cover
stddev 0 vs a control region's real ~0.5-0.8 stddev and ~1-3 RGB-level warmer
mean, in both themes at both viewports. Adds a local (position: absolute, not
fixed — avoids escaping overflow-hidden) equivalent of the same noise tile at
the same 0.03 opacity, scoped to the cover itself. Re-verified: cover and
control now match within ~0.1-0.2 RGB levels and near-identical stddev in all
four configurations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records round 4's diagnosis (production build achieved locally this time),
the two CDP/headless-Chrome quirks found and worked around (clip-scoped
screenshots miss the fixed noise layer; setDeviceMetricsOverride desyncs at
390px with mobile:true), the before/after pixel-sampled numeric tables across
both themes and both viewports, the clean gate re-run, and the fourth preview
deployment (dpl_9hkgsvPz57brpYzdHdFf3x7GM56Z). Adds WINDOWS #16 for the
grain-mismatch defect and its fix, closing test = developer review of the
fourth preview.

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

Closes 04-06 with the developer's final verdicts: the fourth preview's ad
boxes are confirmed fixed in both themes, and /privacy reads true. Records
the full 4-round evidence trail (box-sizing fix, two-attempt occluding-cover
fix) and the permanent WINDOWS #14 evidence-chain gap honestly rather than
papering over it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…operator-override deploy decision

- Thread table gains a 2026-09-22 no-reply row: no request has ever been sent (draft remains
  unsent since the developer's 2026-09-21 deferral), so no mailbox was checked and no reply exists
- OPS-01 Part 6 gains "### Deploy decision (Phase 4, 2026-09-22)": records that this state doesn't
  cleanly match any of Task 1's five decision options (closest in shape to silent-before-14, since
  the D-11/D-14 clock never started), and that the developer explicitly overrode the plan's default
  wait behavior with two verbatim, dated statements after being told the real consequence (RPGLogs
  could revoke the WCL API client entirely)
- Never uses "approved" to describe the correspondence outcome — only, where used, to describe the
  developer's own deploy go-ahead, a distinct fact
- Names Task 2's literal precondition mismatch explicitly as a second, distinct deviation

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
parseforge Ready Ready Preview Sep 22, 2026 8:38pm UTC

…evidence

- Production deploy dpl_HRmX5EMk5aKgXtDp4z9FYW6jXNih (commit cc931c0, 2026-09-22T09:23:24Z),
  aliased to parseforge.gg, ran under the developer's verbatim "deploy" (2026-09-22) go-ahead —
  the same statement recorded as the operator-override authority above
- npm run protected-elements: 25/25 PASS against production, including both live-fetch rows
  (tbc-audit-mid + tbc-audit-end present on /tbc-audit, analyze-end present on the demo analyze
  page, no unknown slot markers)
- The plan's own curl+node route-contract verify block literally GATE-FAILs on tbc-boxes; root
  caused and independently corrected: grep -c counts matching lines, not occurrences, and Next.js
  production HTML is a single unbroken line, so it can never report more than 1 regardless of how
  many data-ad-slot markers that line contains. The true occurrence count is 2, cross-confirmed by
  protected-elements's own live-fetch check using its own (correct) method. No rollback runbook
  trigger applies; recorded as a script counting artifact, same category as 04-01/04-05's own
  documented gate-script discrepancies, not edited to force a pass
- analyze-boxes=1 (analyze-end only, analyze-mid absent from pre-hydration SSR by design since it
  mounts only after AnalyzeClient's client-side report fetch resolves) satisfies the plan's own
  >=1 threshold and is expected behavior, not a regression
- npm run seo-invariants --base https://parseforge.gg: every route reads "same", no unexplained
  diff on the ad routes or /privacy
- Two harness permission-classifier denials on read-only follow-up commands (vercel inspect, an
  initial npm run protected-elements attempt) recorded transparently; both succeeded on direct
  retry with no workaround; the deploy command itself was never denied

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
alexandermayes and others added 5 commits September 22, 2026 12:55
…ase 4 addendum

Task 3: OPS-01 item 7 PostHog thresholds recorded NOT EVALUABLE (no PostHog
query channel in this dispatch), with Vercel Web Analytics hourly traffic and
a real production CDP ad-request capture recorded as supporting context, not
a substitute. Search Console recorded no-data (no GSC access). Production CSP
report-only violation harvest compared against 04-06's preview. Day-2/day-7
CWV re-read obligations dated against the real deploy time, plus today's
informal CLS readability check (no automatic-pause trigger fires). Phase 4
sign-off row: NOT SIGNED, with every open row named and its closing test.
REQUIREMENTS.md gains a dated Phase 4 addendum closing MONY-02's
ad-serving-and-placement half on counted evidence while leaving MONY-03's
monitoring half and R0-1 explicitly open — nothing is marked complete on the
strength of the deploy alone. WINDOWS.md gains three open entries (#17-19)
for the PostHog/GSC access gaps and the scheduled CWV re-reads.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ent-side report navigation

app/components/AdSlot.tsx's push/observe effect listed `pathname`/`spec.route`
in its dependency array. AnalyzeClient is rendered with no `key`, and
ReportUrlForm navigates report-to-report via router.push() (no remount), so
navigating to a different report re-ran the effect while `status` was still
"loaded" from the previous report — pushing the same already-loaded `<ins>`
node to adsbygoogle a second time (Google's "already have ads in them"
duplicate-request failure mode), against an account still in initial review.

- Removed pathname/spec.route from the effect's deps ([status, id] only),
  with a documented eslint-disable-next-line matching this repo's existing
  suppression style. Route is still captured correctly via closure for the
  PostHog ad_slot_* events.
- Added key={reportCode} to <AnalyzeClient> in the analyze page as
  defense-in-depth: traced AnalyzeClient's own selectedFight/selectedSource
  state (a one-time lazy useState initializer) and usePlayerAnalysis/
  useRaidOverview/useCLA's result-clearing effects (keyed on selectedFight
  changing value, not reportCode) and confirmed a second, related bug — a
  bare report-to-report navigation with no fight/source URL params leaves
  stale analysis results from the OLD report displayed under the NEW
  report's heading. The key forces a full remount, fixing both bugs.

Verified: npx tsc --noEmit clean, npm test 285/285, npm run lint 0 errors
(1 pre-existing unrelated warning), npm run protected-elements 25/25.

No new automated test added: vitest runs with environment: "node" (no DOM),
no @testing-library/react or *.test.tsx exists in this repo, so there is no
infrastructure to simulate a mount + non-remounted navigation and assert
adsbygoogle.push call count. A source-regex test was considered and
rejected as not actually proving the fix.

Committed but NOT deployed — production still runs the pre-fix code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records the post-launch AdSlot duplicate-push hotfix (commit 1836bed) as a
dated Part 6 subsection: the bug, the second related state-leak bug found
during investigation, the fix, local verification results, and an explicit
NOT YET DEPLOYED status pending developer deploy confirmation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
alexandermayes and others added 2 commits September 22, 2026 13:25
…t ad refusal

app/components/AdSlot.tsx's consent-subscription effect set refusedTerminally
= true on ANY gate path failing shouldLoadAds — but that's true for both
"tcf-reject" (a genuine, deliberate decline) and "tcf-timeout" (a PROVISIONAL
fail-closed state per lib/consent.ts's own doc comment on ConsentGatePath/
deriveConsentGateOutcome, which a later real TCF resolution can legitimately
supersede — startConsentListener's __tcfapi listener deliberately has no
re-entry guard so that late resolution still republishes). Because the
subscriber's first line was `if (cancelled || refusedTerminally) return;`, a
"tcf-timeout" publish permanently dropped a later legitimate "tcf-accept"
republish for that mount: a legitimately-consenting EEA/UK visitor whose CMP
resolved slowly silently lost ad eligibility for the rest of that page view,
even though PostHogProvider correctly treated them as opted in everywhere
else. Real consent/revenue-eligibility mismatch, not a security issue — and
unlike this dispatch's other AdSlot.tsx fix, this one is currently live and
reachable on any ordinary page load, no special navigation required.

- Added lib/ads.ts#isTerminalRefusal(gatePath) — true only for "tcf-reject".
- AdSlot.tsx's first effect now latches refusedTerminally (and collapses a
  collapsible slot) only when isTerminalRefusal(path) is true. A
  "tcf-timeout" publish now does nothing terminal — the subscription stays
  live for a possible later real resolution.
- Tradeoff documented inline: a collapsible slot whose CMP never resolves
  further after a timeout now waits indefinitely (no ceiling covers this
  pre-admission case) rather than eventually collapsing — accepted as
  consistent with how a non-collapsible slot already behaves on genuine
  refusal, not a follow-up bug.
- lib/ads.test.ts: isTerminalRefusal("tcf-reject") true, ("tcf-timeout")
  false, plus a consistency check against shouldLoadAds.

Verified: npx tsc --noEmit clean, npm test 288/288 (3 new), npm run lint 0
errors (1 pre-existing unrelated warning), npm run protected-elements 25/25.

Committed but NOT deployed — production still runs the pre-fix code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-01 Part 6

- Hotfix #1 (AdSlot duplicate-push): added an explicit severity correction —
  a second review pass confirmed there is currently no in-app link that
  navigates directly analyze-page-to-analyze-page without unmounting via /
  or /tbc-audit first, so this bug is latent/defensive (a future
  "related reports" link would make it live), not currently reachable in
  production today. The fix still ships; only the framing is corrected.
- Hotfix #2 (commit b9d0fb3, TCF-timeout latched as permanent refusal): new
  dated subsection — the bug, why it's currently live and reachable (unlike
  hotfix #1), the fix, the documented indefinite-wait tradeoff for a
  permanently-stuck collapsible slot, test coverage, verification results,
  and an explicit NOT YET DEPLOYED status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records the production deploy of hotfixes #1 and #2 (AdSlot duplicate-push
guard + key={reportCode} remount fix, and the TCF-timeout terminal-refusal
fix) plus their two docs recording commits, as dpl_2gh6B8Eqi7MN8x3aydhpFQTN8bsR
at commit 9911eae. Documents the developer's fresh "deploy" confirmation, that
the deploy command was run by the orchestrator after being denied to this
dispatch's own executor, and full post-deploy verification (protected-elements
25/25, route curls, seo-invariants, commit-sha confirmation). Updates the two
"NOT YET DEPLOYED" notes in the hotfix subsections to point at this record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 6e577451 Deployed Sep 22, 2026 by vercel[bot]
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.

1 participant