# Creator Roles and the Authoring Workspace

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](../features.md).
The editorial calendar, lifecycle, and asset library that this workspace feeds
into are covered in
[Editorial Calendar and Asset & Media Library](./editorial-and-asset-library.md);
taxonomy, localization, and versioning in
[Taxonomy, Localization, and Versioning](./taxonomy-localization-versioning.md);
and collaboration, review, and templates in
[Collaboration, Review, and Templates](./collaboration-and-templates.md).

## 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 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.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't `approved`, or its approval
  timestamp is in the future.
- `source-set-expired` — `nowUnixSeconds` 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-pack` — `research`, `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-enabled` — `narration-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 `PreviewParityIssue`s. 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
`AuthoringEvaluationFinding`s 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 `minReadingEase` → `warn`.
- **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-readiness** — `evaluateLocaleReadiness` 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.

## Related

- [The Tara Content Workbench](./tara-content-workbench.md)
- [Editorial Calendar and Asset & Media Library](./editorial-and-asset-library.md)
- [Taxonomy, Localization, and Versioning](./taxonomy-localization-versioning.md)
- [Collaboration, Review, and Templates](./collaboration-and-templates.md)
- [Sophia Grounding](./sophia-grounding.md)
- [Isis Generation Control](./isis-generation-control.md)
- [Lilith Persona Policy](./lilith-persona-policy.md)
- [Review, Compliance, and Trust & Safety](./review-trust-safety.md)
- [../features.md](../features.md)
