# Journey: Citation drift resolution

A specific, hard-policy slice of incident triage. The shipped operator path
starts from a citation-drift INC, opens the Isis provenance inspector, verifies
the bundle hash chain, classifies affected sources, records a recheck,
re-render, or retract decision, and closes the incident. The upstream Sophia
refresh and the downstream generation/retraction cascade are still unchecked
system legs. This separation matters: recording a correct operator decision is
not evidence that a new artifact or customer notice was produced.

## Personas

- **Sophia ingestion daemon** — non-human actor; produces the
  `source.revision.created` event that triggers the cascade
- **House operator** — picks up the INC in `/operator/admin`
- **Editorial operator** — owns the artifact (TR-, VR-, NS-, etc.); pulled in
  after the operator scopes the impact
- **Domain reviewer** (theology / fact-check / ephemeris depending on artifact
  class) — second-pair-of-eyes for the re-grounding decision
- **Trust-and-Safety operator** — escalation if the drift is severity-class
  `retraction` (per V1/features.md § Source Lifecycle)

## Pre-conditions

- An Isis `ProvenanceBundle` exists for the artifact (every generated artifact
  has one — see
  [`operator/operator-admin-isis-provenance.md`](../operator/operator-admin-isis-provenance.md)
  § Provenance bundle contract)
- The artifact's `sourceLineage.citationIds` (Nisaba contract) or equivalent
  per-domain cite list is non-empty
- Sophia per-claim diff has run on the source revision and produced a
  `material-claim-change` signal (paraphrase edits do not fire this path per
  V1/features.md § Change detection)
- The artifact is in a customer-facing surface (drafts don't fire this; only
  published / live artifacts trigger the cascade)

## Steps

### 1. Source refresh and per-claim diff

Background, non-interactive. The Sophia ingestion pipeline detects that a source
has changed via `changeFingerprint` and runs claim-level diff (not text-level —
per V1/features.md, paraphrase edits should not invalidate).

- [ ] Sophia per-stage event log records: parser version, chunker strategy,
      enrichment classifier version, embedding model version — all in the typed
      event stream per V1/features.md § Per-stage audit
- [ ] Claim diff produces a list of materially-changed claims
- [ ] For each materially-changed claim: the downstream invalidation cascade
      fires (Veritas re-grounding, Metis lesson-validity check, Nisaba passage
      notice, Tara explainer re-check, Living Scenes re-render banner)

### 2. INC opens against affected artifact(s)

- [ ] For each affected artifact, an INC is created with title "Citation drift
      detected · <ARTIFACT-CODE>" matching the fixture pattern
- [ ] Severity:
  - `S1` if the source moved to `retracted` state (retraction event)
  - `S2` if the claim is high-stakes (medical, financial, legal, Metis
    assessment, Veritas published claim, Tara contemplative script)
  - `S3` otherwise (advisory; informational drift)
- [ ] Subtitle carries the drift summary (the fixture: "Confidence dropped from
      strong to moderate after 3 new sources")
- [ ] Detail card carries `driftSources[]` — the fixture surfaces a three-row
      list `[label, what-changed, source-quality-band]`:
      `[['Sci. Adv., 2025', 'May 04 · contradicts §02:14', 'medium-high journal'], ...]`
- [ ] Audit event: `incident.created` with `source_revision_id`,
      `affected_bundle_id`, `materially_changed_claim_count`

### 3. Operator opens `/operator/admin` and picks the INC

- [x] Operator lands at `/operator/admin` (see
      [`incident-triage.md`](./incident-triage.md) steps 2-3)
- [x] Inbox row visible; severity pill paints per `sev`
- [x] Operator clicks the row; detail card shows artifact code, drift summary,
      three drift-source rows
- [x] Detail card includes the "Open provenance" deep link used in step 4

### 4. Deep-link into Isis provenance inspector

- [x] Operator clicks "Open provenance" — navigates to
      `/operator/admin/isis/provenance?inc=INC-NNNN`; bundle selection happens
      inside the inspector because the current static INC fixture does not pin a
      bundle ID
- [ ] Page is RSC; `loadIsisProvenanceData({ tenantId, selectedBundleId })` runs
      server-side
- [x] Page renders two stacked inspectors:
  - `IsisProvenanceInspector` — bundle index + verification verdict + per-
    bundle 16-field detail
  - `GeneratedArtifactProvenanceInspector` — artifact-side provenance
    (`surfaceId="operator-isis-provenance"`, `surfaceRole="editorial"`)
- [x] **Verify**:
      [`operator/operator-admin-isis-provenance.md`](../operator/operator-admin-isis-provenance.md)

### 5. Inspect the hash chain

- [x] Verification card region
      (`<article aria-live="polite" data-verification-ok="true|false">`) reads:
  - **Status** — `"CLEAN — hash chain intact"` (success) OR
    `"BROKEN at index N"` (failure)
  - **Tenant scope** — tenantId or `(no tenant — platform ledger)`
  - **Chain length** — integer; should match expected count
  - **Last checked** — `<time>` element with ISO timestamp
- [x] **If verdict is BROKEN at index N**: this is an integrity event, not a
      drift event. Stop the drift workflow. Open an S1 INC against the ledger
      itself (separate from the drift INC). Escalate to platform security. Do
      not proceed with re-render or retract until ledger is reconciled. Current
      E2E verifies terminal action controls disable on the broken-ledger fixture
- [x] **If verdict is CLEAN**: proceed to step 6

### 6. Inspect the selected bundle detail

The bundle detail `<dl>` enumerates 16 fields plus computed "Emitted at" — each
`<dd>` has `data-testid="isis-provenance-field-<key>"`:

- [x] **Bundle ID** matches the selected bundle detail
- [x] **Generation ID** identifies the specific generation run
- [x] **Invoking user** — author who triggered the generation
- [x] **Tenant** — tenant scope
- [x] **Consent ID** — consent grant in effect at generation time
- [x] **Prompt hash** — hash of the prompt used (privacy-preserving)
- [x] **Model ID + Model version** — what produced the artifact
- [x] **Modality** — text / image / audio / video / 3d-mesh / embedding
- [x] **Watermark hash** — present for image/video/audio; verify against the
      live artifact (a watermark mismatch means the artifact has been tampered
      with — escalate). Current E2E verifies field presence; live-artifact
      watermark comparison remains a downstream gap
- [x] **Endpoint + Region** — provider routing
- [x] **Bundle fingerprint, Chain fingerprint, Prior fingerprint, Sequence** —
      ledger position; first bundle in chain shows `—` for prior fingerprint
- [x] **Emitted at** — ISO from `emittedAtUnixSeconds * 1000`

### 7. Cross-check sources against the drift list

Returning to (or pivoting from) the admin inbox detail card — the operator
compares the bundle's source set against the new `driftSources`:

- [x] For each drift source, determine class:
  - **Refines existing claim** — minor edit; surface a `material-claim-update`
    advisory but do not re-render
  - **Contradicts existing claim** — high-stakes; route to re-grounding
  - **Retracts source** — source moved to `retracted` state; cascade is a
    retraction (per V1/features.md § Retraction event)
- [x] Operator records the per-source decision in the drift detail (note thread
      on the INC)
- [x] Audit events per source: `drift.source_classified` with class + rationale

### 8. Decide: recheck / re-render / retract

Three terminal paths. Each is audit-stamped against both the INC and the bundle.

**A. Recheck-only (no claim materially changed)**

- [x] Click "Mark drift as immaterial" in the INC detail card
- [x] Modal: rationale (≥ 50 chars) explaining why the drift does not require
      re-render
- [x] Confirm
- [x] Audit event: `drift.recheck_immaterial`
- [ ] Artifact is unchanged on customer surfaces; no new provenance bundle is
      emitted; existing bundle gets an annotation entry referencing the reviewed
      source revision
- [x] INC transitions to `resolved`

**B. Re-render (claim materially changed; artifact regenerable)**

- [x] Click "Re-render with updated source"
- [x] Modal: target workflow class (defaults to the bundle's `modelId` +
      version), surface scope (which customer routes to re-render), rationale
- [x] Click "Confirm re-render"
- [ ] Isis enqueues a generation job at the same workflow class; on completion a
      new `ProvenanceBundle` is appended to the ledger with a new `sequence`,
      `bundleFingerprint`, and updated `priorFingerprint` pointing to the prior
      bundle
- [x] Audit event: `drift.re_render` records the operator decision and selected
      bundle ID; the actual new-bundle append remains unchecked below
- [ ] When new bundle is published, customer surface serves the re-rendered
      artifact; old artifact remains in the ledger (append-only)
- [ ] INC transitions to `resolved` only after the new bundle clears Sophia
      publication gates (same gates as in
      [`editorial-review-approval.md`](./editorial-review-approval.md) step 6)

**C. Retract (source was retracted; artifact unsalvageable)**

- [x] Click "Retract artifact"
- [x] Modal: rationale (≥ 100 chars), notify-author toggle, customer-message
      toggle, takedown-cascade scope (V1/features.md § Takedown Cascade Across
      Kept and Shared Artifacts)
- [x] Click "Confirm retract"
- [ ] Customer-facing surface serves a retracted-source notice in place of the
      artifact (per Veritas retraction model — see
      [`customer/05-veritas/veritas-retraction.md`](../customer/05-veritas/veritas-retraction.md))
- [ ] Kept-artifact takedown cascade fires (see V1/features.md § Takedown
      Cascade)
- [x] Audit event: `drift.retract` records the operator decision and selected
      bundle ID; linked notifications and takedown cascade execution remain
      unchecked above
- [x] INC transitions to `resolved` with `resolution_class = 'actioned'` in the
      current local-dev BFF state

### 9. Audit-trail handoff

- [ ] Operator returns to `/operator/admin/isis/provenance?bundle=<id>`
- [ ] Selected bundle detail shows the updated state — for re-render, the new
      bundle's row appears in the recent-bundles `<ol>` with a newer ISO time
- [x] Verification card status remains `CLEAN — hash chain intact`
- [ ] Audit-log queries for `eventTypePrefix=drift.` and
      `eventTypePrefix=incident.` show the full chain: `incident.created` →
      `drift.source_classified` (×N) →
      `drift.<recheck_immaterial|re_render|retract>` → `incident.resolved`

## Post-conditions

- INC state: `resolved`
- Provenance ledger remains hash-chain intact (`data-verification-ok="true"`);
  the current re-render decision does not append a new bundle yet
- The audited decision and source classifications are readable from the BFF and
  audit log, and the shared incident reaches `resolved`
- Customer-surface replacement/retraction, cache invalidation, and author
  notification remain open downstream effects rather than claimed postconditions

## Failure modes

- [x] **Hash chain BROKEN at index N** — entire workflow halts. Verification
      card paints `data-verification-ok="false"`. The drift INC cannot be
      resolved until a separate S1 ledger-integrity INC is closed by platform
      security. The bundle detail still renders but every action is disabled
- [ ] **Watermark hash mismatch** — the published artifact's watermark doesn't
      match the bundle's `watermarkHash` field. Indicates the artifact has been
      replaced outside of Isis. Auto-escalates to T&S as deepfake / tamper class
- [ ] **Source moved to `retired` mid-resolution** — operator selected re-render
      but the source moved to retired between classification and execution;
      re-render is aborted with an "underlying source retired" modal; operator
      must restart with retract path or pick a substitute source (V1/features.md
      § Replacement)
- [ ] **Re-render fails (provider outage or eval-gate failure)** — new bundle
      not appended; INC stays open; an operational sub-INC fires against the
      provider. Original artifact stays live until re-render succeeds or the
      operator pivots to retract
- [x] **Malformed `?bundle=` query param** — `loadIsisProvenanceData` returns
      empty selectedBundle; the detail article renders prompt copy. The
      operator-admin.md flags whether this 404s or shows empty as an open
      question (verify on walk)
- [x] **Empty bundles for a tenant** —
      `data-testid="isis-provenance-empty-state"` paragraph renders: "No
      provenance bundles persisted for this tenant yet."
- [ ] **Operator attempts retract on an artifact that has been kept/shared
      externally** — retract path proceeds but the takedown-cascade modal
      enumerates the kept/shared surfaces (per V1/features.md § Takedown Cascade
      Across Kept and Shared Artifacts); operator must acknowledge each surface
      before confirm
- [x] **Bad actor: operator marks material drift as `recheck_immaterial` without
      rationale** — rationale validation blocks; cannot bypass
- [ ] **Concurrent drift INCs on the same artifact (multiple sources move
      together)** — debouncer merges to one INC with multiple drift-source rows;
      operator handles them as a set in step 7

## E2E coverage

Spec: `apps/oshun/web/e2e/citation-drift-resolution.spec.ts`

Steps covered: operator opens `/operator/admin` and sees the S1 INC (step 3);
follows the detail-card "Open provenance" handoff to
`/operator/admin/isis/provenance?inc=INC-2041`, verifies the hash-chain
verification article (`data-verification-ok`, status text, chain-length,
`<time>` checkedAt), platform tenant-scope fallback, bundle index row metadata
and row `<time>` values, tenant-preserving bundle links, and 16-field detail
(step 5-6); asserts the empty bundle-detail state when no `?bundle=` param is
present; asserts the unknown-bundle fallback, the `isis-provenance-empty-state`
when the tenant has no bundles, an explicit broken-ledger verdict
(`data-verification-ok="false"`, `BROKEN at index 1`) with terminal actions
disabled, and the anonymous redirect-before-render guard; verifies the
`GeneratedArtifactProvenanceInspector` renders with `surfaceRole="editorial"`,
all five artifact kinds, and a valid JSON bundle textarea (step 4 / step 9). The
2026-06-30 pass adds the terminal action surface under the selected bundle: the
browser blocks a too-short rationale, records `drift.recheck_immaterial`,
`drift.re_render`, and `drift.retract` through the real BFF
`POST /v1/admin/citation-drift/:incidentId/resolutions`, reads the per-operator
resolution list back from the BFF, and verifies matching `drift.*` rows in
`/v1/admin/audit-log/events`. The 2026-07-01 pass adds the per-source
classification surface: the browser classifies the three drift sources from the
operator inbox (`Sci. Adv., 2025`, `Nature Cities, May`,
`Lancet Planetary, Apr`) through the real BFF
`POST /v1/admin/citation-drift/:incidentId/source-classifications`, blocks a
too-short classification rationale, reads source classifications back, and
verifies `drift.source_classified` audit-log rows for the same operator. The
2026-06-30 incident-lifecycle follow-up resets the real operator incident store
before each browser run, records a chosen retract decision after source
classification, reads `INC-2041` back from
`/v1/admin/operator-incidents/INC-2041` as `resolved`, proves the incident
history still starts with the seeded `created` entry and ends with the operator
`resolved` entry, and verifies the linked `incident.resolved` audit row carries
the drift resolution id/event payload.

**Coverage depth**: partial — see [`coverage.md`](./coverage.md). The pages are
real (`openAuthenticatedShellPath`) and Playwright starts Next with the
harness-only `OSHUN_ISIS_PROVENANCE_LEDGER_FIXTURE=clean` path, guarded by
`OSHUN_ENABLE_TEST_HARNESSES=true`. That fixture is built through the same
canonical Isis in-memory bundle/ledger helpers the durable store replays, so the
hash-chain verdict and 16-field bundle assertions now run every local E2E pass
instead of sitting behind `test.skip()`. The browser spec also drives explicit
unavailable, unknown-bundle, empty-tenant, broken-ledger, and anonymous-redirect
branches. The terminal write path now records audited BFF receipts for all three
choices, and the source-classification write path now records audited BFF
receipts for each drift source. A chosen terminal decision now also transitions
the shared operator incident state to `resolved` and emits `incident.resolved`
with a payload linking the drift resolution. The journey remains partial because
the actual Isis generation append for re-render, customer-surface retraction
cascade, and explicit `incident.created` admin-audit emission are not yet
driven.

Uncovered: steps 1-2 (background Sophia cascade — no UI); step 8 downstream
effects after the recorded decision (new bundle append/publication gates for
re-render, customer notice and kept-artifact takedown cascade for retract, and
customer-surface cache purge); and the explicit `incident.created` admin-audit
row in the step 9 chain. The `drift.source_classified`, `drift.*` terminal audit
events, `incident.resolved` audit event, and shared incident created→resolved
history are now read back over real BFF routes.

## Per-view files touched by this journey

- [`operator/operator-admin.md`](../operator/operator-admin.md) — INC entry
- [`operator/operator-admin-isis-provenance.md`](../operator/operator-admin-isis-provenance.md)
  — the provenance inspector itself (primary surface)
- [`customer/05-veritas/veritas-retraction.md`](../customer/05-veritas/veritas-retraction.md)
  — customer-facing retraction notice (retract path)
- [`customer/07-nisaba/nisaba-notebook.md`](../customer/07-nisaba/nisaba-notebook.md)
  — Nisaba notebook revision banner (downstream invalidation)
- [`shell/04-auth-session.md`](../shell/04-auth-session.md) — operator role gate
- [`shell/05-notifications.md`](../shell/05-notifications.md) — author /
  customer notification on re-render or retract

## Cross-references

- Sibling journeys:
  - [`incident-triage.md`](./incident-triage.md) — generic INC flow that this
    journey specializes
  - [`editorial-review-approval.md`](./editorial-review-approval.md) — re-render
    path returns through the publication gate
- Feature spec:
  - [`V1/features.md`](../../V1/features.md) § Sophia (Source Lifecycle,
    Downstream Invalidation), § Isis (Provenance Bundles), § Veritas Retraction
- Component sources:
  - `apps/oshun/web/src/components/admin/IsisProvenanceInspector.tsx`
  - `apps/oshun/web/src/components/admin/CitationDriftSourceClassifier.tsx`
  - `apps/oshun/web/src/components/admin/CitationDriftResolutionActions.tsx`
  - `apps/oshun/web/src/components/provenance/GeneratedArtifactProvenanceInspector.tsx`
  - `apps/oshun/web/src/lib/server/isis-provenance-loader.ts`
  - `apps/oshun/bff/src/routes/admin-citation-drift-resolution.ts`
  - `apps/oshun/bff/src/admin/citation-drift-resolution-store.ts`
- Tests:
  - `apps/oshun/web/src/components/admin/__tests__/IsisProvenanceInspector.test.tsx`
  - `apps/oshun/web/src/components/admin/__tests__/CitationDriftSourceClassifier.test.tsx`
  - `apps/oshun/web/src/components/admin/__tests__/CitationDriftResolutionActions.test.tsx`
  - `apps/oshun/bff/src/admin/citation-drift-source-classification-store.test.ts`
  - `apps/oshun/bff/src/admin/citation-drift-resolution-store.test.ts`
  - `apps/oshun/bff/src/__tests__/admin-citation-drift-resolution-route.test.ts`
- Provenance bundle contract: 17-field `IsisProvenanceBundleView` interface,
  enumerated in
  [`operator/operator-admin-isis-provenance.md`](../operator/operator-admin-isis-provenance.md)
  § Provenance bundle contract

## Open questions

- [ ] Confirm whether operators need a separate "re-run hash-chain verification"
      affordance; the current page records the terminal
      `drift.recheck_immaterial` decision but still reads ledger verification
      from server data only
- [ ] Document the BFF endpoint that triggers the actual generation job after a
      recorded `drift.re_render` decision; the new resolution endpoint records
      the audited operator decision but does not append a new Isis bundle
- [ ] Snapshot the per-class severity thresholds: which materially-changed claim
      counts produce S1 vs S2 vs S3
- [ ] Document the downstream invalidation event names emitted to Veritas,
      Metis, Nisaba, Tara, Living Scenes (V1/features.md lists them but the
      typed event names aren't pinned here)
- [ ] Replace the generic takedown-cascade scope picker with a surface-aware
      enumerator for all kept/shared surfaces, or document the route that owns
      that enumeration
- [ ] Determine which tenant-scoped vs platform-scoped drift events appear in
      the `(no tenant — platform ledger)` view
