Disciplines · Audits

V1 Residual Audit — Tara + Arete (features.md 451–657)

Gap audits and as-built reviews.

1section22 minread

On this page

Date: 2026-06-11 · Read-only static audit. Baseline: all 57 tasks of V1_SPEC_GROUND_TRUTH_AUDIT_2026-06-10 are landed (verified in current code where relevant: C1 facades exist and are adapter-wired; C2 check-in + humane streak engine is real; C3 domain-stub reads are real-or-honest; D2 ambient WAV + env-gated guidance routes exist and the hub player plays the ambient loop). Everything below is RESIDUAL — what still falls short of the spec promise or of product polish. Accepted states (D1 banner on /domains/{tara,arete} roots, explicitly recorded V1.x deferrals, env-gated fail-closed providers) are not re-flagged except where the acceptance itself has a hole.


1. Tara/Arete practice records keyed by a spoofable header, defaulting every member to one shared house-user#

Severity: P0-SEC

Evidence:

  • apps/oshun/bff/src/routes/domain-stubs.ts:1037-1056GET /v1/tara/today and GET /v1/tara/room resolve identity as (request.headers['x-oshun-user-id'] as string) ?? 'house-user' and never consult authContext.
  • domain-stubs.ts:1130POST /v1/tara/sittings/:id/complete (the flagship completion write) does the same: header-only, 'house-user' fallback, no auth preHandler (only [originGuard, csrfGuard]).
  • domain-stubs.ts:1062-1065, 1086-1089, 1207-1210, 1252-1255, 1319-1322, 1429-1432 — sittings/ritual/streak/habits/check-in/review prefer authContext?.userId but none of these routes registers an auth preHandler, so request.authContext is never populated (it is set only by createAuthPreHandler, middleware/authz.ts:120); they all fall through to the header → 'house-user'.
  • Nothing anywhere injects the header: grep -rn "x-oshun-user-id" across apps/oshun hits only domain-stubs.ts itself. bff-fetch.ts forwards bearer+cookies only; TaraSitPlayer.tsx:194-202 POSTs completion with credentials:'include' and no bearer.

Spec promise: features.md 497–503 (session state per learner), 520–526 (first-class per-user events), 575–585 (per-member check-in/streak records).

What the code actually does: every member reads and writes ONE shared practice record. One member's sit completions advance everyone's /tara room "todaysSit" and streak; arete check-ins, streaks, review highlights and ritual continuation are likewise shared. Worse, any unauthenticated caller can write completions/ check-ins for any victim user id by setting x-oshun-user-id (reads too: GET /v1/arete/streak with a victim header returns their record). The "per-member" logic C2/C3 built is real, but the member identity feeding it is fabricated.

Fix sketch: register the auth preHandler on all /v1/tara/* + /v1/arete/* domain-stub routes, take userId from authContext.userId only, delete the x-oshun-user-id header path (or restrict it to a service-token-authenticated internal caller), and have the web player send the bearer on its POSTs.


2. Cross-user global stores: habits, review closures, and Living-Offering decisions are shared by all members (and two of them are volatile)#

Severity: P0-SEC

Evidence:

  • apps/oshun/bff/src/routes/domain-stubs.ts:1248-1284, 1396-1422GET/POST /v1/arete/habits read/write domainStubsStore.habits with no user key; PgHabit (domain-stubs-postgres.ts:39) has NO userId column (only check-ins do). Every member's proposed habit — label and "whyItMatters" text, which is sensitive by nature ("Quit drinking…") — is listed to every other member, unauthenticated.
  • domain-stubs.ts:1467-1473GET /v1/arete/review/closed returns areteReviewStore.list() (all closures, all users).
  • domain-stubs.ts:1500-1506 + arete/arete-offering-store.ts:26-59GET /v1/arete/offerings/sent returns every member's offering records including recipient names; the store is an in-memory Map (restart wipes it), as is arete-review-store.

Spec promise: features.md 651–657 ("the private intent layer never leaves the originating user"; offerings "kept in their gallery", per-person).

What the code actually does: personal decisions (who you sent an offering to, which weeks you closed, which habits you keep) are a single global list served to any caller, and the offering/review records evaporate on restart.

Fix sketch: add userId (from authContext) to goal3_stub_habit and to both stores' records; scope every list/read by it; move the two in-memory stores to the goal3 Postgres/file tier like habits/check-ins.


3. /arete consumer hub is still a fully fabricated room — the one hub left on a fixture#

Severity: P0-HONESTY

Evidence: apps/oshun/web/src/lib/lilith-data/arete.ts:14-16getArete() returns getAreteFixture() unconditionally: "Sit, three minutes · 31-day streak · Done today", week heatmaps, weekStats 22/28, a fabricated coach note ("Your reading is at risk on Sundays…"), a fabricated journal entry ("The barista smiled at me…"). Rendered by AreteRoom (components/lilith/rooms.tsx:341-564) with no preview label (the D1 banner covers /domains/* only). Contrast: lilith-data/tara.ts fetches /v1/tara/room and renders an honest unavailable room on failure; rooms.tsx:61,568,805,1114 show tara/veritas/nyx/nisaba all default to *Unavailable() while AreteRoom alone defaults to the fixture.

Spec promise: features.md 567–571 (consumer hub showing the member's habits, streaks, coaching) + the 06-10 headline "fixture hubs" which this slice's fixes were meant to close.

What the code actually does: presents invented personal data as the member's own on the flagship /arete page, while the real plumbing this audit cycle built (/v1/arete/habits with per-member overlay, /v1/arete/streak computed from real check-ins) sits one fetch away, unconsumed by this page.

Fix sketch: build /v1/arete/room in the BFF (habits list + computed streak + honest-empty coach note/evening prompt) exactly as /v1/tara/room was done; getArete() fetches it and returns an honest areteUnavailable() on failure.


4. All eight /arete/* depth pages serve fixtures with intimate fabricated narratives; the real streak/review endpoints go unconsumed#

Severity: P0-HONESTY

Evidence: apps/oshun/web/src/lib/lilith-data/arete-depth.ts — every getter except getAreteHabits resolves to its fixture: getAreteStreak (44-61, fabricated day 109 / 47-day streak + generateStreakHeatmap() arithmetic pattern (i*31)%11), getAreteRecovery (65-95, "Sunday was your daughter's recital"), getAretePlan, getAreteGoal, getAreteReview (192-287, a full fabricated week: "Six of seven mornings, you sat", per-day journal lines), getAreteCoaching (291-349, a letter addressed to "Renata" with fabricated pattern confidences 0.84/0.71/0.62), getAretePatterns, getAreteOffering. Pages: app/arete/{streak,recovery,plan,goal,review,coaching,patterns, offering}/page.tsx. Meanwhile the BFF's REAL GET /v1/arete/streak (domain-stubs.ts:1198, C2's humane engine) and GET /v1/arete/review (domain-stubs.ts:1427, real weekly highlights) have ZERO web consumers (grep "/v1/arete" apps/oshun/web/src → only offerings/habits/coach/review- close/practice-home).

Spec promise: features.md 597–650 (humane streak surface, recovery prompts from real missed cadences, weekly review computed from "habit/goal/routine logs, mood, energy, journal entries", coaching summary from observed patterns).

What the code actually does: /arete/streak shows a 47-day streak the engine never computed; /arete/recovery invents the member's last three days; /arete/coaching greets the member as "Renata". These are consumer routes outside DOMAIN_PREVIEW_SURFACES — only /arete/streak and /arete/recovery carry a "Specimen · Arete mobile" masthead; plan/goal/review/coaching/patterns/ offering carry no register at all. Two of the pages mount REAL mutation components against the fiction (see finding 16).

Fix sketch: wire getAreteStreak/v1/arete/streak and getAreteReview/v1/arete/review now (both exist); for the rest, either wire to honest-empty BFF reads or stamp an explicit preview register on the page until wired.


5. TaraSitPlayer reflection "save" fabricates success; mood-after and post-completion reflections are silently discarded#

Severity: P0-HONESTY

Evidence: apps/oshun/web/src/components/lilith/TaraSitPlayer.tsx:299-316submitReflection online path is await new Promise(r => setTimeout(r, 300)); setReflectionStatus('saved') → UI prints "Saved · attached to today" (line 765). No request is made anywhere. The offline path writes tara:reflection:{id} to localStorage and prints "will sync when you're back online" (768-770) — no sync mechanism exists (the only repo references to tara:reflection:/tara:completion: are the two writes, lines 207/303; the comment at 204 "localStorage queue picks it up on next online tick" describes a reader that does not exist). moodAfter (687-697) is captured and never sent. The completion POST (184-223) includes reflectionText but fires exactly once at the completion transition — before the reflection textarea has been typed into — and completionPostedRef blocks any re-send; the comment at 219-221 ("Future edits … are sent via the existing submitReflection flow") is false.

Spec promise: features.md 524–526 ("Completion events: emit on completion with duration, modality, mood-after, journal capture"), 545–547 (save partial-session reflection), 535 (export-to-journal-entry with reflection capture).

What the code actually does: tells the member their reflection is "attached to today" while dropping it (and their mood) on the floor.

Fix sketch: make submitReflection POST to a real endpoint (extend /v1/tara/sittings/:id/complete with an amend leg, or add /v1/tara/sittings/:id/reflection) carrying { reflectionText, moodAfter }; add a mount-time flusher for the two localStorage queues; only show "Saved" after a 2xx.


6. The C2 check-in engine has zero UI consumers; the only check-in UI is an orphaned component with a simulated save#

Severity: P0-HONESTY

Evidence:

  • POST /v1/arete/habits/:habitId/check-in (domain-stubs.ts:1289) — no caller in apps/oshun/web/src or apps/oshun/mobile/src (grep: only partnership check-ins hit the network).
  • components/domains/arete/DailyCheckInOverlay.tsx:127-151 — the actual check-in flow: // SIMULATED BFF save, fake 600ms delay, console.info, then closes as if saved. And the component itself is orphaned — nothing imports it.
  • The home CTA checkIn: buildLaunch('/check-in') (routes/arete.ts:474,551) lands on /domains/arete?path=/check-in, which AreteSurface.tsx:530 maps to the COACH CHAT view — there is no check-in view. The real-data /arete/habits page (components/lilith/arete.tsx:2255, zero fetch/POST calls in the file) is display-only: no done/partial/skip/ decline control exists anywhere.

Spec promise: features.md 575–595 (check-in schema and status semantics as the core daily loop), 569 ("Mobile daily check-in").

What the code actually does: C2 built a durable, well-tested check-in record and humane fold engine that no member can ever reach. The product still has no way to check in — the 06-10 headline "no check-ins" is only half-closed (backend yes, product no).

Fix sketch: add done/partial/skip/decline buttons to the /arete/habits rows (client component POSTing the C2 endpoint with bearer), repoint the home "Check in" launch at it, and either wire DailyCheckInOverlay to the same endpoint or delete it.


7. AreteAICoach (/domains/arete/coach) fabricates an AI coach — the B8 fix covered the other coach component only#

Severity: P0-HONESTY

Evidence: components/domains/arete/AreteAICoach.tsx:97-139 — every user message gets the SAME canned reply ("That is a thoughtful observation. Let me reflect that back to you…") after a fake setTimeout 1.5s "typing" animation, seeded from a fabricated COACH_SESSIONS history (arete-extended-simulation). Zero network calls in the file. B8 fixed AreteCoach.tsx (now POSTs /v1/arete/coach/responses, line 401) — but /domains/arete/coach/page.tsx renders AreteAICoach, the unfixed twin, on a static route with no preview banner (finding 9).

Spec promise: features.md 631–635 (coaching summary/invitation grounded in observed patterns); CLAUDE.md zero-tolerance for fabricated results.

What the code actually does: simulates an AI conversation and presents it as coaching.

Fix sketch: have AreteAICoach's ChatView call the same POST /v1/arete/coach/responses composer AreteCoach uses (same process, no creds needed), start from an empty session, and drop COACH_SESSIONS.


8. TaraSoundLibrary is a silent fake player — play/pause/mixer/binaural UI with no audio code at all#

Severity: P0-HONESTY

Evidence: components/domains/tara/TaraSoundLibrary.tsx (2,562 lines) — play buttons, "Binaural beat player with frequency display" (line 14), mixer layers, an animated equalizer (isPlaying, line 1239) — and not one AudioContext, OscillatorNode, or new Audio in the file (grep: zero). Rendered at /domains/tara/sounds (static route, no banner — finding 9). Meanwhile the BFF can already synthesize real ambient WAVs (tara/ambient-audio.ts, D2).

Spec promise: features.md 484–487 (sound modality: singing-bowl, drone, mantra), 529–531 (background-sound preferences).

What the code actually does: pressing play animates bars and produces silence — fabricated playback on a customer route.

Fix sketch: feed the library from the D2 synthesizer (an ambient.wav variant per soundscape id — the planner is already deterministic per session id) or a WebAudio oscillator graph for the binaural/drone presets; until then, banner the route.


9. The D1 preview banner covers only the /domains/[domainId] root — all 19 static sub-routes render simulation data with no register#

Severity: P0-HONESTY

Evidence: DomainPreviewBanner is imported solely by components/DomainRouteExperience.tsx:4 (rendered at line 579), whose only route consumer is app/domains/[domainId]/page.tsx. Next.js static routes win over the dynamic segment, so /domains/arete/{affirmations,balance,coach, gamification,goals,habits,journal,plan-review,progress,seven-habits,time, vision} and /domains/tara/{analytics,collections,courses/[id],programs, search,sounds,teachers,teachers/[id]} never mount it (e.g. app/domains/arete/habits/page.tsx renders AreteHabitSystem bare). Those pages render exactly the components D1 was about: AreteHabitSystem.tsx:2709 (useState(sampleHabits)), AreteGoalSystem/AreteJournalSystem (sampleJournalEntries etc.), TaraCollections.tsx:128,254 (SIMULATED_COLLECTIONS/PROGRAMS), TaraCourseDetail, TaraTeacherProfile, AreteJournalReflectionWorkspace.tsx:97-149 (fabricated entries for user-1), plus findings 7 and 8. The D1 closure note ("mounted once … every /domains/* deep surface now carries an explicit register") is false for the sub-routes.

Spec promise: D1's own accepted contract — fabricated data must never be mistakable for the member's own.

What the code actually does: the banner-as-accepted-state holds only on the two root URLs; one click deeper, the same simulated personal data renders with full visual authority.

Fix sketch: mount <DomainPreviewBanner domainId=…/> in a shared layout for app/domains/tara/* and app/domains/arete/* (one layout.tsx per domain folder), keyed to the same registry.


10. Two parallel data planes for the same Tara/Arete facts — goal3 stores vs the C1 facades — and surfaces mix them#

Severity: P0-STRUCT

Evidence:

  • Tara progress: web hub writes/reads BFF goal3 taraCompletions (domain-stubs.ts:1049-1161), while /v1/tara/{streak,history,continue, favorites,recommended} (routes/tara.ts:175-250) read the facade → apps/tara/api Drizzle tables, which never see the hub's completions.
  • Arete streak: /v1/arete/streak computes from goal3 check-ins (domain-stubs.ts:1198); the reminder worker computes at-risk streaks from app.domainAdapters.arete.getStreakStats — the facade's apps/arete/api-side completions (server.ts:645); the web /arete + /arete/streak pages show a third value (the fixture, findings 3-4).
  • Habit completions have two write paths: C2 check-ins (goal3) and routes/routines.ts:74-75 → facade logHabitCompletion (apps/arete/api).

Spec promise: features.md 503–506 (one continuation state), 597–607 (one humane streak the member can trust).

What the code actually does: the streak a member could be nudged about (reminders, facade plane) and the streak the API computes from their check-ins (goal3 plane) and the streak the page shows (fixture) are three independent numbers that can never agree.

Fix sketch: pick one system of record per fact (the C1 facade is the architecture the user chose) and make the other a write-through: hub sit completions forward to the facade's progress write; /v1/arete/streak reads the facade (or the facade reads goal3 check-ins) — then delete the loser.


11. Even inside the goal3 plane, the Tara loop is broken end-to-end: completions write to house-user, analytics reads the real userId#

Severity: P0-STRUCT

Evidence: POST /v1/tara/sittings/:id/complete records under the header-or- 'house-user' id (domain-stubs.ts:1130, no auth, web sends no header/bearer); GET /v1/tara/analytics (domain-stubs.ts:1166-1191, auth-required) reads pg.taraCompletions.listForUser(authContext.userId). The two ids can never match for a real member.

Spec promise: features.md 461–463 (dashboard reflecting the member's practice); B9's "computed from real rows".

What the code actually does: a member completes sits forever and their analytics dashboard remains the honest-empty state forever, while /tara's room progress (read via the same house-user key) shows everyone the shared aggregate. The B9 dashboard is honest but unfeedable.

Fix sketch: same as finding 1 — authContext on the write path; the analytics read then works as-built.


12. The sit player pauses itself mid-meditation: 90s without pointer/keyboard input triggers "drift" while RUNNING#

Severity: P1

Evidence: TaraSitPlayer.tsx:31 DRIFT_IDLE_SECONDS = 90; tick loop 225-256 sets state='drifted' (stopping the timer and the ambient audio, 151-161) when Date.now() - lastInteractionRef ≥ 90s; lastInteractionRef updates only on play/pause/scrub/keyboard (258-296). Every sitting in the catalog is ≥180s (tara/room.ts:42-55).

Spec promise: features.md 500-501 — drift is "idle beyond the per-modality drift_idle_seconds threshold", i.e. detecting an ABANDONED session; sitting still IS the practice.

What the code actually does: no sit can complete unless the member pokes the screen at least every 89 seconds; in practice the flagship flow always ends in "The session paused itself" at 1:30, and the real-progress overlay (room/streak) is unreachable through honest use.

Fix sketch: while state==='started' and ambient audio is playing, treat playback as presence (refresh lastInteractionRef on timeupdate), or count drift only from paused; per-modality thresholds belong in the sitting catalog.


13. Living Offerings: the spec's flagship Arete feature is a fixture composer + a 3-field volatile decision log, and the index can never show what you kept#

Severity: P1

Evidence: /arete/offering renders getAreteOfferingFixture() (arete-depth.ts:389-427 — recipient "J", canned timeline/score/sharing rows); AreteOfferingActions POSTs /v1/arete/offerings/keep which stores {recipient, occasion, action} in the in-memory global store (finding 2) — no scene, no score, no watermark, no shareability matrix, no re-render artifact. The index /arete/offerings/page.tsx:46 reads GET /v1/arete/offerings — a guardedFixtureRoute (domain-stubs.ts:1423, 503 in production) — NOT /v1/arete/offerings/sent where keep/send writes land. A member who keeps a draft then opens the index sees "No drafts in progress" (prod) or fixture rows that aren't theirs (dev).

Spec promise: features.md 651–657 — intention → 4-8 min Living Scene, watermarked, kept in gallery, re-renderable forever, shareability matrix.

What the code actually does: composer is fiction; the only durable thing is a recipient string in RAM; the kept/sent lifecycle is split across two endpoints that never meet.

Fix sketch: point the index at /v1/arete/offerings/sent (merged with drafts) so the loop a member can already perform is visible; the real scene/score pipeline should ride the existing Living-Scenes render route (BFF living-scenes/render-route.ts already does contemplative arcs) as the generation backend.


14. Voice layer: guidance endpoint orphaned, voice-speed slider wired to nothing, and the hub claims "narrated" sessions by real teachers that don't exist#

Severity: P1

Evidence:

  • GET /v1/tara/sessions/:id/guidance (tara/ambient-audio-routes.ts:113-154, env-gated ElevenLabs, fail-closed — correctly built) has ZERO web consumers (grep guidance + /v1/tara in web: none). Even with creds provisioned, no member ever hears it.
  • TaraSitPlayer.tsx:85,541-555 — the "Voice speed · 0.85–1.25×" slider sets voiceSpeed which is read by nothing (no voice element exists; the ambient loop's playbackRate is untouched). A dead control implementing the spec's most specific audio knob.
  • rooms.tsx:160 hub prints "{teacher} · narrated"; tara/room.ts:57-62 hardcodes real public figures (Sam Harris, Tara Brach, J. Goldstein, Pema Chödrön) with fabricated sitting counts (142/96/71/34 — the catalog has 12 sittings); TARA_TODAY_FIXTURE claims "Sam Harris reads twelve short essays". No narration audio exists anywhere in the product, and the deep teacher directory uses a disjoint fictional roster (tara-simulation-data.ts: Sarah Chen et al.), so the hub teachers link to nothing (see finding 21).

Spec promise: features.md 529-531 (voice speed with quality-preserving resample, ambient/voice mix), 469 (named teachers), D2's voice-guidance leg.

What the code actually does: ships the synthesis seam but not the experience; advertises narration and real-teacher content the player cannot produce (content/licensing risk on top of the honesty gap).

Fix sketch: fetch /guidance?phase=opening|midpoint|closing in the player at the matching progress points (graceful skip on 503), drive audio.playbackRate = voiceSpeed (preservesPitch defaults true), and replace the real-people teacher fixture with the platform's own voice persona until licensed content exists.


15. Arete balance + journal backends exist in the facade but no web surface can reach them; the web UIs are client-local fixtures#

Severity: P1

Evidence: facade balance endpoints (domain-service-adapters.ts:1794-1807, /v1/oshun/balance + /latest; journal endpoints likewise per C1) are consumed only by achievement stat counters (routes/achievements.ts:211-214). No /v1/arete/balance or /v1/arete/journal BFF customer route exists. The web components — AreteBalance.tsx (1,557 lines), AreteLifeBalance.tsx (892), AreteJournalReflectionWorkspace.tsx (1,270, fabricated user-1 entries), AreteJournalSystem.tsx (sampleJournalEntries) — make zero network calls.

Spec promise: features.md 570-572 ("Web plan/review workspace, journaling, reflection"), 626-630 (inputs include journal entries, mood, energy).

What the code actually does: a member cannot write a journal entry or a balance assessment anywhere in the Oshun web product, while the storage and API for both sit finished in apps/arete/api.

Fix sketch: add thin BFF routes proxying the arete adapter's journal/balance read+write; wire AreteJournalReflectionWorkspace's editor and AreteLifeBalance's assessment submit to them; fixture entries become the empty state.


16. Real durable actions are mounted on fictional subjects — closures, offerings, and "approved adjustments" record decisions about content that doesn't exist#

Severity: P1

Evidence:

  • components/lilith/arete.tsx:1345AreteReviewClose durably records closure of data.weekLabel = the FIXTURE "week of 28 apr — 4 may" (arete-depth.ts:198) for every member, forever.
  • arete.tsx:2230AreteOfferingActions sends "to J" (fixture recipient).
  • AreteCoachActions.tsx:41-60 — "Approve" prints "two adjustments queued for the next review" but writes only a localStorage flag; the "adjustments" are fixture text; nothing is queued anywhere, and the claim survives only on that browser.

Spec promise: features.md 628-630 (plan adjustments saved with user approval); the codebase's own fail-loud-over-fake rule.

What the code actually does: honest stores, fictional referents — the durable record says the member closed a week they never lived and approved adjustments that were never proposed.

Fix sketch: these resolve automatically once findings 3-4 wire the pages to real data; until then gate the action components behind a real weekLabel/ recipient (render disabled with an honest "no live week yet" note), and make "Approve" POST to a real adjustments record or say what it actually does.


17. getAreteHabits falls back to a fabricated 3-habit fixture when the BFF is unreachable#

Severity: P1

Evidence: lilith-data/arete-depth.ts:444-502 — on !remote, getAreteHabitsFixture() returns invented habits ("Morning grounding sit · 7-day streak · kept yesterday morning") rendered by /arete/habits as the member's own. Tara's equivalent (tara.ts:16-18) returns an honest unavailable register instead.

Spec promise: project rule "Always wire real data — never fixtures/fallbacks"; the C3 convention (honest zeros / unavailable on failure).

What the code actually does: a transient BFF failure silently swaps the member's real habit list for fiction — indistinguishable in the UI.

Fix sketch: return an areteHabitsUnavailable() shape (empty list + an unavailable: true flag the page renders as a quiet notice), mirroring taraUnavailable().


18. Orphaned Tara content plumbing: four library modules with zero consumers#

Severity: P2

Evidence: lib/tara/use-tara-content.ts (10 exported hooks over simulated data; header says "swap queryFn when a real BFF endpoint is available"), tara-cache.ts (imports its query keys; nothing imports tara-cache), tara-monitoring.ts, content-filters.ts — no importer outside the chain (grep across components/app: only content-types and tara-simulation-data are used, by the simulated components themselves).

Spec promise: n/a (dead code; the modules exist to make simulation look like infrastructure).

What the code actually does: maintains a parallel React-Query layer that no surface uses — a trap for future wiring (someone will "wire" the hooks and get simulation).

Fix sketch: delete the four modules, or convert use-tara-content queryFns to the real /v1/tara/* facade routes (which exist and are unconsumed — finding 10) and adopt them in the deep components.


19. Tara hub copy inconsistencies: hardcoded "12 min" eyebrow and a second, contradictory path universe in /v1/tara/today#

Severity: P2

Evidence: rooms.tsx:110 — eyebrow literal "Today's sit · 12 min" regardless of todaysSit.durationSeconds (the Begin link beside it computes the real minutes — for the first sit they read "12 min" and "Begin · 3 min" together). domain-stubs.ts:131-145TARA_TODAY_FIXTURE claims pathLabel 'path IV', sessionIndex 3, pathTotal 21 vs the room's single 12-sitting path; consumed by lilith-studio/tara.

Spec promise: features.md 461-463 (coherent session/path model).

Fix sketch: render the real duration in the eyebrow; align TARA_TODAY_FIXTURE with the SITTING_PATH source of truth (or derive it from buildTaraRoom).


20. /v1/tara/sittings/:id/complete accepts arbitrary sitting ids#

Severity: P2

Evidence: domain-stubs.ts:1123-1147 — any non-empty string is recorded as a completion; no validation against TARA_SITTING_CATALOG/SITTING_PATH.

Spec promise: features.md 497-503 (session state is per-catalog-session).

What the code actually does: garbage ids inflate the streak (streak counts distinct days of any completion) and pollute analytics' per-sitting grouping.

Fix sketch: 404 unless the id is in the shared catalog module (export the id set from tara/room.ts).


21. Dead and misrouted hub controls (Tara + Arete)#

Severity: UX

Evidence:

  • rooms.tsx:67-75 — Tara hub LSubNav tabs "Courses / Teachers / Library" have no href; LSubNav renders href-less items as cursor:pointer spans (design-system/lilith/shells.tsx:179-189) — three dead tabs, while real destinations exist (/domains/tara/collections, /domains/tara/teachers, /library).
  • rooms.tsx:269-278 — each hub teacher row ends in an "↗" that is a span, not a link (and no teacher page exists for the real-person roster — finding 14).
  • rooms.tsx:443-462 — Arete hub habit rows and "+ New ritual" deep-link to /domains/arete/habits?habit=… / ?wizard=loop, but app/domains/arete/habits/page.tsx passes only onBack; both params are ignored, and the destination is the unbannered SIMULATOR while the real wired pages (/arete/habits, /arete/habits/new) exist one path over.

Spec promise: features.md 453-456 / 560-566 (hub → deep-tool continuity).

Fix sketch: give the three tabs hrefs; make teacher rows link to the teacher directory; repoint hub habit links + "New ritual" at /arete/habits and /arete/habits/new.


22. Register whiplash: three disagreeing datasets within one click of /arete#

Severity: UX

Evidence: /arete (fixture: best streak "Sit · 31 days", coach note) → subnav "Habits" → /domains/arete/habits (simulation: "Morning meditation · 22d", no banner) vs /arete/habits (real BFF rows, likely empty) vs /arete/streak (fixture: 47 days, "day 109"). Four surfaces, four answers to "what's my streak?", none of them the C2-computed truth.

Spec promise: features.md 597-607 (one humane streak); D1's register contract.

Fix sketch: findings 3, 4, 9 and 21 collectively resolve this; the acceptance test should be "every visible streak number traces to /v1/arete/streak".


23. Silent error-states: analytics dashboard and offerings index render success-shaped emptiness on failure#

Severity: UX

Evidence: TaraAnalyticsDashboard.tsx:893-913 — fetch failure keeps EMPTY_MODEL with no "couldn't load" notice (honest-empty and error are indistinguishable; with finding 11, every member sees it forever). app/arete/offerings/page.tsx:46-49 — a 503 from the prod-guarded endpoint collapses to offerings: [] → "No drafts in progress", presented as the member's true state.

Spec promise: product polish — distinct empty/loading/error registers.

Fix sketch: track a failed flag alongside loading; render a one-line quiet "couldn't reach your practice history — try again" register distinct from the genuine empty state.


24. DEPLOY register (not code defects; fail-closed as designed)#

Severity: DEPLOY

  • Voice guidance: OSHUN_ELEVENLABS_API_KEY + OSHUN_ELEVENLABS_VOICE_ID (ambient-audio-routes.ts:54-63) — 503 voice_guidance_not_configured until provisioned (but see finding 14: provisioning alone changes nothing for members today).
  • C1 facades: OSHUN_{TARA,ARETE}_API_BASE_URL + OSHUN_DOMAIN_SERVICE_TOKEN / {TARA,ARETE}_OSHUN_FACADE_TOKEN (app.ts:1136-1149, domain-service-discovery.ts) — non-dev boot throws without explicit origins; facades fail closed without the token.
  • goal3 Postgres tier: OSHUN_STUBS_DATABASE_URL/DATABASE_URL — without it, streak/check-in/completion/analytics 503 (correctly) and room/sittings serve the fresh-path default.

Severity counts#

Severity Count Findings
P0-SEC 2 1, 2
P0-HONESTY 7 3, 4, 5, 6, 7, 8, 9
P0-STRUCT 2 10, 11
P1 6 12, 13, 14, 15, 16, 17
P2 3 18, 19, 20
UX 3 21, 22, 23
DEPLOY 1 24
Total 24