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

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-1056` — `GET /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:1130` — `POST /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-1422` —
  `GET/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-1473` — `GET /v1/arete/review/closed` returns
  `areteReviewStore.list()` (all closures, all users).
- `domain-stubs.ts:1500-1506` + `arete/arete-offering-store.ts:26-59` —
  `GET /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-16` — `getArete()`
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-316` —
`submitReflection` 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:1345` — `AreteReviewClose` durably records
  closure of `data.weekLabel` = the FIXTURE "week of 28 apr — 4 may"
  (arete-depth.ts:198) for every member, forever.
- `arete.tsx:2230` — `AreteOfferingActions` 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-145` — `TARA_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** |                        |
