# GOLIVE lane R10 — PG-28 / PG-29 investigation receipt

Date: 2026-08-22
Lane branch: `wt/lane-r10-pg28-journeys-20260822`
Base: `origin/master` `46ee3bd84`
Plugin version under test: `0.9.84`

Status: **PG-28 NOT FLIPPED. PG-29 NOT FLIPPED — real product defect found and fixed here; the
gate's JavaScript-console half remains unmeasured.**

This receipt records a negative result deliberately. Neither gate may be checked on the
evidence gathered here, and one previously reported "clean" signal is retracted below.

---

## 1. Retraction: the earlier "DEBUGLOG-CLEAN" signal was a false pass

Earlier R10 runs reported `DEBUGLOG-CLEAN`. That claim is **withdrawn**.

The scrape matched only `grep -qE "PHP (Fatal|Warning)"`. WordPress writes database
failures with the prefix `WordPress database error`, which that pattern cannot match, so the
oracle was structurally incapable of failing. With the pattern corrected, the same fixture
produces a 4284-byte `debug.log` full of genuine errors.

Lesson for later lanes: a PG-29 style gate must assert on the debug.log being **empty**, not
on the absence of two hand-picked substrings.

## 2. Product defect: `wp_ipz_user_activity` is written but never created

Every admin login on a `WP_DEBUG` fixture emits:

```
[22-Aug-2026 15:16:17 UTC] WordPress database error Table 'wordpress.wp_ipz_user_activity'
doesn't exist for query INSERT INTO wp_ipz_user_activity (...)
made by shutdown_action_hook, do_action('shutdown'), WP_Hook->do_action,
WP_Hook->apply_filters, InternationalPressZone\Team\ActivityTracker->flush_buffer
```

Evidence:

- `includes/Team/ActivityTracker.php` reads/writes `{prefix}ipz_user_activity` in 7 places.
- `includes/Core/Database.php` contains **zero** occurrences of `ipz_user_activity` — no
  installer, migration, or `create_tables()` branch ever creates it.
- `includes/Core/Plugin.php:191` instantiates `ActivityTracker` **unconditionally**.
- `ActivityTracker::__construct` calls `register_hooks()`, which registers `wp_login`,
  `wp_logout`, and the translation/role events.

This is **not** gated behind `ipz_team_workflow`. It therefore affects every install,
including a first customer for whom the Team surfaces are deferred, and it reproduces on a
plain mounted stack with no feature flags beyond the harness default.

PG-29 requires no PHP fatal/warning **or JavaScript console error** through the supported
customer journey with logging enabled. A database error on every login fails that gate.

**PG-29 verdict: NOT FLIPPED — PHP half failed on a real product defect (fixed in this lane);
JavaScript-console half never measured.** PG-29 has two halves. Only the PHP/debug.log half was
evaluated, and it failed on the defect above; that alone is enough to withhold the gate. The
JavaScript console-error half was *not* assessed at all, so this receipt must not be read as a
complete evaluation of PG-29 even once the defect fix is confirmed.

### 2a. Fix applied in this lane

The defect was self-contained, so it was repaired here rather than carried forward:

- `includes/Core/Database.php` — new `create_user_activity_table()`, registered in
  `create_tables()` alongside the other Team tables. Columns and indexes are derived from the
  tracker's own INSERT (`user_id`, `activity_type`, `object_type`, `object_id`, `metadata`,
  `ip_address`, `user_agent`, `created_at`) and its read/delete query shapes.
- `DB_VERSION` bumped `1.2.8` → `1.2.9`.

Reachability of both install paths was checked, not assumed:

- Fresh install → `international-press-zone.php:197` calls `create_tables()` on activation.
- Existing install → `Plugin::checkUpgrades()` calls `needs_upgrade()` (a `version_compare`
  against `DB_VERSION`) and then `create_tables()`. The version bump is what makes existing
  installs self-heal; adding the table without it would have fixed only new installs.

`ActivityTracker` was deliberately left ungated. Gating it would have hidden the symptom while
leaving the Team feature writing to a table that still does not exist; creating the table makes
both the default install and the Team surfaces correct.

### 2b. Fix verified on a remote stack (debian3)

Proven with positive evidence, not by an exit code — this harness can exit 0 having run nothing
(§5), so absence-of-error was not accepted as proof. Mounted-checkout path
(`IPZ_STACK_NO_PLUGIN_MOUNT=0`), `IPZ_STACK_WP_DEBUG=1`. debian2 was skipped on a stale port
8080 (exit 3) and fleet-seat spilled to debian3.

```
SHOW TABLES LIKE '%ipz_user_activity%'   → wp_ipz_user_activity

DESCRIBE wp_ipz_user_activity
  id            bigint(20) unsigned  NO   PRI  auto_increment
  user_id       bigint(20) unsigned  NO   MUL
  activity_type varchar(50)          NO   MUL
  object_type   varchar(50)          NO   MUL
  object_id     bigint(20) unsigned  NO        0
  metadata      longtext             YES
  ip_address    varchar(45)          YES
  user_agent    text                 YES
  created_at    datetime             NO   MUL

admin login (POST wp-login.php)          → HTTP 200
SELECT COUNT(1) FROM wp_ipz_user_activity → 1
cat wp-content/debug.log                  → No such file or directory
grep ipz_user_activity debug.log          → NO_MATCHES_FOUND
```

The row count of 1 is the load-bearing signal: it proves `ActivityTracker::flush_buffer()` ran
on `shutdown` and its INSERT *succeeded*, rather than the tracker having been skipped. A
follow-up check confirmed `wp-config.php` carries `WP_DEBUG=true`, `WP_DEBUG_LOG=true`,
`WP_DEBUG_DISPLAY=false`, so the missing `debug.log` means WordPress logged nothing at all — not
that logging was off.

This closes the PHP half of PG-29 for this specific defect only. It does not flip the gate: the
JavaScript-console half is still unmeasured, and the broader journey suite still fails (§4).

`checkUpgrades()` is guarded by `current_user_can('update_plugins')`, so the upgrade runs on the
first admin page load by a capable user. That is the pre-existing convention for every table in
this plugin, not something introduced here.

## 3. Control experiment: the release ZIP is not the cause of the journey failures

Identical spec set, identical host class, `--project=chromium --retries=0`:

| Stack | Result |
|---|---|
| ZIP-installed (`IPZ_STACK_NO_PLUGIN_MOUNT=1`, `IPZ_STACK_INSTALL_ZIP=…0.9.84.zip`) | 45 failed / 46 passed |
| **Mounted checkout** (control, no ZIP) | **43 failed / 48 passed / 2 skipped (9.2m)** |

The failure sets match, including all three `connect-onboarding.spec.js` cases and
`UJ-002 H1`. The ZIP install path is therefore **exonerated** — these specs have never been
green on any stack in this repository.

Corroboration: no receipt in `docs/launch/receipts/` has ever proven a broad suite green.
Every prior lane proves 1–2 specs. The "88/88" figure in project memory is
`universal-admin-component-redesign` + `admin-snappy-all-pages` across two browsers — two
specs, not the journey suite.

Consequence: chasing ZIP-provisioning parity was the wrong lead. PG-28 says "on the final
release candidate"; nothing in `GOLIVE.md` or `docs/launch/` defines the release candidate as
the ZIP artifact, and PG-33/34/36 already prove the artifact installs, upgrades, and rolls
back. The mounted checkout at the RC commit is a legitimate release candidate for PG-28.

## 4. PG-28 required-set mapping, and why the gate cannot be flipped

`GOLIVE.md:73` names the required set. Mapping to specs:

| Required item | Spec(s) | State |
|---|---|---|
| onboarding/account | `connect-onboarding.spec.js` | FAILING (all 3, both stacks) |
| languages | `journeys/UJ-001..UJ-005`, `languages-page`, `language-setup` | partial |
| post/page translation | `journeys/UJ-011`, `UJ-012`, `metabox-translate` | partial |
| string translation | `journeys/UJ-014`, `UJ-015`, `UJ-016` | partial |
| bulk job | `journeys/UJ-013`, `UJ-019` | partial |
| **cancel** | `journeys/UJ-020-cancel-job.spec.js` | **NEVER RUNS — in `testIgnore`** |
| settings/exceptions | `UJ-006`, `UJ-007`, `UJ-024`, `UJ-025`, `settings`, `scoped-translation-exceptions` | partial |
| frontend language switch | `routing-contract.spec.js` | see rot note |
| update flow | `upgrade-zip.spec.js` | proven under PG-34 |

Two structural blockers, independent of the pass rate:

**(a) The required "cancel" journey is silently excluded.**
`playwright.config.js:51-54` sets `testIgnore` for `UJ-020-cancel-job.spec.js`. Only its A1
branch needs `IPZ_E2E_COMPLETED_JOB_ID`; H1 (cancel a pending job) and P1 (authorization) are
self-provisioning and would run today. The whole file is dropped for one fixture. A gate
requiring "cancel" cannot be flipped while its only spec is excluded by config — no run
result would ever reveal this, because ignored specs are not reported as skipped.

**(b) Frontend-switcher spec rot.**
`LanguageSwitcher.php` emits `.ipz-language-switcher` / `.ipz-language-select` / `.ipz-flag-link`.
`frontend.spec.js` asserts `.mpz-language-switcher` and `.mpz-lang-option`; `language-switcher.spec.js`
asserts `.mpz-language-form` / `.mpz-language-select`. Both are pre-`ipz` rot and cannot pass
against shipped markup. `routing-contract.spec.js` is the only maintained frontend-switch
spec. The stale pair is documented here, not repaired in this lane.

**PG-28 verdict: NOT FLIPPED.** ~47% of the maintained journey suite fails on a clean mounted
stack, and a required journey is excluded by config.

## 5. Harness defect found and fixed: debian1 runtime unlinked + `e2e-remote` fail-open

Three R10 dispatches "succeeded" with exit 0 while never executing anything.

Root cause: `local-gate --remote-doctor` reported
`FAIL debian1 runtime not materialised (runtime-unlinked)`. On healthy debian2/debian3,
`~/.claude/{agents,bin}` are symlinks into
`~/.local/share/overdeck/claude-runtime/content-33d62d6c95e3/`; on debian1 they had been
replaced by real directories. The probe is
`remote-seat-provision.mjs:1155` (`[ ! -L "$link" ] && echo runtime-unlinked`).

Repaired with the sanctioned tool, run from the git root:
`provision-remote-seats debian1` → `OK debian1: claude=2.1.233 runtime=content-33d62d6c95e3`.
`local-gate --remote-doctor` now exits 0 with debian1/2/3 all `status=PASS`.

**Separate, still-open harness defect (fails open — reported, not patched):**
`e2e-remote` dispatches via `local-gate --remote-only`, then checks for a receipt file
`.e2e-remote-status-<id>`. When the payload never runs, the receipt is absent, `gate_status`
is 0, and `e2e-remote` **exits 0**. A gate run that never executed reports success. Proven
with a trivial client (`echo HARNESS-ALIVE`) that produced no output and exit 0.
Tracked as `fire-87d2d9a18244729e`
(`e2e-remote:local-gate-remote-only-noop-exit0-missing-receipt`). Not patched here:
`/home/user/.claude/bin/e2e-remote` is managed overdeck runtime, content-addressed and
outside this repository — a local edit would be reverted by the next provision.

## 6. What a future lane must do before PG-28/PG-29 can be flipped

1. ~~Fix the `ipz_user_activity` defect.~~ Done in this lane (§2a) — confirm the debug.log is
   clean on a fresh remote stack before relying on it.
2. Re-scope PG-29's oracle to assert an **empty** debug.log plus the existing
   `assertNoRuntimeFailures` console/pageerror collection already exported by
   `journeys/journey-support.js`.
3. Provision `IPZ_E2E_COMPLETED_JOB_ID` in `remote-stack.sh` and remove `UJ-020` from
   `testIgnore`, so the required "cancel" journey actually runs.
4. Triage the ~43 pre-existing journey failures on the mounted stack as product defects vs.
   spec rot. This is a real backlog, not a provisioning problem.
5. Only then attempt chromium + firefox green on the mapped required set.
