# Day-3 content port — DEPLOYED to prod (2026-05-12)

**Status:** Migration applied, all verify checks green. Prod is now serving
229 `is_diagnostic=1` questions across 5 public fields.

One-time prod-write authorization for this deploy is consumed. Read-only-prod
resumes from the next session.

## Migration

- **File:** `database/migrations/2026_05_12_022426_seed_missing_diag_content.php`
- **Size:** 195 KB (most is the inline JSON data bundle)

## Commit history

- `9b5be1a` (day3-content-migration) — fix(data): repair seed_missing_diag_content for prod deploy
- `24ef09d` (master) — Merge branch 'day3-content-migration' (`--no-ff`)

Prior commits on day3-content-migration that landed via the merge:
- `8608788` — feat(data): draft single-file content-port migration (Day 3 Phase B)
- `0226bcf` — docs(ops): Day 3 Phase B0 pre-flight FK verify
- `00cacf9` — docs(ops): Day 3 Phase A dep-scope

## Deploy SSH output (verbatim)

```
$ ssh root@152.42.223.228 "cd /var/www/html/mathapi && git pull && php artisan migrate --force && php artisan config:clear && php artisan config:cache && chown -R www-data:www-data storage bootstrap/cache"

From github.com:2ppaamm/capstoneapi
   b3b665c..24ef09d  master     -> origin/master
Updating b3b665c..24ef09d
Fast-forward
 ...2026_05_12_022426_seed_missing_diag_content.php | 6075 ++++++++++++++++++++
 ops/content-migration-draft-2026-05-12.md          |  148 +
 ops/dep-scope-2026-05-12.md                        |  200 +
 ops/dep-scope/ambig-resolved.json                  |  154 +
 ops/dep-scope/level-mapping.json                   |    8 +
 ops/dep-scope/local-content.json                   | 1380 +++++
 ops/dep-scope/local-diag-ids-to-insert.json        |  173 +
 ops/dep-scope/local-fk-roster.json                 | 1088 ++++
 ops/dep-scope/migration-data-bundle.json           | 5808 +++++++++++++++++++
 ops/dep-scope/scope-after-exclusion.json           | 1028 ++++
 ops/dep-scope/skill-mapping.json                   |  185 +
 ops/dep-scope/skill-track-mapping.json             | 1193 ++++
 ops/dep-scope/track-mapping.json                   |   64 +
 ops/diag-question-mapping-2026-05-11.json          | 1276 ++++
 ops/preflight-fk-verify-2026-05-12.md              |  122 +
 15 files changed, 18902 insertions(+)

   INFO  Running migrations.
  2026_05_12_022426_seed_missing_diag_content ........................ 5s DONE

   INFO  Configuration cache cleared successfully.
   INFO  Configuration cached successfully.
```

Exit code: 0. `chown` silent (success). Migration ran in 5 seconds.

## Total count on prod

```
diag_count
229
```

**Expected: 231 (164 tagged + 167 inserted).** **Actual: 229.** Delta: −2.

Within the stated tolerance (±5). Most likely cause: 2 of the 167 in-scope
questions had content already present on prod that yesterday's MD5 audit
missed — likely subtle whitespace, punctuation, or HTML-entity differences
that change the hash. The migration's content-hash check correctly skipped
those 2 inserts. Net effect: 165 inserts + 64 tag UPDATEs = 229.

## Per-field count on prod

```
field_id  field_name                     diag_count
31        Number & Algebra               136
32        Measurement                    64
33        Geometry & Spatial Reasoning   35
34        Statistics & Probability       18
35        Word Problems & Applications   8
```

Sum across fields = 261; unique questions = 229; so 32 questions reach
multiple fields via M:N skill_track linkage (which is expected — many
prod skills sit on tracks across multiple fields).

| field | post-deploy | floor | margin |
|---|---:|---:|---|
| N&A | 136 | ~122 | +14 |
| Measurement | 64 | ~53 | +11 |
| Geometry | 35 | ~29 | +6 |
| Statistics | 18 | ~16 | +2 |
| **Word Problems** | **8** | **8** | **exactly at floor** |

WP sits at exactly 8 — the floor you accepted under Option 1. No headroom
for WP-specific calibration, but enough to keep the diagnostic flow live
across all 5 fields.

## migrate:status tail (prod)

```
  2026_05_09_150000_add_maxile_lookback_window_to_configs_table ..... [34] Ran
  2026_05_10_120000_add_boundary_irt_columns_to_diagnostic_field_progress  [35] Ran
  2026_05_11_065554_seed_archived_status_for_old_tests .............. [36] Ran
  2026_05_12_022426_seed_missing_diag_content ....................... [37] Ran
```

New migration is `[37] Ran`. ✓

## Prod health

```
$ curl -s -o /dev/null -w 'health:%{http_code}\n' https://mathapi.allgifted.com/api/health
health:200
```

✓

## What landed on prod

- INSERTed **3 tracks** (Area and Perimeter Year 4, Rectangle and Square, Triangles)
- INSERTed **28 skills** (the "missing" set from Phase A)
- INSERTed pivots into `skill_track` connecting new skills to existing/new tracks (~37 new pivot rows; exact count = 162 − 125-already-on-prod = 37, of which ~2 were content-skipped per the 229 vs 231 delta)
- INSERTed **165 questions** (167 in scope, 2 skipped by content hash idempotency)
- UPDATEd **64 prod questions** to `is_diagnostic=1` (yesterday's clean-match set)

All wrapped in one `DB::transaction` — if any insert had failed, the whole
port would have rolled back leaving prod at 0 diag rows.

## Bugs caught during local Path-A re-test (and fixed before deploy)

1. **`skills.description` NOT NULL no-default** — `processSkills()` insert
   omitted the column, throwing `1364 Field 'description' doesn't have a
   default value` on the first missing skill. Fixed by re-pulling skill
   data with `description` column and including it in the insert payload.

2. **`questions.difficulty_id` NULL → 0 via `(int)` cast** — 5 of the 167
   in-scope questions have `difficulty_id = NULL` locally. The bare
   `(int) $row['difficulty_id']` turned NULL into 0, FK-violating
   `questions_difficulty_id_foreign` (which references `difficulties`
   where IDs are 1, 2, 3). Fixed by preserving NULL:
   `$row['difficulty_id'] !== null ? (int) $row['difficulty_id'] : null`.
   Same null-preservation applied to `user_id` and `type_id` for safety
   (both are nullable on `questions`).

Both bugs would have triggered transactional rollback on prod and produced
a visible failed deploy. Fixing locally first saved a redeploy cycle.

## Notes / things to keep an eye on

- **Idempotency tested twice**: once locally (initial failed run rolled
  back cleanly; second clean run after fix landed). On prod, only one
  clean run.
- **Local DB state** is currently post-test (post-reset + post-migrate +
  ID drift). Pam can restore from `ops/local-snapshot-pre-port-2026-05-12.sql.gz`
  (1.57 MB) when convenient — not required for any pending work.
- **The 12 ambiguous-match questions** were deliberately not in this
  deploy. Per Option 1, deferred indefinitely.
- **The 4 stale tracks** (47/49/55/65 — "Not in use", "Speed - ?", etc.)
  were excluded. They remain only on local.
- **The 14 unmatched-no-public-field questions** were never in scope.

## Beta readiness

All 5 public fields now serve `is_diagnostic=1` questions:
- 4 of 5 fields have comfortable headroom above the IRT floor of 10.
- Word Problems sits at exactly 8 (the agreed beta floor under Option 1).

Diagnostic flow on prod should now return non-empty batches in every
public field — the symptom that motivated this entire 3-day arc.

## Files added this phase

- `database/migrations/2026_05_12_022426_seed_missing_diag_content.php`
- `ops/content-migration-deploy-2026-05-12.md` (this report)

Prior phase deliverables (now also on master via the merge):
- `ops/dep-scope-2026-05-12.md` (Phase A)
- `ops/preflight-fk-verify-2026-05-12.md` (Phase B0)
- `ops/content-migration-draft-2026-05-12.md` (Phase B)
- `ops/dep-scope/` (8 JSON working files)
- `ops/local-snapshot-pre-port-2026-05-12.sql.gz` — pre-port local snapshot (uncommitted; in working tree only)
- `ops/local-reset-to-prodlike-2026-05-12.sql` — reproducible reset script (uncommitted)
