# Residual Audit — Editorial / Studio / Customer Curation (features.md 1588–1807)

Date: 2026-06-11. Read-only static audit of the residual after the 06-10
ground-truth audit's 57 tasks landed. Evidence = current source only.

## Headline verification

The 06-10 headline — **"No persisted authored artifact; lanes are stateless
analyzers" — is STILL TRUE** for editorial content, with three narrow nuances:

- The `/v1/admin/studio/*` lane "stores" remain **pure analyzers over
  caller-supplied JSON** (verified: `approval-workflow-store.ts`,
  `comment-thread-store.ts`, `asset-rendition-store.ts`,
  `file-ingestion-store.ts` — all documented and implemented as "pure +
  deterministic; 0 Date.now/random" evaluators with no record persistence). No
  comment, approval, asset, or i18n state survives a request.
- What DOES persist now: editorial **release-streams** (durable store + admin
  panel, see finding on enforcement), the C8 communications loops
  (banners/help/templates/ status), opaque per-user **workspace preferences**
  (`studio/workspace-state-store.ts` — explicitly "density, viewport mode, saved
  filters", not artifacts), and the **game-domain** authoring records (Hathor
  story-graph/quest, Bellona — out of this slice's editorial-content scope but
  real persistence).
- `@oshun/studio-authoring` still has exactly **1 consumer**
  (`StudioAuthoringWorkspace.tsx`, importing 7 functions from
  `authoring-blocks` + `evaluatePublishReadiness`); `@oshun/customer-curation`
  gained exactly **1 consumer** (the sharing engine via
  `library/share-policy-store.ts`). Everything else in both libs remains
  orphaned.

---

### Nisaba seeds fabricated notebooks and annotations as the member's own, ungated in production

Severity: P0-HONESTY

Evidence:

- `apps/oshun/bff/src/nisaba/state.ts:847-856` — `getInternalRecord` lazily
  calls `buildSeedRecord(userId)` for EVERY user; no
  `allowFixtures()`/production gate anywhere in the file.
- `apps/oshun/bff/src/nisaba/state.ts:1563-1581` — seed annotation
  `nisaba-annotation-compassion-1` is attributed
  `author: { userId, displayName: deriveDisplayName(userId) }` with note text
  the member never wrote ("The cessation is framed as a rule, not a feeling.")
  and fixed timestamps (2026-03-22/23).
- `apps/oshun/bff/src/nisaba/state.ts:1586-1625` — two seed notebooks
  (`nisaba-notebook-attention`, `nisaba-notebook-compare`) populated with items.
- `apps/oshun/bff/src/search/user-object-candidates.ts:75-79` — C7's universal
  search ranks `nisabaConsumerStateStore.listNotebooks(userId)` as the member's
  "REAL objects", so a brand-new member searches and finds two notebooks "they"
  curated.
- No `seedDataNotice` (the E1 labeling pattern) on any `/v1/nisaba/notebooks`
  response (`grep -i seed routes/nisaba.ts` → 0 hits).

Spec promise: features.md 1768 ("Customer-controlled notebooks…"), 1786-1788
(annotation durability tests). D6 fixed PASSAGE reading state to honest-zero
(state.ts:876 comment) and C4 killed the identical Iris seed pattern — this same
class survived in the notebook/annotation seed.

What the code actually does: serves every member identical fabricated notebooks
and self-attributed annotations as durable-looking personal data, and surfaces
them in search results among real saves.

Fix sketch: start `buildSeedRecord` with empty `notebooks`/`annotations` maps
(keep corpus passages, evidence views), or gate the personal seeds behind
`allowFixtures()` + an explicit seed label, mirroring C4/E1.

---

### Collections exist as two disconnected systems (server store vs localStorage fixtures)

Severity: P0-STRUCT

Evidence:

- Server system: `apps/oshun/bff/src/routes/domain-stubs.ts:2482-2540` —
  `GET/POST /v1/library/collections`, auth-scoped, persisted
  (`domain-stubs-store.ts:164-199`, snapshot/Postgres-backed); written by
  `apps/oshun/web/src/app/library/collections/new/LibraryNewCollectionForm.tsx:51`,
  read by `app/library/collections/page.tsx:45` and by universal search
  (`search/user-object-candidates.ts:57-64`).
- Client system:
  `apps/oshun/web/src/components/library/LibraryDashboard.tsx:106-162` ships 4
  hardcoded fixture collections (`evening-integration`, `research-to-practice`,
  `sky-and-scripture`, `learning-reentry`) + `LIBRARY_COLLECTION_ITEM_IDS`; all
  collection CRUD (`handleCreateCollectionFromFilters` :830,
  `handleSaveCollectionDetails` :880, `handleToggleCollectionItem` :893,
  `handleArchiveCollection` :905) mutates React state + `localStorage`
  (`oshun.library.collections.v1`, :642-650). The dashboard NEVER fetches or
  posts `/v1/library/collections` (zero network calls for collections in the
  file).
- `LibraryDashboard` is the component behind both `/library` and
  `/library/collection/[collectionId]`.

Spec promise: features.md 1768-1772 (customer-controlled collections, ordering,
import); 1788 ("progress sync across devices").

What the code actually does: a collection created on `/library/collections/new`
is durable and searchable but never appears in the dashboard's collection
manager; a collection created/edited/archived in the dashboard is device-local,
invisible to the index page, to search, and to other devices. E2 fixed exactly
this split for saved ITEMS but left collections behind. The fixture collections
are also unlabeled and prod-visible (the BFF-side fixture merge at
domain-stubs.ts:2500 is `allowFixtures()`- gated; the client-side ones are not).

Fix sketch: back the dashboard's collection state with `/v1/library/collections`
(reuse the saved-items LWW sync pattern from `webLibraryStore.ts`); drop or
guest-gate the client fixture collections.

---

### /studio/authoring: unlabeled fixture document, fabricated governance bindings, and a "saved" claim with zero persistence

Severity: P0-HONESTY

Evidence:

- `apps/oshun/web/src/components/studio/StudioAuthoringWorkspace.tsx:23-64` —
  `SEED_DOCUMENT` (`doc-demo`, "Morning Tara Practice"); :101-108
  `DEMO_BINDINGS` with fabricated governance ids
  (`sophiaEvidencePackId: 'ep-tara-baseline'`,
  `lilithToneReviewId: 'tr-2026-05-12-tara'`) never read from
  Sophia/Lilith/Isis.
- :415-438 — edits run the lib's `acceptAutosave` and set
  `autosaveStatus = 'saved @ lamport=N'`; nothing is written anywhere (the only
  network calls in the file are the readability lane, :170/:220). Reload
  discards the document. The component doesn't even use the available
  `/v1/studio/workspaces/:id/state` preference store.
- :740-741 — publish readiness renders "✓ All bindings satisfied — eligible for
  normal pipeline" computed from the fabricated `DEMO_BINDINGS`; :787 — AI
  assist renders "request is eligible for upstream agent" — no upstream agent
  exists or is called.
- No demo/preview labeling anywhere in the render; `DomainPreviewBanner` covers
  `/domains/*` only (`components/domains/DomainPreviewBanner.tsx:1-13`) — this
  surface is **uncovered**.

Spec promise: features.md 1638-1640 ("Save, autosave, draft locking, … recovery,
named checkpoints"), 1620-1624 (authoring "governed by Sophia grounding, Isis …
policy, and Lilith tone policy").

What the code actually does: a demo harness over real lib evaluators, presented
with the same visual authority as a live workspace, telling the operator their
work is "saved" and governance-bound when neither is true.

Fix sketch: label the surface as a fixture rehearsal until persistence lands,
change "saved" to an honest "in-memory only" status, and source bindings from
real reads or render them as explicitly unbound.

---

### Atelier/Lilith-studio scenes: global unscoped store with unauthenticated read and write

Severity: P0-SEC

Evidence:

- `apps/oshun/bff/src/routes/domain-stubs.ts:2543` — `GET /v1/atelier/scenes`
  has NO preHandler (no auth); it lists `domainStubsStore.scenes.list()`.
- `apps/oshun/bff/src/routes/domain-stubs-store.ts:215-246` — the scenes store
  is a global `Map` with no `ownerUserId`; `list()` returns every row to every
  caller.
- `domain-stubs.ts:2565` (`POST /v1/lilith-studio/scenes`) and :2597
  (`POST /v1/atelier/scenes`) take only `originGuard, csrfGuard` — no auth guard
  — and persist into the same global store.

Spec promise: features.md 1786 ("sharing-permission isolation"); cf. A7, which
fixed exactly this for library collections.

What the code actually does: any unauthenticated caller can write scene records,
and every user's authored scene titles (e.g. practice/ritual titles, which are
personal) are served to every other user and to anonymous callers.

Fix sketch: add the auth preHandler to all three routes, add `ownerUserId` to
`StoredAtelierScene`/`StoredStudioScene`, and filter `list()` by owner exactly
as `LibraryCollectionStore.list` does. (Cross-slice note: atelier may belong to
the generation slice; flagged here because it shares the curation route
file/store.)

---

### Share-link journey dead-ends: minted URLs route nowhere and the grant carries no content

Severity: P1

Evidence:

- `apps/oshun/bff/src/library/share-policy-store.ts:318-321` — created links
  point at `https://oshun.app/share/<token>` / `oshun://share/<token>`.
- No `app/share` route exists in the web app
  (`find apps/oshun/web/src/app -d -name "share*"` → only a Bellona studio dir);
  only `lib/deep-link-associations.ts:12-13` declares the path for OS
  association files.
- `apps/oshun/bff/src/routes/library-sharing.ts:213-244` — the recipient
  resolution endpoint `GET /v1/library/shared/:collectionId` returns ONLY
  `{ collectionId, granted, tier, via }` — never the collection's label or items
  — and a repo-wide grep finds **zero callers** (web, mobile, miniapp).
- Mobile's `deepLinkHydration.ts:103-124` parses `/share/<token>` into a share
  route, but `share/mobileShareIngest.ts` is the inbound share-INTO-app system;
  nothing resolves a collection share token to content.

Spec promise: features.md 1773-1775 (shared via link with view/comment/copy
permissions).

What the code actually does: an owner can mint a cryptographically sound link
whose recipient lands on a 404 page, and even calling the API directly yields a
permission verdict with nothing to view. The engine is real; the journey
delivers nothing.

Fix sketch: add a `/share/[token]` web page that calls the resolve endpoint, and
extend the resolve response (on grant) with the collection label + items read
from `domainStubsStore.collections`.

---

### Share-policy ownership is first-writer-wins and collides on fixture collection ids

Severity: P1

Evidence:

- `apps/oshun/bff/src/library/share-policy-store.ts:140-158` (`setVisibility`)
  and :186-194 (`createShareLink`) — when no policy exists for a `collectionId`,
  the caller becomes its owner; ownership is never checked against the actual
  collections store (`domainStubsStore.collections` has `ownerUserId`, never
  consulted).
- `apps/oshun/web/src/components/library/LibraryDashboard.tsx:1051` — share
  controls operate on `selectedCollection.id`, which for every member includes
  the SAME four fixture ids (`evening-integration` …, :106-162).
- Collection ids are guessable (`col-<Date.now()>-<seq>`,
  `domain-stubs-store.ts:174`).

Spec promise: features.md 1786 ("sharing-permission isolation").

What the code actually does: the first member to touch sharing on
`evening-integration` owns it tenant-wide — every other member then gets 403
"Only the collection owner can change its sharing" on a collection their own UI
presents as theirs. A malicious member can likewise pre-claim the share policy
of another member's real collection id and lock them out of sharing it.

Fix sketch: on first policy creation, verify the caller owns `collectionId` in
`domainStubsStore.collections` (404/403 otherwise); becomes automatic once
finding #2 unifies the two collection systems.

---

### Share policies/links are not durable — every minted link dies on deploy

Severity: P1

Evidence: `librarySharePolicyStore` is a plain in-memory `Map`
(`share-policy-store.ts:101`) with a `reset()` but no snapshot wiring; it is
absent from the `wireDurable*` roster in `apps/oshun/bff/src/server.ts` (which
durably wires ~40 sibling stores, including living-scene shares at :106).

Spec promise: features.md 1773-1775; links are offered with 30-day and "No
expiry" presets (`CollectionShareControls.tsx:79-83`).

What the code actually does: hands the member a long-lived URL whose server-side
grant evaporates on the next restart (`library_collection_not_found` 404).

Fix sketch: add `toSnapshot()/hydrate()` + `wireDurableLibrarySharePolicies` in
server.ts, same pattern as `wireDurableLivingSceneShares`.

---

### User-created Nisaba notebooks/annotations are in-memory only

Severity: P1

Evidence: `nisabaConsumerStateStore` (`apps/oshun/bff/src/nisaba/state.ts:859`)
is a plain class instance; no snapshot serialization exists in the file and
`server.ts` has no nisaba wiring (`grep nisaba server.ts` → 0). Meanwhile
`createNotebook` (:704), `addNotebookItem` (:764), annotation CRUD (:552-633)
are real per-user mutations exposed at `POST /v1/nisaba/notebooks` (+items) and
used by the live web forms
(`app/nisaba/notebook/new/NisabaNewNotebookForm.tsx:43`).

Spec promise: features.md 1786-1788 ("annotation durability and progress sync
across devices").

What the code actually does: a member's authored notebooks and annotations — the
one real authored-artifact write path in this slice — are lost on every deploy,
while the fabricated seeds (finding #1) come back.

Fix sketch: add snapshot persistence (`wireDurableNisabaConsumerState`)
following the Telegram-user-state / iris-memory pattern (explicit Map/Date
serialization).

---

### /studio/authoring is an orphan route — nothing links to it

Severity: P1

Evidence: repo-wide grep for `studio/authoring` in `apps/oshun/web/src` matches
only the page itself (`app/studio/authoring/page.tsx`); the studio hub
(`app/studio/page.tsx`) links sibling lanes (e.g. `review-approval-workflows` is
linked from the hub and four other pages) but never authoring. Only e2e specs
visit it by URL.

Spec promise: features.md 1592-1596 (authoring "exposed through Oshun Studio
rather than buried", "first-class product surfaces").

What the code actually does: the slice's flagship §16.2 surface is unreachable
by any in-app navigation.

Fix sketch: add the authoring card/link to the studio hub catalog (and command
palette), or fold the workspace into a linked surface.

---

### Editorial lifecycle state machines (×2) have zero runtime consumers; live content uses ad-hoc states

Severity: P1

Evidence:

- `libs/oshun/studio-authoring/src/editorial-lifecycle/lifecycle.ts:15-44` — the
  full 12-state canonical machine + gate-checked `tryEditorialAdvance`; zero
  imports outside the lib (repo grep).
- `libs/oshun/domain-veritas/src/editorial/state-machine.ts:1-29` — a SECOND
  12-state editorial machine (Veritas variant, exported as
  `EditorialStateMachine`); zero imports in any app.
- The content that actually publishes (admin communications help
  articles/banners, C8) and the release-streams store each use their own small
  status enums; persona lifecycle (`admin/persona-lifecycle-store.ts`) reuses
  'in-review' textually but not the machine.

Spec promise: features.md 1651-1657 ("Canonical content lifecycle states reused
across all domains" + the ARCHITECTURE diagram cross-ref).

What the code actually does: two orphaned canonical machines and N ad-hoc
per-store status enums — the canonical-reuse promise is inverted.

Fix sketch: when any artifact persistence lands (notebooks aside, the
help-article store is the nearest fit), drive its status transitions through
`tryEditorialAdvance`; retire or delegate the Veritas duplicate.

---

### Editorial release streams: embargo/blocked states stored but never enforced

Severity: P1

Evidence: `apps/oshun/bff/src/admin/admin-editorial-release-streams-store.ts` is
a real durable store with kinds incl. `embargo` (requires `embargoLiftsAt`,
:153) and a validated status machine (:209-224); its ONLY consumers are its own
routes (`routes/admin-editorial-release-streams.ts`, mounted at
`/v1/admin/editorial/release-streams`) and the admin panel
(`apps/oshun/admin/src/components/EditorialReleaseStreamPanel.tsx`). No publish,
generation, communications, or delivery path reads stream status or
`embargoLiftsAt` (repo grep for the store outside its own route → 0).

Spec promise: features.md 1664-1666 (embargo/takedown/hotfix flows that bind
editorial decisions), 1671-1673 ("embargo respect" tests).

What the code actually does: an operator can mark a subject "blocked" or
embargoed until a date, and nothing anywhere prevents or delays anything.

Fix sketch: at minimum, have the C8 communications publish path and the
generation release leg consult open release streams for their subject ids before
serving; or document the store as advisory-only in the route header.

---

### Orphaned engine census: 7 of 9 studio-authoring module families and 5 customer-curation modules still have zero consumers

Severity: P2

Evidence (repo-wide import grep, non-test):

- `@oshun/studio-authoring` consumed: `authoring-blocks` (5 files) +
  `editorial-lifecycle/pipeline.evaluatePublishReadiness` — all via the single
  demo workspace. **Zero consumers**: `creator-roles/` (roles, onboarding,
  scorecards), `asset-metadata/` (metadata, approval-queue, bulk-upload,
  EXIF/presets, analytics), `templates/` (+rehearsal), `localization-workflow/`
  (translation, glossary, locale-formats), `versioning/` (+branching),
  `taxonomy-curation/` (+stewardship), `collaboration/` (+routing),
  `editorial-lifecycle/lifecycle+recurrence`.
- `@oshun/customer-curation` consumed: `sharing/sharing` (share-policy-store).
  **Zero consumers**: `collections/` (ordering, smart-collections, templates,
  import-from-another), `annotations/`, `share-cards/`, `version-awareness/`.
  (`bookmarks/` non-use is a documented deliberate deviation in
  `library/saved-items-store.ts:9-16` — NOT a finding.)

Spec promises with no runtime today as a result: smart collections (1771),
drag-and-drop ordering/nesting/import (1771-1772), customer version awareness
"show diff" (1784-1785), embeddable share cards with provenance (1776-1778),
per-artifact versioning/diff/rollback (1734-1746), translator workspace
(1715-1733), taxonomy curation (1694-1714), template gallery (1793-1806),
creator roles/queues (1601-1615).

Fix sketch: this is the wiring backlog the 06-10 audit named ("orphaned
libraries are mostly excellent code — the work is wiring"); none of the 57 tasks
touched it, and no recorded V1.x deferral covers these sections. Needs an
explicit build-or-defer decision per family.

---

### Reading progress is a write-less field

Severity: P2

Evidence: `nisaba/state.ts:891` serves `progressPercent: 0` (honest-zero per D6)
and `lastReadAt: ''`; no route or store method ever updates them
(`grep -i progress routes/nisaba.ts` → serialization only, :2131).

Spec promise: features.md 1782-1783 (reading progress, time-to-finish, resume
across devices).

What the code actually does: honest, but the field can never change — progress
UI downstream will show 0% forever.

Fix sketch: add a `POST /v1/nisaba/passages/:id/progress` (or fold into the
client-state E6 sync) updating the per-user record.

---

### Collections page copy promises interactions that don't exist

Severity: UX

Evidence: `apps/oshun/web/src/app/library/collections/page.tsx:79-81` — "Drag
and drop, reorder, share or keep private". No drag-and-drop or manual reorder
exists anywhere (the `customer-curation/collections/ordering.ts` engine is
orphaned; the dashboard only toggles membership,
`LibraryDashboard.tsx:893-903`); "share or keep private" routes through the
broken journeys in findings #5/#6.

Spec promise: features.md 1771.

Fix sketch: soften the copy until ordering ships, or wire the orphaned ordering
engine into the dashboard's collection item list.

---

### Collections deep-links drop the selected collection

Severity: UX

Evidence: `app/library/collections/page.tsx:101` and
`app/library/collections/new/LibraryNewCollectionForm.tsx:112` both link to
`/library?collectionId=…`, but `/library`'s page reads only `domain`/`kind`
(`app/library/page.tsx:37-44`) and passes no `initialCollectionId`; the working
route is `/library/collection/[collectionId]`
(`app/library/collection/[collectionId]/page.tsx:35`). Compounded by finding #2:
even the correct route can't resolve a BFF collection id, because the
dashboard's collection list comes from localStorage/fixtures
(`resolveInitialCollectionId` over `defaultCollectionSnapshot.collections`,
`LibraryDashboard.tsx:588-590`).

Spec promise: features.md 1768-1772.

Fix sketch: point both links at `/library/collection/<id>`; real resolution
requires finding #2's unification.

---

### Telegram `/save` captures are write-only — no surface ever shows them

Severity: UX

Evidence: B12 wired real capture writes (`telegram/user-state-store.ts`) and a
self-scoped `GET /telegram/captures`, but a repo-wide grep finds no
web/mobile/miniapp reader of that route; captures never appear in the library,
notebooks, or search (`search/user-object-candidates.ts` reads saved
items/collections/notebooks/habits only).

Spec promise: features.md 1768 (notebooks/collections "across all domains") +
the messaging capture journey.

Fix sketch: either surface captures in the library (a `telegram` source lane) or
feed them into the saved-items store instead of a parallel silo.

---

### Annotation system exists only in Nisaba

Severity: UX

Evidence: per-user annotations live solely in `nisaba/state.ts` and the
`/v1/nisaba/annotations` routes; Veritas stories, Metis lessons, and Tara
transcripts have no annotation write path (no annotation routes outside
nisaba.ts; the `customer-curation/annotations` cross-artifact engine is
orphaned).

Spec promise: features.md 1779-1781 ("Annotation system across reading surfaces
(Nisaba editions, Veritas stories, Metis lessons, Tara transcripts) …
cross-artifact backlinks, and export").

Fix sketch: lift the nisaba annotation store to a shared per-user store keyed by
`(surface, artifactId)` (the orphaned lib engine already models this), then
mount on Veritas/Metis/Tara readers.

---

### Studio editorial lanes share no artifact identity — authoring can't flow into review, comments, or localization

Severity: UX

Evidence: `/studio/authoring` links to review-approval-workflows /
commenting-annotation-system / real-time-collaboration-substrate / asset-preview
/ i18n (`app/studio/authoring/page.tsx:20-35`), but each console evaluates
caller-pasted JSON against its stateless analyzer (e.g.
`StudioReviewApprovalWorkflowsWorkspace.tsx:116` POSTs a hand-typed stages
payload to `/v1/admin/studio/approval-workflow`); none accepts a
document/artifact id, so the authored document (itself unpersisted) cannot enter
any of the linked workflows.

Spec promise: features.md 1601-1693 model one pipeline over shared artifacts
(author → review → comments → publish → localize).

Fix sketch: once an authored artifact persists (finding #3's fix), thread its id
through the lane consoles as the default payload source; until then the
cross-links oversell continuity.

---

## Severity counts

| Severity   | Count  |
| ---------- | ------ |
| P0-SEC     | 1      |
| P0-HONESTY | 2      |
| P0-STRUCT  | 1      |
| P1         | 7      |
| P2         | 2      |
| UX         | 5      |
| DEPLOY     | 0      |
| **Total**  | **18** |

Adversarial scan notes: the mandatory stub-grep over
`libs/oshun/studio-authoring`, `libs/oshun/customer-curation`,
`bff/src/library`, `bff/src/nisaba` returned zero actionable hits (both libs are
clean, real domain code — the problem is wiring, not fabrication, except where
listed above). `Math.random()` scan clean. Known lane-console architecture and
recorded deviations (bookmarks engine non-reuse; BFF fixture merges behind
`allowFixtures()`) were excluded as designed/documented behavior.
