Phase 12 · Track Z · v1.1 · Shipped 2026-05-26

Tech debt — only what blocks v1.1.

Four binary in/out fixes, each tied to a downstream phase. Codex review forced a scope correction: two original items (GA4 allow-list removal, per-section catch-and-continue) already shipped in v1.0. The actual remaining work is a DataLayer-side allow-list the audit missed, plus three smaller items.

Gathered2026-05-26 ModeParallel-to-Phase-8 fix track Depends on— (parallel with 8) BlocksPhase 10 start
Phase boundary

Principle: if leaving it in invalidates a v1.1 success criterion, it's in.

Not a full audit. The v1.0 AUDIT.md and BI-USER-FIT-AUDIT.md still own broader cleanup. Each in-scope item below has an explicit binary rationale tied to which downstream phase it unblocks.

In-scope fixes

Four items, each justified.

Z1 · REVISED · Remove DataLayer allow-list

Codex found this; the v1.0 audit missed it.

Where: src/utils/eventUtils.ts:80-86

if (
  event.includes("gallery") ||
  event.includes("player") ||
  event.includes("page") ||
  event.includes("article")
) {
  processEventInMemory(mediaName, "dataLayer", event, functionName);
}

Why in: Phase 10's unclassified bucket requires the matcher to see every captured event. The GA4 path was fixed earlier; this DataLayer path still pre-filters by 4 hardcoded substrings. Any DataLayer event without gallery|player|page|article in its name (e.g. subscription_click, consent_*, cta_*, paywall_view) is silently dropped before storage, then later surfaces as a Phase 10 tracking-broken false-positive.

Fix: drop the inner if — capture every event value. Matching decides relevance. Add length / control-char hygiene (256-char cap, strip non-printable).

Z2 · REVISED, NARROWED · Catch-and-continue around per-section setup

Section flow loop already protected; only setup remains.

Where: src/bi-measurement.ts:145-224 (pre-section setup) and the fatal IIFE catch at :430-434.

Why in: Section flow execution is already wrapped (Phase 7 work, verified at :220-340). But pre-section setup — browser.newContext(...), CncSite.load(...), the consent click + cleanup — is still outside the section-aborted boundary. If site.load() throws because a domain is down, the whole run aborts with no per-section rows, contradicting Phase 2's "no silent gaps" promise.

Fix: wrap each setup step in its own try; on failure, emit one section-aborted row per planned flow on that media with outcome:"section-aborted", failureKind:classifyError(err), and proceed to the next media.

Z3 · Structured TestError with .cause + uiEvidence

Triage needs evidence at the moment of failure.

Where: src/lib/uniweb-site.ts (8 raw throw new Error(...) sites listed in BI-FIT-AUDIT §4)

Why in: Phase 10's robot-broken triage needs to distinguish "modal was up" from "page layout changed". Raw Error doesn't carry that context.

Fix:

  • New class TestError extends Error { uiEvidence?, selectorAttempted?, cause? }
  • Replace all 8 raw throws. Carry original Playwright error as .cause.
  • Precedence change in classifyError: TestError.uiEvidence checked first; if modalBlocking or !consentAcceptedrobot-broken. Then fall through to existing rules (timeout, network, default). Otherwise the strongest Phase 10 signal gets shadowed.
  • Extend classifyError to read err.cause recursively for Playwright detection inside wrapped TestError.
Z4 · REVISED · Cause hypothesis corrected

Leakage is in DataLayer scraping, not handler reset.

Where: src/utils/eventUtils.ts:73-79 (DataLayer scraping) — root cause re-identified by codex.

Why in: EVENT-COVERAGE-AUDIT documents gallery_previous.dataLayer containing leftover gallery_open / gallery_next from prior steps. Original draft suspected attachRequestHandler reset ordering. Real cause: processDataLayerEventsInMemory rereads the entire cumulative window.dataLayer array every call. resetFunctionEventStorage clears the bucket, but the next scrape re-ingests every prior event.

Fix:

  • Track per-page high-water-mark: dataLayerCursor: Map<Page, number>.
  • Each scrape: dataLayer.slice(cursor); update cursor to dataLayer.length.
  • On page navigation / new context, reset cursor.
Already done · verified by codex

Two items the original draft proposed; both shipped in v1.0.

Z-already-done-1 · GA4 capture allow-list

Already removed. handlerUtils.ts:64-68 stores any en.

Tests tests/capture-integrity.test.js:76-99 already cover stream_start / cta_click outside the old allow-list. Out — verify with a quick regression assertion only.

Z-already-done-2 · Per-section flow catches

Six section catches verified in bi-measurement.ts:220-340.

Section flow execution already produces section-aborted rows via Phase 7's classifier. The narrowed Z2 above handles only the remaining setup gap.

Out of scope · binary non-decisions

Seven items intentionally deferred.

ItemWhy out
Remove mariadb depPure cleanup. Doesn't impact v1.1.
Translate Czech log messagesCosmetic. Old strings stay; new code uses English.
Resurrect src/tools/generateConfig.tsAuthoring tooling is v1.2+ scope.
Replace JSON configs with YAML / TOMLNo actual blocker.
Robot signatures → options objectCLAUDE.md hard-no.
Add ESLintCLAUDE.md hard-no.
Refactor mutable singletonsCLAUDE.md preserves the drain/reset contract.
Success criteria

Eight, each verifiable.

  1. Z1 verified: a fixture DataLayer event event:"subscription_click" appears in eventStorage.<media>.<fn>.dataLayer. Today it doesn't.
  2. Z1 hygiene: event names >256 chars or with control chars are truncated/dropped with a single logger.warn; matcher contract unchanged.
  3. Z2 verified: a fixture that forces site.load() to throw on media A still allows media B to run; each planned flow on A produces a section-aborted row.
  4. Z3 verified: every throw new Error(...) in uniweb-site.ts replaced with TestError; grep returns zero. TestError.cause set where original error exists.
  5. Z3 precedence: unit test — TestError with uiEvidence.modalBlocking=true AND wrapped Playwright timeout classifies as robot-broken (evidence wins). Same test without modal flag → timeout.
  6. Z4 verified: regression test runs two gallery flows in sequence; asserts gallery_previous.dataLayer contains exactly the events from the second flow's window. Test fails without cursor; passes with it.
  7. Phase 1–7 contracts preserved: every existing test passes byte-for-byte.
  8. npm run check + npm run test:logic pass; no new npm deps.
Files

Only files that need to change.

modsrc/utils/eventUtils.tsZ1: remove DL allow-list + hygiene; Z4: cursor
modsrc/bi-measurement.tsZ2: setup wrap
modsrc/lib/uniweb-site.tsZ3: TestError replacement
newsrc/lib/TestError.ts
modsrc/utils/failureClassification.tsZ3: uiEvidence precedence + .cause recursion
newtests/datalayer-allowlist-removed.test.js, setup-failure-isolation.test.js, test-error.test.js, classify-error-uievidence-precedence.test.js, datalayer-leakage.test.js
modtests/capture-integrity.test.jsadd regression assertion for already-done GA4 fix
Review

Substantial scope correction.

Original draft was stale against the codebase

CODEX · INTEGRATED

Z1 (GA4 allow-list) and Z2-flow (per-section catch) had already shipped in v1.0. Codex verified line-by-line. Phase 12 rewritten around the actual remaining work.

!

New bug surfaced · DataLayer allow-list

CODEX · INTEGRATED

The audit had documented the GA4 allow-list but missed the equivalent filter in processDataLayerEventsInMemory at eventUtils.ts:80-86. Promoted to new Z1.

Z3 precedence fix · evidence beats timeout

CODEX · INTEGRATED

Without reordering, the existing timeout rule in classifyError would shadow TestError.uiEvidence.modalBlocking — defeating the strongest Phase 10 signal. uiEvidence now checked first; .cause recursion ensures wrapped errors still classify correctly.

Z4 cause hypothesis corrected

CODEX · INTEGRATED

Leakage is from rereading the cumulative window.dataLayer on every scrape, not from handler reset ordering. Fix is a per-page cursor.

← Previous Phase 11 · Slack routing Back to ↑ Plans overview