Disciplines · Audits

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

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

2sections15 minread

On this page

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-856getInternalRecord 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-2540GET/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-64SEED_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:2543GET /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.)


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.


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).


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.


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.