Oshun Platform · Features

Creator Roles and the Authoring Workspace

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

7sections13 minread3tables

On this page

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:

text
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 a real-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.tstwelve 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:

text
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't approved, or its approval timestamp is in the future.
  • source-set-expirednowUnixSeconds is past the set's expiry.
  • duplicate-citation — the same sourceId::evidencePackId::locator triple 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:

jsonc
// 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-packresearch, citation-lookup, and fact-checking require a Sophia evidencePackId; these panels cannot run ungrounded.
  • missing-tone-policy — every invocation requires a Lilith tonePolicyId.
  • instruction-too-long — the creator instruction exceeds 4000 chars.
  • pii-redaction-not-enablednarration-generation and illustration-generation must set piiRedactionRequired: 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 no citation or evidence-pin in the same paragraph group → warn.
  • Citation density — density below minCitationDensity warns; above maxCitationDensity is an info "consider consolidating."
  • Readability — a real Flesch reading-ease score (fleschReadingEase, 206.835 − 1.015·words/sentence − 84.6·syllables/word) below the tone policy's minReadingEasewarn.
  • Tone policy — a TonePolicy's bannedPhrases produce a block-severity finding; cautionaryPhrases produce a warn.
  • Accessibility — an image/diagram with no alt text, or an audio-clip/video-clip with neither transcript nor captions, is a block.
  • Locale-readinessevaluateLocaleReadiness validates a BCP-47 locale and checks that the locale's language matches the document's declared language.

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.