Two Oshun Studio disciplines sit at the human edge of authoring: the
collaboration layer (presence, comments, suggestions, review signoff, and
activity feeds) and the templates library (a role-aware gallery of governed,
pre-bound content scaffolds). They serve editors, reviewers, SMEs, translators,
and tenant operators working inside the §16 Studio surface (a route tree under
apps/oshun/web/src/app/studio/, not a separate app). Both are implemented as
real, domain-specific logic in the source library @oshun/studio-authoring
(v0.1.0, pure ESM, main/types → ./src/index.ts), which re-exports nine
subdomain modules including collaboration and templates. This page is the
feature-side companion to the architecture catalog; the hub for the set is
../features.md.
What ships, honestly#
The logic and contracts are real and tested: notification fan-out
de-duplicates recipients, signoff gating refuses approval while required items
are open, a token-level three-way merge produces explicit conflict markers, the
reconnect-resync delta replays exactly the activity a client missed, and the
template validator and rehearsal harness run real field checks. The web
workspace at apps/oshun/web/src/components/studio/StudioAuthoringWorkspace.tsx
genuinely consumes @oshun/studio-authoring.
What is honestly aspirational: the running CRDT substrate for live
co-editing. The library ships the presence/cursor model, a minimal merge, and a
reconnect-resync algorithm — i.e. the contracts and the logic — but a verified
end-to-end CRDT transport with offline-first replication is not confirmed in
this library. The route
apps/oshun/web/src/app/studio/real-time-collaboration-substrate/ exists as the
surface for it. The 2026-06-22 completeness audit also notes that the
editorial-review-approval walkthrough currently drives an incident decision
panel (INC-2041) rather than the editorial-artifact lifecycle, so the e2e
coverage of the editorial review flow is weaker than the library implies. Where
something is spec-only or gated, this page says so. Honest "planned/gated" beats
fake "shipped."
Collaboration, Comments, and Review Threads#
Implemented in libs/oshun/studio-authoring/src/collaboration/ (§16.8) across
collaboration.ts (presence, comments, suggestions, signoff, activity feed) and
routing.ts (notification fan-out, reconnect, suggestion resolution, text
merge).
Presence and live co-editing#
A PresenceCursor carries creatorId, artifactId, an optional blockId,
selectionStart/selectionEnd, and lastSeenUnixSeconds.
presenceForArtifact returns only the cursors seen within a staleAfterSeconds
window (default 60s), so the "presence, cursors, selection sharing" promise of
the source is a real, time-bounded query rather than a static roster.
Conflict-free merging is realized by mergeBlockText, a token-level three-way
merge: it splits base/left/right on whitespace, takes the side that changed
when only one did, keeps the agreed value when both agree, and emits an explicit
<<<left|||right>>> marker with a conflictMarkers count when both sides
changed the same span. The source code documents this as the "presence-aware
live co-edit path," with richer block-level merges deferred to the §16.2
checkpoint module. This is honest about scope: it is a working merge primitive,
not a full CRDT engine — see the scope note below.
Comments and threads#
CollabComment models an inline comment with a threadId, blockId,
authorId, mentions, body, a reviewerOnly flag (the source's
"reviewer-only comment channels"), and a state from
COLLAB_COMMENT_STATES = open | resolved | rejected. Resolution timestamps are
recorded explicitly, giving the source's "resolved/unresolved threads."
Suggestion mode (track changes)#
A SuggestionEdit is a proposed before → after block change in state
pending | accepted | rejected. resolveSuggestion applies the decision and
records attribution: on accept, it replaces before with after in the
current block text (falling back to the suggested text if the span has moved)
and returns the applied text; on reject, it leaves the block untouched. Either
way, it stamps resolvedById and resolvedAtUnixSeconds — the source's
"accept/reject with attribution."
Review checklists and signoff gating#
ReviewChecklistItem records each gating question with a required flag and
its signedOffById / signedOffAtUnixSeconds. evaluateReviewSignoff returns
{ ok: true } only when every required item has both a non-empty signer and
a timestamp; otherwise it returns { ok: false, missingItemIds }. This is the
mechanism that the §16.7 change-request machine consults: in
transitionChangeRequest, moving a change request to approved while
openRequiredChecklistItems > 0 is rejected with reason open-signoff-items.
That cross-module wiring is why "review checklists embedded in the artifact,
with reviewer signoff blocks, required evaluations, and gating questions" is an
enforced gate, not a UI suggestion. The version-control side is detailed in
Taxonomy, Localization, and Versioning.
Activity feed#
ACTIVITY_FEED_KINDS enumerates the per-artifact event vocabulary — edit,
comment, citation-changed, asset-swapped, reviewer-assigned, approval,
publish-state-transition, suggestion-accepted, suggestion-rejected — and
appendActivityFeed keeps the feed append-only. This is exactly the source's
"activity feed per artifact: edits, comments, citations changed, assets swapped,
reviewers assigned, approvals granted, and publish-state transitions," with the
two suggestion outcomes added.
Notification routing and reconnect behaviour#
routeCommentNotifications (in routing.ts) fans a new comment out to three
recipient classes — mention, thread-participant, and reviewer-assigned —
de-duplicating so that a person who is both mentioned and a reviewer is
notified once. It never notifies the comment's own author. This is the source's
"mentions, assignment, and per-thread notification routing" made deterministic.
Reconnect is handled by buildReconnectResyncDelta: given a
ReconnectResyncCursor (the client's lastFeedEntryId and
lastPresenceUnixSeconds), it returns the missedFeedEntries after the
client's last-seen entry, the missedComments authored since, and the
currentPresences still live within 60 seconds. This directly answers the
source's test promise of "presence/awareness behaviour under reconnect" — the
client gets exactly the diff it needs to catch up, not a full reload.
What the audit flags#
The 2026-06-22 completeness audit calls out that the editorial-review-approval
spec currently exercises an incident decision panel (INC-2041) rather than
the editorial-artifact lifecycle. The collaboration logic above is real and
unit-tested; the end-to-end editorial-review walkthrough is the partial piece.
Recording this honestly: the library is stronger than its e2e coverage here.
Templates Library#
Implemented in libs/oshun/studio-authoring/src/templates/ (§16.9) across
templates.ts (the template model, validation, tenant overrides, binding
checks) and rehearsal.ts (fixture-bound rehearsal, replacement recommendation,
the canonical gallery).
The role-aware gallery#
STUDIO_TEMPLATE_KINDS enumerates the seventeen template kinds the source
promises, one-for-one:
| Group | Kinds |
|---|---|
| Contemplative / personal | ritual-script, meditation, breathwork-session, journaling-prompt, weekly-review, reflection-card |
| Editorial / scholarly | story-brief, claim-card, source-note, passage, edition-comparison, sky-event-briefing |
| Education | lesson-plan, course-outline, assessment, rubric |
| Operations | admin-review-template |
buildCanonicalGallery turns templates into browsable CanonicalGalleryEntry
rows (title, description, availableToRoles, deprecation status, replacement
pointer) — the "canonical, browsable, role-aware template gallery."
Role-awareness is structural: every StudioTemplate carries a roleAllowlist
of StudioCreatorRoles (the twelve studio roles — author, editor,
curator, reviewer, sme, teacher, scholar, translator, illustrator,
narrator, producer, publisher; see
Creator Roles and the Authoring Workspace).
The template contract and validation#
A StudioTemplate declares everything the source promises a template should
pre-bind:
interface StudioTemplate {
templateId: string;
kind: StudioTemplateKind;
version: number; // template versioning
roleAllowlist: StudioCreatorRole[]; // role-aware
fields: StudioTemplateField[]; // required fields + validation
suggestedGroundingSources: string[]; // suggested grounding sources
recommendedEvaluations: string[]; // recommended evaluations
boundPersonaId: string; // pre-bound persona
boundTonePolicyId: string; // pre-bound tone policy
boundAgenticPipelineId: string | null; // pre-bound agentic pipeline
tenantOverrides: ReadonlyMap<string, StudioTemplate | null>;
deprecatedAtUnixSeconds: number | null;
replacementTemplateId: string | null;
}
Each StudioTemplateField carries a typed validation rule — string (with
minLength/maxLength/pattern), number (with min/max), enum, or
block-list (with minBlocks). validateTemplatePayload checks a submitted
payload against the field rules and returns a TemplateValidationResult with
the missingFieldIds, invalidFieldIds, and an ok verdict — the source's
"required fields, validation."
validateTemplateBindings audits the template itself, emitting a typed
TemplateBindingIssue for any unbound governance dimension:
no-role-allowlist, no-required-fields, missing-grounding-source,
missing-recommended-evaluation, missing-persona, missing-tone-policy, or
missing-agentic-pipeline. In other words, a template that fails to pre-bind a
persona, tone policy, grounding source, evaluation set, or agentic pipeline is
flagged before it can mislead an author — the binding discipline that connects
templates to Lilith Persona Policy,
Sophia Grounding, and the §32 generation pipeline in
Isis Generation Control.
Per-tenant overrides for institutional Metis#
Templates support the source's "per-tenant template extensions and overrides for
institutional Metis delivery." resolveTemplateForTenant returns a tenant's
override when one exists. resolveTemplateAvailability is more expressive: it
returns { ok: true, source: 'base' } when no override applies,
{ ok: true, source: 'override' } when a tenant-specific template is provided,
and { ok: false, reason: 'disabled-for-tenant' } when a tenant's entry in the
override map is null — i.e. an institution can disable a template, not just
replace it. See Tenant, Institution, and Operator Toolkit
and Metis — Education and Tutoring.
Versioning, fixture-bound rehearsal, and deprecation#
Templates are versioned (version) and can be deprecated
(deprecatedAtUnixSeconds); isTemplateDeprecated evaluates that against a
supplied clock.
The standout is fixture-bound rehearsal (rehearsal.ts), which fulfills the
source's "fixture-bound rehearsal" test promise. A TemplateRehearsalFixture
pins a concrete payload to its expectedOk, expectedMissingFieldIds, and
expectedInvalidFieldIds. runTemplateRehearsal re-validates the payload and
reports every mismatch between expected and actual, and runAllRehearsals
aggregates a RehearsalSummary (templatesCovered, fixturesRun, passed,
failed). This is the mechanism that lets CI detect when a template change
silently invalidates a downstream artifact — a template edit that breaks an
existing fixture fails the suite.
On deprecation, recommendReplacementTemplates answers "replacement
recommendation" with a real similarity computation: it builds a field-shape
fingerprint (each field as fieldId:required:kind, sorted and joined) and
ranks non-deprecated templates of the same kind by Jaccard overlap of those
shapes. It labels each candidate
identical | near-identical | broadly compatible | partial and returns the
top-k above minSimilarity. A curator deprecating a template is handed the
structurally closest survivors, not an arbitrary list.
How these connect to the rest of V1#
- Signoff gates versioning.
evaluateReviewSignoff(here) is what the §16.7 change-request state machine calls before allowingapproved— the two modules are wired together, and the version-control side lives in Taxonomy, Localization, and Versioning. - Templates pre-bind governance. A template's
boundPersonaId,boundTonePolicyId, andboundAgenticPipelineIdconnect a scaffold to the persona/tone/grounding stack and the §32 content pipeline, so an author who starts from a template inherits its guardrails by construction. - Collaboration surfaces are routed. The Studio route tree includes
commenting-annotation-system/,presence-cursor-systems/,real-time-collaboration-substrate/,review-approval-workflows/, andactivity-change-feeds/underapps/oshun/web/src/app/studio/; those routes are the surfaces these contracts back.
Honest scope notes#
mergeBlockTextis a working token-level three-way merge with explicit conflict markers — not a full CRDT. A verified offline-first CRDT transport for live co-editing is the aspirational piece; the library ships the presence/cursor model, the merge primitive, and the reconnect-resync algorithm, and thereal-time-collaboration-substrate/route is the surface for that transport.- These modules are pure functions over typed records; timestamps are inputs
and persistence is an application/infrastructure concern, not part of
@oshun/studio-authoring. - The
editorial-review-approvalend-to-end walkthrough currently drives an incident decision panel (INC-2041) rather than the editorial artifact lifecycle, per the 2026-06-22 audit. The collaboration logic is unit-tested; the editorial-flow e2e is the partial seam.
Related#
- Taxonomy, Localization, and Versioning — the §16.5–§16.7 sibling whose change-request machine consumes review signoff
- Creator Roles and the Authoring Workspace — the twelve studio roles and the workspace that drive collaboration and templates
- Editorial Calendar and Asset & Media Library — the lifecycle and assets that activity feeds and templates reference
- Lilith Persona Policy, Sophia Grounding, and Isis Generation Control — the persona/grounding/pipeline that templates pre-bind
- Tenant, Institution, and Operator Toolkit and Metis — Education and Tutoring — per-tenant template overrides for institutional delivery
- Product Surfaces and the hub ../features.md