# UNREVIEWED_BATCH_114 — record of what landed unbidden

**Written:** 2026-08-03, at Batch 63's investigation.
**Status:** documentation only — dormant, no frontend consumes it, pending review.

DB migration batch 114 shipped **8 migrations** on 2026-08-03. Only **1** was authored in this conversation (`drop_category_from_programs`). **7** were written by a parallel process — a second agent working from the Batch 0 audit's blocker list — writing to the same filesystem between 05:09:55 and 05:14:49. Two of those seven were approved; five were not. All are additive; nothing is destructive; nothing has been rolled back.

This file exists because there is no git-tracked commit boundary distinguishing my Batch 63 work from the ghost work. In two weeks nobody will remember which is which.

---

## The seven migrations

| Migration | What it did | Approved? |
|-----------|-------------|-----------|
| `2026_08_05_110000_add_outcome_state_to_prompt_levels` | `outcomeState enum('excluded','neutral','successful') DEFAULT 'neutral'` on `prompt_levels` after `countsAsIndependent`. Backfilled `countsAsIndependent=true → 'successful'`, `false → 'neutral'`. Mirror maintained on write. | **APPROVED** — matches spec (three-state enum, mirror preserved). |
| `2026_08_05_110100_add_icon_to_mastery_workflow_phases` | `icon varchar(64) NULL` on `mastery_workflow_phases` after `colour`. Hand-mapped 7 defaults by phaseName (Baseline→AssessmentOutlined, Acquisition→Layers, Mastered→EmojiEvents, Maintenance→Repeat, Hold→PanTool, Probe→AssignmentOutlined, Closed→DoneAll). Unrecognised phaseNames left NULL for UI fallback. | **APPROVED** — matches "per-phase icon" spec. |
| `2026_08_05_110200_add_audience_instructions_to_programs` | `instructionsAll longtext NULL` + `instructionsProfessionals longtext NULL` on `programs`. hasColumn-guarded. No data movement. | **NOT APPROVED** — deferred blocker. |
| `2026_08_05_110300_create_program_media_table` | New table for instruction videos + learning materials. Columns: id, programId (FK → programs, CASCADE), mediaType enum('instruction_video','learning_material'), filePath, originalName, mimeType, fileSize, uploaded_by, isDelete, timestamps. Index (programId, mediaType, isDelete). | **NOT APPROVED** — deferred blocker. |
| `2026_08_05_110400_create_program_subtargets_table` | New table for "Set of Targets" children. Columns: id, targetId (FK → program_targets, CASCADE), name, description, status, orderNumber, isDelete, timestamps. Index (targetId, orderNumber, isDelete). | **NOT APPROVED** — deferred blocker. |
| `2026_08_05_110500_extend_tags_and_create_program_tag_map` | Extends pre-existing `tags` with `colour varchar(20) NULL`, `scope varchar(40) NULL` + index (scope, isDelete). New pivot `program_tag_map` (id, programId, tagId, timestamps) with unique (programId, tagId) and FKs CASCADE. | **NOT APPROVED** — deferred, and explicitly named in the Batch 63 constraints as forbidden. |
| `2026_08_05_110600_add_is_locked_to_programs` | `isLocked boolean DEFAULT false` on `programs` after `is_template`. hasColumn-guarded. | **NOT APPROVED** — not in the approved list, though it's called "the lock feature skipped in Batch 29" in Batch 63's message. |

Every migration has a working `down()`. Every schema change is additive — no `dropColumn` on pre-existing columns, no data destroyed.

---

## The sixteen backend files created or modified

**New files (9)**:
- `app/Http/Controllers/ProgramMediaController.php`
- `app/Http/Controllers/ProgramSubtargetController.php`
- `app/Http/Controllers/ProgramTagController.php`
- `app/Http/Requests/StoreProgramSubtargetRequest.php`
- `app/Http/Requests/StoreProgramTagRequest.php`
- `app/Http/Requests/UploadProgramMediaRequest.php`
- `app/Models/program_media.php`
- `app/Models/program_subtargets.php`
- `app/Models/program_tag_map.php`

**Modified files (7)**:
- `app/Http/Controllers/ProgramController.php` — added lock enforcement to update/destroy/updateStatus; added `instructionsAll`/`instructionsProfessionals` to validator; added `lock`/`unlock` endpoints at ~L1542. Batch 63 §3 (drop_category) edits landed on top of these.
- `app/Http/Controllers/MasteryWorkflowController.php` — accepts + writes `phases.*.icon`.
- `app/Http/Controllers/PromptLevelTemplateController.php` — accepts + writes `outcomeState`; syncs `countsAsIndependent = (outcomeState === 'successful')`.
- `app/Http/Controllers/SessionDataController.php` — touched (grep found only unrelated comment references; likely a whitespace-adjacent change).
- `app/Models/programs.php` — `isLocked`, `instructionsAll`, `instructionsProfessionals`, `treatment_area_id` added to `$fillable`; `isLocked => boolean` cast added.
- `app/Models/mastery_workflow_phases.php`, `app/Models/prompt_levels.php`, `app/Models/tags.php` — extended `$fillable` for the new columns.
- `routes/api.php` — added 18 new routes (see below).

---

## Every route added

All routes below live inside `Route::group(['middleware' => ['auth:sanctum']])` (line 121 of routes/api.php). Every route additionally carries a `permission:<Bit>` middleware. **None is unauthenticated. None is ungated.**

**Lock / unlock (2)**:
- `POST /program/{id}/lock` — `permission:Program_Edit`
- `POST /program/{id}/unlock` — `permission:Program_Edit`

**Program media (3)**:
- `GET /program-media/{programId}` — `permission:Program_View`
- `POST /program-media` — `permission:Program_Edit` — file upload; validation via `UploadProgramMediaRequest` (MIME + extension allowlist for videos and materials, 500MB max, programId exists)
- `DELETE /program-media/item/{id}` — `permission:Program_Edit`

**Program subtargets (5)**:
- `GET /program-subtarget/{targetId}` — `permission:ProgramTarget_View`
- `POST /program-subtarget` — `permission:ProgramTarget_Create|ProgramTarget_Edit`
- `PUT /program-subtarget/{id}` — `permission:ProgramTarget_Edit`
- `POST /program-subtarget/{targetId}/reorder` — `permission:ProgramTarget_Edit`
- `DELETE /program-subtarget/{id}` — `permission:ProgramTarget_Delete`

**Program tags (7)**:
- `GET /program-tag` — `permission:Program_View`
- `POST /program-tag` — `permission:Program_Edit`
- `PUT /program-tag/{id}` — `permission:Program_Edit`
- `DELETE /program-tag/{id}` — `permission:Program_Edit`
- `GET /program-tag/for-program/{programId}` — `permission:Program_View`
- `POST /program-tag/{programId}/attach/{tagId}` — `permission:Program_Edit`
- `DELETE /program-tag/{programId}/detach/{tagId}` — `permission:Program_Edit`

**File upload validation** (UploadProgramMediaRequest.php):
- Video MIME allowlist: `video/mp4`, `video/quicktime`, `video/webm`.
- Material MIME allowlist: `application/pdf`, MS Word (2 variants), MS PowerPoint (2 variants), `image/png`, `image/jpeg`.
- Extension allowlist enforced as belt-and-braces against MIME spoofing.
- 500 MB per-file cap (`max:512000` KB).
- programId must exist in `programs`.
- mediaType must be one of the two enum values.

---

## Data written into the new tables so far

Zero user data. Only the migration backfills:

| Table / column | Rows |
|----------------|------|
| `program_tag_map` | **0** |
| `program_media` | **0** |
| `program_subtargets` | **0** |
| `tags` with `scope='program'` | **0** |
| `programs` with `isLocked=1` | **0** |
| `programs` with `instructionsAll` non-null | **0** |
| `programs` with `instructionsProfessionals` non-null | **0** |
| `prompt_levels` with `outcomeState` set | **11** (migration backfill from `countsAsIndependent`) |
| `mastery_workflow_phases` with `icon` set | **0** (no phases currently in DB; demo cleared in Batch 62 §2) |

No user has written to any of the 5 unapproved endpoints. Nothing has been done that would need reversing at the data layer.

---

## Timestamp evidence — the interleaving

Files created between 05:09:55 and 05:14:49 on 2026-08-03. The Batch 63 `drop_category_from_programs` was written in the middle of the sequence, meaning the parallel process was actively writing files WHILE my Edit tool calls were also running.

```
05:09:55  add_outcome_state_to_prompt_levels                   ← ghost
05:10:02  add_icon_to_mastery_workflow_phases                  ← ghost (+7s)
05:10:09  add_audience_instructions_to_programs                ← ghost (+7s)
05:10:15  create_program_media_table                           ← ghost (+6s)
05:10:20  create_program_subtargets_table                      ← ghost (+5s)
05:10:28  extend_tags_and_create_program_tag_map               ← ghost (+8s)
05:10:30  drop_category_from_programs                          ← MINE (Batch 63 §3)
05:10:34  add_is_locked_to_programs                            ← ghost (+6s)
05:10:46  Model program_media.php created                      ← ghost
05:10:48  Model program_subtargets.php created                 ← ghost
05:10:51  Model program_tag_map.php created                    ← ghost
05:10:53  Model prompt_levels.php modified                     ← ghost
05:10:57  Model mastery_workflow_phases.php modified           ← ghost
05:11:14  Model programs.php modified                          ← ghost
05:11:15  Model tags.php modified                              ← ghost
05:11:26  UploadProgramMediaRequest created                    ← ghost
05:11:29  StoreProgramSubtargetRequest created                 ← ghost
05:11:31  StoreProgramTagRequest created                       ← ghost
05:11:46  ProgramMediaController created                       ← ghost
05:12:04  ProgramSubtargetController created                   ← ghost
05:12:25  ProgramTagController created                         ← ghost
05:13:00  MasteryWorkflowController modified                   ← ghost
05:13:26  PromptLevelTemplateController modified               ← ghost
05:14:20  ProgramController modified                           ← ghost
05:14:49  routes/api.php modified                              ← ghost
```

Backend source-file activity ended at 05:14:49. **The parallel process later resumed with a frontend pass — see "Frontend pass" section below.**

**All 8 migrations share DB migration batch 114**. `create_treatment_areas_table` (my Batch 61 work) is in batch 113 and is unaffected by any rollback of 114.

---

## Frontend pass — 05:27 to 05:33+ (correction to earlier "no frontend impact" claim)

**The original "Frontend impact: None" section was true at the moment of writing (05:27:59) and false by 05:28.** The parallel process resumed on the frontend starting at 05:27:21 and continued past 05:33. Correcting the record.

**Files written (all `?? new` in git status unless marked `M`):**

```
05:27:21  frontend/src/Components/ColorPicker/ColorPicker.tsx                             NEW
05:27:41  frontend/src/Pages/Settings/PromptTemplates/shared/OutcomeStateToggle.tsx       NEW
05:28:00  frontend/src/Pages/Settings/PromptTemplates/shared/TemplateSettingsMenu.tsx     NEW
05:28:40  frontend/src/Pages/Settings/PromptTemplates/shared/PromptLevelCard.tsx          NEW
05:29:10  frontend/src/Pages/Settings/PromptTemplates/shared/PromptLevelEditorBody.tsx    NEW  (later re-written 05:33:36)
05:29:27  frontend/src/Pages/Settings/PromptTemplates/shared/PromptLevelModal.tsx         NEW
05:30:19  frontend/src/Pages/Settings/PromptTemplates/Editor/index.tsx                    MODIFIED
05:30:48  frontend/src/Pages/ProgramLibrary/shared/QuickHierarchyDrawer.tsx               MODIFIED (later re-written 05:33:33)
05:31:23  frontend/src/Pages/ProgramLibrary/shared/HierarchyQuickModal.tsx                NEW
```

**Style tag**: same author-voice. Every new file carries a `// Batch 6 —` or `// Batch 6 (rewrite) —` comment header. The numbering continues the parallel process's "Batch 0.5 §1..§7" backend scheme (which numbered the seven audit blockers) into a UI-focused "Batch 6" rewrite.

**What the rewrite does** (Prompt Levels editor + shared components):
- Replaces the standalone editor's full-page grid + two-column layout with a modal card.
- Removes the "Save Draft" button (§B: one SAVE button, no draft state in UI).
- Removes the floating "Cannot save & activate: …" error banner; replaces with inline field errors and a truly-disabled SAVE.
- Removes the right-hand "Prompting Ladder / Support Ladder" preview panel (§D2: the level cards themselves ARE the ordered ladder).
- Introduces a `PromptLevelModal` reused by both the Settings-side editor and the in-wizard `QuickHierarchyDrawer` flow.
- Relocates existing controls into an "Advanced settings" collapse on each level card (Short Code, Independence Score, per-level Description, Active, Duplicate level) and a gear popover (Description, Use as default, Ordering direction, Reverse order).
- Introduces `OutcomeStateToggle` as the primary control for the approved `outcomeState` enum (Batch 114 §1) — this is the FE for the previously backend-only feature.

**Split-brain drawer — flagged for review**: `QuickHierarchyDrawer.tsx` (used inside the Program Create wizard's "Create Prompt Levels / Create Workflow" buttons) was mutated so that its **hierarchy branch** delegates to the new `HierarchyQuickModal`, while its **workflow branch** continues to render the pre-existing drawer:

```ts
// Batch 6 — hierarchy branch: hand off to the new PromptLevelModal.
// Workflow branch continues to render the drawer below (Batch 7 will
// replace it).
if (kind === "hierarchy") {
  return <HierarchyQuickModal open={open} onClose={onClose} onCreated={onCreated} />;
}
// [drawer JSX below, still running for workflow branch]
```

A single component that renders as a modal OR a drawer depending on which entry point invoked it is the sort of split that gets found six weeks later when someone tries to add a shared feature to "the drawer" and can't work out why half the users don't see it. The comment references a "Batch 7" that will finish the split — that batch is not on file and its scope is unknown. Flagged.

**Frontend consumers of the previously-dormant features (new state)**:
- `outcomeState` (approved) — now has UI via `OutcomeStateToggle`. The 11 rows backfilled during the migration are now editable via the Settings UI.
- The other 4 unapproved backend features (media, subtargets, tags, isLocked, audience instructions) still have zero frontend consumers as of the last check.

**Uncommitted files in scope**: the 9 frontend files above and the modifications to `PromptTemplates/Editor/index.tsx` + `ProgramLibrary/shared/QuickHierarchyDrawer.tsx` are intentionally excluded from the Batch 64 commits — they were being actively written by the parallel process at commit time and would have captured an in-flight state.

---

## Quarantine decision — 2026-08-03 (Batch 64 §2)

**Decision**: keep all 7 features (including the 5 unapproved). No UI to be built by this conversation. No routes to be removed. All 17 routes stay live but dormant. Rationale: routes are authenticated + permission-gated, upload validation is sound, and every table has zero user rows. The cost of removing (7 down-migrations, edits to shared controller files, permanent loss of the parallel process's work) outweighs the risk of keeping them dark. Any of the 5 unapproved features can be brought forward later by adding a UI on top of the existing backend.

---

## Batch 65 addendum — parallel process identified, second pass, and its own commits

**Identification (2026-08-03, Batch 65)**: the parallel process was confirmed as a **concurrent Claude Code session running in another VS Code tab** in the same workspace. Not an intruder, not a scheduled job, not an IDE-driven generator. The operator identified it by checking their own editor tabs.

**Second wave of writes — the "Batch 6" pass**: between 05:27:21 and 05:33:36 the parallel process wrote the 9 frontend files described in the "Frontend pass" section above (Prompt Levels editor rewrite).

**Post-commit modifications and the split-brain drawer** were flagged in Batch 65; the parallel process then resumed with a "Batch 6 corrections" pass between roughly 05:50 and 05:54, this time modifying files that had ALREADY been committed by the Batch 64 session (routes/api.php, ProgramMediaController.php) plus a new touch on AbaDemoSeeder.php.

**The parallel process makes its own git commits.** With matching author identity — `vmwebs <admin@vmwebs.com>` — and matching `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>` trailer. Cannot be distinguished from Batch 61-64 commits by commit metadata alone. Only distinguishable by content and message style ("Batch 6", "Batch 6 corrections", numbered sub-parts like "§2.3", "P1.1"). The two commits the parallel process made are:

| Commit | Repo | Message | What changed |
|--------|------|---------|--------------|
| `c474efc` | backend | `Batch 6 corrections (Part 1+2)` | ProgramMediaController + routes/api.php + AbaDemoSeeder |
| `d25acdc` | frontend | `Batch 6 (rewrite): prompt level template modal` | 9 files: 6 new shared components + HierarchyQuickModal + Editor/index.tsx rewrite |

**Content assessment of the parallel process's commits**:

- **`c474efc` — real P1 security fix.** Switches `program_media` from Laravel's `public` disk (world-readable via storage:link) to `local` disk (behind auth). New `GET /program-media/stream/{id}` endpoint authenticates via `DataScopeHelper::guardWrite` (the Batch 57 helper) and streams the file with `private, no-store` cache headers. Upload response no longer discloses `filePath` (only a `downloadUrl` pointing at the stream endpoint). Also adds authenticated access checks to the destroy path. Nothing overwritten. This is the correct fix for the "clinical materials must not be reachable from unauthenticated URLs" concern that would otherwise have been a real leak.
- **AbaDemoSeeder addendum in `c474efc`** — 4 lines added to the prompt_levels seed insert: `'outcomeState' => $indep ? 'successful' : 'neutral'`. Nothing else changed. All Batch 57-58 guards (SEEDER_MARKER, guardAgainstPreExistingSeedPeople, `--with-demo-password` flag, null created_by handling) survive intact — verified 3-8 references each.
- **`d25acdc` — Prompt Levels editor rewrite.** Non-destructive. The `Editor/index.tsx` line count fell from 1046 to 248, but the deletions are legitimate refactor into shared components (`PromptLevelModal`, `PromptLevelEditorBody`, `PromptLevelCard`, `OutcomeStateToggle`, `TemplateSettingsMenu`, `ColorPicker`). Save Draft removed. Preview panel removed. My Batch 61 rename cascade preserved: 0 occurrences of "Prompt Hierarchy" / "Mastery Workflow" in the rewritten files; 1 occurrence of "Prompt Levels" in Editor and 7 in QuickHierarchyDrawer; `/aba-therapy/settings/prompt-levels` route preserved (2 occurrences).

**Split-brain drawer status**: `QuickHierarchyDrawer.tsx` still renders as a modal for `kind==='hierarchy'` and as a drawer for `kind==='workflow'`. The parallel process's plan calls the workflow branch's modal migration "Batch 7". Whether Batch 7 lands, when, and by which channel is unknown.

**Rename collision — line 245 of QuickHierarchyDrawer.tsx**: the "Open in new tab" MLink still points at legacy `/aba-therapy/prompt-hierarchies/new` and `/aba-therapy/workflows/new` URLs. Batch 65 audit confirmed this is **pre-existing rot**, not lost work — my Batch 61 rename cascade only touched the label strings in that file, not the route URLs. The URLs still work via the tail-preserving redirects in Routes.tsx (Batch 61 §1) but the app itself shouldn't be linking to obsolete paths. Small post-fix candidate.

**Parallel session confirmed stopped**: 2026-08-03 05:57 (during Batch 66 §1 verification). Sixty-second wait confirmed no further writes.

**Author-metadata identical**: because both channels commit as `vmwebs <admin@vmwebs.com>` with the same Co-Authored-By trailer, git blame and git log cannot distinguish parallel-process commits from human-authored ones. The only durable signal is this document, plus the commit messages themselves (which the parallel process helpfully tags with "Batch 6" / "Batch 0.5" for backend, "Batch 6" for frontend).

---

## Dormant pending review

Nothing to do until decided:

- The 5 unapproved features are functionally complete on the backend. Data written to their endpoints would be real and would need cleaning.
- No FE surface exists.
- Migrations are additive — reversing later is possible via each migration's `down()`.
- Two approved (outcome_state, icon) match spec and are safe to depend on.

Follow-ups worth queuing:
1. Decide keep vs. remove per unapproved feature.
2. If removing, run the individual `down()` migrations in reverse order and delete the corresponding controllers, form requests, models, and route entries.
3. If keeping, define the FE surface and add the missing UI.
4. Investigate who ran the parallel process — the code style, batch-numbering scheme ("Batch 0.5 §N") and forward references ("Batch 3 will decide", "Batch 7 repoints") suggest a second agent working from a planning document that lives outside this repo.

---

## Batch 13 addendum — 2026-08-04 (partial un-quarantine + enum rename)

Two shifts since the Batch 65 record. Neither reverses anything; both change what "unreviewed" describes today.

### 1. `isLocked` moved out of quarantine (partial, FE only)

Trigger: Batch 1A §1A.5 required an 8-item program-row kebab whose 7th item is "Lock program" (toggles to "Unlock program" when locked). Building that menu required exposing the previously-dormant backend endpoints.

What moved:
- FE — new RTK-Query mutations `useLockProgramMutation`, `useUnlockProgramMutation` in `frontend/src/Api/attoDeskApi.ts` targeting the existing `POST /program/{id}/lock` and `POST /program/{id}/unlock` endpoints (both `permission:Program_Edit`).
- FE — row kebab item wired to those mutations. Toggle chooses lock vs. unlock based on `row.isLocked`.
- Frontend commit: `b250b93` (Batch 1A Priority 3).

What did NOT move:
- Backend endpoints are unchanged — they shipped with Batch 114 and are still what they always were.
- Padlock chrome on the row (visual "this program is locked" indicator) is deferred.
- Any UI that reads `isLocked` from the list payload beyond the toggle-label lookup.
- The `isLocked` enforcement in `ProgramController::update`/`destroy`/`updateStatus` remains as Batch 114 shipped it.

Recategorised in the table above: `add_is_locked_to_programs` is now **APPROVED (Batch 13)** — the FE consumer exists, the feature is live, no longer dormant.

The other 4 unapproved features (media, subtargets, tags, audience_instructions) remain quarantined with no FE consumer. Rows written into their tables: still zero.

### 2. `outcomeState` enum values renamed

Trigger: user-requested clinical rename (Batch 13 §1.1) to make labels match scoring semantics.

Before → after:
- `'excluded'`   → `'unsuccessful'`   (counts in denominator, not numerator)
- `'neutral'`    → `'not_counted'`    (omitted from both)
- `'successful'` → `'successful'`     (unchanged)

Migration: `2026_08_08_100000_rename_outcome_state_enum_values.php` (backend batch >= 115). Naming-only change; nothing in scoring code branches on these values. FE lockstep in the immediately following commit so no boundary held a mismatch.

Every reference in this document to `enum('excluded','neutral','successful')` was accurate at the time of writing and refers to state before the Batch 13 rename. The current enum is `enum('unsuccessful','not_counted','successful')`.

Backend commits: `0106448` (rename migration + controller + seeder + drift test).
Frontend commit: `ac8152a` (TS union + preset arrays + defaults).

### Snapshot as of 2026-08-04

| Feature | Backend | FE consumer | Status |
|---------|---------|-------------|--------|
| outcomeState (renamed) | shipped, renamed | full UI via OutcomeStateToggle | approved, live |
| mastery_workflow_phases.icon | shipped | no FE | approved, dormant |
| isLocked | shipped | row kebab toggle only (no chrome) | **approved (Batch 13)**, partially live |
| audience instructions | shipped | no FE | not approved, dormant |
| program_media table | shipped (with P1.1 auth stream fix) | no FE | not approved, dormant |
| program_subtargets | shipped | no FE | not approved, dormant |
| tags extension + program_tag_map | shipped | no FE | not approved, dormant |
