# Phase 3 checkpoint — 2026-08-25

**Status: the authoring work is done. Nothing has been executed.**

---

## 1. 🛑 The one blocker that governs everything

The authoring environment has **no `php`, `ddev`, `docker`, `mysql`, `drush` or `composer`**, and cannot reach the D7 database. It mounts the repository for file access only.

So every artefact here is **written, never run**. Test assertions are on **source text and config**, not behaviour — they lock decisions in; they do not prove code works.

`EXECUTION_RUNBOOK.md` is the handover.

---

## 2. What exists

| | count |
|---|---|
| migrations | **52** |
| migrate process plugins | 5 |
| custom modules | 11 |
| Twig templates | **37** |
| D7 capture scripts | **12** |
| test files | 9 |

Static validation is clean: 86 YAML files parse, no dependency cycles, no duplicate destinations, no DI arity mismatches, no Twig block or route errors, no forbidden packages.

---

## 3. 🛑 Genuine STOP conditions — five rules and nine views

**Rules 3, 5, 6, 8, 93 and the nine report views cannot be implemented.** Their definitions exist **only in the D7 database** (`rules_config`, `views_view`). Nothing in the D7 codebase defines them; `grep` finds the modules and theme templates and nothing else. There is no export.

- **CLAUDE.md §42** — "Never implement based on assumptions."
- **CLAUDE.md §43** — STOP if "D7 behavior cannot be determined."

Writing a plausible order-state rule or report view would produce something that **looks right and behaves differently** — the worst available outcome, because nothing would prompt anyone to compare it against D7. For the views specifically the guess would be about the **access handler and contextual filters**, i.e. *which rows each user may see*. That is a data-disclosure risk, not a cosmetic one.

**Unblock:** `scripts/reconcile/d7_config_export.sh` (§X-2 rules, §X-6 views). Run it, commit the output, and both become ordinary implementation work.

What was done instead: `/report` and `/report/history` routes and `ReportPageController` exist and **degrade honestly** — when a view is absent the page says so and logs it, rather than rendering an empty table that is indistinguishable from a working report with no rows.

**Rule 6's third condition remains undecoded** and is a business question for an owner, not a guess.

---

## 4. Findings from this phase

### 4a. A coverage audit found nine tables no migration read

`scripts/reconcile/coverage_audit.py` parses every D7 `hook_schema()` against every migration. Coverage had been tracked from memory; memory missed:

- **`quiz_node_results_answers`** — what students actually answered. Scores migrated, answers did not: 3,973 attempts would have become opaque numbers, impossible to review or defend.
- **`content_access`** — half the entitlement system (§10 names ACL *and* content_access).
- **`commerce_order_revision`** — the **back door for the plaintext passwords**; its own `data` column carries what the order migration strips.
- **`commerce_product`** — 113,271 line items pointing at nothing.
- **`webform_submissions`** — the parent of 441,970 value rows already migrating.

🛑 **All would have failed silently.** A missing migration simply does not run; the ones that do run report success. **No count-based target notices, because an unmigrated table has no target to miss.** Reconciliation would have gone green.

### 4b. Four defects in this repository's own migrations

| defect | impact |
|---|---|
| 17 migrations used `table:` where `CeTable` reads **`table_name`** | all 17 would fail |
| `ce_referral.yml` had 5 of 11 columns | lost `referrer_disc_created` — **the FIFO key balances burn in** |
| duplicate order-item migrations | two writing the same rows |
| deps on core `d7_user` / `d7_user_role` | waited on core while users arrive via `ce_user` |

**None were visible by reading.** All were trivially visible by parsing — which is why `scripts/static_validate.py` now runs these checks every time.

### 4c. Structural mismatches Commerce 3 could not hold

- **Order states** — D7's four `checkout_*` statuses have no Commerce 3 equivalent. Collapsing them onto `validation` would have **passed every count-based target** (64 in, 64 out) while destroying which step each order died at. `ce_order_d7` keeps all nine.
- **Payment states** — Commerce 3 has **no `failed` state** and creates no entity for a decline. D7 stores 5,405. Dropping them destroys history; mapping them to `completed` records **failed charges as successful payments**. `ce_payment_d7` adds `failed`.

### 4d. Near-misses worth remembering

- **A stray `'name' => NULL`** in the payment secret list — inert only by accident. Active, it would have erased **the customer's name from all 26,797 payment records**.
- **`isBlocked()` checking only whether the view existed** — importing the view would have flipped branch A on while `executeView()` was still a stub, printing *"You did not attempt the quiz yet"* on certificates students had earned.
- **`checkout_checkout` unreachable** in the first workflow draft — no order could have entered checkout. Found by asserting reachability, not by reading.

---

## 5. Preserved defects — do not "fix"

| id | behaviour |
|---|---|
| **X10** | 5 quizzes have `pass_rate = 0` → cannot be failed. Defaulting to 75 **retroactively fails students who passed** and could withdraw issued certificates |
| **D-1** | two pass marks disagree: per-quiz `pass_rate` vs a hard-coded 75 in the theme, `>=` in one place and `>` in another |
| **R17** | referral burn compares the **original grant**, not the remaining balance |
| **C-2** | certificate branch B emits stray closing tags |
| **branch C typo** | "does not provide certifiacte" — verbatim |
| **report tabs** | "Report" is always the active tab, including on the admin report page |
| **194 / 27 / 64** | historical entitlement anomalies — preserve exactly |

---

## 6. What must happen before Stage 2

**Six D7 captures**, each irreversible from the D10 side once its migration runs:

1. `d7_webform_components.sh` — the `cid → machine name` map (**20,397 course completions**)
2. `d7_quiz.sh` — Q-3/Q-4: does `35+28+5=68` count rows or quizzes, against **59** quiz nodes?
3. `d7_view_quiz_certificate.sh` — the sort that decides **which attempt dates a certificate**
4. `d7_certificate_snapshots.sh` — S-1: is snapshotting **on**?
5. `d7_payment_audit.sh` — **STOP** on any credential (P-2) or CVV (P-3) hit
6. `d7_config_export.sh` — the five rules and nine views

All are **SELECT-only and verified**. The payment audit is additionally **value-blind**: it counts, never selects payload content — a script that dumped payloads to prove they were safe would itself be the disclosure.

### Two open items needing the live schema

1. **Line-item price columns** are field-API names I assumed. A wrong name yields `NULL`, and **a `NULL` price is a silently free order**.
2. **14 quiz question-type tables** are *probably* empty — §8 records 128 multichoice questions. **Confirm with `COUNT(*)`, do not assume**: a non-zero count means a question type is in use that the rewrite does not implement.

⚠️ The three commerce **revision** migrations are deliberately incomplete — `revision_id` plus the sanitised `data` column only. **A column omitted from `process` is silently dropped, not reported.**
