# WB Listora 1.2.0 — Master Plan (consolidated)

Updated 2026-06-07. THE single source for everything 1.2.0 across both repos.
Supersedes the navigation role of 1.2.0-roadmap.md (rationale/detail) and
1.2.0-tasks-source.yaml (original have/need/customer_value matrix) - both stay
as reference. Execution manifests referenced per stage below.

Owner decisions log: DEC-1 keep deprecated aliases until 2.0 · DEC-2 build
analytics-lite · DEC-3 HivePress deferred (REVERSED by DEC-4) · DEC-W3
wiring-gate FP suppressions (audit/derived/wiring-baseline.json) · DEC-4
(2026-06-07) "cover most of them in 1.2.0": HivePress migrator, search
analytics, bulk-edit, printable invoice MVP pulled back in; recurring billing
+ multi-location stay out (not minor-release-safe) · CR-2 resolution:
per-keyword search tallies get a dedicated listora_search_terms aggregate
table (additive DDL, Pro DB_VERSION bump).

---

## Stage 0 — DONE (on local integration branches, NOT pushed)

Branches: `autovap/wb-listora/integration` + `autovap/wb-listora-pro/integration`.
Versions bumped to 1.2.0. All 14 architecture invariants green, local CI green.

| Done | Detail |
|---|---|
| Free phase-1 (8 dirs) | zip-leak guard · summary 3KB budget · js-only-activation closed · html_to_text verified consolidated · 40px tap targets · image alt fallback + placeholder · **analytics-lite** (dashboard + admin column + REST) · **background demo/CSV import** (AS batches, resumable, progress UI) |
| Pro phase-1 (9 dirs) | HivePress stub reworded · INV-14 hex fix · manifest hygiene · build guard · **coupon_usage 3 entry points** · email-helpers verified · **webhook deliveries REST + retry** · alias note · **14 namespace renames with bridges** |
| Cross-plugin fixes | analytics single-writer contract (Free fallback + Pro explicit filter, live-verified +1/view) · eager Free DB migrator (smoke F2) · need_rejected canonical + bridge (smoke F1) |
| QA self-growth | 9 new journeys + runbook C/D/S13-S16 rows (Free 87 / Pro 75 journeys) |
| Combo smoke | 82 pass / 2 fail -> both fixed, BC cards 9970182527 + 9970182629 in Ready for Testing |

## Stage 1 — Phase-2 feature build (NEXT, on "go")

PLANNER-AUTHORED engine manifests (autovap-planner, manifest-first reuse maps +
expected-flow journeys, both validated PASS): `plan/1.2.0-plan-phase2.yaml`
(Free, 5 waves / 8 dirs / 7 journeys) + `../wb-listora-pro/plan/1.2.0-plan-phase2.yaml`
(Pro, 6 waves / 8 dirs / 6 journeys). Rationale + reuse maps:
`plan/1.2.0-phase2-rationale.md` in each repo. Free and Pro launch in parallel.

| Free wave | Directions | Pro wave | Directions |
|---|---|---|---|
| 5 quick-wins | catalog reconcile · review-reminder · Open-now badge | 5 i18n+plan-ux | .pot/wpml-config · trial + currency |
| 6 extensibility | wb_listora_search_resolved hook · HivePress migrator | 6 refund-ui | admin Refund credits (triggers existing SDK path only) |
| 7 privacy | GDPR exporter/eraser (reviews/claims/favorites) | 7 privacy+receipt | Pro PII privacy · printable HTML receipt |
| 8 email-editor | editor MVP (one non-autoloaded option + existing filters) | 8 search-analytics | listora_search_terms table + admin tab + REST (gates on Free wave-6) |
| 9 bulk-edit | Status_Manager/Featured-routed bulk actions | 9 hivepress-wiring | flip stub + Visual Importer wiring (gates on Free wave-6) |
| | | 10 qa-coverage | journeys for the 8 never-audited features (no Free dep) |

Cross-repo gates: Pro-8 and Pro-9 wait for Free wave-6 to merge. Planner-caught
pre-code defects (the reason this pass exists): hook-name collision
(search_results filter vs new search_resolved action), wrong Migration_Base
abstract names, unprefixed filter names, CR-2 UNIQUE-key collapse.

Escalation protocol: same as phase-1 - 3 remediations -> re-plan -> owner.
Contract breaches hard-halt (contracts + sanctioned migrations[] declared per
manifest).

## Stage 2 — QA completion (after Stage 1 merges)

1. Re-run `/wp-plugin-smoke combo` (the on-disk report records the 2 fixed
   failures; the build gate rightly refuses it until a green re-run).
2. The 4 manual checks from the last smoke: Firefox business-hours picker,
   Safari RTL spot-check, axe-core a11y scan, dark-mode sweep.
3. QA team verifies BC cards 9970182527 (need_rejected bridge) + 9970182629
   (eager migrator) from Ready for Testing -> Done.
4. wppqa baseline re-run if older than 7 days at that point.

## Stage 3 — Parallel / upstream (any time, separate repos)

| Item | Where |
|---|---|
| Credits SDK `maybe_create_table` column-upgrade fix + re-sync libs/ | wbcom-credits-sdk repo |
| Firefox lane decision (automate vs documented manual gate) | tooling |

## Stage 4 — Release pipeline (ONLY on explicit owner release call)

Per the 5-surface checklist (memory: listora-release-surfaces):

1. Push integration branches -> merge to main (owner reviews diff first;
   engine never pushes).
2. readme.txt changelogs BOTH repos: full-history action-prefix format
   (New/Improve/Fix/Security/Dev/Compat, no em-dashes) + CHANGELOG.md entries
   + Upgrade Notice. Lockstep Compat lines.
3. `/wp-plugin-onboard --refresh` both repos (manifest deltas: new REST routes,
   hooks, privacy exporters, email-editor option).
4. docs/website coverage audit vs the 1.2.0 changelog (same method as 1.1.0)
   -> store docs re-sync (store-product-publisher update playbook) -> bump
   store product JSONs.
5. Release-notes sync on wbcomdesigns.com (sync-changelog.mjs reads the new
   TAG's readme - full history already in readme since 1.1.0 backfill).
6. `bin/build-release.sh` both (green smoke report from Stage 2 required;
   zip-leak guard now built in) -> GitHub releases v1.2.0, lockstep
   cross-linked -> Slack #ready-for-release (one message per product).

## Out of scope - 1.3.0 backlog (decided, documented)

Only two items remain parked (DEC-4 pulled the rest in):
- Recurring billing/subscriptions - payment architecture, needs a dedicated
  payments design doc first (1.3.0/2.0 headline).
- Multi-location per listing - schema-wide impact on geo/search/maps; own
  competitive plan defers (<5% demand).

## Standing rules for every stage

wp-plugin-development + ux-foundation for builds; ux-audit gate on UI; 100k
scale (bounded queries, AS for long-running, no autoloaded fat options);
additive-only (rules 7-9); manifest delta + journey in the same PR; 390px
verification on every UI change; no pushes without owner approval.

---

## Execution log - phase 2 (live status, updated 2026-06-07)

| Wave | Status | Notes |
|---|---|---|
| Free 5 quick-wins | MERGED green | catalog reconcile, review-reminder, Open-now badge |
| Free 6 extensibility | MERGED green | wb_listora_search_resolved + HivePress migrator (+ owner fix: CLI sources list) |
| Free 7 privacy | MERGED + owner-fixed (4ba2d1a) | verifier caught 3 silent wiring bugs: wrong class names in class_exists (Privacy_Exporter/Privacy_Eraser) + undefined wb_listora_recompute_listing_rating(); fixed + live-verified; browser pass carried into wave-8 tier |
| Free 8 email-editor / 9 bulk-edit | MERGED green + owner-fixed (aefd54d) | double-verify caught: override read-back admin-only (REST/cron emails ignored edits - fixed always-loaded, live-proven non-admin), bulk approve/reject missing Status_Manager validity guard (fixed), email-templates CSS hex fallbacks (fixed, INV-14 green) |
| Pro 5 i18n+plan-ux | MERGED green | + 2 owner-sanctioned hooks (plan_currency, plan_trial_consumed) |
| Pro 6 refund-ui | MERGED (owner) | REST route owner-sanctioned; browser verify carried into Pro wave-7 tier |
| Pro 10 qa-coverage | MERGED green | S17-S24 + /migration/preview contract pinned |
| Pro 7 privacy+receipt | MERGED + owner-fixed (9ec44eb) | self-boot classes never autoloaded (receipt 404, no refund UI) - class_exists triggers added, routes live-verified; privacy PASSED first try |
| Pro 8 search-analytics | MERGED green + owner-fixed (765e847, b85e273) | feature passed full journey; refund button added to topup rows; double-verify caught the payment_id=0 double-apply - ledger-referenced idempotency via SDK Processed_Events::claim, live-proven 200-then-409 |
| Pro 9 hivepress-wiring | MERGED green | Visual Importer + detector + stub flip, journey passed incl. negative path |

Owner interventions so far: 5 (mis-seeded audit_log contract; mode-blind hivepress
journey; 3x undeclared-additive sanctions; stale-branch false "removal"; wave-7
privacy wiring fixes). Combo smoke SKIPPED for this cycle by owner decision
(2026-06-07) - functionality/UX gaps first; build-release gate stays correctly
locked until a future smoke.

## Flow-closure continuation state (2026-06-08, for session continuity)
- Free run: COMPLETE on the integration branch. All 4 waves landed:
  - wave-1 import-core: csv import feedback + AS pending-only deadlock guard (b359c1e) + prior fixes.
  - wave-2 email-optout: token /unsubscribe (HMAC, logged-out) + review_reminder opt-out (b4272dd).
  - wave-3 flow-closure: views-render-0, lifecycle (7e33c23 Privacy row + #oldest-unanswered deep-link + #reviews SSR alias; 0edbe56 live hours 24h/Closed toggle), mobile-nav scroll affordance.
  - wave-4 architecture: shared wb_listora_is_bot_request() helper + Bot_Detection (KG-1 eager-require fix 9f550e3) + Background_Import canonicalization docs. Owner-merged file-precise (excluded the stale branch yaml + a re-run remediator's divergent /import/queue/demo route on an unmerged wave-1 branch).
  - DO NOT re-run the Free engine (wf_47ed0e77-0e0). It is done; every manifest-edit resume re-busts caches and re-runs green waves with non-deterministic remediator choices. Owner-merge remaining direction branches instead (the documented continuation-manifest escape hatch).
  - New 1.2.0 Free contract surface (all sanctioned): routes /import/queue/csv + /unsubscribe; filters wb_listora_unsubscribable_events, wb_listora_unsubscribe_url, wb_listora_is_bot_request, wb_listora_bot_signatures; actions wb_listora_after_unsubscribe, wb_listora_demo_import_run; review_reminder added to test-email CLI list.
- Pro run: resumeFromRunId wf_0f0764b7-9d1, manifest ../wb-listora-pro/plan/1.2.0-plan-flowclosure.yaml. Wave-1 GREEN + merged (migration-background + migration-ui-polish; owner fix 8bf6a11). Wave-2 (refund-loop): refund-notifier + receipt-discoverability merged, but functional verify found 4 real bugs AND the run hard-halted on a FALSE contract breach (by-token route declared-but-not-yet-built read as "removed" by the wave-2 verifier; the wave-1 verifier correctly called it a non-breach). OWNER FIXES landed 2026-06-08: Free fa16309 (wb_listora_dashboard_credit_row_actions per-row hook, has_action-gated 5th Receipt column + RTL twin) + Pro 2747123 (Receipt stdClass->array cast, rest_pre_serve_request raw-HTML serving, render_history_receipt_link listener, refund modal data-state fix, AND the by-token route built early to clear the contract halt). All live-proven (5 receipt headers/3 links; modal data-state=open; by-token 200-html/403/403). Resumed wh3ihvda1 for wave-2 re-verify (journey nudges added) -> wave-3 (onboarding+analytics) -> wave-4 (token-receipts email wiring + bot-detection-retention).
- CONTRACT-DIFFER LESSON (2026-06-08): a migration-declared additive that belongs to a LATER wave is read as a "removed" contract breach by the wave-level verifier once an earlier wave's functional gate runs (the wave-1 direction verifier classifies it correctly as a non-breach - the two disagree). Fix: build the additive early owner-side to clear the halt, OR accept it will hard-halt every wave until its own wave lands. Do NOT respond by editing the migrations list (that busts all caches like the Free bot_signatures incident).
- PHANTOM-CONTRACT LESSON (2026-06-08): the SECOND wave-2 halt was wb_listora_pro_search_terms_recorded - declared in contracts.hooks.actions but NEVER fired in source (phantom since 1.1.0, 0 mentions). The contract differ reads a declared-but-absent contract surface as "removed". Fixed code-only (657b900) by firing the hook at the real record_search() site (legit public extension point) - no manifest edit, no cache-thrash.
- JOURNAL-REPLAY LESSON (2026-06-08): after the code fix, resumeFromRunId wf_0f0764b7-9d1 returned the SAME contract-halt verdict in 34ms/0-tokens - it REPLAYED the journaled BLOCKED verdict instead of re-evaluating. Lesson: once a runId journals a hard-halt, resuming it replays the halt; you cannot clear a contract halt by fixing code + resuming the same runId. Switch to the CONTINUATION-MANIFEST pattern: owner-complete the halted waves on integration, then start a FRESH run (NO resumeFromRunId) containing only the REMAINING waves (depends_on of the first remaining wave emptied), which re-evaluates the contract against current integration.
- Pro CONTINUATION run: wf_d09f702a-5f5 (task w9i1f13k9) - COMPLETE. waves 3-4 both GREEN + merged. Builds: wave-3 monetization-onboarding (gateway empty-state onboarding copy, live-proven on /buy-credits/) + analytics-actions (zero-result copy/Add-Listing, CSV export both tabs, Top-Listings links, Search subtitle, digest review_reminder routing); wave-4 token-receipts (email entry point - receipt_token_url embedded in credits-refunded email via wb_listora_pro_email_credits_refunded_view_data filter) + bot-detection-retention (Pro Analytics consumes Free's wb_listora_is_bot_request via function_exists guard; analytics retention health line). Double-verify BLOCKED on ONE phpstan L7 error (notification-digest send_digest_email $items missingType) - owner-fixed c0b772e.
- FINAL OWNER CLEANUP (2026-06-08, the composer-ci gate is stricter than the autovap gates and caught 3 things the engine missed):
  - Pro c0b772e: typed send_digest_email() $items array<int,array<string,mixed>> (double-verify phpstan blocker).
  - Pro (by-token Rule 2): moved __return_true -> named check_token_receipt_permission() returning true|WP_Error, validated entry stashed for callback. Live re-proven 200/403/403.
  - Free: allowlisted the public token-gated /unsubscribe (Rule 2) + pre-template-chain inline-style (Rule 6) in bin/coding-rules-check.sh + documented audit/FEATURE_AUDIT.md §3; added void return type to Unsubscribe_Controller::register_routes() (phpstan).
- STATE: BOTH plugins' full local CI gates GREEN (php-lint + WPCS + PHPStan L7 + coding-rules + arch-invariants + manifest; exit 0). 1.2.0 flow-closure COMPLETE on both integration branches (Free autovap/wb-listora/integration, Pro autovap/wb-listora-pro/integration). NOT released; NOT combo-smoked (owner skipped this cycle); branches NOT merged to main - awaiting owner.
- LESSON: the autovap engine's static gates (php-lint/wpcs/phpstan/wiring/contract) do NOT run the plugin's own `composer ci` coding-rules (Rule 2 __return_true allowlist, Rule 6 inline-style). After an engine run, ALWAYS run `composer ci:no-journeys` in BOTH repos as the final gate - it catches public-endpoint + inline-asset rule violations the engine merges green.
- Resume mechanics: pass {dry_run:false, manifest:<validated JSON>} with the resumeFromRunId. LESSON (2026-06-08): editing the manifest between resumes busts caches broadly and re-runs already-green waves - only edit the manifest when a contract sanction is genuinely required, and prefer owner-merging finished direction branches over re-triggering the engine.
