# Execution runbook — D7 → D10

**Why this document exists.** The agent that wrote this migration has **no `php`, `ddev`, `docker`, `mysql`, `drush` or `composer`** in its shell and cannot reach the D7 database. Every artefact in this repository is therefore **authored but never executed**. This runbook is the handover: it is the exact ordered sequence a human runs on the Mac.

Treat every step's output as the first real evidence any of this works.

---

## Stage 0 — 🛑 The five captures (D7, read-only)

**Nothing in Stage 2 may run before these.** Each one is irreversible from the D10 side once its migration has run: the information exists only in D7 and the migration destroys the means of recovering it.

```bash
cd /Users/apple/neerja/ceonline
for s in d7_webform_components d7_quiz d7_view_quiz_certificate \
         d7_certificate_snapshots d7_payment_audit; do
  ddev exec bash /var/www/html/../ceonline-d10/scripts/reconcile/$s.sh \
    | tee /Users/apple/neerja/ceonline-d10/.logs/$s.out
done
```

All five are **SELECT-only and verified** — no `INSERT`/`UPDATE`/`DELETE`/`ALTER`/`DROP`. `d7_payment_audit.sh` is additionally **value-blind**: it never selects payload content, only counts.

| capture | what it settles | 🛑 STOP if |
|---|---|---|
| `d7_webform_components` | the `cid → machine name` map | the two load-bearing components (30 = course, 27 = comment) are absent |
| `d7_quiz` | Q-3/Q-4: does the pass-rate distribution count rows or quizzes? | Q-4 returns rows — pass/fail would depend on which revision a student took |
| `d7_view_quiz_certificate` | the view's **sort order** — which attempt dates a certificate | V-1 empty (view is in code, not the DB) |
| `d7_certificate_snapshots` | S-1: is snapshotting **on**? | S-1 shows `i:1;` and snapshots exist — they must migrate or certificates get regenerated |
| `d7_payment_audit` | whether credentials/CVV sit in payloads | **P-2 non-zero** (live credentials → rotation is a business-owner action) or **P-3 CVV non-zero** |

Then populate `ce_migrate.webform_component_map` from the capture, and set `ce_certificate.settings:snapshots_enabled` to match S-1.

---

## Stage 1 — D10 foundation

```bash
cd /Users/apple/neerja/ceonline-d10
composer install
ddev start
ddev drush si -y --existing-config || ddev drush si -y
ddev drush en -y ce_core ce_commerce ce_quiz ce_certificate ce_migrate \
                 ce_rules_behavior ce_users ce_referral ce_reports
ddev drush cr
```

Composer **`conflict`** blocks `drupal/rules`, `drupal/eca` and `drupal/php` — installation is actively refused, which is stronger than their merely being absent.

Configure the **read-only** D7 connection as migrate key `migrate` in `settings.local.php`. It must have **SELECT only**; never grant write.

---

## Stage 2 — Migrations, in dependency order

```bash
ddev drush migrate:import <id>   # one at a time, checking each
```

**45 migrations.** Nine were added after a coverage audit found business
tables that no migration read — see §"Coverage audit" below.

```
 1. ce_acl_list
 2. ce_acl_node
 3. ce_acl_user
 4. ce_block
 5. ce_user
 6. ce_node
 7. ce_certificate_node
 8. ce_certificate_node_settings
 9. ce_certificate_snapshot
10. ce_commerce_customer_profile
11. ce_commerce_customer_profile_revision
12. ce_commerce_discount
13. ce_commerce_order
14. ce_commerce_discount_usage
15. ce_commerce_line_item
16. ce_commerce_order_revision
17. ce_commerce_payment
18. ce_commerce_payment_transaction_revision
19. ce_commerce_product
20. ce_commerce_product_revision
21. ce_user_role
22. ce_content_access
23. ce_file
24. ce_flag
25. ce_flagging
26. ce_flag_counts
27. ce_quiz_multichoice_answers
28. ce_quiz_node_result_options
29. ce_quiz_properties
30. ce_quiz_question_properties
31. ce_quiz_relationship
32. ce_quiz_result
33. ce_quiz_result_answers
34. ce_referral_admin_discount
35. ce_referral_discount
36. ce_report_history
37. ce_taxonomy_term
38. ce_taxonomy_vocabulary
39. ce_url_alias
40. ce_webform_component
41. ce_webform_definition
42. ce_webform_submission
43. ce_webform_submission_parent
44. ce_wistia_media
45. ce_wistia_media_track
```

⚠️ **Do not use `--group=`.** The migrations are split across `ce_online`, `ce_foundation`, `ce_commerce`, `ce_quiz`, `ce_certificate`, `ce_flag`, `ce_wistia` and `ce_webform`. A single group import silently runs only part of the set. The ordered list above is authoritative.

⚠️ **`ce_node` must preserve `nid` *and* `vid`.** The homepage binds nodes 22, 23, 24, 25, 28, 29; templates bind 29 and 36; the evaluation webform is 27; and quiz results are revision-keyed. `ce_flagging` and the ACL migrations copy ids **raw**, so any renumbering silently gives the wrong people the wrong course access.

---

## Stage 3 — Verification

```bash
ddev drush ev '$r=\Drupal::service("ce_migrate.reconciliation");
               print $r->format($r->run("post-migration"));'
ddev drush ev 'print \Drupal::service("ce_migrate.entitlement_anomalies")->summary();'
ddev drush ev 'print \Drupal::service("ce_migrate.webform_component_map")->summary();'
ddev drush ev 'print \Drupal::service("ce_migrate.homepage_node_ids")->summary();'
ddev phpunit web/modules/custom/ce_migrate/tests web/modules/custom/ce_quiz/tests
```

### 🛑 On any DRIFT — stop

CLAUDE.md §25: reconciliation is **verification, not correction**. A mismatch means the **mapping** is wrong. Fix the mapping, never the data:

- **Do not** grant the 194 — that hands out entitlements nobody has today.
- **Do not** revoke the 27 — that takes access from 27 real people, possibly mid-course.
- **Do not** complete the 64.
- **Do not** edit a target to make a run pass.

### Assertions that catch what totals miss

| assertion | catches |
|---|---|
| `N-4:node:video` / `node:course` | all 53,744 `completed` flaggings landing on one bundle — R-6 still passes |
| `R-3:checkout_*` per state | four checkout states collapsed into one — the total stays 64 |
| `R-7a` **and** `R-7b` | a filter to `is_evaluated = 1` — drops 51 attempts, R-7b still passes |
| `R-7d:pass_rate_0` | someone "fixing" the five unfailable quizzes |
| `ANOMALY-194/27/64` | the historical sets drifting |

---

## Known-unverified

Everything. No file in this repository has been executed. The unit tests assert on **source text and config** precisely because no runtime was available — they lock in decisions, they do not prove behaviour.

**Two open items that need the live D7 schema:**

1. **Line-item price columns** — `commerce_unit_price_amount` / `commerce_total_amount` are field-API names that depend on the field. A wrong name yields `NULL`, and a `NULL` price is a **silently free order**. Confirm before Stage 2.
2. **Quiz pass-rate distribution unit** — `35 + 28 + 5 = 68` against **59** quiz nodes. Q-3/Q-4 settle it.

---

## Coverage audit — how nine missing migrations were found

```bash
python3 scripts/reconcile/coverage_audit.py
```

Runs **offline against files only** — no database, no PHP. It parses every D7 `hook_schema()` and every migration YAML, and lists business tables that no migration reads.

**It found gaps that had been tracked from memory:**

| table | what was missing |
|---|---|
| `commerce_product` | 113,271 line items reference products — every one would point at nothing |
| `webform_submissions` | the **parent** of the 441,970 value rows already migrating. The data existed with nothing recording **who** submitted it or **when** |
| `quiz_node_results_answers` | **what students actually answered.** Scores migrated; the answers behind them did not. 3,973 attempts become opaque numbers that can never be reviewed or defended |
| `quiz_multichoice_answers` | the answer options and their `score_if_chosen` — the mechanism by which a choice is right or wrong |
| `content_access` | **half the entitlement system.** §10 names ACL *and* content_access; only ACL had been migrated |
| `commerce_order_revision` | the **back door for the plaintext passwords** — its own `data` column carries the same payload the order migration strips |
| `commerce_customer_profile`, `commerce_discount`, `commerce_discount_usage`, `ce_referral_admin_discount`, `report_history`, `flag_counts` | historical business data with no migration |

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

### What the audit still reports as open

| category | count | what to do |
|---|---|---|
| genuinely outstanding | 19 | mostly config/derived (`commerce_checkout_pane`, `commerce_tax_*`, `flag_actions`) — decide each |
| await schema confirmation | 5 | automated column extraction bled across adjacent `$schema[]` blocks. **Not guessed at, not migrated.** Read the columns by hand or via `SHOW COLUMNS` |
| unused question types | 14 | `long_answer`, `matching`, `scale`, `short_answer`, `truefalse`. §8 records 128 **multichoice** questions, so these are *probably* empty — **confirm with `SELECT COUNT(*)`, do not assume**. A non-zero count means a question type is in use that the quiz rewrite does not implement |

⚠️ **The three revision migrations are deliberately incomplete.** They carry `revision_id` and the sanitised `data` column only, with the column list flagged for confirmation. **A column omitted from `process` is silently dropped, not reported** — so these must be extended before they run.
