# CHECKPOINT — Phase 3.1 foundation

Project: CE Online Training Courses · Drupal 7.105 → Drupal 10.6.15
Produced: 2026-08-25 · **Nothing committed. Nothing migrated. D7 untouched.**

---

## 1. Task status

| # | Task | Status |
|---|---|---|
| 01 | D10 repo + `.gitignore` | ✅ |
| 02 | `composer.json` | ✅ 45 require / 4 require-dev |
| 03 | `composer install` | ✅ 242 packages, core **10.6.15** |
| 04 | Second DDEV project | ✅ `ceonline-d10`, type `drupal10` |
| 05 | Empty Drupal install | ✅ `minimal` profile |
| 06 | D7 read-only connection | ✅ **READ-ONLY PROVEN** |
| 07 | Core modules, config_split, Claro, baseline config | ✅ 36 modules, 125 config files |
| 08 | Custom module skeletons | ✅ **complete** — 9 enabled, 0 routes, 0 services |
| 09 | `scripts/reconcile/` | ✅ **complete** — 6 scripts: R-1…R-23 counts, **L-1…L-10 relationships**, orphan register, roles/permissions |
| 10 | PHPUnit + `RB-*` test skeletons | ✅ **complete** — 32 methods, all 28 RB ids covered |
| 11 | Verify D7 backups | ✅ **COMPLETE 2026-08-25** — verified independently, see §4 |
| 12 | Git checkpoint `d10-foundation` | ⏳ unblocked — awaiting the **2026-08-27 21:00 IST** freeze-point capture |
| 13 | Enable `field_ui` (D3-8) | ⏳ script prepared, **awaiting a `ddev` run on the host** |

---

## 2. Phase 3.1 gate (PHASE_3_1_TASKS.md)

| Gate | Status |
|---|---|
| Drupal **10.6.15** exactly — not 10.6.x, not 11 | ✅ verified in `web/core/lib/Drupal.php` and `composer.lock` |
| D7 site still runs, unmodified, reachable for comparison | ✅ `ceonline` DDEV project running, `drupal7` |
| D7 database connection from D10 **provably read-only** | ✅ 12 CLI probes + 4 Drupal probes, all denied |
| `drupal/rules` and `drupal/eca` absent from `composer.lock` | ✅ absent, and blocked by `conflict` |
| No business data in the D10 database | ✅ 0 nodes, 0 terms, 2 users |
| Backups verified | ✅ **MET 2026-08-25** — §4 |

## ✅ **ALL 6 PHASE 3.1 GATES MET.**

---

## 3. Verified state

```
D10   Drupal 10.6.15 · PHP 8.3.31 · MariaDB 10.11 · Drush 13.7.6
      http://ceonline-d10.ddev.site · docroot web/
      36 modules · default theme stark · admin theme claro
      node.settings.use_admin_theme = 1
      config sync ../config/sync (project root, tracked) · 125 YAML files
      drush config:status → no differences

D7    Drupal 7.105 · PHP 7.1 · READ-ONLY
      http://ceonline.ddev.site
      course_actions.module md5 f9913edaad4e3feb434ee201ebb2f55c
      git: 11 entries, unchanged throughout Phase 3.1

DB    default  host=db                db=db   ← D10
      migrate  host=ddev-ceonline-db  db=db   ← D7, SELECT-only
```

Secret scan of all 125 exported config files: **clean**. Only email present is
`admin@example.com` (installer placeholder); only URLs are `oembed.com` and `drupal.org`.
No `ceonline`, `ddev.site`, `drupal7` or credential string anywhere in `config/sync`.

---

## 4. ✅ Task 11 — BACKUPS COMPLETE AND INDEPENDENTLY VERIFIED

**Executed 2026-08-25 with explicit approval.** Read-only against D7 throughout.

### Artifacts — `/Users/apple/neerja-backups/d7-20260825-123510/` (outside both repositories)

| Artifact | Bytes | sha256 |
|---|---:|---|
| `ceonline-d7-20260825-123510.sql.gz` | 65,692,030 | `1a0526fdbd589e776b4156dc279c4ab645229fedecad497faacc88d81beada5a` |
| `ceonline-d7-files-20260825-123510.tar.gz` | 639,896,826 | `bcd2d09b9ffdb0da0a0535c31d3c9d1f286352c92acc712f3656e065dead6360` |
| `MANIFEST.txt` | — | checksums, provenance, handling warning |

**DDEV snapshot:** `pre-migration-20260825-123510`. Directory total **687 MB**.

### Verification — all eight checks passed

| # | Check | Result |
|---|---|---|
| 1 | Artifacts exist, outside both repos | ✅ |
| 2 | gzip + tar integrity | ✅ both readable |
| 3 | **Content sanity — CURRENT data** | ✅ **`commerce_order` = 87,640** |
| 4 | Checksums recorded and reproducible | ✅ match `MANIFEST.txt` |
| 5 | Stale `db.sql` distinguished | ✅ `SUPERSEDED`, own sha256 `a031d4b3…`, 554,267,697 bytes, dated 2025-07-04 |
| 6 | DDEV snapshot present | ✅ |
| 7 | D7 unchanged by the backup | ✅ `course_actions.module` `f9913eda…`, 11 git entries |
| 8 | Partial runs reviewed | ✅ one complete directory; nothing deleted |

### Corroborating counts — the dump validated against four independent figures

```
users              13,250   ✅
acl_user           33,003   ✅
flagging          132,256   ✅
commerce_line_item 116,472  ✅
```

All four match live D7 exactly. The backup is not merely well-formed — **it demonstrably contains
the current dataset**, not the 2025-07-04 snapshot (45,592 orders).

### Two notes

⚠️ **A verification bug was caught and fixed.** The backup script's content-sanity check aborted with
`awk: towc: multibyte conversion failure` — macOS BSD awk fails on byte sequences invalid in the
current locale, and the D7 data contains them (spam-registration rows). The row count never printed.
Fixed with `LC_ALL=C` in both scripts. **Had this been accepted at face value, a backup could have
been declared valid without ever proving it held current data.**

ℹ️ **Three DDEV snapshots exist** (`123034`, `123311`, `123510`) from three invocations; only the last
completed. The two extra snapshots are harmless but occupy disk. Removing them is your call —
nothing was deleted automatically.

### ⚠️ Handling

Both artifacts contain **plaintext passwords** (RISK_REGISTER R2 — 4,207 orders carry
`custom_account_info`) and **payment credentials** (R1). They live outside both Git repositories and
must stay there. `MANIFEST.txt` repeats this warning alongside the checksums.

---

## 4-OLD. ~~BLOCKER — Task 11: backups are NOT verified~~ *(resolved above)*

`CLAUDE.md` §5 requires verified backups of D7 **code, database and files** before any
further phase. Current position:

| Asset | Status | Evidence |
|---|---|---|
| **D7 code** | ✅ recoverable | Git HEAD `70fee084e`, remote configured |
| **D7 database** | ⚠️ **STALE** | Only `ceonline/db.sql`, dated **2025-07-04** — over a year old |
| **D7 files** | ❌ **NONE FOUND** | `sites/default/files` is **626 MB**; no archive anywhere under `/Users/apple/neerja` |
| DDEV snapshots | ❌ none | `.ddev/db_snapshots/` empty |

**Why this matters.** Phase 3.5 reads 87,640 orders, 132,256 flaggings and 33,003 ACL grants
from the live D7 database. The migration itself cannot write to D7 — that is proven — but
Docker volumes can be lost, `ddev delete` removes databases, and a 626 MB file tree with
4,330 managed files has no copy at all. The one dump that exists predates the current data
by more than a year and **must not** be treated as a backup of today's D7.

**Recommended before Phase 3.5** (all read-only against D7):

```bash
cd /Users/apple/neerja/ceonline
ddev export-db --file=../backups/ceonline-d7-$(date +%Y%m%d).sql.gz
ddev snapshot --name=pre-migration-$(date +%Y%m%d)
tar czf ../backups/ceonline-d7-files-$(date +%Y%m%d).tar.gz sites/default/files
```

⚠️ Store `../backups/` **outside both Git repositories**. `.gitignore` already excludes
`*.sql` and `*.sql.gz`, but the files archive would not be excluded, and dumps contain the
plaintext passwords (R2) and payment credentials (R1) documented in `RISK_REGISTER.md`.

⚠️ `ddev export-db` and `ddev snapshot` **read** D7 — they do not modify it. But they must be
run from the D7 directory, which is the only time this project touches that directory at all.
**Approval required** before running them.

---

## 4c. 🛑 Task 09 — RECONCILIATION DRIFT (STOP CONDITION)

`scripts/reconcile/d7_baseline.sh` ran against live D7 through the read-only connection.
**13 assertions PASS exactly. 4 DRIFT. One exception set drifts.**

### Passing — the audit is confirmed against live data

R-1 13,249 users · R-2 87,640 / 22,008 / 63,111 orders · R-3 116,472 line items ·
R-4 26,797 / 21,392 payments · **R-5 33,003 ACL grants** · **R-6 132,256 flaggings**
(53,744 / 32,992 / 24,783 / 20,397 / 340 per flag) · R-7 3,973 / 3,922 quiz results ·
R-8 21,064 / 441,970 webform · **R-9 108 / 0 certificates** · R-10 620 / 13,249 / 100 / 4,330 / 13,969 ·
R-11 32,737 / 62,137 Wistia · R-12 361 / 24 referral ledger.

**The 194 and 27 exception sets reproduce exactly.** The 42 `checkout_complete` orders are
confirmed present. R-22 `custom_account_info` = **6,547**, matching the documented figure.

### Drift

| ID | Documented | Live D7 | Assessment |
|---|---:|---:|---|
| R-16 | 142 | **146** | **Real.** +4 `rules_config` rows |
| R-17 | 22 active | **114** | **Probably my query.** I measured `commerce_discount.status`; the doc measures *active promo rules* (`rules_config.active`). Different things |
| R-18 | 92 inactive | **4** | Same cause as R-17 |
| R-21 | 196,546 | **220,536** | **Real.** +23,990 order revisions |
| 64-set | 64 | **74** | **Unresolved.** +10. Either scope definition or real drift |

### The evidence that matters

Counting the same tables in `ceonline/db.sql` (the 2025-07-04 dump) gives a third data point:

| Table | dump (Jul 2025) | doc | live |
|---|---:|---:|---:|
| `commerce_order` | 45,592 | 87,640 | **87,640** |
| `acl_user` | 28,908 | 33,003 | **33,003** |
| `rules_config` | **142** | **142** | **146** |
| `commerce_discount` | **114** | **114** | **118** |
| `commerce_order_revision` | 154,053 | 196,546 | **220,536** |

Orders and ACL grants match the documentation **exactly**, so the audit measured live data and
**no new orders have been placed**. But `rules_config` and `commerce_discount` each gained
**exactly 4 rows** relative to *both* the dump and the audit — consistent with **4 new Commerce
discounts created in D7 after the Phase 1 audit** (each Commerce 1 discount is backed by a
`rules_config` row). Order revisions grew by 23,990 with no new orders, which means **existing
orders were re-saved**.

**Implication:** D7 is not transacting, but it is **not frozen** either. Administrative writes
have occurred since 2026-08-24. If that continues, reconciliation targets are moving targets and
the approved Rules split (**117 + 16 + 9 = 142**) no longer covers the whole population.

### ⚠️ `db.sql` is NOT a backup

It holds **45,592 orders against a live 87,640** — barely half — and 28,908 of 33,003 ACL grants.
It is an early-2025 snapshot, not a recent copy. This makes the Task 11 backup gap materially
worse than recorded in §4: there is **no usable backup of the current D7 database at all**.

### Investigation result — 2026-08-25

`drift_investigate.sh` ran read-only. **Four of five items resolved. One needs a decision.**

#### ✅ RESOLVED — the 64 set was MY query, not drift

Breakdown of the 74: `checkout_complete` 42 · `checkout_payment` 18 · `canceled` 5 · `cart` 5 ·
`checkout_review` 3 · `checkout_checkout` 1.

Excluding cart / canceled / pending gives **exactly 64** — 42 + 18 + 3 + 1. The documented
definition means *stuck before completion*, which correctly excludes carts (never checked out)
and cancellations. **The documented target 64 is correct.** `d7_baseline.sh` corrected.

**194 and 27 re-confirmed exactly.** All three exception sets are intact.

#### ✅ RESOLVED — R-17 / R-18 was MY query, not drift

| Measurement | Live |
|---|---|
| `commerce_discount.status` (what I wrongly measured) | 114 active / 4 inactive |
| `rules_config.active` for `commerce_discount` (**what the doc means**) | **25 active / 94 inactive** |

Against documented 22 / 92, and the four new rules being 3 active + 1 inactive, the expected
figures are 25 / 93. **One inactive row remains unaccounted for** — my filter used
`module = 'commerce_discount' OR name LIKE 'commerce_discount%'`, which is broader than the
documented scope. `d7_baseline.sh` now uses `module='commerce_discount'` exactly.

#### ✅ RESOLVED — D7 is effectively static, but NOT formally frozen

| Table | Newest write |
|---|---|
| users | 2026-08-11 13:39 |
| nodes | 2026-08-11 13:42 |
| flaggings | 2026-08-11 05:01 |
| quiz results | 2026-08-10 03:29 |
| **orders** | **2026-08-24 13:31:48** |
| order revisions | 2026-08-24 13:31:48 |

Business activity stopped **2026-08-11**. A single order was then created on **2026-08-24 at
13:31:48** — `newest_order_created` equals `newest_order_changed` equals the newest revision,
and exactly 1 revision exists for that day.

Critically, that write is **3h 42m BEFORE** the audit checkpoint commit `70fee084e`
(2026-08-24 17:13:28). **No write has occurred since the audit was published**, and none in the
~22 h since.

#### ⚠️ R-21 — the documented target appears to be WRONG, not drifted

| Source | `commerce_order_revision` |
|---|---:|
| `db.sql` (2025-07-04) | 154,053 |
| **RECONCILIATION.md** | **196,546** |
| Live D7 | **220,536** |

Since no D7 write has occurred since before the audit commit, the live value at audit time was
already 220,536. The documented 196,546 matches **neither** the dump nor live, so it was
captured from an earlier state than the document it appears in.

**This is a documentation defect, not source drift.** Under the standing rule I have **not**
edited `RECONCILIATION.md`. R-21 is an *archive completeness* target (decision 12: archive order
revisions to CSV, never drop them), so the acceptance criterion should be
"archived row count = source row count **at the migration freeze point**" rather than a fixed
literal — but that is a documentation decision, not mine to make.

#### 🛑 R-16 — FOUR REAL BUSINESS DISCOUNTS OUTSIDE THE APPROVED SPLIT

`rules_config` now spans ids 1–149, total 146. Ids 21, 53 and 82 confirmed genuinely absent,
exactly as documented. The four extra rows:

| id | name | label | active | module |
|---:|---|---|:--:|---|
| 146 | `commerce_discount_rule_discount__50_off_admin` | $50 Off- Admin | **1** | commerce_discount |
| 147 | `commerce_discount_rule_discount__6_off` | $71off | **1** | commerce_discount |
| 148 | `commerce_discount_rule_discount__65_off` | $65 Off | **1** | commerce_discount |
| 149 | `commerce_discount_rule_discount__25_off_july_4th_` | $25 Off- July 4th | 0 | commerce_discount |

All four are **Commerce 1 discount rules** — Class D (configuration-only), the same category as
the approved 117. **Three are ACTIVE**, so they are live promotional behaviour that customers can
currently receive.

The approved disposition **117 + 16 + 9 = 142** no longer covers the population.
**Decision required — see §5 item 8.**

**No target altered. `RECONCILIATION.md` untouched. No D7 data read beyond SELECT.**

---

## 4b. Task 10 — test contract in place

`phpunit.xml.dist` at the project root, scoped to `web/modules/custom` only (core and contrib
tests are out of scope). Two suites: `rules_behavior` and `custom`.

**6 test classes · 32 methods · all 32 `markTestIncomplete()` · all 28 documented RB ids covered.**

| Class | RB ids |
|---|---|
| `CheckoutCompleteOrderingTest` | ordering contract, rule 7 inactive, RB-4, RB-5, RB-5b, RB-6, RB-8 |
| `EntitlementTest` | RB-14, RB-14a, RB-14b, RB-14c, RB-15, RB-15b, RB-15c, 194/27/64 |
| `CartAndPricingTest` | RB-3, RB-10, RB-10b, RB-16, RB-16b |
| `VideoProgressTest` | RB-11, RB-12, RB-12b, RB-13 |
| `MessagingTest` | RB-93, RB-93b, RB-99, RB-99b, RB-104 |
| `ReferralDiscountTest` | RB-145, RB-145b, referral code resolution |

The tests encode the contract **before** the implementation exists, so it cannot drift silently
while the code is written. Nothing is invented — every assertion traces to a "Test case" row in
`RULES_BEHAVIOR_MAPPING.md`.

**Five preserved defects are asserted as requirements**, each documented in the test that pins it:

| Test | Defect preserved |
|---|---|
| `RB-10b` | Product price never resyncs when the video price changes |
| `RB-16b` | Quantity clamp catches "add twice" but not "add three at once" |
| `RB-12b` | Video in two courses — the last course overwrites the session target |
| `RB-14a` | ACL grant → order owner, `bought` flag → current user; they diverge when an admin completes an order |
| `RB-99` | The "did not reached" typo is part of the string contract |

A D10 build that "fixes" any of these has **failed** its test.

Two ordering contracts are asserted directly rather than left to chance: the checkout-complete
sequence (weights −10, 0, 1, 4) and discount precedence (referral at −1 applies **first**, other
discounts stack on the reduced total).

---

## 5. Open decisions

**Closed 2026-08-25 by the `PHASE_3_1_DECISION_BRIEF.md` approvals** — recorded as D3-4 … D3-9 in
`DECISIONS_PHASE_3.md`:

| Was | Now |
|---|---|
| ~~1~~ `field_ui` | ✅ **D3-8 — YES.** Script `.logs/task13-fieldui.sh` prepared |
| ~~4~~ `/admin/dashboard` | ✅ **D3-9 — REMOVE.** No replacement page built |
| ~~U-10~~ R-5 expression | ✅ **D3-4** — `32,745 live + 258 logged = 33,003` |
| ~~U-14~~ R-23 vs 1,213 | ✅ **D3-5** — R-23 stays 443; **new R-24 = 1,213** |
| ~~U-15~~ Wistia column type | ✅ **D3-6** — VARCHAR, no FK |
| ~~U-16~~ ACL update/delete | ✅ **D3-7** — all three grant flags preserved |

Still open:

| # | Question | Blocks |
|---|---|---|
| 2 | ~~Task 11 backups~~ ✅ **CLOSED** — verified 2026-08-25, §4 | *closed* |
| 3 | `admin_toolbar` restore (Q-A3) — **permitted, not required** (U-4 closed); optional | Phase 3.2 |
| 5 | Two Wistia config pages — **keep separate** (U-6 closed by default) | *closed* |
| 6 | `webform_migrate` — **`^2.0` approved** (U-7 closed; 2.0.0-alpha2 is D10-compatible) | *closed* |
| 7 | Config Split entities — module enabled, zero splits defined | Phase 3.2 |
| ~~12~~ | ✅ **P-7 MET — D3-10.** Freeze scheduled **2026-08-27 21:00 IST** (epoch `1787844600`), ops notified. **All nine preconditions met.** Freeze not yet executed | *closed* |
| **15** | 🛑 **Confirm the freeze mechanism is NOT account blocking.** `users.status = 0` would write business data and corrupt the 27-set / R-1. `freeze_verify.sh` F-2 detects it | **before 2026-08-27** |
| **13** | ⏳ **D7 documentation edits** implied by D3-4/D3-5/D3-7 (R-5, new R-24, L-3, RB-14) are **unauthorised and not made**. `DECISIONS_PHASE_3.md` §5 | Phase 3.5 |
| **14** | ⚠️ **The USER population behind the 105 update/delete ACL bindings is unmeasured.** Required by D3-7 before implementation | Phase 3.5 |
| ~~8~~ | ✅ **RESOLVED — decision D3-1 approved 2026-08-25.** Rules disposition is now **121 + 16 + 9 = 146**. Ids 146–149 are Class D configuration-only; **no `ce_rules_behavior` code** is written for them | *closed* |
| ~~9~~ | ✅ **RESOLVED — decision D3-2 approved 2026-08-25.** R-21's literal 196,546 is a documentation error. Acceptance is now "archived rows = source population **at the declared freeze point**". **Not** replaced by 220,536 | *closed* |
| **10** | ⚠️ **D3-3 — D7 freeze point required before Phase 3.5.** Approved as a requirement, **not yet scheduled**. The implementation must not freeze, stop or modify D7 | **Phase 3.5** |
| **11** | One inactive `commerce_discount` rule unaccounted for under the corrected filter (expect 93, broader filter showed 94). Confirm at the freeze point | Phase 3.5 |

---

## 6. Residual changes outside D10

One, deliberate and approved: the account **`d10_migration_reader`** exists on the D7 MariaDB
server with `GRANT SELECT` only. Reversible with `DROP USER 'd10_migration_reader'@'%';`.
No D7 file, schema or business row was altered — verified by md5 and by before/after row and
schema counts (`users 13250 · node 620 · field_data_body 543 · commerce_order 87640`;
338 tables / 2,498 columns, identical).

---

## 7. Next actions

1. Run `.logs/task08.sh` — enable the nine skeleton modules, verify zero routes and zero services
2. Run `scripts/reconcile/d7_baseline.sh` — confirm the documented R-1 … R-23 targets against live D7
3. Task 10 — PHPUnit + `RB-*` skeletons from `RULES_BEHAVIOR_MAPPING.md`
4. **Resolve §4 backups before Phase 3.5**
5. Task 12 — checkpoint commit `d10-foundation`
