# V1 Residual Audit — Veritas + Nyx (features.md 657–1055)

Date: 2026-06-11 · Auditor slice 03 · Static read-only pass (no builds/tests
run). Baseline: all 57 tasks of `V1_SPEC_GROUND_TRUTH_AUDIT_2026-06-10.md`
landed; this report covers only what is residually broken/dishonest in current
code.

Prior strengths confirmed and NOT re-litigated: the Nyx ephemeris tier is real
(`/v1/nyx/tonight`, `/v1/nyx/sky-almanac`, `/v1/nyx/events.ics`,
`/v1/events/upcoming` all compute from `@oshun/domain-nyx`,
`apps/oshun/bff/src/routes/domain-stubs.ts:1567–1670`); the `/veritas` hub is
honest (real briefing or honest-unavailable,
`apps/oshun/web/src/lib/lilith-data/veritas.ts:29–37`); `guardedFixtureRoute`
fails closed in production (`domain-stubs.ts:840–856`); the Veritas
retraction-cascade engine is wired end-to-end on the write side
(`apps/oshun/bff/src/veritas/retraction-cascade-runtime.ts:48–113`, started from
`server.ts:520–536`). Adversarial grep over the BFF veritas/nyx
route/adapter/store files returned zero stub-comment or `Math.random` hits.

---

### 1. The `/v1/oshun/*` facade does not exist for Veritas or Nyx — 35 BFF adapter operations are 502-by-construction

**Severity:** P0-STRUCT

**Evidence:**

- BFF expectations —
  `apps/oshun/bff/src/adapters/domain-service-adapters.ts:1090–1305` (veritas)
  and `:1306–1552` (nyx). Every method targets `/v1/oshun/...` on
  `OSHUN_VERITAS_API_BASE_URL` / `OSHUN_NYX_API_BASE_URL`
  (`config/domain-service-discovery.ts:42–56`).
- Downstream reality — `apps/veritas/api` mounts only `/api/v1/*`
  (articles/claims/sources/stories/taxonomy/preferences/payments…,
  `src/interfaces/http/routes/v1/index.ts`; Postgres-backed, 503 fail-closed,
  e.g. `articles.ts:95–98, 276–279`). `apps/nyx/api` mounts only
  `/api/v1/{objects,ephemeris,events,satellites}` (`src/app.ts:111–114`).
  `grep -rn "oshun" apps/{veritas,nyx}/api/src` finds only `@oshun/database`
  imports — **no `/v1/oshun` route exists in either service.** Compare:
  `apps/arete/api/src/routes/oshun-facade.ts` and
  `apps/tara/api/src/oshun-facade/` exist (C1).
- Default adapters are the real HTTP ones
  (`apps/oshun/bff/src/app.ts:1136–1149`); every non-existent path → 404 →
  `DomainServiceAdapterError` → the route `guarded()` wrappers reply 502
  (`routes/veritas.ts:227–239`, `routes/nyx.ts:263–275`).

**Spec promise:** features.md 659–684 (Veritas hub/briefing, save/follow,
evidence inspection), 985–1007 + 1011–1032 (Nyx event model, save/follow/remind,
highlights).

**What the code actually does (blast radius, all 502/empty in any real
deployment):**

- `/v1/veritas/adapter/{availability,home-cards,continue-items,search,launch,saved-articles,trending-topics,bridge-contexts}`
  (`routes/veritas.ts:302–501`) and `/v1/veritas/briefing/home` (`:527–530`) →
  the `/veritas` hub renders permanent honest-unavailable.
- `/v1/nyx/adapter/{availability,home-cards,continue-items,search,launch,nightly-highlights,object-search,saved-objects,event-reminders,observation-logs,bridge-moments}`
  (`routes/nyx.ts:314–655`) and `/v1/nyx/perspective/home` (`:782–818`, 503
  every time).
- Assistant tool surface: 23 veritas/nyx ops (`routes/assistant.ts:70–108`);
  wearable (7), desktop (7); achievements — every Veritas and Nyx criterion
  metric degrades to 0 forever (`routes/achievements.ts:163–199`, honest-degrade
  but permanently zero); workspace evidence panel
  (`apps/oshun/web/src/components/workspace/data/useWorkspaceLiveData.ts` →
  `/v1/veritas/adapter/saved-articles`) shows "unavailable".
- **Misleading availability:** `probeDomainHealth` probes `/readyz` (veritas) /
  `/health` (nyx) (`health/domain-service-health.ts:115–133`) — both EXIST
  downstream and return ok, so `/v1/{veritas,nyx}/adapter/availability` reports
  the domain available
  (`libs/oshun/domain-veritas/src/canonical-adapter.ts:75–77` maps availability
  from `getHealth`) while every data read 404s.

**What a bridge router needs (per the C1 tara/arete pattern):**

- _Veritas_ (17 endpoints): `GET briefs/trending`, `GET articles/trending`,
  `GET articles/:id`, `GET articles/continue`, `GET articles/unread-count`,
  `GET articles/reading-stats`, `GET articles/engagement-stats`,
  `GET claims/top`, `GET claims/:id`, `GET topics/trending`,
  `GET topics/followed`, `POST+DELETE topics/:id/follow`, `GET categories`,
  `GET saved`, `POST+DELETE saved/:articleId`. Catalog legs can map straight
  onto the existing Postgres routes (`/api/v1/articles/trending`,
  `/api/v1/claims`, `/api/v1/categories`, `/api/v1/topics`,
  `/api/v1/sources/:id` for credibility fields). **Per-user legs
  (saved/followed/continue/unread/reading/engagement) have NO downstream store**
  — needs a `VeritasOshunStore` (Drizzle/Postgres write-through like Tara's
  shadow-user bridge or Arete's redis facade store).
- _Nyx_ (18 endpoints): `GET events/nightly`, `GET events/:id`,
  `GET events/continue`, `GET events/upcoming`, `GET objects/search`,
  `GET objects/:id`, `GET objects/visible`, `GET+POST observations`,
  `GET observations/stats`, `GET observations/search-activity`, `GET saved`,
  `POST+DELETE saved/:objectId`, `GET+POST reminders`,
  `POST reminders/:reminderId`. Objects/ephemeris legs can use the real
  `@nyx/messier` + `@nyx/ephemeris` services already in `apps/nyx/api`. **Events
  must NOT bridge to `apps/nyx/api`'s events-service (see finding 11 — hardcoded
  2024 samples); compute from `@oshun/domain-nyx` like `/v1/nyx/tonight` does.**
  All per-user legs (observations/saved/reminders/continue/search-activity) have
  no downstream store — new facade store required.
- Both must fail closed in production without `{VERITAS,NYX}_OSHUN_FACADE_TOKEN`
  matching `x-oshun-service-token` (BFF already sends
  `OSHUN_DOMAIN_SERVICE_TOKEN`, `app.ts:1144–1148`), plus contract round-trip
  tests booting the real routers through `createDomainServiceAdapters` (the C1
  tests caught real drift day one).

**Fix sketch:** replicate the C1 bridge-router pattern: `apps/veritas/api`
facade over the existing Postgres catalog + new per-user store; `apps/nyx/api`
facade over Messier/ephemeris + `@oshun/domain-nyx` event solver + new per-user
store; token fail-closed; contract tests.

---

### 2. `/veritas/{claim,counterclaims,mobile,provenance,retraction,source,story,topic}` serve fabricated journalism with zero register label — and the REAL hub deep-links into them

**Severity:** P0-HONESTY

**Evidence:**

- `apps/oshun/web/src/lib/lilith-data/veritas-depth.ts:1–14` ("fixture today,
  BFF tomorrow") and e.g. `:33–135` — invented bylines ("By M. Halvorsen"),
  invented sources ("Statnett · operator data", "NVE · regulator note"),
  invented confidence values (0.92), an entire fake Norway-grid investigation.
  All eight pages import it directly
  (`src/app/veritas/{claim,counterclaims,mobile,provenance,retraction,source,story,topic}/page.tsx`).
- No label anywhere:
  `grep -rn -i "preview|illustrative|example" src/app/veritas src/components/lilith/veritas.tsx`
  → zero hits in 2,126 lines of broadsheet components; no `layout.tsx` under
  `src/app/veritas/`.
- `DomainPreviewBanner` mounts only in `DomainRouteExperience`
  (`src/components/DomainRouteExperience.tsx:579`), which renders only for
  `/domains/[domainId]` — never for top-level `/veritas/*`.
- **Live nav into the fabrication:** the honest `/veritas` hub's subnav links
  "Topic hubs" → `/veritas/topic`, "Sources" → `/veritas/source`, "Story
  updates" → `/veritas/story` (`src/components/lilith/rooms.tsx:578–580`), and
  the real lead's "Read sources" button defaults to `/veritas/claim`
  (`src/components/lilith/VeritasArticleActions.tsx:28`, rendered at
  `rooms.tsx:681`). A member reading a REAL grounded article clicks "Read
  sources" and lands on a FABRICATED claim page about a different, invented
  story — presented with full editorial authority.

**Spec promise:** features.md 659–666 (these surfaces are "presentational depth"
of the Veritas domain), 678–681 (inspect-source interactions with real evidence
bundles), 702–704 (trust display "never numerical scores presented without
context" — these pages show invented confidence as if computed).

**What the code actually does:** the 06-10 headline "Veritas depth fabricates
journalism" is fixed for `/domains/*` (D1 banner) and for the hub (honest
briefing), but the eight top-level `/veritas/*` broadsheet pages — the exact
pages that fabricate journalism — are outside the D1 register and reachable in
two clicks from real content.

**Fix sketch:** either route these pages through the preview register (a
`/veritas/*` layout mounting `DomainPreviewBanner domainId="veritas"`), or point
the hub subnav + `readSourcesHref` at real surfaces (`/domains/veritas?path=...`
claims workspace) until the depth pages are wired; minimally change
`readSourcesHref` so a real article never links to invented sources.

---

### 3. D1 preview-banner coverage gap: the 30 static `/domains/{veritas,nyx}/*` tool pages never render the banner

**Severity:** P0-STRUCT

**Evidence:** banner mounts once in `DomainRouteExperience.tsx:579`, used only
by `src/app/domains/[domainId]/page.tsx`. The deep tools are separate static
Next routes — 11 veritas
(`src/app/domains/veritas/{articles,bias,claims,fact-check,headlines,knowledge-graph,newsletter,nlp,research,story,topics}/page.tsx`) +
19 nyx (`src/app/domains/nyx/...`) —
`grep -rln DomainPreviewBanner src/app/domains/` → **zero hits**; no shared
`layout.tsx` exists under `src/app/domains/{veritas,nyx}/`. These pages render
exactly the simulation-data components the registry exists for
(`VeritasArticleAnalyzer.tsx`, `NyxMoonTracker.tsx`, `NyxDashboardWidgets.tsx`,
… — 31 files import `*-simulation-data`). They are live-linked: `NyxRoom` subnav
→ `/domains/nyx/satellites`, `/domains/nyx/events?filter=meteor`,
`/domains/nyx/observation-log` (`rooms.tsx:815–817`) and event rows →
`/domains/nyx/events/${id}` (`rooms.tsx:1030`).

**Spec promise:** features.md 663–666 + 989–992 list these as the power-user
surfaces; the D1 decision (audit doc line 507) promises "every `/domains/*` deep
surface now carries an explicit Preview register" — the completion note's claim
is false for the static subroutes, which is where the simulation data actually
lives (`DOMAIN_PREVIEW_SURFACES` itself is correct; the mounting is what
misses).

**What the code actually does:** `/domains/veritas` (shell) shows the banner;
`/domains/veritas/articles` (the actual simulation UI) does not. Same for all
nyx tools.

**Fix sketch:** add `src/app/domains/veritas/layout.tsx` and
`src/app/domains/nyx/layout.tsx` (and tara/arete equivalents, out of slice)
rendering `<DomainPreviewBanner domainId=.../>` above `children` — one file per
domain, registry-driven exactly as designed.

---

### 4. `GET /v1/nyx/observations` is unauthenticated and the store is not user-scoped — every member's "private" field log is one global list

**Severity:** P0-SEC

**Evidence:**

- Route: `apps/oshun/bff/src/routes/domain-stubs.ts:1628–1634` —
  `app.get('/v1/nyx/observations', ...)` with **no preHandler at all** returns
  `domainStubsStore.observations.list()`. The POST (`:1600–1627`) has only
  `originGuard`+`csrfGuard` — no auth, and never touches `authContext`.
- Store: `apps/oshun/bff/src/routes/domain-stubs-store.ts:119–162` —
  `NyxObservationStore` records
  `{observationId, eventId, targetLabel, observedAtIso, conditions, note, ...}`
  with **no `userId` field**; `list()` returns everything (Postgres or in-memory
  tier alike). Contrast: tara sittings/analytics in the same file read
  `request.authContext?.userId` and `collectionsAuthGuard`
  (`domain-stubs.ts:1166–1181`).
- The page feeding it explicitly promises privacy:
  `src/app/nyx/observation/page.tsx:44` renders `right="private"` and the
  docblock says "Nyx anchors it to your observation lifetime".

**Spec promise:** features.md 1030–1032 (save/remind is per-user) and the Nyx
model generally treats observation logs as personal data
(`getObservationLogs({userId})` everywhere else, e.g.
`domain-service-adapters.ts:1381–1394`).

**What the code actually does:** any session (or any direct BFF caller — no auth
check) can read every user's observation notes; every write is anonymous and
co-mingled. This is both a privacy leak and a third source of truth for
observations (vs. the downstream `/v1/oshun/observations` per-user store of
finding 1 and the web's localStorage `useNyxStore.observations`).

**Fix sketch:** require auth on GET+POST, add `userId` to `StoredNyxObservation`
(+ Postgres column), scope `list()` per user; when the Nyx facade (finding 1)
lands, fold this store into it so there is one observation log.

---

### 5. Nyx save/follow/remind is double-broken: the store users write is in-memory and never delivered; the delivery worker reads a store that doesn't exist

**Severity:** P1

**Evidence:**

- Write path: `PUT /v1/nyx/event-actions/:eventId` → `nyxEventActionStateStore`
  (`routes/nyx.ts:683–756`), which is a **module-level `Map`**
  (`nyx/event-action-state-store.ts:83–95`) — despite the
  `NyxPersistedEventActionState` naming, there is no durable-snapshot wiring in
  `server.ts` (`grep nyxEventActionState server.ts` → nothing) and no Postgres
  tier. Every saved event, followed event class, and reminder route
  (cadence/channels/`reminderTime`/`offsetMinutesBeforePeak`) evaporates on
  restart.
- Delivery path: the reminder worker's Nyx producer (`server.ts:650–653`) calls
  `app.domainAdapters.nyx.getEventReminders` — the **nonexistent** downstream
  `/v1/oshun/reminders` (finding 1) — and `produceNyxEventReminders` swallows
  the throw per user ("fail-soft", `reminders/nyx-reminder-bridge.ts:39–44`).
  Net effect: **no Nyx event reminder can ever be delivered**, while the UI
  happily stores reminder config in the in-memory store the worker never reads.
- Two sources of truth on the read side too: `/v1/nyx/adapter/event-reminders`
  (`routes/nyx.ts:570–587`) serves the downstream (502) store;
  `/v1/nyx/event-actions` serves the local one.

**Spec promise:** features.md 1030–1032 ("save event, follow event class, set
reminder cadence … per-notification-channel routing") and 1027–1029
(calendar/reminder cadence per user preference).

**What the code actually does:** stores rich reminder config that nothing
enforces; delivers from a store that 404s. Classic "stored config never
enforced".

**Fix sketch:** wire `nyxEventActionStateStore` into the durable snapshot (like
the C16 handoff store), and point `produceNyxEventReminders` at it (it has
everything: `reminderTime`, routes, enabled) until the facade lands; when the
facade lands, make the event-actions route write through to it.

---

### 6. The real retraction cascade has no customer-facing read path — `/v1/veritas/{drift,retractions}` are prod-503 fixtures and no surface shows cascade banners

**Severity:** P1

**Evidence:**

- Write side is real: `POST /api/oshun/domains/veritas/sources/:id/retract`
  persists via contract-persistence and publishes on Redis; the worker
  re-grounds Sophia answers and re-validates Metis lessons
  (`veritas/retraction-cascade-runtime.ts:69–113`).
- Read side is a fixture: `/v1/veritas/drift` and `/v1/veritas/retractions` are
  `guardedFixtureRoute`s (`domain-stubs.ts:1822–1827`) — 503 in production — and
  they are the ONLY data source of the customer evidence page
  (`src/app/veritas/evidence/page.tsx:66–70`). So in production the page always
  renders "0 drift · 0 retracted" **even after a real cascade fires**.
- No story-page/saved-claim/notebook banner exists:
  `grep -rn "retraction" apps/oshun/web/src/components` shows only the
  depth-fixture `VeritasRetraction` letter; nothing reads cascade records.

**Spec promise:** features.md 889–913 (retraction-cascade UX: story-page banner,
per-saved-claim banner, notebook notice, severity gating, "appeal / correct
us"), 732–736 (user-visible correction notes + story-update notifications).

**What the code actually does:** two sources of truth — real cascades in
contract persistence, fabricated drift/retraction lists on the (dev-only)
customer route; the spec's customer experience is unimplemented.

**Fix sketch:** replace the two fixture routes with reads over the cascade
persistence (cascade id, trigger source, notice text, scope, timestamps — all
persisted by `createContractPersistenceRetractionPort`), and surface a banner on
`/veritas` briefing + saved items when a cascade touches them.

---

### 7. The Veritas editorial engines remain runtime-orphaned: source-quality composite, attestor lifecycle, counterclaim balance, topic-hub composer, editorial state machine, contradiction execution

**Severity:** P1

**Evidence:**
`libs/oshun/domain-veritas/src/{source-quality/{composite,banding,domain-weights,rescoring,tenant-floor,vector-store},attestor/{credential-verification,conflict,probation,revocation,expiry,disagreement,audit},counterclaim/{surface,reader-preferences,audit},topic-hub/{composer,editorial-gating,refresh,locale-variant},editorial/state-machine,contradiction/{probe,execution,reviewer-routing,tenant-timebox,monitoring}}`
— full implementations with domain tests. Runtime consumers of
`@oshun/domain-veritas` in the BFF: only `domain-service-adapters.ts:51`
(types), `retraction-cascade-runtime.ts:18`,
`cross-domain-recommendations.ts:14`.
`grep -rln "computeSourceQualityComposite|bandFromComposite|EditorialStateMachine|composeTopicHub" apps libs`
(excluding the lib itself and tests) → only
`libs/sophia/verification/src/contradiction/contradiction-loop.ts` (the Sophia
half of the loop — itself unconsumed by any app route). No route, worker, or
store operates source scoring, attestation, counterclaim surfacing, topic-hub
gating, or the state machine. The `credibilityTier/credibilityScore` shown on
the home briefing comes from the (nonexistent) downstream feed
(`routes/veritas.ts:588–613`), not from the composite.

**Spec promise:** features.md 749–961 — the entire "Editorial Workflow, Source
Verification, and Contradiction Detection" section, whose opening line says the
schemas "are inert without the editorial loop that operates them" (751–753).

**What the code actually does:** the loop is still inert. This is the recorded
systemic failure mode (audit doc lines 49–58); for Veritas, only the cascade
slice got wired since.

**Fix sketch:** when the Veritas facade store lands (finding 1), compute
`credibilityTier` from `computeSourceQualityComposite`+`bandFromComposite` over
real source records instead of passing through a downstream field; add an admin
lane route pair (catalog + verb) driving the editorial state machine +
attestation records, following the studio lane-console pattern.

---

### 8. `USE_MOCK_ADAPTERS=true` serves fabricated journalism and sky data as real, with no production guard

**Severity:** P1

**Evidence:** `apps/oshun/bff/src/app.ts:1136–1139` —
`if (process.env.USE_MOCK_ADAPTERS === 'true') return createMockDomainServiceAdapters();`
— no environment check. The mocks fabricate news ("AI breakthroughs in Q3", "New
CRISPR therapy shows 94% efficacy…",
`adapters/mock-domain-service-adapters.ts:208–296`) and Nyx events/logs, all
flowing through the same routes that label themselves real. Contrast the repo's
own posture: `guardedFixtureRoute` 503s in production
(`domain-stubs.ts:847–856`).

**Spec promise:** features.md 686–707 (trust/confidence display is the product;
fabricated verdicts under real chrome is the worst-case failure).

**What the code actually does:** one env var in a production deployment turns
every Veritas/Nyx/tara/arete surface into convincing fabrications with no
banner.

**Fix sketch:** in `createDefaultDomainAdapters`, refuse mocks when
`resolveOshunRuntimeEnvironment()` is `production`/`staging` (throw at boot),
mirroring the fixture guard.

---

### 9. Dead hydration path: `hydrateDomainStoresFromBff` calls four `/v1/domains/*/state` endpoints that don't exist, and nothing calls it

**Severity:** P2

**Evidence:** `apps/oshun/web/src/lib/stores/domain-stores.ts:606–636` fetches
`/v1/domains/{tara,arete,veritas,nyx}/state`; no BFF route matches
(`grep -rn "domains/veritas/state" apps/oshun/bff/src` → nothing; only
`/v1/domains/:domainId/bootstrap` exists in `routes/entitlements.ts:47`). The
function itself has zero callers (`grep -rn hydrateDomainStoresFromBff src` →
definition only). The zustand stores are honest-empty localStorage, so no
fabrication — but `lastSyncedAt`/`syncFromBff` imply a sync that can never
happen.

**Spec promise:** features.md 677 / 1030 (per-user save/follow state — implies
server truth, not per-browser).

**What the code actually does:** dead code advertising a BFF sync;
`useVeritasStore.savedClaims/followedTopics` and `useNyxStore.observations` live
only in the browser.

**Fix sketch:** delete `hydrateDomainStoresFromBff` (or re-point it at the E6
`/v1/client-state/:stateKey` channel with new slots), and route store mutations
through the facade once finding 1 lands.

---

### 10. Veritas save/follow is three disconnected stores, and the web has no server mutation path at all

**Severity:** P2

**Evidence:** (a) "Save to notebook" on the real lead writes localStorage
`webLibraryStore` (`VeritasArticleActions.tsx:37–55`); (b) the workspace
evidence panel reads server-side saved articles via
`/v1/veritas/adapter/saved-articles` (`useWorkspaceLiveData.ts`) — a different
store (downstream, currently 502); (c)
`useVeritasStore.savedClaims/followedTopics` is a third (localStorage zustand).
The BFF exposes **no** POST route for
`saveArticle/unsaveArticle/followTopic/unfollowTopic` — `routes/veritas.ts` is
GET-only; the adapter mutations (`domain-service-adapters.ts:1198–1244`) are
reachable only through assistant tooling (`routes/assistant.ts:82–85`).

**Spec promise:** features.md 677 ("Save/follow controls for stories, claims,
and topics"), 734–736 (story-update notifications "per-followed-topic" —
impossible when follows never reach the server).

**What the code actually does:** a member's saves never appear in the workspace
evidence panel or count toward `veritas_articles_saved` achievements; follows
can never drive notifications.

**Fix sketch:** add
`POST/DELETE /v1/veritas/adapter/saved-articles/:articleId` +
`topics/:topicId/follow` BFF routes over the adapter mutations; make
`VeritasArticleActions` write through (localStorage as offline cache),
contingent on finding 1.

---

### 11. `apps/nyx/api` events + satellites services are hardcoded 2024 sample data — empty/garbage in 2026

**Severity:** P2

**Evidence:** `apps/nyx/api/src/services/events-service.ts:11–13` —
`// Sample Events Data` — a fixed `ASTRONOMICAL_EVENTS` array of 2024 dates;
`getUpcomingEvents` filters `eventStart >= now` (`:262–276`), so every query
from 2026-06-11 returns `[]` forever (honest-empty, but the endpoint can never
serve a real event). `satellites-service.ts:11–25` — sample satellites with TLE
epoch `24015.5` (Jan 2024); SGP4 propagation from a 2.5-year-stale ISS TLE
produces meaningless pass predictions through the otherwise-real
`@nyx/realtime-satellites` tracker. By contrast `ephemeris-service.ts` (real
`@nyx/ephemeris` Meeus engine) and `objects-service.ts` (real `@nyx/messier`
catalog) are genuine.

**Spec promise:** features.md 1024–1026 — prediction sources "each with version,
license, attribution, freshness"; 1011–1016 event taxonomy.

**What the code actually does:** the only currently-running Nyx event source
that is real is the BFF's own `@oshun/domain-nyx` solver. Any facade (finding 1)
bridged onto `apps/nyx/api`'s events-service would serve nothing.

**Fix sketch:** back `/api/v1/events` with the `@oshun/domain-nyx`
lunar-phase/season solver + the source-backed featured meteor/eclipse table the
BFF already uses; for satellites either ingest a live TLE feed
(DEPLOY-credentialed) or fail closed with `not_configured` instead of
stale-epoch predictions.

---

### 12. `/v1/veritas/briefing/home` and `/v1/nyx/adapter/*` error envelopes are inconsistent: the briefing handler 500s instead of its own 502/503 contract

**Severity:** P2

**Evidence:** `routes/veritas.ts:527–530` —
`Promise.all([getTrendingArticles, getTopClaims])` with no try/catch (unlike
every `/adapter/*` route's `guarded()`); a downstream
`DomainServiceAdapterError` propagates to Fastify's generic 500. The web treats
any non-OK as unavailable so no fabrication results, but ops dashboards see 500s
for a routine downstream outage and the route's own 503 leg (`:533–539`) is
unreachable for the throw case.

**Spec promise:** n/a (robustness); features.md 668–675 implies a graceful
briefing surface.

**Fix sketch:** wrap the two adapter calls in the same try → 502
`domain_unavailable` envelope used at `routes/veritas.ts:227–239`.

---

### 13. NyxRoom navigation sends members from the real sky into unlabeled simulations, with one mislabeled target

**Severity:** UX

**Evidence:** `rooms.tsx:815–817` — the real ephemeris hub's subnav: "Planets" →
`/domains/nyx/satellites` (a satellite tracker, not planets —
`NyxSatelliteTracker.tsx`, simulation-backed); "Meteors" →
`/domains/nyx/events?filter=meteor` (simulation events); "Observations" →
`/domains/nyx/observation-log` (simulation log) even though the member's REAL
logging flow lives at `/nyx/observation` (and their real entries at
`GET /v1/nyx/observations`). Combined with finding 3 (no banner on those pages),
the register seam is invisible: real tonight-card → one click → fabricated
observation history.

**Spec promise:** features.md 985–1007 (the hub and deep tools are one coherent
domain surface).

**Fix sketch:** point "Observations" at `/nyx/observation`; rename "Planets" or
point it at the solar-system/ephemeris view; banner-cover the rest (finding 3).

---

## Severity counts

| Severity   | Count  |
| ---------- | ------ |
| P0-SEC     | 1      |
| P0-HONESTY | 1      |
| P0-STRUCT  | 2      |
| P1         | 4      |
| P2         | 4      |
| UX         | 1      |
| DEPLOY     | 0      |
| **Total**  | **13** |

(DEPLOY: nothing new — once finding 1's facades land,
`OSHUN_{VERITAS,NYX}_API_BASE_URL` + `OSHUN_DOMAIN_SERVICE_TOKEN` + facade
tokens are the existing deploy-tier requirement; live TLE feed in finding 11
would add one credential.)
