# Lane 3 — Measurement chain, data contracts, decision integrity, and orchestration trigger audit

Author: Lane 3 (Opus, high effort). Date: 2026-08-30. Repo `/home/matt/mirror/Fusion`, branch
`round-13-takeover` @ `8351b59`. Read-only. Writes confined to
`outputs/reviews/fusion-ppc-system-audit-20260830/`. Spend USD 0. No live platform, CRM, site or
git mutation. The stopped run was **not** restarted, resumed or repaired.

## Label legend (used on every claim)

`[REQ]` requested behavior (Matt's words) · `[DESIGN]` intended design in a goal/spec document ·
`[CONFIG]` configured state read from a file or account export · `[OBS]` observed behavior ·
`[DEFECT]` a fault I can prove · `[HYP]` hypothesis · `[REC]` recommendation.
`UNAVAILABLE` always means *not measured*, never zero. A **verified zero** is labelled as such.

---

## 0. Headline findings, ranked by decision risk

| # | Finding | Label | Decisive evidence |
| --- | --- | --- | --- |
| 1 | **Both ad platforms optimise on a form *start*, not a submission.** Of Google's 49 "conversions" in 2026-07-25..08-23, **46 are `vhk_form_start`** and only 3 are `VHK_lead`. Meta's 14 "results" are the *same* event via pixel (`conversions:offsite_conversion.fb_pixel_custom.VHK_form_start`). 94% of Google's bidding signal and 100% of Meta's is an interaction proxy. | `[CONFIG]`+`[DEFECT]` | `orchbranch/outputs/loop-runs/2026-08-28-fusiontables-report/report_data.json` → `ppc.conversion_actions` (nonzero: `fusiontables.hu - GA4 (web) vhk_form_start` = 46.0, `… VHK_lead` = 3.0; all other 18 actions = 0.0); `meta_ads.lead_generation.measurement_key`; `client.meta_ads.pixel_lead_event = "VHK_form_start"` |
| 2 | **A fabricated revenue axis exists today.** `conversion_value.current = 49.0` with `"unit": "huf"` — numerically identical to the conversion *count*. Every conversion carries a default value of 1, mislabelled as forint. The pipeline then computes `roas = 0.000532056…` and `cpa = 1879.50 HUF` from it. Any dashboard that renders ROAS from this field publishes a number with no revenue behind it. | `[DEFECT]` | `report_data.json` → `ppc.totals.conversion_value` = `{"current":49.0,"unit":"huf"}`, `ppc.totals.roas` = 5.32e-4, and the identical pair inside `ppc.campaigns[1] "2605-Fusion-Tables_Pmax"` |
| 3 | **Four different 30-day boundary definitions are in play at once.** `google_ads` and `meta_ads` = 2026-07-25..08-23; `crm_sheet` = 2026-07-**26**..08-**24**; `bigquery` = 2026-07-**24**..08-**22**; `work_evidence` = 2026-06-23..08-23 (62 days, `previous: null`). Matt's requested chart puts these on one x-axis. | `[DEFECT]` | `report_data.json` → `windows` (all five blocks) |
| 4 | **Meta's period-over-period comparison passes a 33% delivery threshold.** Previous window delivered on **11 of 30 days** (`ratio 0.3667`) yet `comparable: true` because `min_delivery_day_ratio` is `0.3333`. A "last 30 vs preceding 30" bar therefore shows Meta growth that is partly just more delivery days. | `[DEFECT]` | `report_data.json` → `meta_ads.lead_generation.comparability` |
| 5 | **The stall root cause is not the T1→T2 handoff.** The handoff succeeded *and T2 actually ran for 18 minutes*. Both the T2 child and the architecture parent died **mid-turn** within 74 seconds of each other with no error, no completion event, no rate-limit and no context exhaustion. Nothing was watching, because `~/ops/.codex-direct-mode` disables the entire AI-supervisor layer and no `supervisor.sh tick` exists in crontab, while `launch_child.py:200` stamps `"supervised": true` into every child session record. | `[DEFECT]` | See §6.4 for the full timeline and receipts |
| 6 | **T2's work survives, uncommitted, on disk.** Four audit artifacts totalling 313 lines are sitting untracked in `/home/matt/mirror/Fusion-ppc-children/t2/…/t2-measurement-audit/artifacts/`. GROUND-TRUTH's "T2 produced no artifacts" is true of git and false of the filesystem. | `[OBS]` | `git -C /home/matt/mirror/Fusion-ppc-children/t2 status --porcelain -uall` → 2 ` M`, 5 `??` |
| 7 | **The dependency gate never checks that a child worktree *contains* its inputs.** `launch_child.py` verifies the branch is not detached and nothing else. Every child was based on a branch line that never held `ads-control/` or the 2026-08-28 report package. (ORCH-CORR-1, §7) | `[DEFECT]` | `launch_child.py:159-162, 184-186`; `git merge-base --is-ancestor 638e7524 round-13-takeover` → false |
| 8 | **The dimensions Matt asked for first do not exist in the data schema.** `ppc` has 8 keys and none is an ad group, ad, creative, asset group, placement, device, audience, age or gender. `meta_ads` carries exactly one campaign row. The only demographic evidence anywhere is a set of **JPG screenshots** from a different window (`2026-06-29..07-28`). This is a collector gap, not a dashboard gap. | `[DEFECT]` | ORCH-CORR-2 §4A.3 M-34, M-35 |
| 9 | **Two live conversion actions define a purchase and a lead as a page load.** `Purchase (Page load https://fusiontables.hu/billiardasztal/billiardasztal)` and `Submit lead form (Page load https://fusiontables.hu/biliard/biliard)`. Both read 0.0 today, so they are dormant, not harmless: any traffic to those real URLs manufactures purchases that never happened. | `[DEFECT]` | §4A.1, §4A.3 M-36 |
| 10 | **The monthly report is not currently automatable.** All 7 `source_health` rows are `degraded`, `absent` or `disabled` — none is `ok`. Meta figures came from a manual authenticated Ads Manager export after API error 190/467, and the 7 QA rows were excluded by hand. | `[DEFECT]` | §4A.3 M-38 |
| 11 | **Exactly one subsystem has a real data contract; the rest have none.** The KPC evidence ledger carries a 1,524-row `field_ledger` with a per-field `evidence_state` (`retrieved` 704 / `verified_zero` 266 / `not_applicable` 488 / `unavailable` 3 / `derived` 63), `query_sha256` receipts and an exact reconciliation rule. `report_data.json` has only `unit` and `na`. | `[CONFIG]` | `orchbranch/ads-control/evidence-ledgers/20260824T111713-2aa95a23.json` → `field_ledger`, `evidence_state_counts`, `reconciliation.status = "exact"` |

---

## 1. The three-way capability ledger (the core of this lane)

**SPECIFIED ONLY** = a goal document names it. **IMPLEMENTED** = code or configuration exists.
**VERIFIED** = a readback proves data actually flows.

| Capability | Status | Proof / disproof |
| --- | --- | --- |
| Click-ID capture (`gclid`,`gbraid`,`wbraid`,`dclid`,`fbclid`,`msclkid`,`ttclid`,`li_fat_id`) | **IMPLEMENTED (repo), live presence UNVERIFIED** | `landing/billiardasztal-v2/snippet-footer.txt:85-94` writes all 8 keys to `localStorage['ftlp_attr_v1']`; deploy path `landing/billiardasztal-v2/pw/deploy_snip.py`. `current-live-3098.html` contains **0** occurrences of `ftlp_attr_v1` (page body only, footer snippet is separate), so live state is not provable from the repo |
| Hidden CF7 click-ID fields | **IMPLEMENTED** | `snippet-footer.txt:96-109` (`fillHiddenFields`, re-fired on capture-phase `submit`) |
| Form-SUCCESS semantics (not a button-click proxy) | **IMPLEMENTED, correct** | `snippet-footer.txt:113` binds `wpcf7mailsent` — CF7's server-accepted success event, not `click` or `submit` |
| Browser `generate_lead` push to GTM | **IMPLEMENTED** | `snippet-footer.txt:117` `dataLayer.push({event:'generate_lead', form_location: loc})` |
| GA4 property | **SPECIFIED ONLY** | `report_data.json` → `client.ga4.property_id = null`, `measurement_id = null`, `access = "not_verified"`; `source_health[ga4].status = "absent"` |
| Search Console | **SPECIFIED ONLY** | `client.search_console.access = "not_verified"`; `sources_declared_absent` includes `search_console` |
| BigQuery / GA4 export | **SPECIFIED ONLY** | `client.bigquery.enabled = false`, `reason_disabled = "No verified GA4 BigQuery export is configured."` |
| Paid-traffic source/medium allowlist | **SPECIFIED ONLY, EMPTY** | `client.paid_traffic.session_source_medium_allowlist = []`, `verified_against_ga4 = false` |
| Interaction-event allowlist | **SPECIFIED ONLY, EMPTY** | `client.interaction_events.allowlist = []`, `verified = false` |
| Conversion-event (key-event) allowlist | **SPECIFIED ONLY, EMPTY** | `client.conversion_events.allowlist = []`, `verified = false` |
| Meta Conversions API (server events) | **SPECIFIED ONLY** | Repo-wide `rg 'graph\.facebook\.com'`, `'test_event_code'`, `'_fbp'`, `'_fbc'`, `'fbq('` → **0 code hits**. `client.meta.note` says asset IDs must be verified separately |
| Browser↔server event dedupe (`eventID` / `event_id`) | **SPECIFIED ONLY** | Named in T2's draft taxonomy (`event-conversion-taxonomy.md:15,78`); no `event_id` anywhere in the deployed snippet |
| Offline conversion import (GCLID / enhanced conversions for leads) | **SPECIFIED ONLY** | `rg 'UploadClickConversion|conversion_action|enhanced conversions'` → 0 code hits. Designed only in `edit-manifest.md:EDIT-07` |
| Consent Mode / CMP | **NEITHER — absent** | `rg 'consent_mode|ad_storage|analytics_storage|cookiebot'` → 0 hits repo-wide. `snippet-footer.txt:89-94` writes click IDs to localStorage with **no consent gate at all**. T1 recorded no visible banner in a fresh context (`T1-F-audit-005:T-006`) |
| CRM source/campaign field | **NEITHER — absent** | `report_data.json` → `crm.source_column = null`, `crm.no_source_column_note`; `crm_sheet.source_column = null` |
| Qualified lead / quote / purchase / revenue capture | **SPECIFIED ONLY** | T2 `event-conversion-taxonomy.md` EVT-06..EVT-09 all read "Current count `UNAVAILABLE`"; `crm.totals` carries `leads` and nothing else |
| Google Ads API collector | **IMPLEMENTED (external)** | `client.google_ads.access = "api"`, `login_customer_id = 5179390693`. But `rg 'GoogleAdsClient|google\.ads\.googleads'` → **0 hits in this repo**: the collector lives outside it, credential path `/Users/agency/.config/riport-30d/…` is Mac-local |
| Meta Ads API collector | **BROKEN — browser fallback used** | `ledger.json` → `sources.meta_api = "blocked_190_467"`, `meta_authenticated_browser_export = "reconciled"` |
| KPC keyword evidence ledger | **VERIFIED** | `reconciliation.status = "exact"`, account totals 4,862 clicks / 119,254 impressions / 121,196,492,984 micros reconcile exactly to campaign-type totals; 15 `query_receipts` each with `query_sha256` |
| Decision dashboard (T3) | **NEITHER — never produced** | GROUND-TRUTH; `children/t3-decision-dashboard/acceptance-ledger.md` all rows NOT STARTED |

**Bottom line for Matt's requirement.** "Every step to purchase fully transparently measurable"
`[REQ]` is currently achievable for **stages 1–2 only** (impression, click) on both channels.
Stage 3 (landing-page visit) is structurally unmeasurable with no GA4. Stage 4 (form start) is
measurable but is being *mis-used* as the conversion. Stage 5 (submitted lead) is measurable in the
Sheet but **cannot be split by channel**. Stages 6–9 (qualified lead, quote, purchase, revenue)
have no capture mechanism at all. A funnel chart of Matt's requested shape can honestly show
**2 of 9 steps per channel today**, plus one unattributed total at step 5.

---

## 2. Measurement and data-contract ledger

Columns: **expected contract | specified | implemented | gap | decision risk if wrong | smallest
safe fix | decisive retest.**

### 2.1 Event definitions, sources, destinations, owners

| ID | Expected contract | Actually specified | Actually implemented | Gap | Decision risk if wrong | Smallest safe fix | Decisive retest |
| --- | --- | --- | --- | --- | --- | --- | --- |
| M-01 | One canonical submitted-lead event, named per GA4 recommended semantics (`generate_lead`) | T2 `event-conversion-taxonomy.md` EVT-05 specifies `generate_lead` + Meta `Lead` + CRM `lead_received` | `snippet-footer.txt:117` pushes `generate_lead`; but the *account* counts `VHK_lead` (3) and `vhk_form_start` (46) | Deployed container uses non-standard `VHK_*` names; the repo snippet's names never appear in the 30-day account data | Budget is allocated against form starts, so PMax learns to buy curiosity, not intent | Rename in GTM only after parity QA (`edit-manifest.md:EDIT-03`) | GTM Preview shows exactly one start and one submission per action; GA4 DebugView and Meta Test Events carry matching IDs |
| M-02 | Exactly one platform lead event per accepted submission | EVT-05 dedupe rule: `lead_id` canonical | **`[DEFECT]`** `snippet-footer.txt:117` **and** `:120` both fire on the same `wpcf7mailsent`: `generate_lead` *plus* a re-broadcast `wpcf7mailsent` with `contactFormId 3154`. GTM has tags on both | One success can produce 2 Google lead-like events + 1 Meta `Lead` | Double-counted leads inflate conversion volume; CPL is understated; smart bidding over-bids | Remove the second push, or gate GTM's Meta tag on the same trigger as GA4 | One authorized labelled test submission produces exactly one Sheet row, one notification and one lead event per destination |
| M-03 | Location is a **parameter**, not a separate conversion action | EVT-04/05: "Keep location as a parameter, not separate conversion names" (`edit-manifest.md:EDIT-05`) | **`[DEFECT]`** Three distinct Google Ads conversion actions exist: `fusiontables.hu - GA4 (web) hero_generate_lead`, `cta_generate_lead`, `chat_generate_lead` | One submission can create up to three named conversion actions | "Which form converts best" is answerable, but total lead count triples if all three are Primary | Consolidate to one action with `form_location` as a parameter | Google Ads readback shows exact action IDs, Primary/Secondary, Count setting and campaign-goal inclusion |
| M-04 | Form start is a **diagnostic**, Secondary, counted Once | EVT-04 target: `SECONDARY`, once per session/location | **`[DEFECT]`** `vhk_form_start` was Primary and counted **Every** (`T1-F-audit-005:G-003`) and supplied **46 of 49** conversions | Highest-impact defect in the system | Every budget, bid and creative decision is optimised toward an interaction that costs nothing to produce | Move `vhk_form_start` to Secondary **after** a deduplicated submitted-lead action exists (`EDIT-04`, ordered after `EDIT-02/03`) | Change preview shows the exact action IDs with new Primary/Secondary and Count values; PMax goal set contains only the lead action |
| M-05 | Owner recorded for every event and field | T2 taxonomy demands `owner` on all 9 stages | **`[DEFECT]`** Every one of EVT-01..EVT-09 records `Owner UNASSIGNED`; all 10 `edit-manifest.md` rows record `Owner UNASSIGNED` | No accountable person for any measurement object | Repairs cannot be executed or authorised; the backlog cannot start | Name one measurement owner and one sales owner before any `EDIT-*` runs | The manifest shows a real name in every owner cell |
| M-06 | Consent captured before any identifier is written | `T2-DOC-CONSENT-20260830` registers Google's default-then-update rule | **NOT IMPLEMENTED.** `snippet-footer.txt:89-94` writes 8 click IDs to `localStorage` unconditionally, before any consent signal | No consent gate, no `ad_storage`/`analytics_storage` mapping anywhere in the repo | Legal exposure on EU visitors; and offline uploads later built on this data inherit an unlawful basis | Add default-denied consent before config/event calls (`EDIT-09`), decided by the privacy owner | Four-state browser matrix (fresh / accept / reject / revisit) confirms defaults, updates, storage writes and outgoing collector requests |
| M-07 | QA method and receipt defined per event | Specified for all 9 stages in `event-conversion-taxonomy.md` | No QA receipt exists for any event | Nothing has been validated end to end | Any repair could silently regress | Run the per-event QA already written in the taxonomy | Each event has one dated receipt file |

### 2.2 Identity, dedupe, click-ID and UTM persistence

| ID | Expected contract | Actually specified | Actually implemented | Gap | Decision risk if wrong | Smallest safe fix | Decisive retest |
| --- | --- | --- | --- | --- | --- | --- | --- |
| M-08 | Browser and server copies of one Meta event share `eventID`/`event_id` | `event-conversion-taxonomy.md:15,78` (sourced to Meta's dedup doc, `T2-DOC-META-DEDUP-20260830`) | **No CAPI at all** (0 code hits for `graph.facebook.com`), therefore no dedupe key. `dataLayer.push` at `:117` carries no ID | Server-side measurement does not exist | When CAPI is added later without an ID, every lead is double-counted | Emit an opaque `lead_id` at success and use it as `eventID` from day one | Meta Test Events shows one deduplicated pair per test submission |
| M-09 | Click IDs persisted with a bounded lifetime and a capture timestamp | Taxonomy: "first-touch value, plus latest-touch values and landing URL" | **`[DEFECT]`** `snippet-footer.txt:86-94` stores into `localStorage['ftlp_attr_v1']` with **no TTL, no timestamp, no first/latest distinction**. Keys accumulate forever and are never cleared | A visitor who clicked a Google ad in June and returns organically in October still submits June's `gclid` | Offline conversion imports built on this would credit spend that did not cause the sale; Google's own guidance caps GCLID import at 90 days | Store `{value, first_seen, last_seen}` per key and drop entries older than the platform's import window | Two synthetic sessions 100 days apart: the second must not carry the first's click ID |
| M-10 | Click IDs preserved case-sensitively | `T2-DOC-GADS-GCLID-20260830` records Google's case-sensitivity requirement | Implemented correctly — `params.get(k)` preserves case verbatim | None | — | — | Assert stored value equals the URL value byte for byte |
| M-11 | UTM values captured **per visitor** | Taxonomy: store the 5 UTM fields as separate CRM fields | **`[DEFECT]`, two ways.** (a) `snippet-footer.txt:85` **does not capture UTM at all** — only click IDs and `landing_page`. (b) The one paid landing URL recorded by the collector already contains a hard-coded UTM string, and T1 observed a clean cache **HIT serving a prior visitor's UTM and GCLID** in CF7 form actions (`T1-F-audit-005:T-004`) | UTM is effectively a page constant, not a visitor attribute | Every "which campaign produced this lead" answer would be the same value regardless of truth — worse than having no attribution | Fix the cache rule first (`EDIT-01`), then add UTM keys to the capture list | Clean-cache HIT/MISS matrix with two distinct synthetic IDs proves neither visitor receives the other's values |
| M-12 | Persistence survives cross-page navigation and sessions | Implied by "across the funnel" comment at `snippet-footer.txt:83` | `localStorage` survives both, correctly | The snippet is on **one** landing page. A visitor entering elsewhere on `fusiontables.hu` never gets captured | Multi-product funnels (Strategy B) would have zero attribution | Deploy the same snippet site-wide via the footer, not per page | Land on a non-billiard page with `?gclid=TEST`, navigate to the form, confirm the hidden field carries `TEST` |
| M-13 | Cross-domain and cross-device limits stated | Not specified anywhere | Not implemented | No stated position | A phone-first, desktop-later buyer of a >1M HUF table is the *normal* case here; single-device attribution will systematically under-credit | State the limit explicitly on the dashboard; do not attempt device stitching | The dashboard renders an explicit "single-device only" caveat next to every attributed number |
| M-14 | One opaque journey key (`lead_id`) linking site → CRM → platform | `event-conversion-taxonomy.md:11` (Journey key), `edit-manifest.md:EDIT-02` | **Not implemented.** No `lead_id` exists in the snippet, the Sheet headers, or any platform event | The join key on which every downstream stage depends does not exist | Stages 6–9 remain permanently unmeasurable no matter how many dashboards are built | Generate `lead_id` server-side on accepted submission — this is the single highest-leverage change in the whole system | One test submission produces the same `lead_id` in the Sheet row, the GA4 event and the Meta event |

### 2.3 CRM, lead quality and repeat handling

| ID | Expected contract | Actually specified | Actually implemented | Gap | Decision risk if wrong | Smallest safe fix | Decisive retest |
| --- | --- | --- | --- | --- | --- | --- | --- |
| M-15 | CRM records the traffic source of each enquiry | `edit-manifest.md:EDIT-06` lists source/medium/campaign/content/term + all click IDs | **Absent.** `crm.source_column = null`; `crm_sheet.source_column = null`; `crm.no_source_column_note` states the breakdown is by *which form* recorded it | Channel-level CPL, CAC and ROAS are **UNAVAILABLE** and will stay so | Budget reallocation between Google and Meta — the literal question Matt asked — cannot be answered from data | Add the columns; the capture snippet already holds the values in `localStorage` | Schema readback shows the new columns; one synthetic row carries a non-empty source |
| M-16 | Duplicate-lead handling defined | EVT-05: "`lead_id` is canonical", `EDIT-06` adds `lead_id` | Not implemented. Dedupe today is "one non-empty row" | A person who enquires twice counts twice | Repeat enquirers inflate lead volume and deflate apparent CPL | Deduplicate on `lead_id`, keep both rows with a `duplicate_of` pointer | Two submissions from one browser produce two rows and one distinct `lead_id` lineage |
| M-17 | Repeat purchase handled distinctly from first purchase | **Not specified anywhere** — neither the T2 goal nor the draft taxonomy mentions repeat purchase | Not implemented | Silent gap | For a considered, high-ticket product this matters less, but LTV-based bidding would be wrong | Add `is_repeat` to the purchase contract when EVT-08 is built | — (not yet buildable) |
| M-18 | Test/QA submissions excluded durably | Recommendation `R-2026-07-02` (P1) | **Manual.** "Seven rows in the product form worksheet were verified as QA submissions and excluded" — a one-off human judgement inside a collector run | The exclusion is not reproducible; a re-run could include or exclude a different set | The headline enquiry count (13 vs 7) is not reproducible | Add a boolean `is_test` column set at submission time | Re-running the collector on the same window returns exactly 13 without human intervention |
| M-19 | Lead-quality feedback loop to the platforms | `EDIT-07` (Google offline `qualified_lead`), `EDIT-08` (Meta CRM events) | Not implemented; no qualification field exists to feed it | The loop cannot even be started | Platforms keep optimising for volume of the cheapest signal | Build EVT-06 (`qualify_lead`) in the CRM **before** attempting any upload | Provider diagnostics show accepted / rejected / duplicate rows and a CRM aggregate matching the upload aggregate for one matured cohort |
| M-20 | Match-quality visibility for offline uploads | `EDIT-07` requires "provider diagnostics show accepted, rejected, and duplicate rows" | Not implemented | No visibility because no upload exists | An upload with a 20% match rate looks like a 20% conversion collapse | Surface the diagnostics as a first-class dashboard tile from the first upload | Diagnostics panel readback for a matured cohort |

### 2.4 Attribution, windows and denominators

| ID | Expected contract | Actually specified | Actually implemented | Gap | Decision risk if wrong | Smallest safe fix | Decisive retest |
| --- | --- | --- | --- | --- | --- | --- | --- |
| M-21 | Google and Meta attribution assumptions stated and kept apart | `event-conversion-taxonomy.md:16` "Report platform attribution separately from first-touch and latest-touch CRM attribution" | Partially: `report_data.json` keeps `ppc.*` and `meta_ads.*` in separate blocks and never sums them | The **models** are never stated. Google PMax uses data-driven attribution with its own lookback; Meta's export uses its account default (7-day click / 1-day view unless changed) — neither is recorded anywhere | Adding 49 Google "conversions" to 14 Meta "results" would double-count the same person and mix two attribution models | Record each account's attribution setting and lookback as a field in the source register | Account-settings readback stored next to the totals it explains |
| M-22 | Attribution windows recorded per platform | Not specified in any goal document | Not recorded | Silent | A 7-day vs 28-day window difference alone can change a channel comparison by a factor of two | Same as M-21 | — |
| M-23 | "Last 30 completed days vs preceding 30 completed days" has **one** definition | `[REQ]` Matt: "the last 30 days vs the preceding 30 days". T3 goal repeats it (`goal-v1.md:85`) | **`[DEFECT]`** Four different boundaries co-exist: Ads/Meta `07-25..08-23` + prev `06-25..07-24`; CRM `07-26..08-24` + prev `06-26..07-25`; BigQuery `07-24..08-22`; work evidence `06-23..08-23` with `previous: null` | The CRM window is shifted **+1 day** at both edges relative to the ad windows | A funnel chart placing clicks and enquiries on one axis silently compares non-identical periods. With 13 enquiries, a one-day shift can move the number by ~8% | Pick one boundary (account timezone `Europe/Budapest`, last fully closed day) and derive all five from it; keep collector lag as a separate stated field | Every `windows.*` block prints identical `current` and `previous` pairs, or the dashboard refuses to draw a joint chart |
| M-24 | Period-over-period comparisons only between comparable periods | `meta_ads.lead_generation.comparability` implements a threshold | **`[DEFECT]`** `min_delivery_day_ratio = 0.3333`; previous window had delivery on **11/30 days** (`ratio 0.3667`) and was still marked `comparable: true` | A 33% floor is far too permissive | Meta spend rose 30,865 → 89,778 HUF; part of that "growth" is simply 19 more delivery days. A budget shift decision made on that comparison is unsound | Raise the floor, or always render the delivery-day ratio next to the delta | The dashboard shows "previous period: 11 of 30 days delivered" beside every Meta comparison |
| M-25 | Denominator consistency inside a funnel | `T1-CAVEATS` (`date-account-caveats.md:12`): "No subtraction, trend join, or denominator mixing is valid across [these windows]" | Enforced only in prose. Nothing in code prevents it | The rule exists but has no mechanism | A single spreadsheet formula reintroduces the error | Encode the rule: tag every metric with its window ID and refuse cross-window division | A unit test that attempts a cross-window ratio fails |
| M-26 | Cohort logic: a lead counted in the window it arrived, an outcome in the window it matured | EVT-06 "matured cohort based on qualification timestamp and original click" | Not implemented | Not yet reachable | Reporting a July quote against July spend when the enquiry was a June click overstates July efficiency | Define maturation lag before building EVT-06 | — |

### 2.5 Currency, tax, revenue and grain semantics

| ID | Expected contract | Actually specified | Actually implemented | Gap | Decision risk if wrong | Smallest safe fix | Decisive retest |
| --- | --- | --- | --- | --- | --- | --- | --- |
| M-27 | Currency stated and consistent | `client.google_ads.currency = "HUF"`; KPC ledger `currency = "HUF"`; `account_timezone: Europe/Budapest` | Consistent — HUF everywhere. Google Ads micros are kept exact (`spend_micros = 121196492984`) with an explicit rounding rule | None on currency itself | — | — | `reconciliation.rounding_rule` present |
| M-28 | Revenue is money, never a count | Implied by every "keep grains distinct" rule; EVT-09 says revenue "is a value attached to a transaction" | **`[DEFECT]`** `ppc.totals.conversion_value = {"current": 49.0, "unit": "huf"}` equals `conversions.current = 49.0` exactly. Same in the PMax campaign row. `roas` and `cpa` are computed from it | A count is being carried in a HUF-typed field and consumed by two derived money metrics | This is the single most likely way a false revenue claim reaches a client deck. ROAS 0.0005 either looks catastrophic or gets "corrected" into fiction | Set the field to `null` with `na: "no verified revenue"` until real values exist; suppress `roas` whenever `conversion_value` has no verified source | Dashboard renders ROAS as `UNAVAILABLE`, not 0.0005 |
| M-29 | VAT basis stated (gross vs net) for both spend and revenue | **Not specified anywhere** in any goal, config or artifact | Not implemented | Complete silence on VAT | Hungarian ÁFA is 27%. A gross-vs-net mismatch between spend and revenue moves ROAS by ~27% before any real effect | Declare the basis per metric in the metric dictionary | Metric dictionary shows a VAT basis for spend and for revenue |
| M-30 | Grains never joined | `date-account-caveats.md:23-29` defines 5 distinct grains; `T1-CAVEATS` governs | Enforced in the T1/T2 prose and honoured in `report_data.json`'s block separation | Prose only, no mechanism | See M-25 | Same fix as M-25 | Same |
| M-31 | Per-field evidence state on every number | Implemented **once**: KPC `field_ledger` with `evidence_state` ∈ {`retrieved`,`verified_zero`,`not_applicable`,`unavailable`,`derived`} (704/266/488/3/63) | `report_data.json` has only `unit` and `na` — no evidence state, no source resource, no query receipt | The good contract exists but was never generalised | A missing value and a verified zero look identical downstream; the KPC search zero (a **real** zero) and the GA4 blank (UNAVAILABLE) would be rendered the same way | Adopt the `field_ledger` shape for `report_data.json` | Every rendered number resolves to a `field_ledger` row with a state and a `query_sha256` |
| M-32 | Demographic and placement breakdowns with privacy thresholds | `[REQ]` Matt explicitly asked for demographic distribution and placement performance. T3 goal restates it (`goal-v1.md:91-93`) | **UNAVAILABLE — never produced.** `ppc.search_terms = []`, `ppc.change_events = []`, `seo.pages = []`, `seo.queries = []`, `cro.landing_pages = []`, `ppc.landing_pages` has exactly **1** row. No demographic block exists in `report_data.json` at all | The dimension Matt named first has no collector | The whole point of the request — reallocating budget by creative, placement and targeting — has no data behind it | See M-34 / M-35 — PMax reports asset-group and search-term-insight only; collect Meta breakdowns (age / gender / publisher_platform) on the export path that already works | A breakdown table exists with a stated minimum-cell threshold and suppressed small cells |

### 2.6 Reconciliation between source totals and dashboard totals

| Check | Result | Evidence |
| --- | --- | --- |
| KPC account totals vs campaign-type totals | **EXACT** — 4,862 clicks / 119,254 impressions / 121,196,492,984 micros both sides | `evidence-ledgers/20260824T111713-2aa95a23.json` → `reconciliation.status = "exact"` |
| KPC PMax share | 100% of delivery: `PERFORMANCE_MAX` carries all 4,862 clicks; `SEARCH` (2 campaigns) and `DISPLAY` (2 campaigns) are a **source-verified zero**, not missing | `campaign_type_totals` |
| Google 30-day report vs KPC 60-day snapshot | **Not reconcilable, correctly not attempted** — 105,555 vs 119,254 impressions over different windows | `report_data.json:windows.google_ads` vs `kpc_snapshot.window` |
| Conversion actions sum vs reported total | **EXACT** — 46.0 + 3.0 = 49.0 | `ppc.conversion_actions` |
| Platform conversions vs CRM enquiries | **49 (Google) + 14 (Meta) vs 13 (CRM)**. Not a discrepancy to reconcile — three different grains over two different windows. Any dashboard that renders these as one funnel is wrong | `ppc.totals.conversions`, `meta_ads.lead_generation.results`, `crm.totals.leads` |
| Which forms produced the 13 enquiries | **All 13 from the legacy sheet only.** `Főoldal Hero`, `Főoldal Ajánlatkérés`, `Kapcsolat Oldal`, `Termékoldal Ajánlatkérés` each returned **0** | `crm.sources` |
| Legacy sheet date column | **`[DEFECT]`** header is blank (`{"index":0,"header":""}`); the collector fell back to "authenticated column A" | `crm.date_column` |
| KPC advice outcome | 61 of 61 criteria = `wait_for_evidence`; 0 approve, 0 investigate; `executed: false` | `kpc_snapshot.advice_summary`; `advice_output-…json:summary` |

`[OBS]` The four new forms returning zero is consistent with two very different worlds — the forms
are not wired to the sheet, or they received no submissions. `[HYP]` Given that the three
`*_generate_lead` conversion actions also recorded **0.0** for the whole window while
`vhk_form_start` recorded 46, the more likely reading is that the new landing's event chain is not
live on the traffic-receiving page. This is the single cheapest thing to check and it is not
checkable from the repo alone.

### 2.7 Do the existing recommendations follow from verified evidence?

Six recommendations exist in `report_data.json:recommendations` (Hungarian, client-facing).

| ID | Priority | Claim | Grounded? |
| --- | --- | --- | --- |
| R-2026-07-01 | P1 | Audit the Google Ads conversion actions and separate their business meaning | **YES.** Directly supported by `ppc.conversion_actions` (46 form starts of 49) and `crm.totals.leads = 13` |
| R-2026-07-02 | P1 | Durably separate test submissions in the enquiry register | **YES.** Supported by the manual 7-row QA exclusion in `source_health[crm_sheet].caveat` |
| R-2026-07-03 | P1 | Separate Meta form-start from an actual enquiry | **YES.** Supported by `meta_ads.lead_generation.measurement_key` and `notes[1]` ("not mapped to a lead label") |
| R-2026-07-04 | P2 | GA4 access and per-page funnel restoration plan | **YES.** Supported by `client.ga4.access = "not_verified"` |
| R-2026-07-05 | P2 | Quality breakdown of the PMax traffic increase | **PARTIALLY.** The increase is verified (13,697 → 105,555 impressions). The *quality* breakdown it proposes is not currently collectable: `search_terms = []` and PMax exposes no keyword grain |
| R-2026-07-06 | P3 | Search Console property plan | **YES.** Supported by `sources_declared_absent` |

**Verdict:** all six trace to a cited figure; none invents a number; none claims revenue. `[OBS]`
This is the one part of the system where evidence discipline held. The failure mode is not
fabrication — it is that the recommendations are correct and **still unexecuted** (`kpc_snapshot.executed = false`).

---

## 3. Part B — Orchestration trigger audit

### 3.1 What the trigger mechanism actually is

`outputs/loop-runs/20260830-fusion-ppc-architecture-orchestration/launch_child.py` (291 lines) is
the *only* automation in the run. Read in full, it does exactly this:

1. Validates the title against the frozen contract (`:154-157`).
2. Validates the worktree has a `.git` and the prompt lives inside it (`:161-162`).
3. Refuses to launch if a tmux session with that slug exists (`:163-170`) — **the only idempotency guard in the system**.
4. Checks the launch prompt contains the codename/session marker (`:171-173`).
5. Boots the Codex daemon and enables remote control (`:175-181`).
6. Writes `~/ops/sessions/<slug>.json` including `"supervised": true` and `"waiting_for": <upstream>` (`:191-206`).
7. Writes a mission-control card via `status.sh` with `current = "WAITING_DEPENDENCY: <x>"` (`:207-213`).
8. Writes `~/ops/prompts/<slug>.run.sh` whose body is `exec codex … "$(cat prompt)"` (`:73-78`) and launches it in a detached tmux session (`:221`).
9. Captures the thread ID from `~/.codex/state_5.sqlite`, verifies the model/effort pin, decorates and pins the task (`:222-253`).

**There is no trigger code.** No watcher, no poller, no dependency resolver, no completion hook, no
retry, no timeout. `waiting_for` is written into a JSON file that nothing ever reads back. The
runner is a **one-shot, non-interactive `exec`**: when the Codex turn ends, the process exits and
the tmux session dies. The only thing that can advance a child is a human-or-parent message sent
into its thread over the app-server remote-control channel.

`[OBS]` Every one of the six specified triggers is therefore executed by **a parent agent noticing
a state change and typing a message**. There is no automated trigger in this system at all.

### 3.2 Trigger-by-trigger ledger

| Field | **TRG-01 · T1 acceptance releases T2** |
| --- | --- |
| Specified precondition | Parent-accepted child commit integrated into `round-13-takeover`; "a visible child in `WAITING_DEPENDENCY` is not a release receipt" (`dependency-handoff-ledger.md:3-4`) |
| Actual precondition as coded | **None in code.** Enforced only by the parent's own reasoning and by T2's launch prompt telling it to hold |
| Signal mechanism | A free-text `DEPENDENCY_RELEASE:` chat message, `01a05297-0211-7440-9f1f-eecb0653e82e`, delivered at `2026-08-30T12:13:44.656Z` (rollout line 20) |
| State mutation | Commit `a286b77` at 12:15:31Z touched `child-registry.md`, `t1-evidence-corpus/run-state.md`, `dependency-handoff-ledger.md`, parent `run-state.md` — **and not `children/t2-measurement-audit/run-state.md`** |
| User-visible feedback | Mission-control card `~/status/fusion-ppc-t2.json` moved to `running` at 12:23:31Z with step 1 `done`, step 2 `doing` |
| Idempotency | None. Re-sending the message would restart T2's verification from scratch |
| Duplicate protection | Only the tmux-session-exists check at launch (`launch_child.py:163-170`); nothing guards a duplicate release |
| Latency | T1 goal closed 12:10:20Z → release 12:13:44Z ≈ **3m24s**, entirely parent-typing time |
| Failure recovery | None |
| Next-step visibility | Parent ledgers only |
| Evidence path | `rollout-2026-08-30T11-02-45-01a05256-….jsonl` lines 20, 25; `dependency-handoff-ledger.md:8`; commit `a286b77` |
| **Verdict** | **FIRED — and it worked.** This is the one trigger that succeeded. `[DEFECT]` Its state mutation went to the supervisor's bookkeeping, not to the child's own `run-state.md`, which still reads `Status: WAITING_DEPENDENCY` / `Started: PENDING` at `children/t2-measurement-audit/run-state.md:3,33` |

| Field | **TRG-02 · T2 acceptance releases T3 / T4A / T4B** |
| --- | --- |
| Specified precondition | T2 parent-accepted and integrated (`dependency-handoff-ledger.md:9`) |
| Actual precondition as coded | None |
| Signal mechanism | Would have been a chat message. Never sent |
| State mutation | None. `dependency-handoff-ledger.md:9` = all `Pending` |
| User-visible feedback | **`[DEFECT]` Misleading.** `~/status/fusion-ppc-t3.json`, `-t4a`, `-t4b` all read `state: "running"` with `at` frozen at `2026-08-30T11:03`, over 7 hours stale, and no consumer of staleness exists |
| Idempotency / duplicate / latency / recovery | N/A — never fired |
| Next-step visibility | `child-registry.md:10-12` still says `WAITING_DEPENDENCY on T2` |
| Evidence path | `~/status/fusion-ppc-t{3,4a,4b}.json`; thread `updated_at_ms` for T3/T4A/T4B = 11:03:07 / 11:03:20 / 11:03:39 — **9 to 12 seconds after creation, then never again** |
| **Verdict** | **NEVER FIRED.** T3/T4A/T4B each ran one turn (25,261 tokens for T3), printed `WAITING_DEPENDENCY`, and their processes exited |

| Field | **TRG-03 · T4A + T4B release T7** |
| --- | --- |
| Specified precondition | Both strategies parent-accepted (`goal-v1.md:48`; `t7/run-state.md:23`) |
| Actual precondition as coded | None. `launch_child.py --waiting-for` accepts a free-text string; nothing parses "T4A and T4B" |
| Everything else | Never fired. `~/status/fusion-ppc-t7.json` stale `running` since 11:04:09Z |
| **Verdict** | **NEVER FIRED.** Additional `[DEFECT]`: a two-parent AND-join is expressed only as English prose inside a status card |

| Field | **TRG-04 · T3 + T4A + T4B + T7 release T5** |
| --- | --- |
| Specified precondition | Four-way AND (`t5/run-state.md:23`) |
| Actual precondition as coded | None |
| Everything else | Never fired. `~/status/fusion-ppc-t5.json` stale since 11:03:42Z |
| **Verdict** | **NEVER FIRED.** A four-way AND-join with no machine representation whatsoever |

| Field | **TRG-05 · All predecessors release T6** |
| --- | --- |
| Specified precondition | Seven-way AND (`t6/run-state.md:23`) |
| Actual precondition as coded | None |
| Additional blocker | `access-ledger.md:14` records T6 as `PARTIAL` — "current publisher correctly blocks on 284 missing mirrored pages, so mirror restoration is required before authorized publish". A known, pre-identified blocker that was never scheduled as work |
| **Verdict** | **NEVER FIRED**, and would have failed on a known access gap even if it had |

| Field | **TRG-06 · Parent acceptance closes a child goal and updates naming/state** |
| --- | --- |
| Specified precondition | Parent independently revalidates, cherry-picks, then instructs the child to close its runtime goal |
| Actual precondition as coded | None; the goal-close is a message |
| State mutation | For T1 it worked completely: title gained `↺✅` (`child-registry.md:8`), status `PARENT_ACCEPTED`, cherry-pick `b1013882`, close receipt `01a05293-afd2-7dd0-b347-f395156f74c9`, final usage 555,380 tokens |
| Idempotency | The `↺✅` rename is idempotent in effect; the cherry-pick is not |
| **Verdict** | **FIRED ONCE, correctly.** The most rigorous single step in the run — and entirely manual |

| Field | **TRG-07 · Liveness / stall detection (implied, never specified)** |
| --- | --- |
| Specified precondition | Not specified in `goal-v1.md` at all — the architecture contract has **no** liveness requirement |
| Actual precondition as coded | `launch_child.py:200` writes `"supervised": true`. `~/ops/supervisor.sh:10` reads `[ -f "$OPS/.codex-direct-mode" ] && exit 0`. That file exists (`~/ops/.codex-direct-mode`, mode `-r--r--r--`, dated 2026-08-20) and states: *"…dispatcher ticks, and AI supervisor bootstrap are disabled."* Independently, `crontab -l` contains **no** `supervisor.sh tick` line at all |
| Signal mechanism | `status.sh` stamps an `at` heartbeat on every write (`status.sh:heartbeat()`); nothing consumes it for these slugs |
| Evidence path | No `~/ops/supervisors-fusion-ppc-t*-bootstrap.log` and no `~/ops/fusion-ppc-t*-resume.log` exists, while 20+ such files exist for other run slugs (`coldemail`, `fusion-r12-*`, `proposal-studio`, …) |
| **Verdict** | **`[DEFECT]` — the single point of failure.** Every child was recorded as supervised and none was |

### 3.3 Which triggers fire only because a parent process manually notices a state change?

**All six.** Classification:

| Instance | Classification | Reasoning |
| --- | --- | --- |
| Parent independently revalidates T1's 11 frozen rows, 159 derived rows, 131 git blobs, 11 artifact hashes before accepting (`dependency-handoff-ledger.md:8`) | **KEEP — intentional safety control** | This is the acceptance gate itself. Automating "is this evidence corpus correct" would defeat the contract. It caught nothing here only because T1 was genuinely clean |
| Parent cherry-picks the accepted child commit onto the integration branch | **KEEP — intentional safety control** | An irreversible-ish integration step under a frozen goal. Human/parent judgement is appropriate |
| Parent *notices* that T1 reached `READY_FOR_PARENT_ACCEPTANCE` | **DEBT** | The child already writes a machine-readable state (`~/status/fusion-ppc-t1.meta.json` step 4 `done`, main card `state: "done"`). Nothing polls it. Pure operational debt |
| Parent *types* `DEPENDENCY_RELEASE` with hand-copied SHAs | **DEBT** | Every value in that message (`74f7aef…`, `b101388…`, `f83db2cf…`, the goal SHA) already exists in files. Hand-copying four hashes into a chat message is transcription risk with no safety benefit |
| Parent must *notice* a child stopped | **DEBT — and the fatal one** | No mechanism at all. See §3.4 |
| Parent must update the child's own `run-state.md` after a release | **DEBT** | It didn't happen. `children/t2-measurement-audit/run-state.md` still reads `WAITING_DEPENDENCY` / `Started: PENDING` while the child had in fact been running for 18 minutes. The one file a recovering agent would read first is the one file that was never updated |
| Parent must decide the AND-joins (T4A∧T4B→T7, and the 4-way and 7-way joins) | **DEBT** | These are pure boolean predicates over accepted-commit facts. They are the most mechanisable part of the whole design and the least mechanised |

**Score: 2 of 7 supervision points are worth keeping. 5 are operational debt.**

### 3.4 Why T2 never finished, and why the failure was silent

`[OBS]` **The mission's premise needs one correction: T2 did start.** GROUND-TRUTH reads T2's
repo `run-state.md` (`WAITING_DEPENDENCY`, `Started: PENDING`) and concludes T2 never started. The
live evidence says otherwise.

Reconstructed timeline, all receipts:

| Time (UTC) | Event | Receipt |
| --- | --- | --- |
| 11:02:45 | T2 launched, one-shot `exec codex` in tmux `fusion-ppc-t2` | `~/ops/sessions/fusion-ppc-t2.json`, `session_id 01a05256-0aec-…` |
| 11:02:57 | T2 returns `WAITING_DEPENDENCY`, process idles | rollout line 15 |
| 12:13:44 | Parent sends `DEPENDENCY_RELEASE` with T1 receipts | rollout line 20 |
| 12:14:59 | T2 verifies goal hash, corpus hash, all 11 artifact hashes, git ancestry — **all pass** | rollout line 65 |
| 12:15:12 | T2 creates its runtime goal and starts the access-scout wave | rollout line 77 |
| 12:17–12:29 | T2 completes Meta, CRM, Google, form-sheet, documentation and privacy scouts; writes 4 artifact files | rollout lines 166–344; files on disk |
| 12:23:31 | T2's last mission-control heartbeat | `~/status/fusion-ppc-t2.json` mtime |
| **12:32:31.773** | T2's **last event**: an `AgentMessage` with `phase: "commentary"`, turn `01a05297-0214-…`, saying *"I'm finishing the prioritized repair order, channel recommendations, blocker register, and downstream handoff index next."* Then the file ends | rollout line 353 of 354 — **no `turn_completed`, no error, no abort** |
| **12:33:45.930** | Architecture parent's **last event**: also an `AgentMessage`, `phase: "commentary"`, turn `01a0525f-0800-…`, saying *"I'm checking the remaining backlog, gap register, handoff receipt, and pushed-branch proof next."* Then the file ends | rollout line 2747 of 2748 |
| 12:40 onward | Unrelated ColdEmail and client-control-panel Codex threads continue normally | thread `updated_at_ms` 12:42, 12:47, 12:48 |

**What it was not.** Not a rate limit: the final `token_count` on both threads reports
`primary.used_percent: 21.0`, `rate_limit_reached_type: null`, `spend_control_reached: null`,
`plan_type: "pro"`. Not context exhaustion: `model_context_window` is 258,400 and the final
`last_token_usage.input_tokens` was 36,238 (parent) and 125,748 (T2). Not a reboot: `uptime -s`
= 2026-08-21, nine days before. Not a Codex-daemon crash: other threads kept working after 12:33.

**What it was.** `[HYP, high confidence]` Two independent Codex processes on a box now showing
`load average: 8.02 28.73 21.97` and 6.1 GiB of 7.6 GiB used died mid-turn 74 seconds apart with
no application-level error. That signature is an external process termination — most plausibly
memory pressure or a tmux/session teardown. I could not confirm an OOM record: `dmesg` requires
root here and `journalctl` for 12:25–12:45 shows only an unrelated `contractor2500.service`
failure at 12:34:59.

**The exact single point of failure — and it is not the cause, it is the silence.**

> `~/ops/supervisor.sh:10` — `[ -f "$OPS/.codex-direct-mode" ] && exit 0` — combined with
> `launch_child.py:200` writing `"supervised": true`.

Every child was *labelled* supervised and *none* was watched. `~/ops/.codex-direct-mode` has
existed since 2026-08-20 and explicitly disables "dispatcher ticks, and AI supervisor bootstrap".
Independently, `crontab -l` contains no `supervisor.sh tick` entry, so even deleting that file
would not have started a watcher. The launcher never checks either condition. The result: five
mission-control cards have said `state: "running"` since 11:03 and one since 12:23, and **nothing
in the system reads an `at` timestamp and asks whether it is old**. The parent's own
`run-state.md:3` still says `Status: ACTIVE`.

Three compounding design faults made the silence total:

1. **The runner is a one-shot.** `launch_child.py:73-78` builds `exec codex … "$(cat prompt)"`. There is no `--resume`, no supervising loop, no restart-on-exit. Process death equals task death, and tmux session death erases the only visible trace.
2. **The child's own state file is not the source of truth.** The release commit `a286b77` updated four parent-side files and skipped `children/t2-measurement-audit/run-state.md`. A recovering agent reading that file today is told T2 never started — which is exactly the wrong conclusion, and exactly the conclusion GROUND-TRUTH reached.
3. **Work was held uncommitted for 18 minutes.** T2 itself narrated this at 12:20:59Z: *"my child edits still uncommitted and scoped."* The frozen goal only requires a commit at `T2-12`, the very last acceptance row. So the design deliberately concentrates all durability risk at the end of the run. The files survived here only because the worktree was never cleaned — that is luck, not design.

**Smallest change that would have made the stall visible or self-healing.**

`[REC]` Ranked by cost, cheapest first:

1. **One cron line** (≈1 line, zero new code): re-enable the existing, already-written watcher —
   `*/5 * * * * /home/matt/ops/supervisor.sh tick`. `supervisor.sh` already implements
   `SUPERVISOR_STALE_MIN=20` staleness detection, `SUPERVISOR_COOLDOWN_MIN`, and
   `SUPERVISOR_MAX_RECOVERIES=3`. The capability was built, then switched off by a
   `.codex-direct-mode` flag set for an unrelated reason ten days earlier. **This alone would have
   surfaced both dead threads within 20 minutes.**
2. **One guard in the launcher** (≈3 lines): if `~/ops/.codex-direct-mode` exists, refuse to write
   `"supervised": true` — or refuse to launch a dependency-gated child at all. A launcher that
   promises supervision it cannot deliver is the lie at the centre of this failure.
3. **Commit-as-you-go** (≈1 line in each child prompt): require a WIP commit on the child branch
   after each artifact, not only at `T2-12`. T2's 18 minutes of correct work would then be in git
   rather than surviving by accident in an uncleaned worktree.
4. **Write the release into the child's own `run-state.md`** — the release commit must be authored
   by the child, or must include the child's state file, so that the file a recovering agent reads
   first is never a lie.

`[REC]` Do **not** build a new dependency engine. The AND-joins matter, but the run did not die of
a missing scheduler; it died of a disabled watchdog. Fix the watchdog first.

---

## 4. ORCH-CORR-1 — the base-branch precondition defect

Independently re-verified by me at `/home/matt/mirror/Fusion`:

- `git merge-base --is-ancestor 638e7524 round-13-takeover` → **false**.
- `git branch -a --contains 638e7524` → only `remotes/origin/codex/fusion-ppc-architecture-orchestrator`.
- `git merge-base 638e7524 round-13-takeover` → `08f52cd31cdcc64bad5915839bc5ea39865afd1f`.
- **Three** commits carry the identical message *"plan: freeze Fusion PPC architecture delegation"*, all with author date `2026-08-30T12:25:09+02:00`: `638e7524` (parent `310758d`, orchestrator branch), `975e6c33` (parent `7619997`, the branch every child worktree was cut from — see T1 `run-state.md:20` "Parent-accepted base commit SHA: `975e6c33…`"), and `ce93cef7` (parent `f427516`, `round-13-takeover`).
- 41 files exist on `638e7524` and not on `round-13-takeover`, including all of `ads-control/`, `deliverables/fusion-meta-ads-review-2026-08-24/`, 18 of the 19 files in `outputs/loop-runs/2026-08-28-fusiontables-report/` (only `ledger.json` survives), and all of `outputs/loop-runs/20260824T085117Z-fusion-kpc-report-recovery/`.

**(a) Did the dependency gate ever verify that a child's worktree CONTAINS its input artifacts?**

**No.** The gate verifies *provenance of an upstream commit*, never *presence of an input file*.
`dependency-handoff-ledger.md:14` states the standard exactly: *"`PASS` requires every cell in a
transition row to cite an exact immutable receipt."* Receipts are SHAs and hashes — statements
about what was produced, never about what is reachable from the child's working directory.

`[OBS]` T1 survived this because its inputs were Codex session JSONLs and Notion pages, plus git
blobs it read out of immutable objects (`dependency-handoff-ledger.md:8` — "131 Git blobs"). A git
blob is reachable from any branch by hash. That is a workaround the child improvised, not a
property the design guaranteed. T3 (dashboard) and T5 (report) need `report_data.json`,
`evidence.json` and the `ads-control/` ledgers **as files in a working tree**. T3 would have started
its first turn on a branch where `ads-control/` does not exist and `outputs/loop-runs/2026-08-28-fusiontables-report/`
contains one 5 KB file. It would have discovered this only after burning its access-scout wave.

**(b) Is the base-branch choice checked anywhere in `launch_child.py` or the ledgers?**

**No.** The entire worktree validation is:

```
launch_child.py:161-162   if not (workdir / ".git").exists() or not prompt.is_file() or workdir not in prompt.parents:
                              raise RuntimeError("workdir or prompt is not a valid isolated Fusion worktree")
launch_child.py:184-186   branch = run("git", "-C", str(workdir), "branch", "--show-current")
                          if not branch:
                              raise RuntimeError("child worktree is detached")
```

That is: *has a `.git`*, *prompt is inside it*, *not detached*. The branch **name** is recorded
into the session JSON and never validated. No commit is checked for ancestry. No file is checked
for existence. `access-ledger.md:6` claims the corresponding row PASS with the evidence *"isolated
worktree and child branch created from parent commit `ede99a5`"* — which is true and entirely
beside the point, because `ede99a5` is on the line that lacks the data.

**(c) Does any acceptance row assert input availability rather than output hashes?**

**No.** Across all eight children, every acceptance row I read asserts an *output* property.
T2's `T2-01` comes closest — "Accepted-input and evidence boundary" — but its completion proof is
"T1 parent-acceptance state, commit SHA, corpus-index hash, complete list of T1 sources used", all
of which are hashes of things T1 produced. `T2-D03` in the access ledger asks the child to "Verify
`/home/matt/mirror/Fusion-ppc-children/t2`, branch `codex/fusion-ppc-t2-audit`, parent-accepted base
SHA containing the accepted T1 cherry-pick" — again an ancestry check on the *upstream child's*
commit, never a check that the *pre-existing repository evidence* the goal depends on is present.

**Exact missing precondition check:** *no child goal declares its required input paths, and no
launcher, ledger or acceptance row asserts that those paths resolve inside the child's worktree at
the frozen commit.*

**Smallest change that would have caught it at freeze time.** `[REC]` Add a `required_inputs:`
list of repo-relative paths to each child's frozen goal, and one loop in `launch_child.py` before
the tmux launch:

```
for p in required_inputs:
    if not (workdir / p).exists():
        raise RuntimeError(f"missing required input in child worktree: {p}")
```

Roughly six lines. T3's goal would name `ads-control/`, `outputs/loop-runs/2026-08-28-fusiontables-report/report_data.json`
and `evidence.json`; the launch would have failed loudly at **11:02**, before any child ran, instead
of silently producing a run whose downstream tasks were unbuildable from the start. This is a
strictly better check than a branch-name assertion, because it survives rebases and re-freezes —
exactly the operation that created the three duplicate freeze commits.

`[OBS]` Note the compounding: had the run not died at 12:33, T3 would have failed anyway. The
stall masked a second, independent fatal defect.

---

## 4A. ORCH-CORR-2 — verified decomposition of the measurement signal

Every claim below was re-verified by me directly against
`/tmp/claude-1000/-home-matt-mirror-Fusion/3429ff84-e206-4a61-8d4c-770f9bcc2c74/scratchpad/orchbranch/outputs/loop-runs/2026-08-28-fusiontables-report/report_data.json`.
All five points of ORCH-CORR-2 **confirmed**. Three further defects surfaced that the correction
did not name; they are M-35, M-36 and M-38.

### 4A.1 The 49 conversions, action by action (all 20 rows, current window 2026-07-25..08-23)

| Conversion action | conversions.current | conversions_value.current (`unit: "huf"`) |
| --- | --- | --- |
| `fusiontables.hu - GA4 (web) vhk_form_start` | **46.0** | **46.0** |
| `fusiontables.hu - GA4 (web) VHK_lead` | **3.0** | **3.0** |
| `fusiontables.hu - GA4 (web) hero_generate_lead` | 0.0 | 0.0 |
| `fusiontables.hu - GA4 (web) cta_generate_lead` | 0.0 | 0.0 |
| `fusiontables.hu - GA4 (web) chat_generate_lead` | 0.0 | 0.0 |
| `fusiontables.hu - GA4 (web) phone_click` | 0.0 | 0.0 |
| `fusiontables.hu - GA4 (web) purchase` | 0.0 | 0.0 |
| `Purchase (Page load https://fusiontables.hu/billiardasztal/billiardasztal)` | 0.0 | 0.0 |
| `Submit lead form (Page load https://fusiontables.hu/biliard/biliard)` | 0.0 | 0.0 |
| `Call (+36309149330)` | 0.0 | 0.0 |
| `Call (+36309149330) (1)` | 0.0 | 0.0 |
| `Google Forwarding Number` | 0.0 | 0.0 |
| `Galéria után érdeklődő felhasználók (Minden webhelyadat)` | 0.0 | 0.0 |
| `Kapcsolati aloldal megtekintések (Minden webhelyadat)` | 0.0 | 0.0 |
| `Legalább 1 percet böngésző felhasználók (Minden webhelyadat)` | 0.0 | 0.0 |
| `Legalább 2 percet böngésző felhasználók (Minden webhelyadat)` | 0.0 | 0.0 |
| `Legalább 2 aloldalt megtekintő felhasználók (Minden webhelyadat)` | 0.0 | 0.0 |
| `Tervezőre kattintás (Minden webhelyadat)` | 0.0 | 0.0 |
| `YouTube channel subscriptions` | 0.0 | 0.0 |
| `YouTube follow-on views` | 0.0 | 0.0 |

`46.0 + 3.0 = 49.0` = `ppc.totals.conversions.current`. **Reconciles exactly.**

`[OBS]` Two facts jump out beyond the 94%-form-start headline. First, **every one of the three
`*_generate_lead` actions that the repo snippet is built to fire recorded 0.0 across 30 days**,
while `vhk_form_start` recorded 46 — the deployed measurement and the repo's measurement code are
not the same system. Second, **`conversions_value` carries the count with `unit: "huf"` at the
per-action level too**, not merely in the totals: `VHK_lead` is reported as 3 HUF of value and
`vhk_form_start` as 46 HUF. The fabricated revenue axis (M-28) is baked into the account's
conversion-action configuration, not introduced by the reporting layer.

### 4A.2 The budget consequence, stated explicitly

`[OBS]` 100% of Google delivery sits in `2605-Fusion-Tables_Pmax` (`campaign_id 23822598651`,
`PERFORMANCE_MAX`, `ENABLED`): 105,555 impressions, 4,410 clicks, 92,095.56 HUF, 49 conversions.
The other four campaigns — `2605-Fusion-Tables_Search` (SEARCH), `Keresési - KMO` (SEARCH),
`Display - KMO` (DISPLAY), `Remarketing` (DISPLAY) — are all `PAUSED` at 0/0/0.0/0.0. That is the
origin of the "Search delivery is a source-verified zero" fact, and it is a **verified zero, not
UNAVAILABLE**.

`[DEFECT]` The consequence chain is mechanical and it is the most expensive thing in this system:

1. Performance Max is a fully automated, smart-bidding channel. It has no manual bids, no keyword control, and only limited placement control. **Its conversion goal set is the only steering wheel that exists.**
2. That goal set is dominated by `vhk_form_start` at a 15:1 ratio over `VHK_lead`.
3. Smart bidding therefore optimises delivery, creative and audience selection toward *people who begin typing in a form*, not people who submit one. A form start costs the advertiser nothing to produce and is trivially generated by accidental taps, curiosity, and bounce traffic.
4. Observed unit economics under that steering: avg CPC fell from **64.38 HUF to 20.88 HUF** while impressions rose **7.7×** (13,697 → 105,555) and clicks rose **9.8×** (452 → 4,410) on a **3.2×** budget increase (29,101 → 92,096 HUF). `[HYP, high confidence]` That is the classic signature of an algorithm finding progressively cheaper, lower-intent inventory because the signal it is being paid on is cheap to satisfy.
5. Meta is steered by **the identical event**: `lead_generation.measurement_key = "conversions:offsite_conversion.fb_pixel_custom.VHK_form_start"`, 14 results at 6,412.71 HUF each. `limitations[0]` states it outright: *"a form-start signal and not a verified enquiry."*
6. Ground truth over the same period: **13 recorded enquiries**, from a register that cannot attribute a single one to either platform.

`[REC]` The budget consequence is that **both channels' entire optimisation is currently pointed at
a proxy**, and combined paid spend for the window is 92,096 + 89,778 = **181,874 HUF** steered by
it. Nothing in the funnel below "form start" is feeding back. Fixing M-04 (demote
`vhk_form_start` to Secondary once a deduplicated submitted-lead action exists) is the single
highest-value change in Part A, and it must be ordered **after** M-14 (`lead_id`) and M-02
(double-push removal) or PMax will be left with no goal at all.

### 4A.3 Contract rows added by ORCH-CORR-2

| ID | Expected contract | Actually specified | Actually implemented | Gap | Decision risk if wrong | Smallest safe fix | Decisive retest |
| --- | --- | --- | --- | --- | --- | --- | --- |
| M-33 | Bidding optimises on a business outcome, not an interaction proxy | T2 `event-conversion-taxonomy.md` hierarchy: Diagnostic / Lead / Commercial, with form-start explicitly Diagnostic | **`[DEFECT]`** 46 of 49 Google conversions and 14 of 14 Meta results are `VHK_form_start`. Specified correctly, implemented backwards | The whole optimisation target is a proxy | 181,874 HUF of window spend is steered toward the cheapest possible signal; PMax has already moved CPC from 64 to 21 HUF chasing it | Demote to Secondary **after** a deduplicated lead action exists (ordered M-14 → M-02 → M-04 → M-33) | Google Ads readback: PMax goal set contains only the lead action; 14 days later, CPC and click volume have moved back toward the pre-scale range while lead count holds |
| M-34 | Sub-campaign dimensions available for the comparison Matt asked for | `[REQ]` Matt asked for creative, placement, targeting and demographic comparison. T3 goal restates it (`goal-v1.md:91-93`) | **`[DEFECT]` — the fields do not exist in the schema.** `ppc` has exactly 8 keys: `campaigns`, `change_events`, `conversion_actions`, `conversion_tracking_break`, `landing_pages`, `notes`, `search_terms`, `totals`. There is **no** ad-group, ad, creative, asset-group, placement, device, audience, age or gender object anywhere. `meta_ads` has 5 keys and exactly **1** campaign row carrying only spend/impressions/clicks | The pipeline stops dead at campaign level on both channels | **This is a data-contract gap, not a dashboard gap.** A dashboard built on this file *cannot* render Matt's comparison — no amount of front-end work creates the fields | Extend the collector: Google Ads `asset_group` + `campaign_search_term_insight` for PMax; Meta `breakdowns=age,gender,publisher_platform,platform_position` on the export path that already works | `report_data.json` contains a non-empty breakdown array with a stated minimum-cell suppression threshold |
| M-35 | Demographic evidence is structured data, not an image | Not specified anywhere as a data requirement | **`[DEFECT]`** The only demographic and placement evidence in the entire corpus is **JPG screenshots**: `deliverables/fusion-ppc-2026-07-30/screenshots/raw/meta-age-*.jpg`, `meta-gender-*.jpg`, `meta-platform-breakdown-*.jpg` (and copies under `deliverables/fusion-ppc-council-2026-07-31/`), all for window `2026-06-29..2026-07-28` — a **different** window from every other figure in the report | Manually captured, once, for a stale window, unqueryable, unreconcilable, not refreshable | A monthly deliverable whose most-requested dimension depends on a human re-screenshotting an Ads Manager panel will silently go stale, and cannot be joined to any other metric | Same collector change as M-34; keep the screenshots as an audit receipt, never as the data source | The demographic table in the dashboard resolves to a JSON field with a query receipt, and its window matches `windows.meta_ads` exactly |
| M-36 | A "Purchase" conversion action means a completed purchase | Implied by EVT-08; never written as a rule | **`[DEFECT]`, live account objects.** Two conversion actions are defined as **page loads**: `Purchase (Page load https://fusiontables.hu/billiardasztal/billiardasztal)` and `Submit lead form (Page load https://fusiontables.hu/biliard/biliard)`. Both currently 0.0 | A page view is configured as a purchase | **Dormant, not harmless.** Both URLs are real paths on the live site. Any traffic reaching them — a sitemap change, an internal link, a crawler, a future campaign — instantly manufactures "purchases" and "leads" that never happened, with a HUF value attached. This would be the most damaging possible failure for a client report | Remove or rename both, or gate them behind a real transaction event. Zero risk to current data because both read 0.0 | Change history shows the action removed or redefined; a synthetic visit to both URLs produces no conversion |
| M-37 | Call tracking state is known and declared | `ads-control/config.yaml` has a `conversion_tracking:` block with `call_tracking_live_from`, `reliability`, `note` | **`[DEFECT]` all three are literal unfilled TODO strings** (`'TODO: ISO date...'`, `'TODO: full \| partial \| none'`, `'TODO: one paragraph...'`). Consistently, `advice-bundles/…json → tracking_context.call_tracking = "unknown"`, and all three call-related conversion actions (`Call (+36309149330)`, its duplicate `(1)`, `Google Forwarding Number`) read 0.0 | The system knows it does not know, and the field to record it was created and never filled | For a >1M HUF considered purchase, phone is a primary channel. If calls convert and are untracked, every channel's true CPL is overstated and the KPC engine's 61 `wait_for_evidence` verdicts stay permanently stuck | Fill the three fields from the account's actual call-tracking state — a 5-minute human answer that unblocks a whole advice engine | `config.yaml` has no `TODO:` substring; `tracking_context.call_tracking` is no longer `"unknown"` |
| M-38 | The monthly refresh is reproducible without a human | `ledger.json` treats the run as complete (`status: "complete"`, `loop_closeout.achieved: true`, `unmet_conditions: []`) | **`[DEFECT]`** `source_health` is 7 rows and **not one is `ok`**: `google_ads` degraded (change_event history capped at 30 days by the API), `meta_ads` degraded (**API session invalid, error 190/467** — figures came from a manual authenticated Ads Manager export), `search_console` absent, `ga4` absent, `bigquery` disabled, `crm_sheet` degraded (blank date header, 7 QA rows excluded **by hand**), `work_evidence` degraded | Two of the report's load-bearing inputs — Meta figures and the CRM QA exclusion — required human action inside an authenticated browser session | `[REQ]` Matt asked for a monthly report. **It is not currently automatable.** Next month it either does not run, or it runs and silently omits Meta | Re-authorise the Meta system-user token, and replace the hand QA exclusion with an `is_test` column (M-18). Neither needs new architecture | A collector run with no browser session present returns `meta_ads.status = "ok"` and the same 13-lead CRM total |

### 4A.4 Attribution is structurally blocked at the CRM (confirmed)

`[OBS]` `crm.source_column = null`. `crm.no_source_column_note`: *"this spreadsheet has no source
or campaign column, so enquiries cannot be attributed to a traffic source."* All 13 enquiries came
from the single worksheet `Régi Billiárd Ajánlatkérések`; `Főoldal Hero`, `Főoldal Ajánlatkérés`,
`Kapcsolat Oldal` and `Termékoldal Ajánlatkérés` each returned 0.

`[DEFECT]` This is the **hard stop** in the whole chain, and it is worth stating plainly against
Matt's requirement. Everything upstream can be repaired — better events, better dedupe, better
windows — and the question *"should I move budget from Google to Meta?"* will still be
unanswerable, because there is no column in which a channel could be written and no key on which a
join could be performed. M-14 (`lead_id`) and M-15 (source columns) are not two improvements among
many; they are the **precondition for every other measurement repair having any business value**.
Ordered correctly, the repair sequence is: **M-15 + M-14 (make attribution possible) → M-02
(stop double-counting) → M-03 (consolidate actions) → M-04 + M-33 (repoint bidding) → M-19/M-20
(feed outcomes back) → M-34/M-35 (add the dimensions Matt asked for).**

---

## 5. UNAVAILABLE register (never zero)

| Item | State | Note |
| --- | --- | --- |
| GA4 property, measurement ID, all landing-page funnel metrics | UNAVAILABLE | `client.ga4.access = "not_verified"`; declared absent |
| Search Console | UNAVAILABLE | declared absent in client config |
| BigQuery / 20-second engagement facts | UNAVAILABLE | `bigquery.enabled = false`; reported as N/A by design, correctly |
| Qualified leads, accepted quotes, purchases, sales, revenue | UNAVAILABLE | No capture mechanism exists at any layer |
| Channel attribution of the 13 enquiries | UNAVAILABLE | `crm.source_column = null` |
| Demographic distribution | UNAVAILABLE as data — exists only as stale JPGs | No demographic object in `report_data.json`; only `deliverables/fusion-ppc-2026-07-30/screenshots/raw/meta-{age,gender,platform}-*.jpg` for window `2026-06-29..07-28` (M-35) |
| Creative / placement / targeting performance breakdown | UNAVAILABLE — the fields do not exist in the schema | `[REQ]` Matt asked for this first. `ppc` has 8 keys, none dimensional; `meta_ads.campaigns` has 1 row (M-34) |
| Call-tracking state (live-from date, reliability) | UNAVAILABLE — field created, never filled | `ads-control/config.yaml` `conversion_tracking:` all three values are literal `TODO:` strings; `tracking_context.call_tracking = "unknown"` (M-37) |
| Automatable monthly refresh | UNAVAILABLE | No `source_health` row is `ok`; Meta needed a manual browser export after 190/467 (M-38) |
| Search terms, Ads change events (pre-2026-07-26) | UNAVAILABLE | `search_terms = []`; `source_health[google_ads].caveat` — 30-day API limit |
| Fresh Google Ads / GA4 / GTM ownership / Meta readback on 2026-08-30 | UNAVAILABLE | HTTP 401 `CREDENTIALS_MISSING` and fail-closed missing-token receipts (`fresh-evidence-register.md:42-43`) |
| T3 dashboard, T4A/T4B strategies, T5 report, T6 review, T7 targeting pack | UNAVAILABLE — never produced | GROUND-TRUTH |
| T2 repair backlog, traffic-quality recommendations, gap register, audit index, return receipt | UNAVAILABLE — never produced | T2 died mid-artifact; `acceptance-ledger.md` rows T2-06..T2-12 all NOT STARTED |
| `ads-control/reports/stage-3-20260824T111713-2aa95a23.html` (rendered report) | UNAVAILABLE | Not in git on any branch; only the template and bundles exist |
| Live deployment state of `landing/billiardasztal-v2/snippet-footer.txt` | UNAVAILABLE | Deploy script exists; live presence not provable from the repo |
| **Verified zeros (NOT unavailable)** | — | KPC Search + Display delivery 2026-06-25..08-23 (`campaign_type_totals`); 18 of 20 Google conversion actions at 0.0 in 2026-07-25..08-23; 4 of 5 CRM worksheets at 0 enquiries |

---

## 6. Limitations of this lane

1. I did not confirm the OOM hypothesis: `dmesg` needs root and `journalctl` showed no matching kill record for 12:32–12:34. The *mechanism* of process death is `[HYP]`; the *absence of any watcher* is `[OBS]` and fully proven.
2. I did not open live Google Ads, Meta, GA4, GTM, Search Console, the Notion CRM or the Google Sheet. Every account-level statement here comes from the accepted T1 corpus, T2's registered fresh-evidence receipts, or the materialised `report_data.json`.
3. The live presence of the `billiardasztal-v2` footer snippet on production is unverified — the captured `current-live-3098.html` is page body only.
4. I read `report_data.json`, `evidence.json` and the KPC ledgers structurally and sampled values; I did not audit all 1,524 `field_ledger` rows or all 21.7k lines of each evidence ledger.
5. I did not restart, resume, repair or commit anything belonging to the stopped run. The four surviving uncommitted T2 artifacts were read in place and left untouched.
6. ORCH-CORR-1 and ORCH-CORR-2 were both re-verified independently before being folded in (§4, §4A); I did not take either correction on trust.
7. `evidence.json`, `recommendations.json`, `report.md`, `narration.*` and the receipts under `2026-08-28-fusiontables-report/` were read only in part; a full audit of the Hungarian report copy against `report_data.json` belongs to whichever lane owns client-facing claims.
