# STAGE_0_CAPTURES.md

**The five D7 captures — run, for the first time, on 2026-08-25.**

`EXECUTION_RUNBOOK.md` Stage 0: *"Nothing in Stage 2 may run before these."* All five are now
executed and recorded. **No STOP condition was triggered.**

This is also the **first time any artefact in this repository has ever been executed.**
`MIGRATION_STATE.md` warned that every claim in it was static-analysis only. Running things found
three defects that reading could not — §6.

Raw output is kept in `.logs/d7_*.out`. **D7 was not modified: no file, no schema, no row.**

---

## 1. Results at a glance

| # | capture | STOP condition | result |
|---|---|---|---|
| 1 | `d7_webform_components` | load-bearing cids 30/27 absent | ✅ both present |
| 2 | `d7_quiz` | Q-4 returns rows | ✅ empty — explanation (i) |
| 3 | `d7_view_quiz_certificate` | V-1 empty (view in code) | ✅ view is in the DB, sort captured |
| 4 | `d7_certificate_snapshots` | S-1 = `i:1;` with snapshots present | ✅ OFF, table empty |
| 5 | `d7_payment_audit` | P-2 or P-3 non-zero | ✅ both zero |

---

## 2. Capture 1 — the webform component map ⭐

The mitigation `MIGRATION_STATE.md` calls *"the single highest-risk detail in the whole
migration"* is now in place.

```
node 27, 28 components captured
  cid 30 -> 'course'                                     ⭐ course_actions reads data[30]
  cid 27 -> 'please_comment_on_any_aspect_of_the_course'  ⭐ webform_document reads data[27]
```

`ce_migrate.webform_component_map` is **populated and live**:

```
isCaptured()               : true
hasLoadBearingComponents() : true
```

The YAML was **generated by parsing `.logs/d7_webform_components.out`**, not transcribed, so no
value was retyped. The config had never been installed at all (§6.2), so it was imported and
exported to `config/sync`; `drush config:status` is clean.

**Volumes all match the register exactly:** 21,064 submissions · 441,970 data rows · 20,873 with
cid 30 · 20,580 resolving to a course · 3 to a non-course · **4 non-numeric** · 73 dead-uid
submissions.

> 🛑 **Residual risk, recorded in the config file itself.** Webform limits an element key to 64
> characters and **6 of the 28 form_keys exceed it** (cids 6, 7, 16, 19, 21, 25 — up to 99 chars).
> `webform_migrate` will alter those. They must be re-verified against the real migrated webform.
> **Neither load-bearing component is affected** — `course` is 6 chars and the comment key is 41.

---

## 3. Capture 2 — the quiz pass-rate question, settled

**Explanation (i). The documented `35 + 28 + 5 = 68` counts `quiz_node_properties` ROWS, which are
per-revision.** No quiz ever changed its pass mark, so no student's pass/fail depends on which
revision they took. Q-4 returned no rows, and a restated query confirmed it
(`.logs/task27-quiz-verify.sh`).

| | rows | distinct nids |
|---|---|---|
| pass_rate 100 | 35 | 31 |
| pass_rate 75 | 28 | 24 |
| pass_rate 0 | 5 | 5 |
| **total** | **68** | **60** |

**The distinct count is 60 against 59 quiz nodes.** The 60th is **`nid = 0`** — a garbage row in
`quiz_node_properties` pointing at no node. Real quizzes reconcile at 59. A migration that treats
`nid` as a valid node reference will trip over it; `ce_quiz_properties` reads
`quiz_node_properties` and must tolerate `nid = 0` the same way the webform migration must
tolerate the 4 non-numeric cid-30 values.

Other figures: 3,973 attempts = 3,922 evaluated + 51 abandoned ✅ · 59 quizzes · 128 questions ·
275 relationships ✅ · 2,595 distinct users · **3 quizzes never attempted** (56 of 59 have
attempts) · 4 attempts by deleted users · 0 results pointing at a missing revision · `takes = 0`
(unlimited) on all 68 rows.

**Q-5 — the five unfailable quizzes** (272, 278, 284, 291, 405) are confirmed, and
`would_fail_at_75` is **0 for every one of them** across 202 attempts. The "tempting fix" would
retroactively fail nobody. That does **not** make it safe — it changes the rule going forward —
but the historical blast radius is zero, which is worth knowing.

**Q-6 — 0 attempts** are a pass by one mark and a fail by the other. The theme's hard-coded 75 and
the per-quiz `pass_rate` do not currently disagree on any real attempt.

---

## 4. Capture 3 — the certificate sort ⭐ THE ANSWER

The `quiz_certificate` view **is in the database** (vid 32), so V-1's STOP does not apply. The sort
that decides *which attempt dates a certificate*:

```
display: page          (path quiz-certificate)
  pager    : some, items_per_page = 1, offset 0     <- exactly one row; the sort is decisive
  sorts    : score     DESC
             time_end  DESC
  filters  : type = quiz_result, is_evaluated = 1, is_invalid = 0
  arguments: nid (index 0), uid (index 1)            <- correct order
  fields   : result_id (excluded), score (suffix %), title (via nid), time_start, time_end
```

> **The certificate shows the HIGHEST-SCORING attempt, tie-broken by the LATEST `time_end`.**
> It is **not** "the most recent attempt". A D10 rebuild that sorts by date — the intuitive
> choice — prints a different score and a different date.

**Blast radius (V-5/V-6):** 153 user/quiz pairs have more than one evaluated attempt, covering 377
attempts, with one user reaching 10 attempts. For **all 153**, the earliest and latest `time_end`
differ — so all 153 certificates would change if the sort were guessed wrong.

`quiz_certificate` (schema 0) and `certificate` (7002) are both enabled; `certificate_test` and
`signup_certificate` are not.

**Next:** the V-3 blob is in `.logs/d7_view_quiz_certificate.out` and belongs in
`CERTIFICATE_EXTRACTION.md` §3. Branch A stays inert until the view is rebuilt —
`EXECUTE_VIEW_IMPLEMENTED` must still be flipped by hand.

---

## 5. Captures 4 and 5 — snapshots off, payloads clean

**Snapshots (S-1): OFF, and the table is empty.** The `certificate_snapshots` variable was never
set, so D7 uses its default of 0, and `certificate_snapshots` holds **0 rows**. The two facts
agree, which is what §4's warning asked for. `ce_certificate.settings:snapshots_enabled` already
shipped `false` and now **matches a measured D7 value rather than an assumption** — the config was
missing from active storage entirely (§6.2) and has been imported.

Certificates in D10 will therefore be **generated live**, exactly as D7 does today. Template
mapping is intact: 108 `certificate_node` rows over 108 courses, 64 `certificate_node_settings`
rows, all `landscape`.

**Payment audit — no STOP.**

| check | result |
|---|---|
| P-2 `x_login` / `x_tran_key` / acceptjs / transaction key | **0 / 0 / 0 / 0** of 26,797 rows |
| P-3 full PAN / CVV | **0 / 0** |
| P-3 masked PAN | 26,590 — normal in an AIM response, not a violation |

**No merchant credentials and no cardholder data are stored.** The code reading in
`MIGRATION_PLUGINS.md` §8e is confirmed by measurement. No payload stripping is required and no
credential rotation is triggered.

Volumes: 26,797 transactions / 21,392 successful ✅ matches R-4 · 28.6 MB of payload (avg 1.09 KB)
· 38 transactions with a missing order · 158 with a dead uid.

**P-5 — the test gateway is real:** `commerce_payment_example` holds **25 transactions, all
marked successful**. Decision 4 stands — it must never become an active D10 gateway, and those 25
remain historical data.

**P-7 — R-3 reconciles exactly.** Successful payments against non-completed orders, by state:

```
checkout_complete   42
checkout_payment    18
checkout_review      3
checkout_checkout    1   -> checkout_* = 64  ✅ the 64-set
canceled             5
cart                 5
```

This is the per-state breakdown the runbook wanted precisely so that four checkout states
collapsing into one cannot hide behind a correct total.

---

## 6. 🛑 Three defects that only execution could find

### 6.1 Four of the five capture scripts read the WRONG DATABASE, with errors suppressed

`d7_quiz.sh`, `d7_view_quiz_certificate.sh`, `d7_certificate_snapshots.sh`, `d7_payment_audit.sh`
and `d7_config_export.sh` all used:

```bash
run() { mysql -u db -pdb -h db "$DB" -e "$1" 2>/dev/null; }
```

Two defects in one line:

- **`-h db` is the container's own database.** Run from the D10 container — which is where the D7
  reader credential lives — this pointed at D10's *empty* database. Every table was missing
  (error 1146) and `2>/dev/null` swallowed it.
- **Run from the D7 container instead, `-u db -pdb` is DDEV's default account, which is
  WRITE-CAPABLE against D7** — contradicting each script's own "SELECT ONLY" docblock and
  `MIGRATION_STATE.md` §8.

**Why it mattered more than a broken script normally would.** The first run of `d7_quiz.sh`
printed every heading and **not one row**. Its STOP condition reads *"Q-4 empty = explanation (i),
harmless"* — and **every** Stage 0 STOP condition is phrased "empty / zero = safe":

| capture | what silence would have looked like |
|---|---|
| `d7_quiz` | Q-4 empty → "no quiz changed pass mark" |
| `d7_view_quiz_certificate` | V-1 empty → "the view is in code" |
| `d7_certificate_snapshots` | S-1 absent → "snapshotting is off" |
| `d7_payment_audit` | P-2/P-3 zero → **"no credentials, no CVV"** |

A silent connection failure is **indistinguishable from a clean pass**, and would have been
recorded as one — the payment audit especially, which exists to clear a PCI question.

**Fixed.** All five now use the proven read-only path (`ddev-ceonline-db` /
`d10_migration_reader`, whose SELECT-only privileges are demonstrated in `MIGRATION_STATE.md` §3)
with errors **visible** (`2>&1`). Originals are in `.logs/prefix-backup-2026-08-25/`. Only
`d7_webform_components.sh` was already correct — which is why capture 1 worked first time.

**The runbook's Stage 0 command is also wrong** and should be corrected: it says
`cd /Users/apple/neerja/ceonline` and reaches the scripts via `/var/www/html/../ceonline-d10/…`.
The scripts read D10's `settings.local.php`, so they must run from the **D10** project:

```
ddev exec bash /var/www/html/scripts/reconcile/<script>.sh
```

### 6.2 Custom-module config was never installed

`ce_migrate.webform_component_map` and `ce_certificate.settings` **did not exist in active
config** — the modules were enabled while they were still bare skeletons (`MIGRATION_STATE.md` §5:
"nine `.info.yml` files and nothing else"), and `config/install` is only read at install time.

Both are now imported and exported; `drush config:status` is clean. The 8
`migrate_plus.migration_group.*` configs are **still missing** and cannot be imported until
`migrate_plus` is enabled at Stage 1 — expected, not a defect, but it means the migration groups
do not exist yet.

The container was also stale: `ce_migrate.services.yml` had been added after the module was
enabled, so **every `ce_migrate` service was missing from the container** until a rebuild.

### 6.3 `HomepageController` was a fatal error — it brought down `drush cr`

```
PHP Fatal error: Type of Drupal\ce_core\Controller\HomepageController::$entityTypeManager
must not be defined (as in class Drupal\Core\Controller\ControllerBase)
```

`ControllerBase` already declares `protected $entityTypeManager;` **untyped**. PHP forbids a
subclass redeclaring an inherited untyped property with a type, so the promoted
`protected EntityTypeManagerInterface $entityTypeManager` is fatal **at class-compile time** — not
merely when `/front` is requested. It broke the entire container rebuild.

The sibling `ReportPageController` avoids the identical clash by naming its property
`$entityTypeManagerService`, so the hazard was known somewhere in the codebase.

**Fixed** by taking the parameter unpromoted and assigning it to the inherited property, leaving
every `$this->entityTypeManager` call site unchanged. `php -l` clean; `drush cr` now succeeds. A
sweep of all custom controllers found **no other collision**.

---

## 7. 🛑 8 of 51 migration source tables DO NOT EXIST IN D7

> ⚠️ **CORRECTED 2026-08-25 — five of these eight were NOT source tables.**
> This section was produced by grepping `table_name:` across each YAML, which matches both
> `source.table_name` and `destination.table_name`. Re-parsed with a YAML parser: the four
> `ce_webform_*` names and `commerce_payment` are **destination** tables; all five of those
> migrations have valid, existing sources. **The real count is 3 absent source tables, all
> Commerce.** The `ce_` prefix theory below is wrong — there was no prefix slip.
> Full investigation and the resolution: **`W1_W2_RESOLUTION.md`**.
> The table below is left unedited as the record of what was originally reported.

Found by running `drush migrate:status`, then checking every declared `table_name` against
`information_schema` (`.logs/task31-table-audit.sh`, output `.logs/task31-table-audit.out`).
43 of 51 are fine. These 8 are not:

| declared `table_name` | migration | what D7 actually has |
|---|---|---|
| `ce_webform_component_archive` | `ce_webform_component.yml` | `webform_component` — **28 rows** |
| `ce_webform_settings` | `ce_webform_definition.yml` | `webform` — **1 row** |
| `ce_webform_submissions` | `ce_webform_submission_parent.yml` | `webform_submissions` — **21,064 rows** |
| `ce_webform_submitted_data` | `ce_webform_submission.yml` | `webform_submitted_data` — **441,970 rows** |
| `commerce_coupon_usage_transaction` | `ce_commerce_coupon_usage.yml` | `commerce_coupon` — 120 rows |
| `commerce_payment` | `ce_commerce_payment.yml` | `commerce_payment_transaction` — 26,797 rows |
| `commerce_tax_rate` | `ce_commerce_tax_rate.yml` | *(no table)* |
| `commerce_tax_type` | `ce_commerce_tax_type.yml` | *(no table)* |

None is a D10 staging table: `ce_migrate.install` creates only `ce_migrate_orphan_log` and
`ce_migrate_reconcile_run`.

**Two different problems.**

- **The four `ce_webform_*` entries look mechanical** — the module's own `ce_` prefix leaked onto
  the *source* table names. Every one has an obvious D7 counterpart with a healthy row count.
  These are the **highest-risk migrations in the project** (the 20,397 course completions), and as
  written all four fail immediately.
- **The four commerce entries need a real mapping decision.** `commerce_payment` may be intended
  as `commerce_payment_transaction`, but that table already has its own migration
  (`ce_commerce_payment_transaction_revision`), so this could be a duplicate rather than a rename.
  Commerce 1 stores **tax rates and types in `variable` rows, not tables**, so
  `ce_commerce_tax_rate` / `ce_commerce_tax_type` cannot be fixed by renaming a table at all —
  they need a different source plugin.

**Not fixed here.** Renaming a source table changes what a migration reads, and the commerce four
have no single correct answer. Recorded for decision — see §8.

This is the same class of defect as the 17 migrations that used `table:` instead of `table_name`,
and it was found the same way: **checking machine-readably against the real thing** rather than
reading the files.

---

## 8. What is now blocked, and on what

| | |
|---|---|
| Stage 1 foundation | ✅ **eligible now** — nothing blocks it |
| Stage 2 · the 4 webform migrations | 🛑 **decision W-1** — confirm the `ce_` prefix slip and the column mappings |
| Stage 2 · the 4 commerce migrations | 🛑 **decision W-2** — `commerce_payment` duplicate-or-rename; tax rates/types need a variable-based source |
| Stage 2 · everything else (37 migrations) | ✅ source tables verified present |
| `ce_quiz_properties` | ⚠️ must tolerate the `nid = 0` row (§3) |
| `ce_webform_submission` | ⚠️ must tolerate the 4 non-numeric cid-30 values (§2) |
| Certificate branch A | ⏳ rebuild the view with `score DESC, time_end DESC` (§4), then flip `EXECUTE_VIEW_IMPLEMENTED` by hand |
| Homepage | 🛑 unchanged — needs `ce_node`; see `HOMEPAGE_DEPENDENCY_INVENTORY.md` |

`ce_migrate.homepage_node_ids` ran for the first time and correctly reports all eight ids
(22, 23, 24, 25, 27, 28, 29, 36) **MISSING** — the expected result against 0 nodes, and proof the
check works rather than silently passing.

---

## 9. Provisionality

The D7 freeze is **2026-08-27 21:00 IST** — still ahead. Every figure here is a **pre-freeze
reading** and must be re-taken at the freeze point before it becomes the reconciliation baseline.
What will *not* change is the structural findings: the sort order, the component map's two
load-bearing keys, the absent source tables, and the three defects in §6.

---

*Read-only against D7 throughout. The only writes were to the D10 project: two config imports, one
controller fix, five script fixes, and this document.*
