Disciplines · Audits

V1 Residual Audit — Nisaba + Metis (features.md 1055–1588)

D1 open question answered: DOMAIN_PREVIEW_SURFACES.nisaba must stay true (finding 7 lists the exact wiring needed to lift it).

1section22 minread

On this page

Date: 2026-06-11 · Auditor slice 04 · Baseline: V1_SPEC_GROUND_TRUTH_AUDIT_2026-06-10.md (all 57 tasks landed)

Evidence rule honored: every claim below is from current source read this session; comments/tests/docs were not trusted as proof. Recorded V1.x deferrals (full Nisaba service, Sefaria/CDLI), D1-bannered /domains/* illustrative data, and env-cred-gated providers are NOT re-reported as findings except where the residual goes beyond the recorded deferral.

What was verified GOOD first (no finding):

  • B2 metis ingest is real. apps/oshun/bff/src/metis/ingest-pipeline.ts derives a deterministic study outline (paragraph segmentation, stopworded key-term frequency, prompt synthesis, 200-wpm stats) from actual provided text; URL/PDF-without-bytes jobs are recorded honest awaiting_source_content (ingest-job-store.ts), owner-scoped GET, durable via wireDurableMetisIngestJobs (server.ts:410), wired to the web form (apps/oshun/web/src/app/metis/ingest/). No fabricated queued jobs remain.
  • D6 corpus is genuine. apps/oshun/bff/src/nisaba/public-domain-corpus.ts — 9 real public-domain quotations (Müller 1881, Carter 1758, Long 1862, Legge 1891, KJV 1611) with real translator attribution; buildCorpusPassage (nisaba/state.ts:872-906) is honest-zero on progress/saved/lastReadAt; stats derived from actual text.
  • The nisaba consumer route surface (routes/nisaba.ts, 2,567 lines) is real store-backed CRUD with auth + scope + pagination + revision-keyed caching; notebooks/annotations create/update flows are real and owner-scoped.
  • Themis adjudication/appeal HTTP surface (routes/metis-integrity.ts) wires the real MetisThemisCanonicalBridge; tutor-session-memory route wires the real @metis/models/session-memory engine and is snapshot-durable (server.ts:422). BYOM operator decisions are durable (server.ts:407) and UI-wired (MetisByomDecisionPanel.tsx:80).
  • /metis/session falls back honestly to empty lessons (app/metis/session/page.tsx:48-51); /nisaba and /metis hubs fetch the BFF server-side with honest *Unavailable() fallbacks (lib/lilith-data/nisaba.ts, metis.ts).

1. Metis academic-integrity reads have no ownership check — any learner can read any verdict audit and any appeal workspace#

Severity: P0-SEC

Evidence: apps/oshun/bff/src/routes/metis-integrity.ts:199-229getWorkspaceHandler and getAuditHandler call requireScope(request, reply, hasMetisScope, 'metis') and then discard the returned userId; academicIntegrityAppealStore.getWorkspace(appealId) / getAuditRecord(verdictId) (metis/academic-integrity-appeal-store.ts:48-54) take only the id — no appellant/respondent/learner binding is ever compared to the caller. openAppealHandler (:131-166) likewise lets any domain:metis holder open an appeal against any verdictId, including another learner's.

Spec promise: features.md 1498-1509 — appeals belong to the learner ("learner can contest"); 1300-1301 / 1506-1509 — the audit record carries detection signals, classifier output, evidence excerpts, decision rationale; 1566-1568 — "per-tenant visibility rules govern who sees what... evidence packs are linkable only to operators with the right scope."

What the code actually does: every authenticated consumer holds domain:metis (it is a standard domain scope), so any member who learns or guesses a verdictId/appealId can read another learner's integrity evidence excerpts and file appeals on their verdicts. Adjudicate/resolve are operator-gated correctly; the read/open legs are not.

Fix sketch: persist learnerId/appellantId on verdicts and appeals; in the two GET handlers and openAppeal, require authContext.userId === record.learnerId (or operator scope). Return 404 on mismatch to avoid id-probing.

2. /v1/activity fabricates Nisaba and Metis timeline items for every member, unconditionally, in production#

Severity: P0-HONESTY

Evidence: apps/oshun/bff/src/routes/activity.ts:559-660 (nisaba branch) and :662-712 (metis branch) inside fetchDomainTimelineItems. Unlike the tara branch (which gates on a real attemptDomainCall), the nisaba and metis branches unconditionally return hardcoded items: "Notebook handoff... 8 annotations", a passage card for 'Bhagavad Gita 2.47' / 'compassion-across-lineages' / 'Comparative Study Edition' — ids and edition from the pre-D6 fixture corpus that no longer exists in any store — and a metis card claiming "Calculus I recovery path · 24 lessons · 34% complete · tutoring support attached" with grounding: { state: 'grounded', citationCount: 8 } and provenance: { state: 'verified' }. Action hrefs point at /library/passage/compassion-across-lineages (no such passage), /courses/crs-002 (no /courses route exists in apps/oshun/web/src/app — verified by ls), and /domains/metis?...path=/assessments/calc-checkpoint-1 (lands on the Metis launch-readiness preview gate, not a mastery check).

Spec promise: features.md 1068-1070 / 1182-1184 — "First-class shell recognition across ... activity timeline" means the member's real Nisaba/Metis activity; the platform-wide rule (and the 06-10 audit's C-series convention) is honest-empty per-user reads.

What the code actually does: every member's Activity timeline asserts they completed reading, annotating (8 annotations), and 34% of a calculus course they never opened, stamped "verified" and "grounded" — fabricated provenance/grounding metadata on a first-class authenticated shell surface with no preview register.

Fix sketch: mirror the tara branch — derive nisaba items from nisabaConsumerStateStore (real notebooks/annotations, honest empty otherwise) and metis items from the real course-proposal + ingest-job stores; delete the fabricated grounded/verified badges and the dead hrefs.

3. /v1/metis/room serves fabricated per-user learning state — unguarded in production#

Severity: P0-HONESTY

Evidence: apps/oshun/bff/src/metis/room.ts:90-103buildMetisRoom always returns weekStats: { lessonsRead: '4 · 6', assessments: '2 · 3', tutorMinutes: '34', streak: '11 days' }, resume: { sectionNumber: 4, ... minutesToday: 14 }, an openAssessment "due Sun", curated courses with per-user progress: 0.34/0.18/0.42, and tutorNudge: '"You marked Discourses I.4 yesterday — would you like to walk through the second half together?"'. The route (routes/domain-stubs.ts:1694-1707) is not wrapped in guardedFixtureRoute (unlike /v1/metis/lessons at :1673) and the web hub renders it as the member's schoolroom (apps/oshun/web/src/lib/lilith-data/metis.ts:14-19, fetched by /metis).

Spec promise: features.md 1223-1224 — "Learner-facing progress, mastery, study recommendations, continuity" (the learner's real ones); 1186-1190 — honest empty/loading/degraded states are first-class.

What the code actually does: a brand-new member opens /metis and is told they have an 11-day streak, studied 14 minutes today, are in week 2 of Stoic ethics, have an assessment due Sunday, and that the tutor remembers them marking Discourses I.4 yesterday — none of which happened. Only the drafted-course overlay is real (C3). The tutor-nudge is fabricated memory in quoted tutor voice — the exact pattern C4 purged from Iris.

Fix sketch: compute weekStats/resume from real stores (ingest jobs, course proposals, tutor-session memories — all exist and are durable); replace openAssessment/tutorNudge with honest null states the MetisRoom component already needs for its unavailable variant; keep the curated catalog but at progress: 0 / no week cursor until real play state exists.

4. Nisaba per-user seed fabricates member-authored annotations, notebooks, and "grounded/verified" evidence views with invented citations — on unbannered consumer routes#

Severity: P0-HONESTY

Evidence: apps/oshun/bff/src/nisaba/state.ts:908-1635buildSeedRecord(userId) gives EVERY member, at first touch: (a) three annotations authored as the member (author: { userId, displayName: deriveDisplayName(userId) }, lines 1524-1580) with notes they never wrote and timestamps 2026-03-22/23; (b) two pre-built notebooks ("Attention notebook", "Cross-tradition compare", lines 1586-1624) presented as theirs; (c) grounded evidence views (lines 1074-1460) with groundingStatus: 'grounded', fabricated processingTimeMs: 1480/1845, claimChecks with status: 'verified' and support counts, and invented citations: excerpts attributed to "Majjhima Nikaya, MN 118" that are not MN 118 quotations ("Attend to the breath as it lengthens and shortens...", "When attention wanders, return without violence" — paraphrases presented as scripture, the exact thing the D6 corpus header at public-domain-corpus.ts:5-7 forbids) plus a nonexistent commentary "Practices of Recollection, M. Anand, 2024, https://oshun.app/sources/recollection" with reliabilityScore: 0.78 and scholarlyConsensus: 'moderate'. Served by /v1/nisaba/notebooks, /v1/nisaba/passages/:id/ annotations, /v1/nisaba/passages/:id/grounded-evidence (routes/nisaba.ts) — consumed by the /nisaba hub pages and NisabaSurface. D6 fixed reading-state and labeled the manuscript witness ("Illustrative example — not a real repository holding", state.ts:920) but left these.

Spec promise: features.md 1115-1117 (annotations are the member's, with visibility rules); 1131-1134 ("assistant-assisted explanation whose every interpretive claim exposes inspectable Sophia citations" — i.e., real ones); C-series convention: new member starts honestly empty.

What the code actually does: fabricated user authorship (C4's "identical seeded Nisaba-study memories for every user" pattern, surviving here), and result-faking grounding: 'verified' claim checks and 'grounded' status from a pipeline that never ran, citing a fabricated source and misquoted scripture. The annotation/notebook counts (annotationCount: 2, state.ts:1043) are internally consistent with the seeds, which makes the fabrication more convincing, not less.

Fix sketch: start members at zero annotations/notebooks/evidence views (the stores already handle empty); move the three curated annotations to clearly-attributed editorial content if wanted ("Oshun editors", not the member); either delete the seeded evidence views or re-ground them through the real Sophia extractive composer over the corpus with honest processingTimeMs and citations that quote only the actual public-domain bodies.

5. The Metis adaptive-learning spine remains fully orphaned — IRT, mastery bands, adaptive loop, gradebook emission, BYOM-model safety harness have zero runtime consumers#

Severity: P0-STRUCT

Evidence: import sweep over apps/ (excluding tests/node_modules): zero imports of @metis/assessment (IRT 1PL/2PL/3PL in libs/metis/assessment/src/irt-models/, adaptive selection in adaptive/), @metis/learning (mastery transitions libs/metis/learning/src/mastery/ transitions.ts, personalization signals), @metis/tutoring (the adaptive loop libs/metis/tutoring/src/loop/adaptive-loop.ts), @metis/gradebook (emission.ts + LTI-AGS / xAPI / Caliper / file-export adapters), libs/shared/inbound-integrations/src/byom.ts (the tenant-model sandbox; the only @oshun/inbound-integrations consumers are calendar/SSO/LMS files), and libs/metis/integrations/src/byom (applyByomSourceApprovalDecision). The only @metis/* runtime consumers anywhere are: models/session-memory (BFF tutor-memory store + apps/metis/web), integrations/themis-canonical-wiring (BFF integrity store), integrations/oshun-continuity-bridge (assistant handoff), integrations/cross-domain-flows and llm-client (apps/metis/web).

Spec promise: features.md 1363-1370 — "The loop is launch-blocking: Metis ships to institutions, and that contract turns on its integrity"; 1372-1402 (mastery transition model), 1433-1465 (adaptive loop, P95 ≤ 600 ms), 1546-1571 (gradebook contract: "Without this layer Metis cannot ship to schools"), 1515-1544 (BYOM safety surface).

What the code actually does: the engines exist and are real (this is the same state the 06-10 audit flagged), but no item turn anywhere produces evidence, no mastery band ever transitions at runtime, no GradebookEntry is ever emitted to any LMS adapter, and no BYOM tenant-model endpoint can be onboarded, sandboxed, or kill-switched — there is no registry, no call path, no enforcement. The tutor-session-memory route folds understanding into a mastery posterior (real) but nothing connects it to item delivery, next-item selection, or gradebook emission.

Fix sketch: this is the largest残 piece of launch-blocking scope. Minimum credible wiring: drive adaptive-loop from the tutor-memory route's turn input; persist MasteryTransition records; emit GradebookEntry through the existing file-export/xAPI adapters on assignment completion. BYOM-model onboarding needs a tenant endpoint registry + the existing sandbox harness on the call path — or an explicit recorded deferral, which does not currently exist for this (unlike the Nisaba service).

6. /v1/nisaba/room is a third "today" with fabricated marginalia counts and a non-public-domain translation#

Severity: P1

Evidence: apps/oshun/bff/src/nisaba/room.ts — the room passage is hardcoded "Anattalakkhaṇa Sutta, SN 22.59 ... trans. Bhikkhu Bodhi" (lines 29-33) with six body paragraphs of that translation, while the store's daily passage everywhere else (E2's "one today") is nisaba-passage-attention (Dhammapada I.1-2). buildNisabaRoom(realAnnotationCount) overlays the member's annotation count for the Dhammapada passage (routes/nisaba.ts:1935-1941) under the SN 22.59 display, and ships fabricated marginalia counts: "Cross-references: 4", "Saved excerpts: 2" (room.ts:86-88 — the comment admits "default to the curated counts until those stores are surfaced"). The Bhikkhu Bodhi SN translation (Wisdom Publications, 2000) is not public domain, unlike every D6 corpus text.

Spec promise: features.md 1131-1134 (one daily passage per plan/interest); 1141-1143 ("exports are gated on the source edition's rights metadata"); D6's recorded principle: genuine public-domain quotation with real attribution.

What the code actually does: /nisaba (hub) shows a different "today" than /v1/nisaba/ passages/daily, /v1/nisaba/adapter/daily-passage, and NisabaSurface (E2 unified those two, the room stayed out); the annotation count shown belongs to a passage the member isn't looking at; and the platform serves a copyrighted translation verbatim on its most-visible Nisaba surface while claiming a public-domain-only corpus posture.

Fix sketch: build the room from nisabaConsumerStateStore.getDailyPassage(userId) (real body, sections from paragraphs, real annotation/saved counts, honest 0 cross-references); if the Anattalakkhaṇa Sutta is wanted, use a public-domain rendering and add it to the corpus with real attribution.

7. NisabaSurface still mixes fixture data into live mode — the nisaba preview label CANNOT be lifted yet#

Severity: P1

Evidence: apps/oshun/web/src/components/domains/NisabaSurface.tsx — (a) buildDerivedPassage (:11328-11359): opening a compare target renders SUPPLEMENTAL_PASSAGE_DETAILS[target.passageId] (:549) instead of fetching the real passage; for the REAL corpus id nisaba-passage-speech (Enchiridion XXXIII, tr. Carter in the BFF) the fixture body is "Hatred is not quieted by hatred..." with tradition: 'Buddhist', sourceName: 'Annotated Practice Reader' — the member sees the Enchiridion reference/title over the wrong tradition and an invented body, in live mode; defaults fabricate annotationCount ?? 4, progressPercent ?? 20. (b) :1135-1137 — a live-but-EMPTY notebooks response is replaced by FALLBACK_NOTEBOOK_LIST_RESPONSE ("Morning grounding notebook", 11 items), defeating the honest-empty contract even when the BFF answered. (c) :1224-1227groundedViews falls back to fixture views with groundingStatus: 'grounded'. (d) :1273-1276buildNisabaSavedPassageCard(SAVED_PASSAGE...) renders a fixture saved passage ("Dhammapada 1.5", 9 annotations, saved 2026-03-23) unconditionally, even in surfaceState === 'live', while the real /v1/nisaba/adapter/saved-passages endpoint goes unused. (e) :1133 + :2720-2727 — on a BFF error the fixture daily (Gita 2.47, progressPercent: 62, saved: true, annotationCount: 12 at :589-620) is shown under the label "Cached Nisaba workspace" — nothing was cached; that is the embedded fixture. Mitigations that DO exist: DOMAIN_PREVIEW_SURFACES.nisaba === true (DomainPreviewBanner.tsx:32) keeps the banner on, and the surface carries a live/fallback/curated status label; real BFF hooks (daily passage, annotations read/create/update, notebooks, manuscript) are wired and primary.

Spec promise: features.md 1060-1063 (the /domains/nisaba power-user surface), 1118-1121 (split-view compare renders the editions), plus D1's own bar: "fabricated data must never be mistakable for their own" and the banner registry's instruction to flip nisaba to false only once wired.

What the code actually does: the spine is live, but compare bodies, empty-state notebooks, grounded views, the saved-passage card, and the error path still substitute fabricated content — including misattributed real-corpus ids — so the surface is not yet honest enough to drop the banner.

Verdict on the open D1 question: keep nisaba: true. Lift it only after: compare targets fetch /v1/nisaba/passages/:id for real bodies; live-empty notebooks render honest empty; fixture grounded views and SAVED_PASSAGE card removed (wire adapter/saved-passages or drop the card); error state labeled as unavailable, not "Cached".

8. BYOM source-approval decisions gate nothing — stored-config-never-enforced#

Severity: P1

Evidence: apps/oshun/bff/src/metis/metis-byom-decision-store.ts (real, durable, audited decision records) — but the only consumers of the store are its own admin route (routes/admin-metis-byom-decision.ts) and the operator panel. Nothing in the generation path reads the decision state: /v1/metis/ingest (domain-stubs.ts:1756) derives an outline with no approval check, and the lib that maps decisions to canonical bundle states (libs/metis/integrations/src/byom, named in the store's own header) has zero runtime imports.

Spec promise: features.md 1208-1209 — "Source approval before high-stakes course generation, tutoring, or publication."

What the code actually does: an operator can record approve/reject/request-changes verdicts that are faithfully persisted and listed, and ingestion/generation proceeds identically regardless. Note the naming hazard: this /metis/byom is bring-your-own-material; the spec's "BYOM safety surface" (1515-1544) is bring-your-own-model and is entirely unimplemented at runtime (finding 5).

Fix sketch: have ingest-to-course / study-guide generation look up listForSource(sourceId) and refuse (fail-loud source_not_approved) for high-stakes outputs when the latest decision is not 'approved'; document which outputs count as high-stakes.

9. Tutor runtime: real session-memory engine has no producer; learner-facing tutor surfaces render fixture transcripts and fixture KPIs#

Severity: P1

Evidence: POST /v1/metis/tutor-session-memories (routes/metis-tutor-memory.ts) folds turns into a real mastery posterior and is durable — but a repo-wide grep finds no UI or runtime producer; only e2e specs call it. Meanwhile the member-facing /metis/tutor page (app/metis/tutor/page.tsx:14) renders getMetisTutorFixture() (lib/lilith-data/metis-depth.ts) — a hardcoded transcript including turns in the member's own voice ("I keep getting tangled on apatheia..."); /metis/byom renders fixture materials including "My Discourses notes (last semester)" with rights "mine"; /metis/assessment a fixture rubric; and /operator/metis (app/operator/metis/page.tsx:15) fixture operational KPIs — "Tutor turns · 24h: 1,820" and "OneRoster · in sync ✓" — with no register label on any of these pages.

Spec promise: features.md 1259-1279 (tutor runtime modes with logged switching), 1186-1190 (honest states), 1193-1197 (admin dashboards inspect real telemetry).

What the code actually does: two sources of truth — a real engine wired to HTTP with nothing feeding it, and product surfaces feeding members (and operators) fabricated conversations, uploads, and an "in sync" OneRoster claim. The operator KPI fixture is the most hazardous: it asserts operational health that was never measured.

Fix sketch: drive /metis/tutor from the real session-memory store (list the member's sessions, honest empty state otherwise) and post turns to the existing route; replace /operator/metis KPIs with real counts (tutor-memory store, ingest store, OneRoster apply store all exist) or honest "not measured"; label any remaining presentational content as such.

10. Nisaba depth pages and notebooks index: unlabeled fixture user-data, silent fixture fallback#

Severity: P1

Evidence: (a) lib/lilith-data/nisaba-depth.ts ("fixture today, BFF tomorrow", :3) — every /nisaba/{scholar,compare,manuscript,lexicon,graph,notebook,plan,daily} page renders fixtures: scholar marginalia include 'Renata · 6 May · early morning · the wet stone.' (:39 — the same fabricated "Renata" persona C4 purged from memory controls) presented as reader marginalia; getNisabaDailyFixture (:570-592) fabricates dayNumber: 109 and "Tuesday, 6 May" study-day state (and cites "trans. Farquharson 1944", a translation whose US public-domain status is doubtful). No page carries a preview/illustrative register (checked app/nisaba/scholar/page.tsx, daily/page.tsx). These are NOT covered by D1's banner (that covers /domains/* only). (b) app/nisaba/notebooks/page.tsx:70-71 — the real /v1/nisaba/notebooks read silently swaps to NOTEBOOK_FIXTURE whenever the BFF refuses (including auth failures and outages), with dynamically fabricated updatedAtIso: Date.now() - 32d timestamps and "N held" masthead counts; the only acknowledgment is a code comment ("for layout review").

Spec promise: features.md 1057-1060 records these as "presentational depth" — but presentational copy is one thing; first-person reader marginalia, day counters, and notebook counts presented in the member's own register without a label is the "mistakable for their own" hazard D1 exists for.

What the code actually does: a signed-in member on /nisaba/scholar sees marginalia attributed to a reading life they don't have; on /nisaba/notebooks during a BFF blip they see notebooks they don't own, dated relative to today.

Fix sketch: smallest honest fix — render the depth pages' user-shaped regions (marginalia, day counter, notebook list) from the real stores or honest-empty, keeping the curated scholarly apparatus; for notebooks, show the existing empty/unavailable state instead of NOTEBOOK_FIXTURE. Alternatively extend the D1 banner mechanism to these routes.

11. Nisaba consumer state and integrity verdicts are not durable — member annotations/notebooks and academic-integrity evidence vanish on restart#

Severity: P1

Evidence: nisabaConsumerStateStore is a bare in-memory Map (apps/oshun/bff/src/nisaba/state.ts:431-432, 859) with no wireDurable* call in server.ts (verified: the durable wiring list includes metis ingest/byom/tutor-sessions but nothing for nisaba). Likewise academicIntegrityAppealStore holds verdicts/appeals only inside the in-memory bridge (metis/academic-integrity-appeal-store.ts:32), unwired to any snapshot store.

Spec promise: features.md 1115-1117 ("durable re-anchoring" of annotations), 1121 (reading resumes across devices); 1496-1497 ("evidence preserved beyond routine retention" for severe violations).

What the code actually does: a member's real created annotations, notebooks, and notebook items — and every adjudicated integrity verdict, appeal, and audit record — are lost on every BFF deploy. The codebase's own createSnapshotSink pattern (used one directory over for metis stores) is the established remedy and was simply not applied here.

Fix sketch: add wireDurableNisabaConsumerState / wireDurableIntegrityCases snapshot sinks mirroring wireDurableTutorSessions (explicit Map serialization, write-through on mutation).

12. Nisaba scholarship engines remain orphaned (collation/philology/languages) — consistent with the V1.x deferral, but the only "consumer" is a coverage assertion#

Severity: P2

Evidence: zero imports of @nisaba/philology, @nisaba/criticism (the Needleman-Wunsch collation engine at libs/nisaba/criticism/src/collation-engine.ts + block-alignment + transposition detection), @nisaba/languages (28 ancient-script modules), @nisaba/editions, @nisaba/canon etc. anywhere outside libs/nisaba/. The single runtime touch of the whole 23-library Nisaba estate is NisabaSurface.tsx:5/123 importing assertNisabaWebReadingWorkspaceCoverage — a static registry self-check (libs/nisaba/workspace/src/web-reading-workspace.ts:451) that validates the lib's own constant table, not any product behavior.

Spec promise: features.md 1095-1114 (manuscript/edition schema, critical apparatus, lexicon/ morphology lookup) — but the full Nisaba service is a recorded V1.x deferral, so this is logged as residual state, not a violation.

What the code actually does: nothing — the engines are pointed at nothing, exactly as on 06-10. No fabrication is involved; the in-repo state is honest. Flagged so the V1.x service work knows the runtime gap is unchanged and so nobody mistakes the workspace-coverage import for integration.

Fix sketch (optional, pre-V1.x): the cheapest real consumer is the compare lane — run the collation engine over the corpus's compare pairs server-side to generate the alignments the compare view already displays (currently curated text in state.ts).

13. Cross-device study-state sync is write-only and the study-plan-drafts slot is dead — Nisaba study artifacts remain device-local#

Severity: P2

Evidence: NisabaSurface persists notes/drafts/collections/study-plans/density to localStorage only (lib/nisaba-offline.ts:14-18, surface hydration at NisabaSurface.tsx:1855-1869). The E6 sync lib declares 'study-plan-drafts' as a ClientStateKey (lib/client-state-sync.ts:26) but a repo-wide grep finds zero pushClientState('study-plan-drafts'...) producers and zero pullClientState callers of any key anywhere — the only push is workspace-layout (MultiPanelWorkspace.tsx:463), which itself is never pulled back.

Spec promise: features.md 1120-1121 — "reading position is a ContinuationToken surface so study resumes mid-passage across devices"; 1127-1130 (study plan learner tracking).

What the code actually does: a member's Nisaba study notes, collections, and plans exist on one browser; the durable per-user client-state route exists (E6) but the read-back leg is missing, so even the one synced slot is write-only. (Cross-slice note: the pull gap affects all E6 consumers, not just Nisaba — flagged here because the dead key is a study-plan key.)

Fix sketch: push the Nisaba study-plan/notes payloads through pushClientState('study-plan-drafts',...) on persist, and call pullClientState + mergeByUpdatedAt during the surface's hydration effect.

14. Metis search/assistant seeds contradict the six-discipline spec focus and point at routes that don't exist#

Severity: UX

Evidence: routes/metis-search-seeds.ts:9-33 — the assistant/search seed catalog leads with "Calculus I", "Data Structures and Algorithms ... for technical interviews and production systems", "Introduction to Machine Learning"; consumed by the assistant at routes/assistant.ts:285-292 (falls back to the FULL seed list when no filter matches). crs-002 hrefs from these and sibling fixtures point at /courses/crs-002 (routes/library.ts:613-628, routes/cross-domain-recommendations.ts:617,703, routes/assistant.ts:161) — there is no /courses route in apps/oshun/web/src/app — and at /domains/metis?... deep paths that land on the MetisShellEntryPreview launch gate, not the promised content. The metis room's curated catalog (PHL/AST/GRK, room.ts:51-63) tells a third, different catalog story.

Spec promise: features.md 1166-1178 — Metis "centers heavily on six core disciplines — philosophy, religion, psychology, neuroscience, anthropology, and astronomy ... technical foundations included as scaffolding rather than as the headline curriculum."

What the code actually does: the assistant recommends an interview-prep STEM catalog the spec explicitly de-centers, three surfaces disagree about what courses exist, and the recommended links dead-end.

Fix sketch: replace the seed list with the room's six-discipline curated catalog (one source of truth), and route hrefs to surfaces that exist (/metis, /domains/metis entry, or drop the link).

15. Nisaba manuscript witness text does not match its passage — labeled illustrative, but incoherent#

Severity: UX

Evidence: nisaba/state.ts:916-1028 — the (correctly labeled) illustrative manuscript witness for nisaba-passage-attention (Dhammapada I.1-2, "Mind Precedes All States") carries a transcription/ translation of the Ānāpānasati breath-mindfulness text ("Anapanassatim bhikkhave bhavetham...") and folio regions glossing breath practice, while its own summary claims it keeps the lane connected to "the exact excerpt behind the translated passage." The web fixture duplicates the mismatch (NisabaSurface.tsx:400+, witness attached to "Bhagavad Gita 2.47").

Spec promise: features.md 1095-1100, 1118-1120 — manuscript image alongside its transcription of the passage being read.

What the code actually does: the scholarly showcase teaches the wrong lesson — the witness "behind" the translation has no textual relationship to it.

Fix sketch: either bind the witness to a breath-text passage added to the corpus, or write an illustrative witness whose transcription is the actual Pali of Dhammapada I.1-2.

16. /v1/metis/adapter/* is permanently unavailable (no service implements the adapter contract) and the Sophia/Telegram retriever can never return evidence#

Severity: DEPLOY

Evidence: all eleven /v1/metis/adapter/* reads (routes/metis.ts) and /v1/metis/{recommended, continue,search} delegate to app.domainAdapters.metis dialing oshun.domain.metis.adapter paths at OSHUN_METIS_API_BASE_URL (config/domain-service-discovery.ts:74-81, adapters/domain-service-adapters.ts:2056+); the only Metis service in the repo (apps/metis/api-gateway) serves /api/v1/*, not that contract — no implementation of the adapter surface exists anywhere (repo-wide grep). Same for nisaba's /v1/oshun/* adapter paths (domain-service-adapters.ts:1894+) — and the BFF's Sophia retriever (telegram/sophia-grounder-bridge.ts:55-103, wired at app.ts:1126) depends solely on domainAdapters.nisaba.searchLibrary, so it always catches, returns zero citations, and abstains.

Spec promise: features.md 1182-1190 (Metis shell recognition / learner reads); 1212-1214 (source-aware tutor answers with citation inspection).

What the code actually does: everything fails honest (502 / abstention) — no fabrication — but these are not env-cred gaps: there is no deployable implementation to point the env vars at. The Nisaba service half is the recorded V1.x deferral (not re-found); the Metis half has no recorded deferral, and the assistant's grounding keystone always abstains even though the BFF holds a real, searchable 9-passage corpus locally in nisabaConsumerStateStore.

Fix sketch: short-term, back searchLibrary (and the nisaba adapter daily/saved reads, per the E2 pattern) with the local consumer store so grounding has real evidence; for Metis, either implement the /v1/oshun/* adapter facade inside apps/metis/api-gateway or record the deferral explicitly.


Severity counts#

Severity Count Findings
P0-SEC 1 1
P0-HONESTY 3 2, 3, 4
P0-STRUCT 1 5
P1 6 6, 7, 8, 9, 10, 11
P2 2 12, 13
UX 2 14, 15
DEPLOY 1 16
Total 16

D1 open question answered: DOMAIN_PREVIEW_SURFACES.nisaba must stay true (finding 7 lists the exact wiring needed to lift it).