# MIGRATION_STATE.md

## 📍 RESUME HERE — 2026-08-26: THE MIGRATION HAS BEEN EXECUTED

⭐ **Read `MIGRATION_EXECUTION_2026-08-26.md` first.** It is the record of the first end-to-end
run: what actually migrated, the 22 defects that only execution could find, and the **four
decisions the migration now waits on**. Everything below this block predates that run and
describes the *authored* state, not the executed one.

**D7 remains strictly read-only.** Baseline re-verified before and after:
`users 13,250 · node 620 · field_data_body 543 · commerce_order 87,640 · flagging 132,256 · acl 261`.

### Migrated and verified against the D7 baseline

| | source | in D10 |
|---|---:|---:|
| `acl` / `acl_user` | 261 / 33,003 | **261 / 33,003** (R-5 exact) |
| users / roles | 13,250 / 5 | **13,250 / 5** (485 permission grants) |
| orders | 87,640 | **87,640**, state distribution exact, **42 `checkout_complete` preserved** |
| customer profiles | 19,590 | **19,590**, all with billing addresses |
| order items | 116,472 | **113,425** + 3,047 skipped and logged (W-7) |
| products | 162 | **162** variations |
| files / url aliases | 4,330 / 13,969 | **4,330 / 13,969** |
| wistia media / tracks | 32,737 / 62,137 | **32,737 / 62,137** |
| taxonomy / referral / reports | 107 / 385 / 779 | **107 / 385 / 779** |
| flags | 5 | **5**, with D7 bundles and labels |

### 🛑 Four decisions block the rest

1. **W-5** — a start date for the 27 discounts D7 left unbounded → blocks 4 Commerce migrations,
   26 coupons, 924 usage rows.
2. **Content model (Phase 3.3)** — D10 has **zero** content types → blocks **22** migrations.
3. **W-7** — how to represent 3,047 order-level discount line items → **2,812 completed orders**
   currently derive a total higher than D7's.
4. **Payment gateways** — `payment_gateway` is required and none exists; decision 4 forbids one for
   `commerce_payment_example` → blocks 26,797 payments.

### ✅ C-2 RESOLVED AND IMPLEMENTED

Coupon 69 → promotion **57**; its link to promotion 58 recorded as intentionally lost. See
`COMMERCE_DESTINATION_ARCHITECTURE.md` §C-2.

---

## The state as authored, before the run


**Where the work stands, 2026-08-25.** Phase 3.1 is complete; implementation is under way.

### The document map — read in this order

| Document | What it holds |
|---|---|
| `../ceonline/CLAUDE.md` | **the governing rules.** 45 sections. Everything defers to this |
| `DECISIONS_PHASE_3.md` | approved decisions **D3-1 … D3-10** + the U-list (all closed except U-19/U-20) |
| `D7_FREEZE_CHECKLIST.md` | freeze preconditions P-1…P-9. **All met; freeze not yet executed** |
| `SPIKE_FINDINGS.md` | the 3 blocking spikes. ① resolved · ② open · ③ resolved (custom) |
| `PHASE_NUMBERING_MAP.md` | ⚠️ `CLAUDE.md` and the implementation plan number phases **differently** |
| `HOMEPAGE_FLOW.md` | what D7 serves at `/` — a Page Manager page over **6 hard-coded nids**. ⚠️ corrected 2026-08-25: **five panes, not four** |
| `HOMEPAGE_DEPENDENCY_INVENTORY.md` | the homepage bill of materials, measured against D10's current state. Open decision **H-1** |
| `STAGE_0_CAPTURES.md` | ⭐ **the five captures, RUN.** Results and three execution-found defects. ⚠️ its §7 table-audit is superseded |
| `W1_W2_RESOLUTION.md` | ⭐ the source/column audit RESOLVED. 6 migrations fixed + runtime-verified. Open: **W-4** unmapped columns |
| `MIGRATION_SCHEMA_TABLES.md` | ⭐ **the 16 destination schemas, AUTHORED AND CREATED.** Generated from D7 information_schema; 16/16 verified empty |
| `COMMERCE_DESTINATION_ARCHITECTURE.md` | ⭐ **W-2a/W-2b/W-3 resolved.** Commerce destinations are **entities** per the register; tax retired. Open: **D-1** order status mapping, **D-2** the required store |
| `CERTIFICATE_EXTRACTION.md` | the 290-line theme function → `ce_certificate`, defects C-1…C-10 |
| `EVALUATION_DOWNLOAD.md` | the Word download + **E-3 security fix (approved, implemented)** |
| `COURSE_TEMPLATE_CONVERSION.md` | course/video templates, defects **D-1…D-4** |
| `FRONTEND_CONVERSION_STATUS.md` | 33/36 templates, defects **P-1, Q-1, M-1** |
| `RULES_BEHAVIOR_IMPLEMENTATION.md` | `ce_rules_behavior` — 9/16 Class C rules addressed |
| `MIGRATION_PLUGINS.md` | migration plugins. **§4f webform cid map** and **§5 quiz** are the two highest-risk sections |

### Migration destination coverage — 2026-08-25

| | |
|---|---|
| `ce_table` destinations declared | 36 |
| **exist in D10** | **26** |
| still missing | **10 — all Commerce, all gated on W-3** |

The 16 missing schemas were authored from the live D7 `information_schema` and created
(`MIGRATION_SCHEMA_TABLES.md`). Enabling `drupal/acl`, `drupal/content_access` and `drupal/flag`
supplied 4 more and made `ce_acl_list` / `ce_acl_node` / `ce_acl_user` discoverable for the first
time (261 / 239 / 33,003 rows). `ce_quiz` was enabled per the Stage 1 list.

**50 `ce_*` migrations now report a real source count.** The only one still erroring is
`ce_commerce_coupon_usage`, pending W-2b. **No business data has been migrated** — every
destination table is empty.

Creating these also installed five tables whose schemas existed but had never been installed:
`ce_certificate`'s three, and `ce_migrate`'s own `ce_migrate_orphan_log` and
`ce_migrate_reconcile_run` — so the orphan logger and reconciliation runner previously had
**no tables to write to**.

### What is implemented (all RUNTIME-UNVERIFIED)

### ✅ FIVE things must be captured from D7 BEFORE their migrations run — ALL FIVE CAPTURED 2026-08-25

> Results in `STAGE_0_CAPTURES.md`. 1 ✅ map populated · 2 ✅ explanation (i) · 3 ✅ sort is
> `score DESC, time_end DESC` · 4 ✅ snapshots OFF and table empty · 5 ✅ no credentials, no CVV.
> The list below is the original statement of why each mattered.

Both are irreversible from the D10 side once the migration has run.

1. **Webform component map** (`MIGRATION_PLUGINS.md` §4f) — `webform_migrate`
   converts numeric `cid`s to machine names. `course_actions` reads `data[30]`
   as the course nid. Without the map, **20,397 course completions stop being
   recorded, silently**. `ce_migrate.webform_component_map` ships deliberately
   **empty**; run `scripts/reconcile/d7_webform_components.sh` first.

2. **Quiz pass-rate distribution** (§5f) — the documented `35 + 28 + 5 = 68`
   cannot be counting distinct quizzes, because there are **59**. Run
   `scripts/reconcile/d7_quiz.sh` Q-3/Q-4. If it shows quizzes that changed
   pass mark between revisions, that is a **new finding** — pass/fail would
   depend on which revision a student took. Report it; do not absorb it.

3. **The `quiz_certificate` view** (`CERTIFICATE_EXTRACTION.md` §10) — D7 takes
   result row **`[0]` blindly**, so the view's own `ORDER BY` decides *which
   attempt dates the certificate*. The view exists only in the D7 database;
   nothing in code records the sort. Run
   `scripts/reconcile/d7_view_quiz_certificate.sh`. Branch A stays inert until
   then — `EXECUTE_VIEW_IMPLEMENTED` must be flipped **by hand**, because
   importing the view alone would otherwise print *"You did not attempt the
   quiz yet"* on certificates students actually earned.

4. **`certificate_snapshots` + its on/off variable**
   (`CERTIFICATE_EXTRACTION.md` §11) — D7 serves **already-issued certificates
   from frozen stored HTML**, bypassing the generator entirely. If the setting
   is ON in D7 and OFF in D10, every affected user's certificate is silently
   regenerated by new code — a different document from the one they
   downloaded. Run `scripts/reconcile/d7_certificate_snapshots.sh` (S-1).

5. **Payment payload audit** (`MIGRATION_PLUGINS.md` §8e) — whether merchant
   credentials or CVV sit in `commerce_payment_transaction.payload`. The D7
   code stores the gateway **response** (credentials are request-only), so
   they *should* be absent — but that is a code reading, not a measurement.
   Run `scripts/reconcile/d7_payment_audit.sh`. **STOP** on any P-2 or P-3 hit.

### ✅ RUNTIME BLOCKER LIFTED 2026-08-25 — the first execution has happened

A runtime is now available (`ddev`, `drush`, `mysql`, `php` all reachable; the D7 reader
connection works). **Stage 0's five captures have been run — see `STAGE_0_CAPTURES.md`.**
No STOP condition was triggered.

Executing things immediately found **three defects that reading could not**, plus a fourth by
machine-readable checking:

1. **Four of the five capture scripts read the wrong database with errors suppressed.** Every
   Stage 0 STOP condition is phrased "empty / zero = safe", so a silent connection failure was
   indistinguishable from a clean pass — including the payment audit. Fixed.
2. **Custom-module config was never installed.** `ce_migrate.webform_component_map` and
   `ce_certificate.settings` did not exist in active config; the container was also stale, so
   every `ce_migrate` service was missing. Fixed.
3. **`HomepageController` was a fatal error** — a typed promoted `$entityTypeManager` clashing
   with `ControllerBase`'s untyped one. It broke `drush cr` entirely, not just `/front`. Fixed.
4. **Migration source/column audit** — corrected: **3** absent source tables (not 8; five were
   destination names). Six migrations had real column defects and are **fixed and runtime-verified**,
   including ⭐ **the "silently free order"** — `commerce_line_item` has no price column at all, so
   all 116,472 line items would have migrated at NULL. Open: **W-2a** (tax: no source data in any
   form), **W-2b** (coupon usage: wrong module version, 2,629 real rows uncovered), **W-3** (all 38
   `ce_table` destination tables missing, and their names collide with Commerce 3).

⭐ `ce_migrate.webform_component_map` is **populated and live** — `hasLoadBearingComponents()`
returns true, `cid 30 -> 'course'`. The project's highest-risk mitigation is in place.

⭐ The certificate sort is captured: **`score DESC, time_end DESC`, one row** — the
highest-scoring attempt, *not* the most recent. 153 certificates would differ if guessed.

The text below is retained as the record of how things stood before.

### ~~🛑 RUNTIME BLOCKER — nothing here has ever been executed~~ (superseded)

The agent's shell has **no `php`, `ddev`, `docker`, `mysql`, `drush` or
`composer`**, and cannot reach the D7 database. It mounts the folder for
file access only.

So every artefact in this repository is **authored, never run**. The unit
tests assert on **source text and config** rather than behaviour, precisely
because no runtime was available — they lock in decisions; they do not prove
the code works.

**`EXECUTION_RUNBOOK.md` is the handover.** It is the exact ordered sequence to
run on the Mac: five captures → foundation → 26 migrations in dependency
order → reconciliation.

### Defects found in this repository's own migrations (2026-08-25)

| defect | impact had it shipped |
|---|---|
| **17 migrations used `table:`** where `CeTable` reads **`table_name`** | every one would have failed at runtime |
| `ce_referral.yml` carried **5 of 11** ledger columns | lost `referrer_disc_created` — **the FIFO key balances burn in** — plus `referrer_uid`, both `referred_*` columns, `data`, `updated`. Superseded by the older, complete `ce_referral_discount.yml` |
| `ce_commerce_order_item.yml` duplicated `ce_commerce_line_item.yml` | two migrations writing the same rows. Merged; the older file's orphan handling and R-3 documentation kept, the price conversion folded in |
| `ce_acl_user` depended on `d7_user`, `ce_block` on `d7_user_role` | waited on **core** migrations while users actually arrive via `ce_user` |

All four were found by **checking machine-readably** — parsing the YAML and
comparing against the plugin's actual config key — not by reading the files.


```
web/themes/custom/cetc_d10/        33 Twig templates + 22 preprocess functions
web/modules/custom/ce_core/        homepage route + controller
web/modules/custom/ce_certificate/ body builder, assets, branch-A stub
web/modules/custom/ce_reports/     evaluation download (E-3 security fix)
web/modules/custom/ce_migrate/     node-ID requirement check
web/modules/custom/ce_rules_behavior/  6 subscribers/services + 6 test classes
```

### 🛑 The five things that will bite whoever resumes

1. **Node IDs must survive the migration.** `22, 23, 24, 25, 27, 28, 29, 36` are hard-coded in the
   homepage, two templates and `webform_document`. If they are renumbered, the homepage renders the
   wrong content **silently**. `ce_migrate.homepage_node_ids` checks this.
2. **`checkout_complete` is NOT `completed`.** One string comparison in `OrderStateSubscriber`
   keeps 106 orders (64 stuck + 42) without entitlement.
3. **The ACL grant and the `bought` flag go to DIFFERENT users** — order owner vs current user.
   §15 preserves it. ~800 diverged relationships.
4. **`permission_check` is per call site**: 0 for rules 11/12/13, **1** for rule 14.
5. **Nothing has been runtime-tested.** No DDEV access from the authoring session. Every claim above
   is static-analysis only.

### Genuinely blocked, waiting on someone else

| Blocked | Waiting on |
|---|---|
| D7 freeze capture | **ops** to block `/user/login` + `/admin`, then a timestamp |
| Spike ② `commerce_authnet` | a host + sandbox Authorize.Net account |
| Homepage content · certificate branch A · evaluation rows | node / views / webform migrations |
| 3 panel-pane templates | Page Manager rebuild |
| Mailchimp block | `drupal/mailchimp`, restored at Phase 3.3 |

---

**Phase 3.1 progress — read this first.**
Project: CE Online Training Courses · Drupal 7.105 → Drupal 10.6.15
Last updated: 2026-08-25

Governing rules: `../ceonline/CLAUDE.md` (45 sections). Task list: `../ceonline/PHASE_3_1_TASKS.md`.
This file records **what has actually been executed and verified**, not what is planned.

---

## 1. Task status

| # | Task | Status |
|---|---|---|
| 01 | D10 repo + `.gitignore` | ✅ complete |
| 02 | `composer.json` authored and approved | ✅ complete |
| 03 | `composer install` | ✅ complete — 242 packages, core **10.6.15** |
| 04 | Second DDEV project | ✅ complete — `ceonline-d10`, type `drupal10` |
| 05 | Install empty Drupal | ✅ complete — **minimal** profile |
| 06 | D7 read-only DB connection | ✅ complete — **READ-ONLY PROVEN** |
| 07 | Core modules, config_split, Claro, baseline config export | ✅ complete — 125 config files exported, scan clean |
| 08 | Custom module skeletons | ✅ complete — 9 enabled, **0 routes, 0 services** verified |
| 09 | `scripts/reconcile/` | ✅ complete — 6 scripts, R-1…R-23 + L-1…L-10 |
| 10 | PHPUnit + RB-* test skeletons | ✅ complete — 32 methods, all 28 RB ids |
| 11 | Verify D7 backups exist | ✅ **complete** — verified 2026-08-25, 8/8 checks |
| 12 | Git checkpoint `d10-foundation` | ⏳ blocked — needs freeze-point reconciliation |

**ALL 6 PHASE 3.1 GATES MET (2026-08-25).** Nothing has been committed in either repository.

### Decisions received 2026-08-25 — U-list now CLOSED except U-19 / U-20

`PHASE_3_1_DECISION_BRIEF.md` was approved in full: **U-10 · U-14 · U-15 · U-16 APPROVE ·
U-3 YES · U-5 REMOVE**, recorded as decisions **D3-4 … D3-9** in `DECISIONS_PHASE_3.md`.
Freeze preconditions **P-3, P-4, P-5 and P-6 are now MET** — eight of nine.

✅ **P-7 MET — freeze scheduled (D3-10).** **2026-08-27 21:00:00 IST (UTC+05:30) = epoch
`1787844600`**, mechanism *block D7 admin logins*, ops team notified.
**ALL NINE PRECONDITIONS MET.** The freeze itself is still ~2 days away, so every figure in
`DECISIONS_PHASE_3.md` remains **provisional** until the §4 capture replaces it.

🛑 **One clarification outstanding:** *"block admin logins"* must NOT be implemented as
`users.status = 0` — that writes business data and would corrupt the 27-set and R-1.
Network-level block or maintenance mode only. `freeze_verify.sh` **F-2** detects it.

⏳ **`RECONCILIATION.md` (D7 repo) is now narrower than the approved behaviour** for R-5, R-24 and
L-3. Those edits are **not authorised** and have not been made; `DECISIONS_PHASE_3.md` §5 holds the
approved wording. Same for `RULES_BEHAVIOR_MAPPING.md` C-10 / RB-14.

---

## 2. Verified environment

```
Host        PHP 8.3.29 · Composer 2.8.9 · DDEV v1.25.3 · Docker Desktop
D10 DDEV    ceonline-d10  · drupal10 · PHP 8.3.31 · MariaDB 10.11 · docroot web/
            http://ceonline-d10.ddev.site
D7  DDEV    ceonline      · drupal7  · PHP 7.1     · MariaDB 10.11 · docroot ""
            http://ceonline.ddev.site        ← SOURCE, READ-ONLY, UNTOUCHED
Drupal      10.6.15 exactly (verified in web/core/lib/Drupal.php)
Drush       13.7.6.0
Site        minimal profile, 36 modules enabled, 0 nodes, 2 users, no business data
Themes      default = stark (frontend, UNCHANGED) · admin = claro
Config      ../config/sync → project root, 125 YAML files, drush config:status clean
Admin       admin / see .logs/task05.log   (local dev only)
```

**Database connections** (`web/sites/default/settings.local.php`, gitignored):

```
default   host=db                  db=db    ← D10's own database
migrate   host=ddev-ceonline-db    db=db    ← D7 source, SELECT-only
```

---

## 3. Task 06 result — read-only is PROVEN, not assumed

Account `d10_migration_reader`@`%` holds **`USAGE ON *.*` + `SELECT ON \`db\`.*`** and nothing else.
Every write column in `mysql.user` and `mysql.db` is `N`.

- 12 CLI write probes → **all `ERROR 1142` privilege denied**, including three against the real `users` table
- 4 writes through Drupal's `migrate` connection → **all denied by the server**
- D7 row counts identical before/after: `users 13250 · node 620 · field_data_body 543 · commerce_order 87640`
- D7 schema identical: **338 tables / 2,498 columns**

Live D7 counts that **confirm the Phase 1 audit**: `commerce_order` **87,640** and `flagging` **132,256**
match `MODULE_MAPPING.md` exactly. Also `acl` 261.

**Residual change to D7:** the `d10_migration_reader` account exists on the D7 MariaDB server.
Reversible: `DROP USER 'd10_migration_reader'@'%';`. No D7 file, schema or business row was touched.

---

## 4. Traps already hit — do not repeat

1. **`MYSQL_PWD` is silently overridden** by option files in the DDEV web container
   (`/home/apple/.my.cnf`, `/etc/mysql/my.cnf`). Cost two failed runs.
   → Always use `mysql --no-defaults ... --password=...`.
2. **zsh does not treat `#` as a comment interactively.** Trailing `# comments` in a pasted command
   block get passed as arguments and break the command.
   ⚠️ **RECURRED 2026-08-25 — twice in one message.** Two failure modes, both from trailing comments:
   - `... task13-fieldui.sh   # field_ui (D3-8)` → zsh parsed `(D3-8)` as a **glob qualifier** →
     `zsh: unknown file attribute: 3`
   - `... drupal/bootstrap:3.40.0   # unblocks cetc_d10's base theme` → the apostrophe in
     `cetc_d10's` opened an **unterminated quote** → the shell hung at a `quote>` prompt
     (escape with Ctrl-C).

   > **RULE: never append a trailing `#` comment to a command given for pasting.**
   > Put the explanation on a separate line ABOVE the command block, never inside it.
   > Apostrophes in prose are especially dangerous — they survive as quote openers.
3. **Never put `*/` inside a PHP docblock** — e.g. writing the literal path
   `/web/sites/*/settings.local.php` in a comment closes the block and breaks bootstrap.
4. **`grep -i "denied"` cannot distinguish** a privilege denial from `Access denied` (auth failure).
   Classify database errors by **MySQL error code**: 1142/1044 = privilege, 1045 = auth, 1146 = table missing.
5. **Appending to `.gitignore` without a leading newline** concatenates onto the previous line.
   Composer's scaffold leaves the file with no trailing newline.

---

## 4b. Task 07 result

36 modules enabled. The approved §3 list installed cleanly; Drupal added three dependencies:
`menu_link_content` and `link` (from `menu_ui`), and — importantly — **`phpass`** (from `migrate_drupal`).

> ⚠️ **`phpass` is load-bearing for Phase 4.** It is the D10 module that validates legacy Drupal 7
> password hashes. Without it, migrated users could not log in with their existing passwords.

`system.theme.default = stark` (frontend untouched) · `system.theme.admin = claro` ·
`node.settings.use_admin_theme = 1` (reproduces D7 `node_admin_theme = 1`, so course/video/quiz/
certificate/webform node forms render in the admin theme).

`config_split` enabled with **zero split entities**. 125 config files exported; `drush config:status`
reports no differences. Secret scan clean — the only email in the export is `admin@example.com`
(Drupal's installer placeholder) and the only URLs are `oembed.com` and `drupal.org` core defaults.
No `ceonline`, `ddev.site`, `drupal7` or credential string appears anywhere in `config/sync`.

**~~Still open~~ ✅ RESOLVED — decision D3-8, approved YES 2026-08-25.** `field_ui` is enabled in D7
and restores admin-parity check W1. Enable script prepared as `.logs/task13-fieldui.sh`; **not yet
run** (ddev is not reachable from the implementation sandbox). Run with:
`ddev exec bash /var/www/html/.logs/task13-fieldui.sh`

---

## 5. Task 08 — skeletons created, not yet enabled

`web/modules/custom/` now holds **nine** `*.info.yml` files and nothing else — no `.module`, no
`.php`, no `.services.yml`, no `.routing.yml`. All nine parse as valid YAML, declare
`core_version_requirement: ^10`, and **declare no dependencies** (business contrib is not yet
enabled; declaring it would block installation).

```
ce_core  ce_users  ce_course  ce_rules_behavior  ce_referral
ce_wistia  ce_certificate  ce_reports  ce_migrate
```

> Note: `PHASE_3_1_TASKS.md` task 08 says "eight custom module skeletons" and then lists nine.
> Nine created, matching `D10_ARCHITECTURE.md` §2. Documentation inconsistency, not a decision.

To enable: `ddev exec bash /var/www/html/.logs/task08.sh`

---

## 5b. Task 07 script (retained for reference)

`.logs/task07.sh` (run: `ddev exec bash /var/www/html/.logs/task07.sh`)

**Already applied to files:**
- `settings.php` line 275: `$settings['config_sync_directory'] = '../config/sync';`
  → resolves to project root `config/sync`, outside the docroot, **not gitignored** (verified)
- `config/sync/` directory created, empty

**The script will:** enable the approved core modules, enable `config_split` (module only, **no split
entities** — those are Phase 3.2+), install Claro, set `system.theme.admin = claro` and
`node.settings.use_admin_theme = true`, **leave `system.theme.default = stark` alone**, rebuild cache,
export config, then run a 9-pattern secret scan reporting **paths only, never values**.

**Module list is `PHASE_3_IMPLEMENTATION_PLAN.md` §3 verbatim** — not invented:

```
node user taxonomy field file image text options datetime views views_ui path
menu_ui block filter search comment contact media telephone migrate migrate_drupal
update config serialization
```

`rest` is **omitted**: §3 makes it conditional on a D7 endpoint existing, and D7's `system` table has no
`rest` / `restws` / `services` row.

⚠️ **Open question:** `field_ui` is enabled in D7 but is **not** on the approved §3 list. Without it there
is no UI for managing fields, which affects admin-workflow parity (W1, course node editing).
**Not added** — needs a decision.

---

## 6. Decisions already made and approved

| Area | Decision |
|---|---|
| Frontend | Custom `cetc_d10`, reproducing D7 `cetc_new` as **visual golden master**. Sub-theme of `drupal/bootstrap` **3.40.0** (option A, confirmed D10-compatible). **Not** Bootstrap 5, Barrio, Radix, Olivero or Claro |
| Admin | **Claro** — admin theme only, never the frontend |
| Commerce migration tooling | `commerce_migrate` **permanently excluded** — no release supports Commerce 3. Custom `ce_migrate` instead |
| Rules / ECA | Excluded via composer `conflict`. 142 rules → 117 Commerce 3 config + 16 `ce_rules_behavior` + 9 not reproduced |
| Install profile | `minimal` (deviates from task 05's "standard" wording; chosen because the content model comes from the live D7 database) |

**Deferred packages, with the phase that must restore them:**

| Package | Version proven compatible | Restore at |
|---|---|---|
| ~~`drupal/bootstrap`~~ | **3.40.0 — ✅ RESTORED 2026-08-25** | *done* |
| `drupal/views_bootstrap` | **3.12.0** (Bootstrap 3 line — **not** 5.x) | Phase 3.4 |
| `drupal/better_exposed_filters` | 7.1.3 (stable) | Phase 3.4 |
| `drupal/ckeditor_accordion` | 2.3.0 | Phase 3.4 |
| `drupal/libraries` | 4.1.1 | Phase 3.4 |
| `drupal/admin_toolbar` | 3.6.3 | Phase 3.2 |
| `drupal/mailchimp` | 3.1.3 (avoid 3.2 — pulls the campaign submodule) | Phase 3.3 |
| `drupal/webform_migrate` | **dev-1.x only** — no stable release | Phase 3.5, needs a decision |

---

## 7. Files in the D10 project

| Path | Tracked | Note |
|---|---|---|
| `composer.json` | yes | md5 **`745ad235d31767cb06dba4081437ad7f`** · 46 require *(was `e1592784…`, 45)* |
| `composer.lock` | yes | md5 **`9438345079f9147815b725605199dd8f`** · **245** = 130 prod + 115 dev *(was `48a0d110…`, 242)* |
| `D10_FRONTEND_MIGRATION.md` | yes | frontend audit — 36 templates, 19 functions, 11 regions |
| `D10_ADMIN_THEME_MIGRATION.md` | yes | Seven → Claro audit |
| `MIGRATION_STATE.md` | yes | this file |
| `.ddev/config.yaml` | yes | project-specific comments appended after DDEV's template |
| `web/sites/default/settings.php` | yes | config_sync_directory + settings.local include. **No secret** |
| `web/sites/default/settings.local.php` | **ignored** | holds the D7 reader credential |
| `.logs/` | **ignored** | task scripts and run logs |
| `config/sync/` | yes | empty until task 07 runs |

---

## 8. Non-negotiable rules

- **D7 is read-only.** No file, schema, config or business-row change. `course_actions.module` must stay
  at md5 `f9913edaad4e3feb434ee201ebb2f55c` with its pre-existing modification uncommitted.
- Drupal core stays **exactly 10.6.15**. Never `composer update` without an explicit instruction.
- The `default` database connection stays the D10 database. `migrate` stays SELECT-only.
- Historical anomalies **194 / 27 / 64** are reconciliation targets, **not** data-cleanup targets.
- No business behaviour change, no silent bug fixes, no frontend redesign.
- STOP conditions in `CLAUDE.md` §43 apply — do not resolve them unilaterally.
