# Taxonomy, Localization, and Versioning

These three Oshun Studio disciplines — concept-graph curation, the translation
workspace, and per-artifact version control — are the connective tissue that
keeps published content coherent, multilingual, and reversible as it ages. They
serve curators, translators, reviewers, and editors who work behind the customer
experience, and they sit inside the §16 Studio surface (a route tree under
`apps/oshun/web/src/app/studio/`, not a separate app). All three are
**implemented as real, domain-specific logic** — not CRUD stubs — in the source
library `@oshun/studio-authoring` (v0.1.0, pure ESM, `main`/`types` pointing at
`./src/index.ts`), which re-exports nine subdomain modules including
`taxonomy-curation`, `localization-workflow`, and `versioning`. This page is the
feature-side companion to the architecture catalog; the hub for the set is
[../features.md](../features.md).

## What ships, honestly

The **contract and logic layer is real and tested**. Taxonomy curation,
localization, and versioning are pure functions over typed records — cycle
detection runs a real three-color DFS, stale-translation detection compares a
stored `sourceHash` against the current source, the version chain validator
emits five distinct issue kinds, and a translation-memory lookup computes real
Jaccard similarity. The web workspace at
`apps/oshun/web/src/components/studio/StudioAuthoringWorkspace.tsx` genuinely
imports from `@oshun/studio-authoring`, so this is consumed code rather than a
shelf library.

What is **honestly less than the prose implies**: these modules are pure
functions whose timestamps are _inputs_, not a clock, and there is no database
persistence inside the library itself — durability lives at the application and
infrastructure layer (Postgres for state, MinIO/S3 for binary assets). The
ontology-impact "preview" computes affected-artifact counts from
operator-supplied maps; it does not crawl a live index. Where something is
spec-only or relies on an external substrate, this page says so. Honest
"planned/gated" beats fake "shipped."

---

## Taxonomy, Ontology, and Concept Graph Curation

Implemented in `libs/oshun/studio-authoring/src/taxonomy-curation/` (§16.5),
this is the operator-and-curator surface for shaping the shared vocabulary that
every domain reads from: tags, themes, lineages, moods, modalities, topics,
claim clusters, prerequisite chains, and the concept graph itself.

### Object kinds and operations

The library names the curatable object kinds and the operations as canonical
constants in `curation.ts`:

| Constant                           | Members                                                                                                                       |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `TAXONOMY_OBJECT_KINDS` (10)       | `tag`, `theme`, `lineage`, `mood`, `modality`, `topic`, `claim-cluster`, `prerequisite-chain`, `concept-node`, `concept-edge` |
| `TAXONOMY_CURATION_OPERATIONS` (5) | `add`, `merge`, `split`, `deprecate`, `reparent`                                                                              |

A curator's intent is captured as a `TaxonomyChangeRequest` carrying the
`operation`, `targetKind`, the `targetIds` it acts on, an optional `newParentId`
(for reparenting), `newNodeIds` (for splits), `evidenceReferences`, a
`contestStatus` of `uncontested | contested | resolved`, and proposer
provenance. This is the structural answer to the source's promise of "add,
merge, split, deprecate, and re-parent" with "contested-edge review."

### Typed, evidence-bearing edges

The concept graph uses **typed edges**, not bare links. `CONCEPT_EDGE_TYPES`
enumerates seven relationships — `broader-than`, `narrower-than`,
`prerequisite-for`, `contradicts`, `supports`, `lineage-derived-from`, and
`locale-variant-of` — and a `ConceptEdgeRecord` carries a `provenanceActorId`,
`evidenceReferences`, a `contested` flag, and a `resolvedByCreatorId`. The
function `validateConceptEdgePolicies` is where "evidence requirements per edge
type" becomes enforceable: four edge types (`prerequisite-for`, `contradicts`,
`supports`, `lineage-derived-from`) are held in an
`EVIDENCE_REQUIRED_EDGE_TYPES` set, and any such edge with no
`evidenceReferences` yields a `missing-evidence` issue. The validator also flags
`invalid-endpoint` (an edge pointing at a non-existent node),
`missing-provenance` (no actor recorded), and `unresolved-contest` (a contested
edge with no resolver) — directly realizing "edge typing, edge-provenance
attribution, evidence requirements per edge type, contested-edge review, and
conflict resolution."

### Ontology change preview, staged rollout, and rollback

Before a change lands, `buildTaxonomyChangePreview` answers the source's
"downstream impact" promise: given maps of `artifactsByTag`,
`recommendationSlotsByTag`, and `searchResultsByTag`, it returns a
`TaxonomyChangePreview` with the de-duplicated `affectedArtifactIds`, the count
of `affectedRecommendationSlots`, and the count of `affectedSearchResults` — so
a curator can see _which artifacts shift, how recommendations re-rank, and how
search results change_ before committing.

Rollout is a **gated state machine**. `ONTOLOGY_ROLLOUT_STAGES` =
`preview → staged → rolled-out → rolled-back`, and `tryAdvanceOntologyRollout`
enforces the legal adjacency (`preview→staged`,
`staged→{rolled-out, rolled-back}`, `rolled-out→rolled-back`, `rolled-back→∅`).
It refuses with a typed reason — `illegal-transition`,
`commit-timestamp-already-set`, or `rollback-timestamp-already-set` — rather
than silently no-op'ing, and stamps `committedAtUnixSeconds` /
`rolledBackAtUnixSeconds` on the appropriate transition. This is the "staged
rollout, and rollback" guarantee made mechanical.

### Cross-domain mapping

A `CrossDomainMapping` binds one `conceptId` to its local identity across the
domains `nisaba | veritas | nyx | metis | tara | arete`, so the source's example
— _a Nisaba concept ↔ a Veritas topic ↔ a Nyx phenomenon ↔ a Metis learning
objective_ — is a real shape, not just a sentence. See
[Search, Discovery, Recommendations, and Knowledge Graph](./search-discovery-recommendations.md)
for how these mappings feed retrieval.

### Stewardship, freshness, drift, and audit

`stewardship.ts` makes ontology _maintenance_ a first-class concern.

- **Stewards.** `TAXONOMY_STEWARD_ROLES` = `lead-curator`, `domain-curator`,
  `lineage-steward`, `linguistics-reviewer`. Each object carries a
  `TaxonomyStewardship` with a `leadStewardId` and `delegateStewardIds`.
- **Freshness.** `gradeTaxonomyFreshness` grades each object
  `fresh | stale | overdue` from time-since-last-review against
  operator-supplied `staleAfterSeconds` / `overdueAfterSeconds` thresholds.
- **Drift.** `detectTaxonomyDrift` watches _artifact mass_ attached to a node
  over a sliding window and emits a `DriftSignal` with a `deltaRatio` and a
  `shrinking | growing | stable` direction — the documented intuition is that a
  node that used to anchor many artifacts but has lost mass "usually deserves
  attention" because drift can indicate ontology rot.
- **Backlog priority.** `prioritizeCurationBacklog` re-scores the backlog by
  bumping priority for `stale` (+1), `overdue` (+3), large drift (`|delta|>0.5`,
  +2), and contested objects (+4), then sorts descending.
- **Audit history.** `AuditTrailEntry` records each
  `add | merge | split | deprecate | reparent | rolled-out | rolled-back | contested | review-signoff`
  with actor, justification, and evidence; `auditTrailForObject` filters the
  immutable, append-only trail.

### Integrity tests

`checkTaxonomyIntegrity` returns a `TaxonomyIntegrityVerdict` with `orphanCount`
(nodes with zero edge incidence), `cycleDetected` (a real DFS over prerequisite
edges using `VISITING`/`VISITED` colors), `invalidEndpointEdgeCount`,
`missingProvenanceEdgeCount`, and `contestedCount`. This is the engine behind
the source's "cycle detection, orphan detection, prerequisite consistency,
edge-provenance completeness" tests — and it is genuine graph logic, not a
truthiness check.

---

## Localization and Translation Workspace

Implemented in `libs/oshun/studio-authoring/src/localization-workflow/` (§16.6),
this is the translator's surface for segment-level translation, glossary
control, per-locale QA, and stale-translation detection. The launch locale set
it works against is fixed 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 right-to-left, and the i18n library records that in
`RTL_LOCALES` with a `localeDirection()` helper, which is why the QA layer
treats RTL as a real concern rather than a checkbox.

### Segment model, translation memory, and glossary

The core records in `translation.ts`:

- **`TranslationSegment`** — `segmentId`, `sourceLocale`, `targetLocale`,
  `sourceText`, `targetText`, a **`sourceHash`** (the linchpin of stale
  detection), `translatedAtUnixSeconds`, and `reviewedByCreatorId`.
- **`TranslationMemoryEntry`** — a source/target text pair with a `score`.
  `fuzzyMatchTranslationMemory` finds the best candidate above `minScore` using
  a real token Jaccard similarity, realizing the source's "translation memory,
  fuzzy match."
- **`GlossaryEntry`** — `term`, locale pair, `preferredTranslation`, a
  `doNotTranslate` flag, and a `lineageScope` so terminology can be scoped to a
  specific tradition. `lookupGlossary` resolves a term for a given locale pair.

The glossary editor (`glossary-editor.ts`) applies `add | update | remove`
operations through `applyGlossaryEdit`, returning a typed verdict that refuses
duplicates (`reason: 'duplicate'`) and missing entries (`reason: 'not-found'`)
rather than corrupting the list. `enforceGlossaryInSegment` then audits a
translated segment: if the source contains a glossary term, it flags
`do-not-translate-violated` when a DNT term was translated away, or
`preferred-term-missing` when the preferred translation is absent — the
mechanical form of "glossary enforcement."

### Locale-specific QA

Two layers of QA run over segments. `evaluateLocaleQA` covers the
`LOCALE_QA_CHECKLIST_AREAS` — `rtl-layout`, `text-expansion`, `date-format`,
`time-format`, `number-format`, `currency-format`, `honorifics`,
`contemplative-sensitivity`, `accessibility` — and, for example, warns when a
target string expands past a `maxExpansionRatio` (text-expansion is a genuine
RTL/CJK layout risk) and emits an RTL render-parity note for `ar-*`/`he-*`
targets.

`locale-formats.ts` adds **format-leak detectors** that catch source-locale
conventions bleeding into a translation:

| Detector                         | Catches                                                                                                                                     |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `detectSourceDateLeak`           | "April 5, 2026" or `M/D/Y` left in a non-English target                                                                                     |
| `detectSourceTimeLeak`           | `am/pm` clock surviving into a locale that uses 24h                                                                                         |
| `detectNumberFormatMismatch`     | `,` group separator where the locale expects `.` or a space (per `NUMBER_FORMAT_BY_LANG`)                                                   |
| `detectCurrencyFormatMismatch`   | `$`/`USD` formatting left in a non-`en-US` segment                                                                                          |
| `detectHonorificGap`             | a source honorific with no `-さん`/`-様` (`ja`), `-님`/`-씨` (`ko`), or equivalent in honorific-gated languages (`ja`, `ko`, `hi`, `ar`)    |
| `detectContemplativeSensitivity` | sensitive religious/contemplative terms (`guru`, `rinpoche`, `imam`, `rabbi`, `lama`, `sensei`, …) flagged for lineage-steward confirmation |

The contemplative-sensitivity scanner exists because Oshun's content spans
prayer-style and lineage-rooted practice; it routes a finding back to a lineage
steward rather than auto-deciding. See
[Tara — Rituals and Contemplative Practice](./domain-tara.md) for the lineage
model these terms touch.

### Queues, stale detection, and re-translate triggers

`prioritizeLocaleQueue` orders `LocaleQueueItem`s by `priority`, then by
`deadlineUnixSeconds`, each item carrying its `locale`, `artifactId`,
`segmentIds`, `assignedTranslatorId`, and `assignedReviewerId` — the source's
"localization queues per locale with priority, deadline, reviewer assignment."

Stale detection is the workspace's most load-bearing piece.
`detectStaleTranslations` compares each segment's stored `sourceHash` against
the `currentSourceHashes` map and returns `stale: true` with reason
`source-text-changed-since-translation` when they diverge.
`buildReTranslateTriggers` emits a `ReTranslateTrigger` (with `priorSourceHash`
and `newSourceHash`) for every changed segment, and
`surfaceCustomerStalenessBanners` produces the customer-facing message — _"This
translation is being updated to reflect a recent revision of the source text."_
— fulfilling the source's promise that "stale-translation indicators surfaced in
the customer experience."

### Launch readiness scorecards

`buildLocaleLaunchScorecard` produces a per-locale, per-domain
`LocaleLaunchScorecard`: translated vs. total segment counts, stale-segment
count, blocking-QA-finding count, and a `readinessRatio` in `[0,1]` computed as
`(translated − stale) / total − 0.1 × blockingFindings`, clamped. This is the
quantitative "locale launch readiness scorecards per domain and per surface" the
source describes, and it feeds the broader launch gates discussed in
[Content, Localization, Documentation, Launch, and Exit Criteria](./content-localization-launch-exit.md).

---

## Versioning, Diff, and Rollback

Implemented in `libs/oshun/studio-authoring/src/versioning/` (§16.7), this is
the PR-style change-control layer for shared content — version history, visual
diff, branch/propose/merge, and rollback with cascade detection.

### Version records and chain integrity

Every revision is an `ArtifactVersionRecord` (`versioning.ts`):

```ts
interface ArtifactVersionRecord {
  artifactId: string;
  version: number;
  authorId: string;
  authoredAtUnixSeconds: number;
  changeSummary: string;
  approvalState: 'pending' | 'approved' | 'rejected' | 'merged';
  reviewPackageId: string | null;
  priorVersion: number | null;
  contentFingerprint: string;
}
```

`appendArtifactVersion` links each record to its predecessor by setting
`priorVersion`. `validateVersionChain` is the integrity check the source calls
"version chain integrity," and it emits five distinct `VersionChainIssue` kinds:

| Issue kind               | Meaning                                                      |
| ------------------------ | ------------------------------------------------------------ |
| `artifact-mismatch`      | a record belongs to a different artifact than the chain head |
| `non-monotonic-version`  | a version number that does not strictly increase             |
| `prior-version-mismatch` | `priorVersion` does not point at the actual predecessor      |
| `missing-review-package` | an `approved`/`merged` record with no `reviewPackageId`      |
| `empty-change-summary`   | a blank `changeSummary`                                      |

That an `approved` or `merged` version _must_ carry a review-package link is the
audit-linkage guarantee made enforceable, not aspirational.

### Visual diff

`buildVersionDiff` produces a `VersionDiff` whose entries are typed by **what
changed**, matching the source's "visual diff for prose, structured blocks,
citations, source bindings, assets, metadata, and translations":
`VersionDiffEntry.kind` is one of
`prose | structured-block | citation | source-binding | asset | metadata | translation`.
It diffs two field maps, skips unchanged paths, and labels each change by the
supplied `kindByPath` (defaulting to `metadata`).

### Branch, propose-change, request-review, and merge

`branching.ts` models the change request as its own state machine.
`CHANGE_REQUEST_STATES` =
`draft → review-requested → changes-requested → approved → merged | abandoned`,
with a strict `ALLOWED_CR_TRANSITIONS` adjacency. `transitionChangeRequest`
refuses bad moves with typed reasons:

- `illegal-transition` — not an allowed edge;
- `missing-reviewer-assignment` — moving to `review-requested` with no
  reviewers;
- `open-signoff-items` — approving while required checklist items remain (the
  count is supplied by the §16.8 collaboration layer's `evaluateReviewSignoff`);
- `conflicts-unresolved` — merging while `hasUnresolvedConflicts` is set.

A `ChangeRequest` also names its `mergeStrategy` —
`branch-takes-precedence | trunk-takes-precedence | three-way-merge` — and
`mergeChangeRequestVersion` mints the trunk version that the merge produces
(stamped `approvalState: 'merged'` with the change-request id as its
`reviewPackageId`). `projectTrunk` reconstructs the trunk as of a given version,
which is what a "request-changes / merge analogous to PR review" workflow needs.

### Rollback with cascade detection

Rollback is deliberately hard to do by accident. `buildRollbackCascade` throws
unless every precondition holds: a non-empty `artifactId`, a positive integer
`toVersion`, a non-empty `userVisibleNote`, and — critically — that the
operator-supplied `confirmedDownstreamArtifactIds` **exactly match** the
computed `downstreamArtifactIds` (same set, same size). Only then does it return
a `RollbackCascade` with `operatorConfirmedCascadeScope: true`. This is the
source's "operator-confirmed cascade scope" over "linked artifacts, citations,
derived courses, derived study plans": you cannot roll back without
acknowledging the exact blast radius. `rollbackWithChangeRequest` wraps the same
guarantee inside a change-request so the rollback is itself auditable, requiring
both an `initiatedByCreatorId` and a `changeRequestId`.

### Public-facing change notes

`buildPublicChangeNote` emits a `PublicChangeNote` with a `summary`, a `reason`,
and a `visibility` of `public | subscriber | tenant` — the _"updated on …
because …"_ transparency note the source requires where policy demands it, with
linkage back to the relevant correction or evidence revision.

---

## How these connect to the rest of V1

- **The editorial lifecycle** (§16.3, `editorial-lifecycle/`) is the upstream
  state machine these three disciplines support; its 12-state model
  (`idea → … → published → updated → deprecated → sunset → archived → takedown`)
  and the `nextOccurrences` recurrence engine — which literally encodes _daily
  Veritas briefings, daily Tara passages, weekly Arete reflections, nightly Nyx
  highlights_ as cadence logic — are detailed in
  [Editorial Calendar and Asset & Media Library](./editorial-and-asset-library.md).
  A small caveat worth recording: the `EDITORIAL_LIFECYCLE_STATES` array has
  **no `retracted` and no `rejected` member** (the canonical terminal/feedback
  names are `takedown` and `changes-requested`); an older mermaid diagram in
  `architecture/oshun-studio.md` uses the non-existent `retracted`/`rejected`
  names, so trust the code names quoted here.
- **The §32 agentic content pipeline** is where many artifacts _originate_
  before they enter taxonomy/localization/versioning. The deployable
  `@oshun/content-service` (`apps/oshun/content-service`, project
  `@oshun/content-service-app`) boots `createContentHttpServer` over an
  Iris-routed writer and a three-member judge panel, and **fails loud** without
  an `ANTHROPIC_API_KEY` (`NotConfiguredError`). Its router (`http-router.ts`)
  exposes `POST /v1/content/briefs`, `GET /v1/content/runs`,
  `GET /v1/content/runs/:id`, `POST /v1/content/runs/:id/replay`, and the
  operator views `GET /v1/operator/runs[/:id]`. The generated, gated artifact is
  what a curator tags, a translator localizes, and the versioning layer tracks —
  see [Isis Generation Control](./isis-generation-control.md) and
  [Sophia Grounding](./sophia-grounding.md) for the generate→gate handoff.

## Honest scope notes

- The three modules are **pure functions over typed records**; persistence
  (Postgres) and asset storage (MinIO/S3) are real but live at the application
  and infrastructure layer, not inside `@oshun/studio-authoring`.
- `buildTaxonomyChangePreview` computes impact from **operator-supplied maps**;
  it does not query a live recommendation or search index.
- Real-time co-editing and the customer-facing rendering of stale banners depend
  on running substrates documented elsewhere; the library supplies the logic and
  the contracts, and reports absence honestly rather than faking success.

## Related

- [Collaboration, Review, and Templates](./collaboration-and-templates.md) — the
  §16.8/§16.9 sibling that owns review signoff and reusable templates
- [Editorial Calendar and Asset & Media Library](./editorial-and-asset-library.md)
  — the editorial lifecycle and asset metadata these disciplines support
- [Creator Roles and the Authoring Workspace](./authoring-workspace-and-roles.md)
  — the roles and the workspace that consume this library
- [Search, Discovery, Recommendations, and Knowledge Graph](./search-discovery-recommendations.md)
  — what the curated concept graph feeds
- [Content, Localization, Documentation, Launch, and Exit Criteria](./content-localization-launch-exit.md)
  — the launch-readiness gates the locale scorecards inform
- [Isis Generation Control](./isis-generation-control.md) and
  [Sophia Grounding](./sophia-grounding.md) — the §32 generate→gate pipeline
  upstream of authoring
- [Product Surfaces](./product-surfaces.md) and the hub
  [../features.md](../features.md)
