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:
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):
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:
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:
idea → draft → in-review → changes-requested → approved →
scheduled → published → updated → deprecated → sunset →
archived → takedown
Reconciliation — canonical names. Earlier prose in
../ARCHITECTURE.mdand an older mermaid diagram showed a 7-state "core subset" with the terminal namesrejectedandretracted. Those state names do not exist in the code. The canonical enum useschanges-requested(notrejected) andtakedown(notretracted), and addsidea,updated,deprecated, andsunset.V1/features.mdalready 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 |
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 (grepforminio/@aws-sdk/putObjectreturns 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_KINDS — tag,
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:
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_KINDS — edit, 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 surfacereal-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 JudgePanel — judge-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
TRANSITIONStable and gate-checking; the authoring-evaluation → publish gate binding; versioning withvalidateVersionChainand typed diffs; taxonomy add/merge/split/deprecate/ reparent with contest status and staged rollout; localization translation memory, glossary, do-not-translate, andsourceHashstale 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-approvalwalkthrough 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.
Related#
- Agentic AI Studio — the generate→gate side of the handoff
- Generation Audience Tiers — tiered generation policy
- Sophia — Grounding Substrate — evidence packs behind AI-assist governance
- Isis — Generation Control Substrate — generation control
- Lilith — Contemplative Policy Substrate — release gates and policy
- Content, Localization, Documentation, and Launch Readiness — localization at launch scale
- Trust, Safety, and Privacy — editorial → trust-and-safety handoff
- Customer Curation — the customer-facing curation surface
V1/features.md§ Content Authoring, Curation, and Editorial Operations- Hub: ../ARCHITECTURE.md