UAT — admin / supervisor workflow (ops console acceptance)
Test account: man_on_rock@redpoint-vision.com (admin, real inbox).
Site: https://wakecut.com — the ops surfaces live under /ops.
Runs alongside docs/ops/UAT-CLIENT-WALKTHROUGH.md
(client = dorian@redpoint-vision.com) and
docs/ops/UAT-EDITOR-WALKTHROUGH.md (editor = rp.editor@example.com). Use
separate browser profiles — many admin steps QC / approve / assign the jobs the
other two sheets create.
This is the overall supervisor's sheet: the console tour, finance review,
the editor-supervision surface (1E), and the GDPR / retention operator jobs.
Every step is Pass / Fail-able. /ops is admin-only — a non-admin session is
redirected to /jobs, so all of the below fails closed for the wrong role
(worth confirming once as the client/editor).
The promise this whole surface must keep: you can see and steer every job and editor across every tenant, money never moves from a review screen, and every destructive or income-affecting action is deliberate, attributed, and audited.
🧱 Migration state. The ops surfaces below read migrations
0034–0040, applied and probe-verified live 2026-07-18 (the0040paste was verified this session: the replacedrecord_supervisionresolves with the0040signature and therejectedstatus filter returns 200). The supervision confirm-queue + manual-action steps (§4.3–§4.4) depend specifically on0040(admin attribution in the audit + therejectedaudit status). Expected present — but rule 8 still stands: verify before relying, and use the §4.4 functional probe as belt-and-braces.
📧 Emails you verify here.
man_on_rock@…is a real inbox, so the admin-facing SLA-breach email is directly checkable. Editor/client emails are confirmed via the admin-readablenotificationstable (one row per event,status = sent, uniquededupe_key) — see the editor sheet's email callout. RequiresRESEND_API_KEYin the Vercel prod env.
1 · Ops console tour (/ops)
Precondition: logged in as man_on_rock@…; at least one job exists (run the
client sheet §1 once, or use seed jobs).
| # | Do | Expect |
|---|---|---|
| 1.1 | Open /ops |
Ops console — all jobs across all tenants, one table (Job · Tenant · Package · Client · Editor · Status · SLA), sorted by deadline with breaches first (a past-deadline live job floats to the top, row tinted) |
| 1.2 | Use the status filters (Open / All / each status) | The table filters in place; Open hides DELIVERED/CANCELLED. A tenant filter row appears only when more than one tenant exists (wakecut + expocut) |
| 1.3 | On a NEW, unassigned job read the pool chip |
"in pool" (info) when the effective dispatch mode is pool, "withheld" (muted) when managed — the effective mode is override → package → pool |
| 1.4 | If any pool job has stalled (editors passed it, or a client scope-change is pending) | A "Needs attention" section above the table: pushback cards show "{N} of {M} editors passed" + the pass-reason chips (ops-only), with a release-prompt button; scope-change cards show "Scope change — needs ack" + the client's note, with an ack button. (To generate one: editor sheet §2.2 passes, or ops releases the prompt.) |
| 1.5 | Click a job id | Lands on /ops/jobs/[id] — the job detail (§2) |
Pass bar (§1): every tenant's jobs visible in one deadline-sorted list, breaches surfaced, and the needs-attention queue reflects real pushback / scope state.
2 · Job detail — lifecycle, QC, dispatch (/ops/jobs/[id])
Precondition: a job mid-pipeline. Best run interleaved with the editor sheet (§3 there submits for QC; §4 here QC-passes).
| # | Do | Expect |
|---|---|---|
| 2.1 | Open a job detail | Header: short job id + an admin status chip. Meta block: tenant, package (with revisions used / limit), client (company/name + email — ops sees full client identity), assigned editor pseudonym + quality readout, deadline, footage readiness, created. A money card (§3 cross-ref), the rendered brief, the files list, and an events timeline |
| 2.2 | On a NEW job with footage not ready |
The assign panel shows "Footage not ready — assignment is blocked…"; the Assign button stays disabled until at least one original has landed and no import is in flight |
| 2.3 | On a NEW, footage-ready job: pick an editor + a deadline, Assign |
Editor dropdown lists this tenant's approved + available editors with load/cap (options disabled when unavailable or at cap). Assign transitions NEW → ASSIGNED, sets the internal deadline. Assigning fires the "New job assigned" email (an admin assign, unlike a board self-claim) — verify a job_assigned row in notifications |
| 2.4 | Walk a job through the lifecycle buttons | ASSIGNED → Mark in progress; IN_PROGRESS → Mark submitted for QC; the actions panel only ever offers legal next states (from the transition rules) |
| 2.5 | QC pass on an IN_REVIEW job: "QC pass → client review" |
The job does not jump straight to client review — it enqueues a watermarked preview transcode and stays in QC, surfacing a "Preview queued — awaiting transcode" note; it advances to CLIENT_REVIEW automatically once the preview lands (Phase 9). The assigned editor's QC-pass counter increments |
| 2.6 | QC reject on an IN_REVIEW job: type the required QC note, then "QC reject" |
Button stays disabled until the note is non-empty; on reject the job → QC_REJECTED and the editor gets "Changes needed before review (…)" carrying the note verbatim (editor sheet §3.4📧 — verify via notifications) |
| 2.7 | On a CLIENT_REVIEW job use "Approve (admin override)" or "Request revision" |
Admin can approve on the client's behalf or push a revision (optional note); these mirror the client review actions |
| 2.8 | Dispatch toggle on a NEW, unassigned job |
A pool ⇄ withheld override (jobs.dispatch_override) lets you hold a job back from the self-claim board or release it, independent of the package default |
| 2.9 | Cancel a cancellable job → "Cancel job (void authorization)…" | A confirm dialog explains it voids the payment authorization; on confirm the job → CANCELLED and the Stripe auth is voided (never captured) |
Pass bar (§2): every transition button offers only legal next states, QC pass queues a preview (not an instant advance), QC reject demands a note and emails the editor, and cancel voids the auth.
3 · Finance — margins & commissions (/ops/finance)
Precondition: admin session. Realized figures are empty until a job has been captured (client sheet §4.7).
3a · Margins (/ops/finance)
| # | Do | Expect |
|---|---|---|
| 3a.1 | Open /ops → Finance |
Heading Finance, "Modeled vs realized — realized from the verified Stripe ledger". Sub-nav to Pricing / Margin builder / Commissions / Jobs |
| 3a.2 | Read Per package — modeled vs realized | Each package's modeled margin (£ + %) from price vs editor fare, and a Realized column (from the F2/F3 Stripe ledger) — "no data" until a capture lands. A realized figure far below modeled tints red (drift; the threshold is billing_config.drift_alert_bps, tenant config not code) |
| 3a.3 | If the Realized — by month table shows | Per-currency rows from the verified ledger (revenue, Stripe fees, fares, refunds, gross margin, jobs) — GBP and EUR never summed together |
| 3a.4 | If a "N jobs missing a price snapshot" warning shows | Investigate — those jobs' money terms are undefined (should be zero) |
3b · Partner commissions — dry-run (/ops/finance/commissions)
Precondition: migration 0035 applied (else the page shows a "Migration
0035 is not applied…" notice and records nothing). This page is read-only —
no money moves from it.
| # | Do | Expect |
|---|---|---|
| 3b.1 | Open Finance → Commissions | Heading Partner commissions, "Dry-run accruals — review before arming" |
| 3b.2 | Read the Arming state panel | Global env PARTNER_COMMISSION_ENABLED shown as "off (dry-run)" (or ARMED). A real transfer needs both that env and a tenant's commission_live on — both default off, so nothing transfers |
| 3b.3 | Look for first-party tenants (wakecut) in the Partner tenants table | Absent — wakecut is the merchant, keeps everything, and is not listed as a partner. Only tenants with a Connect destination appear. Its capture path is byte-identical (no accrual) |
| 3b.4 | On a partner row with no platform_fee_bps |
A "fee unset" chip (tooltip: "No platform_fee_bps set — Redpoint's full cut accrues to the partner (accrual uses 0)") instead of a bare 0% |
| 3b.5 | If any accrual mixed currencies (EUR gross vs GBP fare) | An FX-held banner — those accruals are HELD: never auto-transfer even when armed, and excluded from the would-transfer subtotals, pending a real EUR→GBP conversion |
| 3b.6 | Read the Accruals table + commission_live column |
Per-accrual audit (gross − editor − platform fee − Stripe fee = commission), newest first; commission_live shows off (muted) for every tenant by default. Confirm the subtotal note: currency = gross currency, Stripe fee is a model estimate, nothing moves money |
Pass bar (§3): modeled and realized margins read from config + the verified ledger (never guessed); commissions are dry-run with first-party tenants excluded, the fee-unset chip and FX-held banner visible where they apply, and no control on either page moves money.
4 · Editor supervision — the 1E ops surface (/ops/editors/supervision)
The supervisory layer scores each (editor, tenant) pair from client ratings + QC/on-time/revision counters and can warn / throttle / suspend / reinstate. It is OFF by default and, even when on, queues every action for a human (auto-actions default false). This section proves the surface; enable it on a test tenant first, then disable after.
4.0 · Precondition — enable / disable supervision (config, no migration)
Supervision reads tenants.dispatch_config.supervision. With it off, the
roster shows every editor as "unscored" / "off" and no action is
possible — that is the correct default state and worth confirming first. To
exercise §4.1–§4.5, enable it on the tenant you are testing (wakecut, or the
e2e/test tenant), then disable it afterwards if you don't intend to run
supervision live.
The paste merges the supervision node into dispatch_config (top-level
|| replaces only that key, preserving the sibling pushback/dispatch knobs).
Run it as the operator (Supabase SQL editor, or the Management API per CLAUDE.md)
— pick the tenant by slug.
ENABLE (documented engine defaults — enabled:true, auto_actions:false,
so actions queue for you, never auto-apply):
update public.tenants
set dispatch_config = coalesce(dispatch_config, '{}'::jsonb) || jsonb_build_object(
'supervision', jsonb_build_object(
'enabled', true,
'auto_actions', false,
'min_sample_size', 5,
'score_window', jsonb_build_object('kind', 'recent_jobs', 'n', 20),
'weights', jsonb_build_object(
'client_rating', 0.40, 'qc_pass', 0.30,
'on_time', 0.20, 'revision', 0.10),
'ladder', jsonb_build_object(
'warn', jsonb_build_object('below_score', 0.70),
'throttle', jsonb_build_object('below_score', 0.55,
'cap_to', 1, 'deprioritise', true),
'suspend', jsonb_build_object('below_score', 0.40)),
'reinstate', jsonb_build_object('mode', 'manual', 'auto_above_score', 0.65),
'guards', jsonb_build_object('max_auto_suspends_per_day', 2)
))
where slug = 'wakecut'; -- or your e2e / test tenant slug
These are the exact
DEFAULT_SUPERVISIONvalues fromsrc/lib/supervision.ts.score_window.n(20) is ≥min_sample_size(5) on purpose — a window below the floor would trip the config warning chip (§4.1) and leave the layer silently inert.auto_actions:falseis the safe stance for an income-sensitive layer: transitions queue, a human confirms.
DISABLE (restore the default — everyone reads unscored/off, no teeth):
update public.tenants
set dispatch_config = coalesce(dispatch_config, '{}'::jsonb) || jsonb_build_object(
'supervision', jsonb_build_object('enabled', false, 'auto_actions', false))
where slug = 'wakecut'; -- match the slug you enabled
| # | Do | Expect |
|---|---|---|
| 4.0.1 | With supervision off, open /ops/editors/supervision |
Every roster tenant shows a muted "supervision off" chip and every editor an "unscored" / "off" standing — no ladder line, no breaker chip. This is the correct dormant default |
| 4.0.2 | Apply the ENABLE paste to your test tenant, reload | That tenant's chip flips to "supervision on" and shows its floor + ladder line (warn/throttle/suspend cutoffs) + a breaker count — all read from the parsed config (rule 5) |
| 4.0.3 | After finishing §4, apply the DISABLE paste, reload | The tenant returns to "supervision off"; standings read unscored again; the board (editor sheet §2) is byte-identical to dormant |
4.1 · Roster
Precondition: supervision enabled on the test tenant (§4.0). Meaningful
scores need editors with ≥ min_sample_size client ratings (client sheet §4b) —
below the floor an editor stays "unscored" even when the layer is on.
| # | Do | Expect |
|---|---|---|
| 4.1.1 | Read the per-tenant roster | Editors grouped by tenant, worst standing first. Each row: a standing chip (good / warn / throttle / suspend, or muted unscored), the editor pseudonym + real name (ops-only identity — never leaks to a client/editor surface), and readouts: score (composite %), rating (avg + n=count), QC pass / on-time / revisions rates, sample size (with "below floor" when thin) |
| 4.1.2 | Read the tenant header chips | floor N · ladder warn<…/throttle<…/suspend<… (from config), and a breaker M/K chip (auto-suspends applied today vs the per-day cap) that tints danger when the cap is hit |
| 4.1.3 | If you set score_window.n below min_sample_size |
A config warning appears: "score window is smaller than the sample-size floor — no editor can ever clear the floor, so the layer is silently inert…". Behaviour is unchanged (editor-safe); the warning just surfaces the foot-gun |
| 4.1.4 | Click an editor pseudonym | Opens the per-editor audit detail (§4.2) |
4.2 · Per-editor audit detail (/ops/editors/supervision/[editorId])
| # | Do | Expect |
|---|---|---|
| 4.2.1 | Open an editor's audit page | Their standing by tenant (chip + score + rating + sample per tenant) and a global QC/on-time/revision readout |
| 4.2.2 | Read the Action history table | Append-only supervisory_actions: when · tenant · action · status · by (a system chip for engine/auto, or the acting admin's name for a manual/confirm action) · prior → new standing · score-at-the-time · reason. This is the audit trail every §4.3–§4.4 action lands in |
4.3 · Confirm queue — approve / reject
Precondition: supervision enabled; migration 0040 (applied live — see the
migration callout). A queued proposal appears when the engine proposes an action
under auto_actions:false — e.g. an editor's score drops below the warn cutoff
after a low rating (client sheet §4b, 1–3★), then the per-rating hook (or the
supervision cron) queues it.
| # | Do | Expect |
|---|---|---|
| 4.3.1 | On the roster (or the editor detail) find a queued proposal | A Confirm queue section at the top of the roster lists each (editor, tenant) with a "Proposed: {verb}" chip, current standing, and score; the roster row also carries a "queued: {verb}" chip |
| 4.3.2 | Approve a proposal: type the required reason, click Approve | The proposed action applies (standing changes, teeth engage); a new applied audit row lands attributed to you with your reason. The queue entry clears |
| 4.3.3 | Reject a different proposal: type the reason, click Reject | The standing is unchanged, the queue clears, and a rejected audit row is written attributed to you (re-proves 0040 in passing — a pre-0040 DB would reject the status) |
| 4.3.4 | Both buttons with an empty reason | Blocked — "a reason is required" (enforced client- and server-side) |
4.4 · Manual actions — warn / throttle / suspend / reinstate
| # | Do | Expect |
|---|---|---|
| 4.4.1 | On a roster row (or the editor detail) click "Manual action…" | A form: a verb select (Warn / Throttle / Suspend / Reinstate) + a required reason field |
| 4.4.2 | Warn an editor with a reason, Apply | The standing → warn; an applied, auto=false, admin-attributed audit row lands with your reason. Warn is informational (no dispatch teeth) |
| 4.4.3 | Throttle an editor | Standing → throttle; the row shows a "cap {N}" chip (reduced concurrent cap, magnitude from config); their pool jobs sink below good-standing work |
| 4.4.4 | Suspend an editor | Standing → suspend; that (editor, tenant) is removed from that tenant's board. A manual suspend is not subject to the auto-suspend breaker (the breaker guards automatic mass-suspends only) |
| 4.4.5 | Reinstate a held editor | A distinct reinstate audit row lifts the hold to good; the fresh ladder re-evaluation is left to the next refresh (never fused into the reinstate row) |
| 4.4.6 | Any manual action with an empty reason | Blocked — "a reason is required" |
| 4.4.7 | (0040 belt-and-braces probe — expected to pass) |
Every manual action succeeds and its audit row carries your name. If one 500s, or an audit row reads system instead of you, the live DB has regressed from the verified 0040 state — re-probe and re-apply before continuing |
4.5 · Supervision at assign time (ties §2.3 to §4)
Precondition: an editor is throttled or suspended for a tenant
(§4.4), and that tenant has a NEW, footage-ready, unassigned job.
| # | Do | Expect |
|---|---|---|
| 4.5.1 | Open that job's assign panel; read the editor dropdown | A throttled/suspended candidate's option is annotated ", throttled" / ", suspended" alongside its load/cap — a warning, not a block (the 1D warn-not-block rule: an admin may still deliberately assign) |
| 4.5.2 | Select the suspended/throttled editor | A warning chip appears: "{editor} is suspended for this tenant. Assigning overrides supervisory dispatch — allowed, but review the supervision roster first." The Assign button still works |
| 4.5.3 | A good / unscored candidate | No annotation, no warning — the assign panel is byte-identical to §2.3 |
Pass bar (§4): off by default (unscored everywhere); enabling shows
config-driven ladder + breaker chips and the config-warning foot-gun; the confirm
queue and manual levers each demand a reason and write an attributed audit row
(applied or rejected); a suspended/throttled candidate is warned, not
blocked, at assign time.
5 · Voice notes — admin view (/ops/jobs/[id])
Precondition: the client recorded ≥1 voice note on a job (client sheet §2b);
migration 0034 applied.
| # | Do | Expect |
|---|---|---|
| 5.1 | Open the job detail → Files | The client's voice note appears as a file of kind voice_note in the list, one-click downloadable with your admin session (the double-blind player is the editor workspace; ops sees it as a downloadable file) |
| 5.2 | Read the Events timeline | A voice_note_added event per note (the audit that a note was recorded) |
| 5.3 | Confirm the client identity boundary is intact | The voice note is audio only — nothing about it exposes anything the brief doesn't already show to the editor |
Pass bar (§5): voice notes are visible and downloadable to the admin as
voice_note files with a matching event, nothing more.
6 · Rate-your-edit — admin visibility
Precondition: at least one client rating exists (client sheet §4b);
migration 0037 applied. Supervision on (§4.0) to see the aggregate on the
roster.
| # | Do | Expect |
|---|---|---|
| 6.1 | On the supervision roster / editor detail read the rating readout | The editor's average stars + n=count per tenant (the aggregate that feeds the composite score). The raw stars — never the client's free-text comment — are what ops sees here |
| 6.2 | Confirm the comment boundary | The client's rating comment is client PII and is not surfaced on the roster/audit (the roster reads stars only). Admin does not have a general screen that shows rating comments |
| 6.3 | Cross-check the tie-in is editor-demand signal, not an admin lever | A 4–5★ rating offers the client the paid specify-editor re-request (client sheet §4b) — the admin sees its effect only as held jobs (editor sheet §2b) and, if priced, commission/margin, never as a rating override |
Pass bar (§6): admin sees rating aggregates (avg + n) on the supervision surface, never individual client comments, and the 4–5★ tie-in reads as a demand signal, not an admin control.
7 · In-app UAT widget — admin view
The floating UAT checklist widget is gated by enrollment in uat_testers
(not by admin role). To see it as the admin, enroll man_on_rock@….
Precondition: a uat_testers row for the admin profile; migration 0023
applied. (No row → the widget renders nothing and ships zero client JS — the
safe default.)
| # | Do | Expect |
|---|---|---|
| 7.1 | Enroll the admin in uat_testers, then open any live page as man_on_rock@… |
A small "UAT n/total" pill appears top-right — it never overlays a primary CTA, the cookies control, or the dev overlay |
| 7.2 | Open the pill | The panel shows the Client and Editor checklist tabs, each step with Pass / Fail toggles + a note field + a build stamp |
| 7.3 | Mark a step, then check from a second tester account | State is shared via uat_results — the aggregate reads "you: pass · team: 1✓ 1✗", so the whole team's verdicts are visible |
| 7.4 | Confirm the fence | A signed-out visitor and a signed-in non-tester see nothing (unchanged DOM, no widget) — it is invisible and zero-cost to real users |
Pass bar (§7): the widget appears only for enrolled testers, carries the shared team state, and is wholly absent for everyone else.
8 · Right-to-erasure (GDPR Art.17) — DESTRUCTIVE, admin-only
⚠️ IRREVERSIBLE. Run against a THROWAWAY client subject only — never a real
client. This is an operator action (POST /api/ops/erasure) with no ops
page; it deletes/scrubs the subject's PII across R2 + the DB and proves the
result with an independent sweep. It targets client profiles only — an
editor subject returns 422 (editor erasure is a separate manual escalation).
Migration 0031 applied.
| # | Do | Expect |
|---|---|---|
| 8.1 | Create a throwaway client (a fresh magic-link account), give it a small order → job → footage upload, a brand kit, and (optional) a rating comment | A disposable subject with PII across files / events / brief_data / brand_kits |
| 8.2 | As man_on_rock@…, POST /api/ops/erasure with { "subjectProfileId": "<throwaway id>" } (admin session cookie) |
200 with verifiedEmpty: true — R2 objects deleted-and-verified-empty, PII scrubbed/pseudonymized, and an independent sweep reporting zero residual PII across every store |
| 8.3 | Re-read the subject | Profile pseudonymized in place (tombstone email/name, erased_at set) — the row survives so financial FKs stay valid; ledger/order money columns are untouched (only the creative brief_data jsonb is nulled) |
| 8.4 | Check the erasure_requests table |
One audit row recording the request + verdict |
| 8.5 | Try it on an editor profile id | 422 — "erasure targets client profiles only…" (fails loudly rather than running an incomplete erasure) |
| 8.6 | Try it signed-out / as a non-admin | 401 / 403 — and the pure runner refuses a non-admin role as defense in depth |
Cleanup: none required — the tombstone is the intended end state; do not try to "undo" it. Just don't reuse the throwaway account.
Pass bar (§8): an erasure on a throwaway client returns verifiedEmpty,
pseudonymizes the profile while preserving financial records, writes an audit
row, and refuses editors/non-admins.
9 · Retention dry-run — reading the cron audit
The retention cron (/api/cron/retention) deletes a DELIVERED job's
originals once delivered_at + retention_days passes — and nothing else.
It is dry-run by default: the scheduled Vercel run carries no query string, so
it computes the would-delete set, records it, and deletes nothing. A live
purge needs all of ?commit=1 and RETENTION_ENABLED=1 and no
footage-vault tables present. Bearer CRON_SECRET-gated (a missing Bearer → 401
before any DB/R2 access). Migration 0030 applied.
| # | Do | Expect |
|---|---|---|
| 9.1 | curl GET /api/cron/retention without a Bearer |
401 — the gate is the route's first statement |
| 9.2 | curl with Authorization: Bearer $CRON_SECRET, no ?commit |
200 JSON summary with mode = dry-run; jobs_considered / would-delete counts computed; nothing deleted. A voiceNotes sub-result reports the (separate, additive) voice-note refund-window pass, also dry-run |
| 9.3 | Read the retention_runs table |
One row per invocation: mode (dry_run), jobs_considered, jobs_purged (0 in dry-run), objects_deleted, bytes_estimated — the audit you review before ever arming |
| 9.4 | Confirm going live is deliberate | Live mode requires editing the cron path to …?commit=1 and setting RETENTION_ENABLED=1, only after reviewing these dry-run rows — and the vault gate refuses live mode entirely if vaults/vault_items tables exist |
Pass bar (§9): the cron 401s without the secret, runs dry (deletes nothing)
by default, records a reviewable retention_runs audit row, and cannot go live
without the deliberate two-guard + vault-gate conditions.
10 · Operator / system notes (not a click-path)
CRON_SECRETmust be set in the Vercel prod env or the retention (§9),sla-check,retainer-nudge,specify-editor,webhook-deliveries, andsupervisioncrons all 401. Cadence is once-daily on the Hobby plan.- SLA breach email — when a job's internal deadline passes while the editor
still owes work, each admin (
profiles.role = 'admin') gets one "SLA breach — job … overdue" email per breached deadline (a reassignment sets a new deadline → a fresh alert; never re-spams).man_on_rock@…is a real inbox — force it by giving a job a past internal deadline andcurl-ingsla-checkwith the Bearer secret. - Supervision cron (
/api/cron/supervision) is the backstop for the per-rating refresh hook; its daily cadence only affects backstop latency, and it is a no-op for tenants with supervision disabled (the default). - Partner-commission arming stays off until the operator's money-topology
ruling (split/VAT) — leave
PARTNER_COMMISSION_ENABLEDunset and everycommission_liveoff (§3b).
Overall pass bar: every tenant's jobs and editors are visible and steerable
from /ops; QC pass queues a preview and QC reject demands a note; finance reads
modeled + verified-realized figures with commissions dry-run and first-party
tenants excluded; the supervision surface is off by default, config-driven when
on, warns-not-blocks at assign time, and writes an attributed applied/rejected
audit row for every human action; and the erasure + retention operator jobs are
gated, audited, and — for retention — dry-run by default with no money or footage
moving from any review screen.