# Journey result: Tenant bulk operations and exports

- **Walked**: 2026-06-25; updated 2026-07-02 by Codex against local real-BFF dev
  infrastructure
- **Verdict**: **partial pass** - the local BFF API spine now proves
  platform-only scope gates, `users` bulk-operation
  validate/dry-run/stage/commit with real row-level commit outcomes,
  `entitlements` bulk-operation commit into the customer profile entitlement
  projection, `rosters` bulk-operation commit into the OneRoster roster
  projection, `persona_assignments` bulk-operation commit into the active
  persona projection, unsupported `content` commit failure without false
  `committedAt`, validation-error blocking, per-operator isolation, bulk-export
  manifest creation/read/verification, export matrix failure behavior, and
  audit-search read-back. The operator `/operator/tenant` browser panel now
  covers content export creation, manifest/file hash inspection, verification,
  release approval, released-file download, response hash/residency headers, and
  release/download audit read-back through the live BFF. CSV upload parsing,
  upload preview/row diff UI, multi-actor approval separation, and real
  content/taxonomy commit executors remain open.
- **Primary specs**:
  `apps/oshun/web/e2e/tenant-bulk-operations-export-bff.spec.ts`,
  `apps/oshun/web/e2e/tenant-bulk-operations-export-ui.spec.ts`

## Observations

- Bulk-operation routes reject missing auth and workspace-scoped tenant admins,
  then accept platform operators.
- Clean `users` operations advance through the enforced state machine one stable
  state at a time and cannot be committed twice.
- Clean `entitlements` operations use the seeded account id, commit through the
  real `customerAuthStateStore.updatePlan` path, and are verified by reading
  `/v1/profile` before and after the commit (`free` -> `premium`).
- Clean `rosters` operations commit through `oneRosterApplyStore.apply` and are
  verified by reading `/v1/admin/lms/oneroster` for the deterministic bulk-row
  commit id and tenant roster totals.
- Clean `persona_assignments` operations commit through `activePersonaStore.set`
  and are verified by reading `/v1/personas/active` as the assigned subject for
  the committed persona and surface.
- Valid `content` operations can validate, dry-run, and stage, but the commit
  boundary proves the missing executor fails loud: terminal `failed`,
  `committedAt: null`, empty `commitOutcomes`, explicit
  `no commit executor is wired` failure reason, and failed audit-search event.
- The dry-run plan classifies `:create`, `:update`, and `:skip` target refs, and
  commit records one outcome per row.
- Invalid rows validate into visible issues but cannot advance to dry-run.
- Bulk exports return secret-free summaries first, inline file content only on
  detail read, and an integrity verifier that recomputes the manifest/files.
- Unsupported export matrix pairs and mismatched record kinds fail closed.
- The `/operator/tenant` bulk-export panel now drives the same real BFF from the
  browser: create content JSON export, inspect manifest/file hashes, verify,
  approve release with a rationale, download the released file, and read back
  structured executed/release-approved/file-downloaded audit events.

## Out of scope

- Browser UI for bulk CSV upload, preview, row diff, and commit confirmation.
- Real residency metadata lookup for each exported row.
- Multi-actor export approval separation and retention-expiry enforcement beyond
  the current single-operator release handoff.
- Real commit executors for content and taxonomy rows beyond the now-covered
  loud-failure contract.
