Oshun V1 ships large amounts of grounded, expert-curated, and AI-assisted content across rituals, practices, stories, claims, sources, passages, sky events, lessons, courses, and assistant-explainer modules. That content does not appear by magic: it is produced by named creators working a structured pipeline inside Oshun Studio, a first-class product surface rather than an afterthought buried in the admin shell. This page covers the two foundations of that surface — who is allowed to do what (the creator role and contributor model) and the authoring workspace itself (the structured block editor, the live source panel, the inline AI-assist panels, preview-as-customer parity, and the authoring-time evaluation harness). It is the feature-side companion to the architecture catalog; the hub for the set is ../features.md. The editorial calendar, lifecycle, and asset library that this workspace feeds into are covered in Editorial Calendar and Asset & Media Library; taxonomy, localization, and versioning in Taxonomy, Localization, and Versioning; and collaboration, review, and templates in Collaboration, Review, and Templates.
What ships, honestly#
The substance of this surface is real, non-stub, domain-specific code, and
it lives in one library: libs/oshun/studio-authoring (package
@oshun/studio-authoring, version 0.1.0). The package is a pure-ESM source
library — its package.json points main and types straight at
./src/index.ts, with no tsup build step — and src/index.ts re-exports nine
subdomain modules:
creator-roles authoring-blocks editorial-lifecycle asset-metadata
taxonomy-curation localization-workflow versioning collaboration templates
The web workspace genuinely consumes this library: the React component
apps/oshun/web/src/components/studio/StudioAuthoringWorkspace.tsx (824 lines)
imports the real exported functions acceptAutosave, applyDragToCite,
citationDensityByBlock, evaluateAiAssistGuardrails,
evaluateAllPreviewSurfaces, evaluateAuthoringWithPolicy, and
evaluatePublishReadiness, plus the types AiAssistRequest,
AuthoringDocument, PreviewParityVerdict, PublishingBindings,
SourceSnippet, and TonePolicy directly from @oshun/studio-authoring. The
role matrix, the lifecycle state machine, the AI-assist guardrails, the
preview-parity checks, and the authoring evaluator are all wired into a running
surface, not described in the abstract.
Two things are specified more strongly than they are confirmed end-to-end, and this page says so rather than overselling:
- Real-time co-editing with CRDT/presence. The library exposes a
collaboration/module and the web app has areal-time-collaboration-substrate/route, but the autosave/merge logic shipped in the library is a Lamport-clock convergence model (acceptAutosave) plus a block-level three-way merge (mergeBranchIntoTrunk), not a confirmed running CRDT substrate with live presence. Treat presence/CRDT as the contract-and-skeleton layer until the running substrate is verified. - Persistence. The library is pure functions: timestamps are inputs
(
nowUnixSeconds), not reads off a clock, and nothing in it writes to a database. That makes it deterministic and testable, but the durable home of a document, the lock store, and the version chain belong to the surfaces and services that call it, not to this library.
The v1-completeness-audit-2026-06-22.md also flags editorial-review-approval
as a partial walkthrough whose spec currently drives the incident decision
panel (INC-2041) rather than the editorial artifact lifecycle — i.e. the
end-to-end coverage for the editorial flow is weaker than the depth of the
library implies. The library logic is real; the e2e proof for the editorial
journey is the thinner part.
Creator and contributor roles#
Creators and contributors occupy a distinct identity scope, separate from both
customers and Oshun operators. The canonical role taxonomy is the exported
constant STUDIO_CREATOR_ROLES in
libs/oshun/studio-authoring/src/creator-roles/roles.ts — twelve roles:
| Role | What they do |
|---|---|
author |
Drafts the artifact from scratch in the block editor |
editor |
Edits and reviews drafts, can request changes |
curator |
Stewards taxonomy and the asset library |
reviewer |
Reviews submissions for quality/policy |
sme |
Subject-matter expert; reviews for domain accuracy |
teacher |
Authors and reviews instructional artifacts |
scholar |
Authors and reviews with lineage attestation |
translator |
Produces localized editions |
illustrator |
Produces and manages visual assets |
narrator |
Produces and manages voice/audio assets |
producer |
Manages assets and schedules publish |
publisher |
Approves, schedules, and unpublishes |
The role/permission matrix#
Permissions form a fixed vocabulary of ten, the exported constant
STUDIO_ROLE_PERMISSIONS: edit-draft, request-review, review,
approve-publish, schedule-publish, unpublish, translate,
curate-taxonomy, manage-assets, and mint-certification. The binding is the
ROLE_PERMISSION_MATRIX, checked at the call site through
roleHasPermission(role, permission). This is the concrete matrix the docs
previously did not name:
| Role | Granted 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 shape is deliberately tight: no single role both authors and approves
its own publish. An author can request-review but cannot approve-publish;
only a publisher can. mint-certification appears in the permission
vocabulary but is not granted to any role in the default matrix — it is reserved
for the operator/certification path rather than a self-serve creator action.
This is the mechanical backbone behind the prose promise of "per-role
permissions" and "creator permission isolation."
Creator profiles, rights, and disclosure#
Each creator carries a CreatorProfile with creatorId, displayName,
handle, bio, a lineage list, a credentials list, a
CreatorAttributionPreference (displayName, handle, bylineVisible),
rightsConsentRecordIds, provenanceAttestationIds, a tenantId, and
disclosurePreferences — the last of which encodes the AI-assist disclosure
stance ('always' | 'on-request' | 'opt-out') and a piiSafetyConsent boolean.
Profile completeness is enforced per role by
validateCreatorProfileCompleteness, which returns a verdict listing missing
fields drawn from CreatorProfileMissingField. The rules are role-sensitive.
The bio must be at least 24 trimmed characters, and attribution display name and
handle are always required. Credentialed roles (editor, curator,
reviewer, sme, teacher, scholar, translator, publisher) must list
credentials. Lineage-attested roles (author, curator, sme, teacher,
scholar, translator, illustrator, narrator) must declare a lineage.
Every profile also needs at least one rights-consent record, at least one
provenance-attestation, and an affirmative PII-safety consent. This is why
illustrators and narrators must attest lineage even though they don't write
prose — provenance for generated and recorded media starts at the person.
Onboarding, certification, and revocation#
Creators move through an eight-state lifecycle, CREATOR_LIFECYCLE_STATES:
invited → onboarding → training → certified → sandbox → graduated, with
suspended and revoked as enforcement states. Transitions are gated by
isCreatorLifecycleTransitionAllowed against a fixed adjacency map (for
example, revoked is terminal — it transitions to nothing).
Onboarding is not a checkbox; requiredOnboardingModules(role) returns a
role-specific set drawn from ONBOARDING_MODULE_KINDS (twelve module kinds
including consent-disclosure, attribution-preferences,
lineage-attestation, rights-recording, sophia-citation-101,
lilith-tone-policy, isis-release-gates, authoring-block-basics,
translator-glossary-workflow, curator-ontology-workflow,
accessibility-essentials, and safety-and-takedown). An author, for
instance, must complete sophia-citation-101, lilith-tone-policy,
authoring-block-basics, and accessibility-essentials, while a translator's
required path centers on translator-glossary-workflow.
attemptOnboardingTransition enforces the gates. A move to certified or
sandbox fails with missing-required-modules (and lists them) if any required
module is incomplete. A move to graduated fails with sandbox-quota-not-met
unless the creator's sandboxApprovalCount meets the per-role threshold in
SANDBOX_GRADUATION_THRESHOLDS (e.g. author: 3, editor: 5, translator: 6,
sme: 2). suspended/revoked transitions fail with
missing-enforcement-reason unless a non-empty reason is supplied. This is the
"graduation criteria, suspension, and revocation paths" promise rendered as a
real state machine that refuses to fabricate a graduation.
Cross-role hand-offs, queues, and SLA#
Hand-offs travel an ordered five-stage pipeline, STUDIO_HANDOFF_STAGES:
author → reviewer → editor → publisher → translator. A StudioHandoffTicket
tracks the artifact, the current stage, the assigned creator, an slaSeconds
budget, enqueue/escalation timestamps, and a historyStages trail.
advanceStudioHandoff only permits the next ordered stage (returning
invalid-transition or no-next-stage otherwise), and checkStudioHandoffSla
reports whether a ticket has breached its SLA and by how many seconds.
SLA breaches don't just light up red — they escalate. evaluateSlaEscalation
walks the DEFAULT_SLA_ESCALATION_POLICY: after 4 hours over SLA it notifies an
editor (no reassignment), after 12 hours it notifies the publisher and
reassigns, and after 24 hours it notifies an editor and reassigns. Per-role
queue health is summarized by summarizeReleasePipeline (queue length, oldest
ticket age, breached count). Per-creator quality is summarized by
computeCreatorScorecard, which produces a CreatorScorecard with publishing
throughput, review count, median and p90 cycle time, defect rate, SLA-adherence
ratio, and a compositeScore weighted 0.35 throughput + 0.25 (inverse) cycle
time + 0.2 (inverse) defect rate + 0.2 SLA adherence, normalized to [0,1].
Low scores gate suspension; high scores gate graduation from sandbox — the same
scorecard feeds both ends of the lifecycle.
The authoring workspace#
The editor persists a canonical structured-block document, AuthoringDocument
(documentId, title, templateId, locale, language, version, a
blocks tree, and a nullable checkpointId). Every block is an
AuthoringBlockBase carrying a blockId, a kind, optional children, an
attrs bag, and a typed accessibility record (altText, transcript,
captionTrackId). The block vocabulary, AUTHORING_BLOCK_KINDS, covers
eighteen kinds:
heading paragraph callout citation evidence-pin source-panel footnote
glossary-ref embed diagram code math list image audio-clip video-clip
persona-disclosure accessibility-note
That vocabulary is exactly the "headings, callouts, citations, evidence pins,
source side-by-side panels, footnotes, glossary references, embeds, diagrams,
code, math, and accessibility metadata" the prose promises — plus
persona-disclosure (so a piece can declare which persona/avatar authored or
voiced it) and a dedicated accessibility-note block.
The live source panel and drag-to-cite#
The live source/evidence panel pulls from approved Sophia source sets. A
SourceSnippet carries the sourceSetId, its sourceSetApprovalState
(approved | pending | rejected | revoked), approval and expiry timestamps, the
sourceId, evidencePackId, a locator, the snippet text, and recommended
tags. When a creator drags a snippet onto a block, applyDragToCite constructs
a new citation block (id cite-<snippetId>) immediately after the target, but
only after validateSourceSnippetForCitation clears the snippet. The rejection
reasons are explicit and load-bearing:
target-block-not-found— the drop target doesn't exist.invalid-source-snippet— any of the required fields is blank.source-set-not-approved— the set isn'tapproved, or its approval timestamp is in the future.source-set-expired—nowUnixSecondsis past the set's expiry.duplicate-citation— the samesourceId::evidencePackId::locatortriple is already cited.
This is why the panel can only cite approved, unexpired Sophia evidence and
never silently double-cites the same locator. collectCitationBindings and
indexSourceUsage then let the workspace show citation-density indicators and
"where is this source used" views, and citationDensityByBlock computes the
per-paragraph density that surfaces in the UI.
Inline AI-assist panels, under governance#
There are ten AI-assist panels, the exported AI_ASSIST_PANELS: research,
drafting, rewriting, summarizing, citation-lookup, fact-checking,
translation-suggest, illustration-generation, narration-generation, and
accessibility-pass. The prose says these are "governed by Sophia grounding,
Isis prompt and provider policy, and Lilith tone policy" — and the code makes
that governance a concrete object, AiAssistGovernance, attached to every
request:
// AiAssistGovernance (authoring-blocks/ai-assist.ts)
{
"tonePolicyId": "tone-lilith-contemplative-v1", // Lilith tone policy (required)
"evidencePackId": "sophia-pack-...", // Sophia evidence binding (nullable)
"releaseGateIds": ["isis-gate-..."], // Isis release gates
"piiRedactionRequired": true, // must be true for generation panels
"attributionRequired": true,
}
evaluateAiAssistGuardrails runs before any upstream agent and refuses the
request with a typed reason:
unsupported-panel— the panel isn't registered.missing-evidence-pack—research,citation-lookup, andfact-checkingrequire a SophiaevidencePackId; these panels cannot run ungrounded.missing-tone-policy— every invocation requires a LilithtonePolicyId.instruction-too-long— the creator instruction exceeds 4000 chars.pii-redaction-not-enabled—narration-generationandillustration-generationmust setpiiRedactionRequired: true, to prevent source-text PII from leaking into a generation prompt.
So a fact-check with no evidence pack, or an illustration generation with PII
redaction off, is blocked at the Studio boundary — the guardrail is the seam
that keeps the panels from bypassing policy. The successful outcome,
AiAssistOutcome, carries an attributionStatement, disclosureText, and a
governanceTrailId so the provenance of assisted text travels with it. (The
content service that actually generates is described under Generation handoff
below.)
Save, autosave, locking, checkpoints, branch, and merge#
Autosave is convergence-safe. An AutosaveRecord carries a clientId and a
lamport clock. acceptAutosave rejects a write with stale-lamport if the
incoming Lamport value is not strictly greater than the latest, and with
lock-held-by-another if a live DraftLock is held by a different creator
(isLockHeldBy checks holder identity and expiry). Named checkpoints
(NamedCheckpoint) snapshot the document for branching; DocumentBranch
captures a branch from a checkpoint; and mergeBranchIntoTrunk performs a
block-level three-way merge against the common ancestor. Identical edits
collapse, one-sided edits apply, and a two-sided non-identical edit produces a
MergeConflict of kind edited-on-both-sides. A block deleted on one side but
present in the base raises deleted-on-one-side. The merge result bumps the
trunk version and clears the checkpointId. This is the real shape behind
"named checkpoints, branch-from-checkpoint, and merge."
Preview-as-customer parity#
Before publish, the workspace renders the document as each customer surface and
checks parity. PREVIEW_SURFACES are web, mobile, voice, and avatar,
and evaluateAllPreviewSurfaces returns a PreviewParityVerdict per surface
with a list of blocking PreviewParityIssues. The checks are surface-specific
and substantive:
| Surface | Sample parity rule |
|---|---|
web |
An embed URL must be https:// |
mobile |
source-panel (side-by-side) blocks are unsupported and must convert to inline citations; code lines over 80 columns will overflow |
voice |
image/diagram need alt text to be describable; a math block needs a speechExpression; embed cannot be voiced |
avatar |
audio-clip/video-clip need a synchronized captionTrackId; a persona-disclosure block needs an avatarPersonaId |
A customer-facing surface is never handed a document that fails parity for that surface — the verdict is the gate.
The authoring-time evaluation harness#
The harness, evaluateAuthoringWithPolicy, is pure and deterministic so it can
run on autosave, on review-package build, and in CI fixture rehearsal. It emits
AuthoringEvaluationFindings across six kinds — citation-integrity,
unsupported-claim, tone-policy, accessibility, readability, and
locale-readiness — each at a severity of info, warn, or block:
- Unsupported-claim — a logical paragraph containing claim-marker prose (the
harness screens phrases like
"studies show","research proves","guaranteed","always","never","cure") with nocitationorevidence-pinin the same paragraph group →warn. - Citation density — density below
minCitationDensitywarns; abovemaxCitationDensityis aninfo"consider consolidating." - Readability — a real Flesch reading-ease score (
fleschReadingEase,206.835 − 1.015·words/sentence − 84.6·syllables/word) below the tone policy'sminReadingEase→warn. - Tone policy — a
TonePolicy'sbannedPhrasesproduce ablock-severity finding;cautionaryPhrasesproduce awarn. - Accessibility — an
image/diagramwith no alt text, or anaudio-clip/video-clipwith neither transcript nor captions, is ablock. - Locale-readiness —
evaluateLocaleReadinessvalidates a BCP-47 locale and checks that the locale's language matches the document's declaredlanguage.
The critical detail is that these findings are not advisory-only. The function
editorialGateFromAuthoringFindings folds any block-severity finding into a
required editorial gate
(AUTHORING_EVALUATION_GATE_ID = 'authoring-evaluation:no-blocking-findings'),
and both evaluatePublishReadiness and evaluateHotfix (in the editorial
pipeline) treat a block finding as blocking-findings that refuses publish —
even a hotfix cannot route around a banned tone-policy phrase. That closes the
loop the docs previously left implicit: a block finding actually blocks
publish rather than merely surfacing in review.
The studio surface tree#
The workspace is not a single page; apps/oshun/web/src/app/studio/ is a route
directory with roughly fifty-five subroutes. The authoring-relevant ones include
authoring/ (the block editor), review-approval-workflows/,
commenting-annotation-system/, real-time-collaboration-substrate/,
asset-preview-pipeline/, internationalization-localization/,
activity-change-feeds/, and audit-compliance-surfaces/, alongside
domain-specific studio rooms (tara/, bellona/, hathor/, neith/, aja/,
yemaya/). This matches the architecture note that Studio is a subroute under
apps/oshun/web/src/app/studio/ with no separate app — that statement is
verified correct against the route directory.
Generation handoff (an enrichment seam)#
The authoring workspace is the human authoring surface; the §3 agentic
content pipeline is a separate, real, deployable service that can generate a
draft and gate it before a human takes over. That service is
apps/oshun/content-service (project @oshun/content-service-app). Its
main.ts boots createContentHttpServer over an Iris-routed writer and a
three-member JudgePanel (judge-strict 0.2, judge-balanced 0.4,
judge-exploratory 0.6), and it fails loud without ANTHROPIC_API_KEY,
throwing NotConfiguredError rather than fabricating output. Its router
(libs/oshun/content-service/src/http-router.ts) exposes
POST /v1/content/briefs, GET/POST /v1/content/runs[/:id][/replay], and
GET /v1/operator/runs[/:id]. A ContentBrief is
{ briefId, contentType, prompt, submitter }, where contentType is the
ContentType from @oshun/content-quality-judge. The natural handoff —
generate a gated draft via the content service, then open it as an
AuthoringDocument in the workspace for human review, citation, and publish —
is not yet wired as a single flow in the docs, and is called out here as an
honest enrichment opportunity rather than a shipped feature.
Cross-references and tests#
The tests that the prose calls for — role-scoping, queue-routing,
hand-off-correctness, and permission-isolation — back this section; the broader
backlog and dependency context live at §16 in V1/TODOS.md. (Module
fileoverviews tag themselves §16.1 roles, §16.2 authoring blocks, §16.3
editorial lifecycle, §16.4 asset metadata.) Note: libs/seshat (@seshat/*,
sustainability/craft/fabrication) and libs/saraswati (@saraswati/*,
EV/energy tech) are unrelated domains and do not back V1 content authoring.