Oshun Platform · Architecture

Oshun Studio — Authoring, Editorial, Curation

A focused page within the Oshun Platform Architecture documentation. The full map and every sibling page live in the Architecture hub.

14sections15 minread1diagram5tables

On this page

Oshun Studio is the operator- and creator-facing surface where every authored artifact — Veritas stories, Nisaba editions, Metis lessons, Tara passages, ritual scripts, persona/voice packs, and Living Scene scores — is drafted, reviewed, curated, localized, versioned, and published. It is not a separate application: it ships as a subroute tree under apps/oshun/web/src/app/studio/ (see the V1/TODOS.md audit snapshot, §16). It is backed by the real domain library @oshun/studio-authoring. Studio builds on the platform substrates — Sophia (grounding/evidence), Isis (generation control), Lilith (policy), and Iris (memory) — and is consumed by customer-side creators and operator-side editors alike. Product scope lives in V1/features.md § Content Authoring, Curation, and Editorial Operations; backlog is §16.

Where the logic actually lives#

The bulk of Studio's domain behavior is a pure-functions TypeScript library: @oshun/studio-authoring (package version 0.1.0, private, pure ESM, with main/types pointing directly at ./src/index.ts). This is a source library, not a tsup-built artifact. Its libs/oshun/studio-authoring/src/index.ts barrel re-exports nine subdomain modules:

Module (src/<dir>/) Backlog Responsibility
creator-roles/ §16.1 Role taxonomy, per-role permission matrix, creator profiles, onboarding/certification state machine, scorecards
authoring-blocks/ §16.2 Block model, AI-assist guardrails, evaluation/tone policy, citation density, preview parity, autosave checkpoints, drag-to-cite source panel
editorial-lifecycle/ §16.3 12-state lifecycle machine, gates, calendar, embargo/hotfix flows, recurrence engine, findings→publish gate
asset-metadata/ §16.4 Asset metadata/rights/provenance/accessibility logic, approval queue, bulk-upload, EXIF/presets, analytics
taxonomy-curation/ §16.5 Tag/theme/lineage/concept-graph curation operations, contest status, ontology rollout, stewardship
localization-workflow/ §16.6 Translation segments, translation memory, glossary, do-not-translate, stale detection, locale QA, launch scorecards
versioning/ §16.7 Per-artifact version chain, validation, diff, rollback cascade
collaboration/ §16.2 Presence/cursors, inline comments, suggestions, review sign-off, activity feeds, reconnect resync
templates/ §16.2 Studio template kinds, payload/field validation, tenant resolution, rehearsal fixtures

Honesty note on the layer split. This library is real, non-stub, domain-specific code — but it is a contract and logic layer. Every function is pure: timestamps, hashes, and "current" states are inputs, not read from a clock or a database. There is no persistence engine, no running CRDT substrate, and no object-storage client inside the library; those are infra concerns wired up elsewhere (and several are still aspirational — see Aspirational vs. shipped below). The web workspace at apps/oshun/web/src/components/studio/StudioAuthoringWorkspace.tsx (824 lines) genuinely imports and exercises the library.

Creator and contributor roles (§16.1)#

Studio defines a creator/contributor taxonomy that is separate from customer and operator scopes. STUDIO_CREATOR_ROLES (in creator-roles/roles.ts) enumerates twelve roles:

text
author · editor · curator · reviewer · sme · teacher · scholar ·
translator · illustrator · narrator · producer · publisher

Capabilities are governed by STUDIO_ROLE_PERMISSIONS, ten distinct permissions, applied through a real ROLE_PERMISSION_MATRIX and the roleHasPermission(role, permission) predicate. The matrix is least-privilege by design — for example, only publisher can approve and unpublish, only translator can translate, and only curator can curate taxonomy:

Role Permissions
author edit-draft, request-review
editor edit-draft, review, request-review
curator curate-taxonomy, manage-assets
reviewer review
sme review
teacher edit-draft, review
scholar edit-draft, review
translator translate
illustrator manage-assets
narrator manage-assets
producer manage-assets, schedule-publish
publisher approve-publish, schedule-publish, unpublish

The full permission vocabulary is: edit-draft, request-review, review, approve-publish, schedule-publish, unpublish, translate, curate-taxonomy, manage-assets, mint-certification. (mint-certification is the certification-granting permission used by the onboarding/certification flow; it is reserved rather than handed out broadly in the base matrix.)

Profiles carry provenance and consent as first-class fields. CreatorProfile records lineage, credentials, an attribution preference (displayName/handle/bylineVisible), rightsConsentRecordIds, provenanceAttestationIds, and disclosure preferences including an aiAssistDisclosure choice of always | on-request | opt-out and a piiSafetyConsent flag. validateCreatorProfileCompleteness({ profile, role }) enforces role-sensitive completeness: a bio of at least 24 characters and present attribution for every role. The credentialed roles (editor, curator, reviewer, sme, teacher, scholar, translator, publisher) must also have non-empty credentials, and the lineage-attested roles (author, curator, sme, teacher, scholar, translator, illustrator, narrator) must have non-empty lineage. It returns a typed list of CreatorProfileMissingField reasons rather than a bare boolean, so the UI can show exactly what is missing.

The creator lifecycle itself is its own state set, CREATOR_LIFECYCLE_STATES = invited → onboarding → training → certified → sandbox → graduated → suspended → revoked, which the onboarding module drives with visible queues and SLA tracking.

Authoring workspace and AI assist (§16.2)#

The authoring workspace is a multi-pane editor for stories, claims, passages, lessons, ritual scripts, Living Scene scores, and persona/voice packs. StudioAuthoringWorkspace.tsx imports concrete library functions and types straight from @oshun/studio-authoring:

  • functions: acceptAutosave, applyDragToCite, citationDensityByBlock, evaluateAiAssistGuardrails, evaluateAllPreviewSurfaces, evaluateAuthoringWithPolicy, evaluatePublishReadiness
  • types: AiAssistRequest, AuthoringDocument, PreviewParityVerdict, PublishingBindings, SourceSnippet, TonePolicy

AI assist is governed, not free-form#

Ten AI_ASSIST_PANELS are defined (authoring-blocks/ai-assist.ts):

text
research · drafting · rewriting · summarizing · citation-lookup ·
fact-checking · translation-suggest · illustration-generation ·
narration-generation · accessibility-pass

Crucially, every assist request carries an explicit governance binding rather than relying on prose like "governed by Sophia/Isis/Lilith." The AiAssistGovernance shape makes the governance contract concrete:

ts
interface AiAssistGovernance {
  readonly tonePolicyId: string; // which TonePolicy applies
  readonly evidencePackId: string | null; // Sophia evidence binding (required for fact-check/citation panels)
  readonly releaseGateIds: readonly string[];
  readonly piiRedactionRequired: boolean;
  readonly attributionRequired: boolean;
}

evaluateAiAssistGuardrails(request) then enforces that binding: it rejects an unknown panel, requires an evidencePackId for evidence-bearing panels (fact-checking/citation lookup), requires a tonePolicyId, and refuses panels that would generate PII-bearing output when piiRedactionRequired is not set. This is the seam where Studio composes with Sophia (the evidence pack), Lilith (release gates), and Isis (generation). But the enforcement is real local logic, so a missing binding fails loud rather than silently passing.

Evaluation, tone policy, and the publish gate#

evaluateAuthoringWithPolicy(document, config) walks the block tree against a TonePolicy (policyId, bannedPhrases, cautionaryPhrases, minReadingEase) and emits a list of AuthoringEvaluationFindings with block / warn / info severity. It flags overconfident claim language (a CLAIM_MARKERS set including phrases such as "studies show", "research proves", "guaranteed", "cure", "eliminates") that lacks an adjacent citation or evidence-pin block, checks Flesch reading ease via fleschReadingEase(text) against minReadingEase, and treats a banned phrase as a block-severity finding. citationDensityByBlock returns a per-block citation count so the UI can surface thin-evidence passages.

These findings are not merely advisory — they are folded into the publish gate. editorialGateFromAuthoringFindings(findings) produces a required EditorialGate whose gateId is the canonical AUTHORING_EVALUATION_GATE_ID = 'authoring-evaluation:no-blocking-findings', marking it unsatisfied whenever any finding is block-severity. Because the lifecycle machine consumes that gate (next section), a blocking tone-policy violation literally refuses the transition into a published state until the finding is resolved. warn/info findings surface in review but never gate.

Editorial lifecycle and calendar (§16.3)#

Every authored artifact moves through the same lifecycle regardless of type. EDITORIAL_LIFECYCLE_STATES (in editorial-lifecycle/lifecycle.ts) is a twelve-state machine:

text
idea → draft → in-review → changes-requested → approved →
scheduled → published → updated → deprecated → sunset →
archived → takedown

Reconciliation — canonical names. Earlier prose in ../ARCHITECTURE.md and an older mermaid diagram showed a 7-state "core subset" with the terminal names rejected and retracted. Those state names do not exist in the code. The canonical enum uses changes-requested (not rejected) and takedown (not retracted), and adds idea, updated, deprecated, and sunset. V1/features.md already documents the full twelve-state set and notes the older subset was an intentional simplification; this page treats the code enum as authoritative.

Transitions are constrained by an explicit TRANSITIONS adjacency map:

From Allowed next states
idea draft
draft in-review, archived
in-review changes-requested, approved
changes-requested draft, archived
approved scheduled, published, changes-requested
scheduled published, changes-requested
published updated, deprecated, takedown
updated published, deprecated
deprecated sunset, published
sunset archived
archived (terminal)
takedown archived
stateDiagram-v2 [*] --> idea idea --> draft draft --> in_review: submit draft --> archived: discard in_review --> changes_requested: needs changes in_review --> approved: approve changes_requested --> draft changes_requested --> archived approved --> scheduled: schedule approved --> published: publish now approved --> changes_requested: send back scheduled --> published: scheduled time scheduled --> changes_requested: hold published --> updated: hotfix published --> deprecated published --> takedown: takedown /<br/>correction updated --> published updated --> deprecated deprecated --> sunset deprecated --> published sunset --> archived takedown --> archived archived --> [*] note right of takedown Takedown fans out via the cascade — kept and shared artifacts, Sophia evidence packs, downstream notebooks and feeds. end note

Two predicates back the machine: isEditorialTransitionAllowed(from, to) (pure adjacency check) and tryEditorialAdvance({ from, to, gates }), which returns a typed EditorialAdvanceResult. The failure reasons are explicit: transition-not-allowed (the edge is not in TRANSITIONS) or unsatisfied-gates (one or more required gates are not satisfied, with the offending missingGateIds returned). This is exactly how the authoring-evaluation gate above blocks a publish: a block-severity finding makes that required gate unsatisfied, so tryEditorialAdvance refuses the move into a published state.

Calendar, embargo, and the recurrence engine#

EditorialCalendarEntry carries scheduling and dependency context — deadlineUnixSeconds, embargoUntilUnixSeconds, scheduledPublishUnixSeconds, scheduledUnpublishUnixSeconds, an assignedReviewer, a recurrence, and upstream dependencies. evaluateEditorialPublishWindow({ entry, nowUnixSeconds, dependencies }) returns one of embargoed, not-yet-scheduled, publish-window-not-open, publish-window-closed, or blocked-dependencies — so a scheduled artifact will not auto-publish while embargoed, outside its window, or while an upstream dependency is not yet published.

The recurrence engine (editorial-lifecycle/recurrence.ts) is a real primitive that the docs previously only hinted at by example. Its module header literally encodes the platform's recurring cadences — daily Veritas briefings, daily Tara passages, weekly Arete reflections, nightly Nyx highlights — as EditorialRecurrence (cadence: daily | weekly | monthly, an anchorUnixSeconds, and tags). nextOccurrences(...) computes the next N timestamps from an anchor; nextPublishableOccurrences(...) filters out embargoed occurrences. exportEditorialCalendarAsIcal(...) emits a real RFC-5545-style BEGIN:VCALENDAR … END:VCALENDAR string (with PRODID:-//Oshun Studio//Editorial Calendar//EN, per-occurrence VEVENTs, ORGANIZER/ATTENDEE lines) that operator dashboards and partner calendars can import. buildCrossTeamCalendarView(...) rolls entries up per domain with owner counts and an upcoming-deadline count.

Asset and media library (§16.4)#

asset-metadata/ implements asset logic — metadata, rights, provenance, accessibility, approval, bulk-upload, EXIF/presets, and analytics — across metadata.ts, approval-queue.ts, bulk-upload.ts, exif-and-presets.ts, and analytics.ts.

Accuracy fix — storage backing. Earlier docs said the asset library is "backed by MinIO/S3, with provenance bundles travelling with every asset." The binding to object storage is not in this library: asset-metadata/ contains no MinIO or S3 client (grep for minio/@aws-sdk/putObject returns nothing). MinIO does exist as a docker-compose dev dependency at the infra level (docker/docker-compose.dev.yml, ports :9000/:9001), so storage is real at infra level. But the authoring library handles metadata and rights, not binary blobs. Treat blob-store ↔ metadata wiring as an integration that the library is designed for but does not itself perform.

What the library does model is rich. STUDIO_ASSET_KINDS spans image, audio, voiceover, video, avatar-rendering, diagram, illustration, manuscript-scan, motion-clip, ambient-loop, and ritual-sound-pack. Rights are a first-class field via STUDIO_ASSET_LICENSES (cc0, cc-by, cc-by-sa, cc-by-nc, cc-by-nd, platform-commercial, platform-internal, creator-reserved, public-domain, all-rights-reserved). StudioAssetMetadata carries a contentHash and optional perceptualHash, a license with optional rightsExpiryUnixSeconds and usageScopes, and a StudioAssetAccessibility block (altText, transcript, captionTrackId, audioDescriptionId, altTextCoveragePct). It also carries a StudioAssetProvenance bundle (provenanceBundleId, generatorId, modelId, promptHash, lineageIds), an optional StudioAssetWatermark, nsfwLabels, and an approvalStatus of pending | approved | rejected | taken-down. validateAssetMetadata(...), findDuplicateAssets(...) (perceptual-hash de-duplication), and buildAssetReplacementCascade(...) (which artifacts must be revalidated when an asset is swapped) round out the metadata/rights/provenance logic.

Taxonomy, ontology, and concept-graph curation (§16.5)#

This is an operator-tier surface that promotes Nisaba concept-graph nodes/edges and Metis knowledge-graph promotions through Sophia evaluation gates. taxonomy-curation/curation.ts defines ten TAXONOMY_OBJECT_KINDStag, theme, lineage, mood, modality, topic, claim-cluster, prerequisite-chain, concept-node, concept-edge — and five TAXONOMY_CURATION_OPERATIONS: add, merge, split, deprecate, reparent.

A TaxonomyChangeRequest carries the operation, targetKind, targetIds, optional newParentId/newNodeIds, evidenceReferences, a proposedBy, and a contestStatus of uncontested | contested | resolved — so a curation move can be challenged and must be resolved before it lands. buildTaxonomyChangePreview shows the effect before commit; checkTaxonomyIntegrity(...) returns a verdict including a contestedCount; and rollout is staged through ONTOLOGY_ROLLOUT_STAGES = preview → staged → rolled-out → rolled-back via tryAdvanceOntologyRollout/advanceOntologyRollout. Concept edges have their own typed policy validation (validateConceptEdgePolicies), which flags unresolved-contest when a contested edge has no resolvedByCreatorId. The localization workspace cross-references the wider localization story in Content, Localization, Documentation, and Launch Readiness.

Localization and translation workspace (§16.6)#

localization-workflow/translation.ts models translation as governed, memory-assisted work rather than raw string replacement. A TranslationSegment carries a sourceHash used for stale detection: detectStaleTranslations(...) recomputes the current source hash and flags any segment whose translation was authored against an out-of-date source, returning a TranslationStalenessVerdict. A TranslationMemoryEntry carries a fuzzy-match score, surfaced through fuzzyMatchTranslationMemory(...) (Jaccard similarity above a minScore threshold). GlossaryEntry supports doNotTranslate (protected brand/lineage terms) and a lineageScope so glossary rules can be lineage-specific; lookupGlossary(...) resolves the applicable entry. evaluateLocaleQA(...) checks segments against LOCALE_QA_CHECKLIST_AREAS, and buildLocaleLaunchScorecard(...) produces a per-locale readiness scorecard.

The launch locale set is defined centrally in libs/oshun/i18n/src/index.ts: OSHUN_LAUNCH_LOCALES = en-US, es-US, fr-FR, de-DE, ar, he, ja-JP, pt-BR (eight), with OSHUN_DEFAULT_LAUNCH_LOCALE = en-US. Two of those — ar and he — are RTL, which is why the localization workspace and design system carry RTL parity concerns. The library's stale-detection plus glossary do-not-translate rules are precisely the machinery that keeps an eight-locale launch in sync as source artifacts change.

Versioning, diff, and rollback (§16.7)#

Every authored artifact is version-tracked. versioning/versioning.ts defines an ArtifactVersionRecord:

ts
interface ArtifactVersionRecord {
  readonly artifactId: string;
  readonly version: number;
  readonly authorId: string;
  readonly authoredAtUnixSeconds: number;
  readonly changeSummary: string;
  readonly approvalState: 'pending' | 'approved' | 'rejected' | 'merged';
  readonly reviewPackageId: string | null;
  readonly priorVersion: number | null;
  readonly contentFingerprint: string;
}

appendArtifactVersion(history, next) links each record to its predecessor, and validateVersionChain(history) audits the whole chain, returning typed VersionChainIssues of five kinds:

Issue kind Meaning
artifact-mismatch a record's artifactId differs from the chain's
non-monotonic-version version numbers do not strictly increase
prior-version-mismatch priorVersion does not point at the actual predecessor
missing-review-package an approved/merged record has no reviewPackageId
empty-change-summary the changeSummary is blank

buildVersionDiff(...) produces a typed VersionDiff whose entries are tagged by kind (prose, structured-block, citation, source-binding, asset, metadata, translation), so rollback can reason about what changed and which downstream bindings need re-evaluation. Rollback therefore respects downstream invalidation rather than blindly reverting bytes.

Collaboration, comments, and review threads (§16.2)#

collaboration/collaboration.ts and collaboration/routing.ts model presence, comments, suggestions, and review sign-off. PresenceCursor tracks who is on an artifact. presenceForArtifact(...) treats a presence as stale after a window, and buildReconnectResyncDelta(...) computes the catch-up diff a reconnecting client needs, including comments newer than its last presence Lamport timestamp. CollabComment states are open | resolved | rejected; suggestions resolve via resolveSuggestion(...). Review sign-off runs through ReviewChecklistItems and evaluateReviewSignoff(...). The activity feed enumerates nine ACTIVITY_FEED_KINDSedit, comment, citation-changed, asset-swapped, reviewer-assigned, approval, publish-state-transition, suggestion-accepted, suggestion-rejected. The comment substrate is shared with admin review queues, so editorial threads can hand off to trust-and-safety where required (see Trust, Safety, and Privacy).

Honesty note on real-time co-editing. The "conflict-free merge" path here is deliberately modest. mergeBlockText(...) is a token-level three-way merge that emits explicit <<<left|||right>>> conflict markers when both sides change the same span. Its own comment calls it a "minimal" merge for the "presence-aware live co-edit path," with richer block-level merges left to the §16.2 checkpoint module. There is no confirmed running CRDT substrate in this library. The collaboration module is presence/comment/merge contract and logic, and a production CRDT/presence transport is still aspirational. (The route surface real-time-collaboration-substrate/ exists, but the substrate behind it is not verified here.)

Templates (§16.2)#

templates/templates.ts defines seventeen STUDIO_TEMPLATE_KINDS (ritual-script, meditation, breathwork-session, journaling-prompt, weekly-review, reflection-card, story-brief, claim-card, source-note, passage, edition-comparison, sky-event-briefing, lesson-plan, course-outline, assessment, rubric, admin-review-template). validateTemplatePayload(...) and validateTemplateBindings(...) enforce field-level and binding-level correctness; resolveTemplateForTenant(...) and resolveTemplateAvailability(...) handle per-tenant resolution. The rehearsal.ts fixtures (runTemplateRehearsal, runAllRehearsals, recommendReplacementTemplates) let operators dry-run a template against canonical inputs before a deprecated template is retired (isTemplateDeprecated(...)).

The Studio route tree#

The apps/oshun/web/src/app/studio/ directory contains roughly 54 subroutes — far more than the authoring core alone. The directly relevant ones include authoring/, review-approval-workflows/, commenting-annotation-system/, real-time-collaboration-substrate/, presence-cursor-systems/, asset-preview-pipeline/, file-media-ingestion/, internationalization-localization/, activity-change-feeds/, audit-compliance-surfaces/, rbac-permission-policy/, and accessibility-governance/, alongside domain-specific authoring entry points (bellona/, hathor/, neith/, tara/, aja/, isis/, yemaya/, project-obsidian/, concordia-workbench/). The earlier "subroute, no separate app" claim is therefore verified-correct.

Generate → gate → author: the §3 content service handoff#

Studio is the authoring surface; the agentic content pipeline that machine-generates candidate artifacts is a separate, deployable service: apps/oshun/content-service (project @oshun/content-service-app). Its main.ts boots createContentHttpServer over an Iris-routed creative writer plus a calibrated three-member JudgePaneljudge-strict (temperature 0.2), judge-balanced (0.4), and judge-exploratory (0.6). It fails loud (NotConfiguredError) when ANTHROPIC_API_KEY is absent: no provider means no generation, never fabricated output. (Ops can disable specific model ids via CONTENT_SERVICE_DISABLED_MODELS, and the resolver downgrades through a fallback chain or fails loud with NoAvailableModelError if the whole chain is disabled.)

The @oshun/content-service router (http-router.ts) exposes:

Method + path Purpose
POST /v1/content/briefs submit a brief → gated artifact + run id
GET /v1/content/runs list runs
GET /v1/content/runs/:id retrieve a persisted (replayable) run
POST /v1/content/runs/:id/replay replay a run, reproducing the artifact
GET /v1/operator/runs operator dashboard run list (filters)
GET /v1/operator/runs/:id operator dashboard run detail

A ContentBrief is { briefId, contentType, prompt, submitter }, where contentType is typed as ContentType from @oshun/content-quality-judge, and a run carries a ContentRunStatus of completed | blocked | failed (a promptSha256 of the winning prompt makes runs reproducible). The natural handoff is: the content service generates and gates a candidate, then a human authors and publishes it through @oshun/studio-authoring's lifecycle. The generated artifact still has to clear the same editorial gates, tone policy, and Sophia evidence bindings before it can advance to published. The agentic generation topology itself is described in Agentic AI Studio and Generation Audience Tiers; its deployment is honestly tracked as gated ([~]) on real provider credentials.

Aspirational vs. shipped#

To match the docs' existing candor, the line between implemented and aspirational here is explicit:

  • Shipped (real, non-stub, domain-specific logic): the 12-state editorial lifecycle with its TRANSITIONS table and gate-checking; the authoring-evaluation → publish gate binding; versioning with validateVersionChain and typed diffs; taxonomy add/merge/split/deprecate/ reparent with contest status and staged rollout; localization translation memory, glossary, do-not-translate, and sourceHash stale detection; the recurrence engine and iCal export; role/permission matrix and profile completeness. The web workspace genuinely consumes the library.
  • Aspirational / not verified here: a running CRDT/presence substrate for real-time co-editing (the library has presence/merge contracts and a minimal token merge, not a confirmed live substrate); MinIO/S3-backed binary asset storage wired to asset-metadata/ (the library is metadata/rights logic, with no storage client; MinIO is infra-level dev tooling); and database persistence (the library is pure functions: timestamps and states are inputs, not read from a clock or store).
  • Coverage caveat (completeness audit, 2026-06-22): the editorial-review-approval walkthrough is a partial spec whose end-to-end steps currently drive the incident decision panel (INC-2041) rather than the editorial artifact lifecycle — so the e2e coverage of the editorial flow is weaker than the library's depth would suggest. This is recorded honestly as a test-coverage gap, not a logic gap.

Backlog detail lives at §16 (with subsections §16.1–§16.7); the content-service pipeline is tracked under §3. Cross-domain and provider dependencies are in ../DEPENDENCIES.md.