# Journey: Metis BYOM ingest, operator decision, and draft proposal

Metis currently ships three useful but independent seams: deterministic text
ingest, an operator decision record attached to a source id, and a learner-owned
course proposal. This journey walks all three without claiming the missing
linkage—there is no runtime path from an ingest job through Lilith/Sophia safety
and grounding into an approved bundle, published `CourseBuild`, lesson, tutor,
or assessment.

## Personas

- **Signed-in learner or teacher** — submits text, a URL, or a file reference
  and inspects the resulting ingest state.
- **Metis operator** — records approve, request-changes, or reject against a
  source id in the fixture review queue.
- **Course proposer** — submits a learner-owned draft that appears in that same
  learner's schoolroom.
- **Auditor** — needs to distinguish stored decisions from a verified BYOM
  approval gate and published-course transition.

## Pre-conditions

- The customer has a signed-in session and a verified bearer. The ingest and
  proposal routes do not currently enforce a Metis-specific domain scope.
- `/metis/byom` can render its authored fixture of four materials and a five-row
  outline. Those rows are specimen data, not the caller's ingest-job list.
- `POST /v1/metis/ingest` and `GET /v1/metis/ingest/:jobId` are available. The
  deployable requires the `metis-ingest-jobs` snapshot sink, and the write path
  acknowledges only after its ordered candidate snapshot commits.
- The operator has `admin:*`, `admin:metis`, or `admin:operator` for the BYOM
  decision route.
- Course proposals are a separate authenticated resource under
  `/v1/metis/courses`.

## Steps

### 1. Orient on the BYOM specimen page

- Open `/metis/byom`.
- The heading, materials table, rights labels, parse-state pills, drop zone, and
  generated outline come from `getMetisByomFixture()`.
- Treat the displayed 50 MB/file and 200 MB/total limits as interface copy. The
  current ingest request accepts text or source references and does not enforce
  that complete multi-file quota model.

### 2. Submit real text

- Use `/metis/ingest` or the Browse/drop controls on `/metis/byom` with a
  Markdown or text file.
- The browser posts actual text to `POST /v1/metis/ingest` with authenticated
  origin and CSRF headers.
- `deriveStudyOutline` deterministically segments the supplied text, derives
  headings and summaries, estimates reading time, extracts repeated key terms,
  and builds study prompts.
- The BFF stores an owner-scoped `completed` job and returns its real outline.

### 3. Submit a URL or PDF reference

- **Paste URL** posts a source locator without fetching it.
- A PDF selection supplies a filename reference but not PDF bytes.
- Both result in `awaiting_source_content` with no fabricated outline. EPUB and
  DOCX parsing are not wired through these controls.

### 4. Read the ingest job back

- `GET /v1/metis/ingest/:jobId` returns the job only to its owner.
- An unknown job returns `404`; a different owner receives `403` and cannot read
  the record.
- Source-level tests prove restart restoration, failed-write rollback, export,
  account erasure, and a hashed recreation fence. A deployable without its sink
  fails closed; a persistence failure returns `503` without publishing the job.

### 5. Record an operator decision

- Open `/operator/metis`. The KPI strip and generated-lessons queue are
  fixtures; the OneRoster card may be adapted from a live BFF read.
- `MetisByomDecisionPanel` posts `{ sourceId, decision, summary }` to
  `POST /v1/admin/metis/byom/decision`.
- A summary must contain at least 12 characters. The BFF maps the decision to
  `approved`, `changes-requested`, or `rejected`, then commits the authoritative
  review row before returning success.
- A searchable admin-audit mirror is attempted only after that commit. Audit
  failure is explicitly best-effort and can leave `auditEventId: null` without
  undoing the complete review record.
- The route does not resolve the source id to an ingest job, run
  `ByomSourceApprovalGate`, or mutate a `MetisLearningSourceBundle`.

### 6. Submit a separate course proposal

- Open `/metis/courses/new` and submit title, domain path, weeks, and optional
  rationale.
- `POST /v1/metis/courses` returns `202 state=awaiting-syllabus-review` and an
  owner-scoped course id.
- `/metis` reads the caller's stored proposals and renders the new row as a
  draft at the head of the schoolroom course list.
- The proposal request contains no ingest-job or operator-decision reference;
  its appearance does not mean a BYOM bundle was approved or published.

### 7. Stop at the real boundary

- No route composes a `MetisLearningSourceBundle` from the ingest job.
- No runtime call invokes Lilith safety classification, rights review, Sophia
  ingestion, embedding, or knowledge-graph anchoring for this request.
- No operator decision triggers a `CourseBuild`.
- The shared lesson, assessment, and tutor pages therefore cannot read this
  newly ingested material as a published course.

## Post-conditions

- A text request can produce an owner-scoped, reproducible ingest job and
  outline.
- URL and PDF references remain honestly pending until source content exists.
- An operator can persist a decision record for a supplied source id.
- An owner can export and erase ingest jobs; deletion durably fences recreation.
- Operator-account deletion pseudonymizes internal review identity and rationale
  for compliance retention rather than exporting that review package.
- A learner can persist a separate course proposal in `awaiting-syllabus-review`
  and see it on `/metis`.
- None of those records is linked into a safety-reviewed, grounded, published
  BYOM course.

## Failure modes

- **Anonymous ingest or proposal** — returns `401`.
- **Empty, unsupported, or malformed ingest** — returns a validation error; no
  outline is invented.
- **URL/PDF without bytes** — remains `awaiting_source_content`.
- **Cross-user job read** — returns `403 job_owner_mismatch` without returning
  the record.
- **Ingest snapshot unavailable** — returns `503`; the candidate is not
  published.
- **Ingest recreated after account erasure** — returns `410` behind the durable
  subject fence.
- **Short proposal title** — returns `422 course_title_too_short`.
- **Decision without admin scope** — returns `401`/`403`.
- **Short decision rationale** — returns `400 invalid_payload`.
- **Unknown decision source id** — the current route still accepts it; source
  existence is not validated.
- **“Approved” interpreted as publishable** — incorrect: the decision record is
  not applied to a bundle gate.
- **Fixture material or queue row interpreted as stored user data** — incorrect:
  both page rosters are authored specimens.

## E2E coverage

- [`apps/oshun/web/e2e/metis-upload-alias.spec.ts`](../../apps/oshun/web/e2e/metis-upload-alias.spec.ts)
  covers `/metis/upload` query-preserving redirect, signed-in browser/PWA
  landing, and anonymous gating.
- [`apps/oshun/web/e2e/metis-byom-ingest.spec.ts`](../../apps/oshun/web/e2e/metis-byom-ingest.spec.ts)
  covers the BYOM specimen, live text/URL/Browse/drop requests, rendered ingest
  outcomes, operator fixture, and independent course-proposal read-back.
- [`apps/oshun/web/e2e/metis-ingest-bff.spec.ts`](../../apps/oshun/web/e2e/metis-ingest-bff.spec.ts)
  covers real HTTP owner-scoped ingest jobs, completed versus awaiting-source
  states, course proposals, and validation boundaries.
- [`apps/oshun/web/e2e/metis-byom-decision.spec.ts`](../../apps/oshun/web/e2e/metis-byom-decision.spec.ts)
  covers all three decision verbs, summary and scope gates, UI read-back, and
  audit-event ids when the audit sink succeeds.
- `libs/metis/integrations/src/byom/` unit tests cover deep approval-gate rules;
  they do not prove the BFF/UI invokes those rules.

## Per-view files touched

- [`customer/08-metis/metis-byom.md`](../customer/08-metis/metis-byom.md) —
  specimen roster and live ingest controls.
- [`customer/08-metis/metis-upload.md`](../customer/08-metis/metis-upload.md) —
  legacy alias.
- [`operator/operator-metis.md`](../operator/operator-metis.md) — fixture queue
  and durable decision panel.
- [`customer/08-metis/metis.md`](../customer/08-metis/metis.md) — independent
  draft proposal read-back.
- The lesson, tutor, and assessment views are future consumers, not current
  post-conditions of this journey.

## Cross-references

- `apps/oshun/bff/src/metis/ingest-pipeline.ts` — deterministic outline
  derivation.
- `apps/oshun/bff/src/metis/ingest-job-store.ts` — owner-scoped durable job
  boundary.
- `apps/oshun/bff/src/routes/admin-metis-byom-decision.ts` and
  `metis-byom-decision-store.ts` — independent operator record.
- `apps/oshun/bff/src/routes/domain-stubs.ts` — independent course proposal.
- `libs/metis/integrations/src/byom/index.ts` — richer uninvoked bundle and
  approval-gate contract.
- [`metis-lesson-completion.md`](./metis-lesson-completion.md) — current fixture
  lesson and assessment flow.

## Open questions

- [ ] Link an ingest job to a typed `MetisLearningSourceBundle` and expose its
      real lifecycle rather than a fixture material row.
- [ ] Wire ordered safety, rights, PII, Sophia ingestion, and knowledge-graph
      evidence with tenant-scoped audit receipts.
- [ ] Validate operator decisions against a real source and invoke
      `ByomSourceApprovalGate` before recording `approved`.
- [ ] Define and implement the approved-bundle → `CourseBuild` publication
      trigger and downstream lesson/tutor/assessment reads.
- [ ] Enforce actual upload size, aggregate quota, MIME parsing, and license
      declaration rather than presenting those limits as copy only.
- [ ] Add course-proposal export, account erasure, retention, and recreation
      fencing; ingest jobs and internal review packages now have their distinct
      lifecycle paths.
