# Veritas — Grounded Stories and Claims

> **Release: V1.2.** Veritas is not part of V1.0. Everything on this page stays
> in the tree and stays worth building — the room has to be stocked and reviewed
> before it opens — but no V1.0 console may advertise, link to, count, or
> compose it. V1.0 opens Tara, Nyx, Arete, and Nisaba; see
> [`V1/BRAND.md`](../BRAND.md).

Veritas is the grounded story, claim, evidence, source, and explainer domain of
Oshun V1 — the place a reader meets the news and the empirical record with the
_sourcing_ attached: every claim carries a confidence band, every source carries
a quality band and a reviewer attestation, every competing view is surfaced side
by side rather than silently hidden, and when a source later retracts, the
correction _cascades_ to every dependent surface the reader ever saw. It serves
the reader who wants to know not just _what_ is claimed but _how strongly it is
supported and by whom_. Veritas sits inside the single Oshun shell alongside the
contemplative domain
([Tara — Rituals and Contemplative Practice](./domain-tara.md)), the goals
domain ([Arete — Goals, Habits, and Reflection](./domain-arete.md)), the
scholarly-study domain ([Nisaba — Scholarly Study](./domain-nisaba.md)), the
education domain ([Metis — Education and Tutoring](./domain-metis.md)), and the
assistant.

This page is a product-and-code reading of Veritas. The canonical domain logic
is exceptionally code-complete — the source-quality composite, the editorial
state machine, the attestor workflow, the counterclaim balance rules, the
topic-hub composer, the retraction-cascade UX, and the contradiction probe are
all real, deterministic, and contract-enforced. What remains aspirational is
called out honestly throughout: per-tenant composite tightening, weekly drift
re-computation, credential round-trip proof artifacts, reader quiet-hours, and
the _fully wired_ end-to-end retraction journey (the ports exist; the live
fan-out is at the deployable boundary). The completion state of every item lives
in [`../TODOS.md`](../TODOS.md); cross-domain build ordering lives in
[`../DEPENDENCIES.md`](../DEPENDENCIES.md).

---

## Two libraries, two adapters, ~64 cousins — read this first

Veritas is one domain name attached to several distinct code locations, and
mixing them up is the single biggest source of confusion. The canonical V1
domain is **`@oshun/domain-veritas`** at `libs/oshun/domain-veritas`. Its
event-bus companion is **`@oshun/veritas-cascade-worker`** at
`libs/oshun/veritas-cascade-worker`. The canonical contracts live at
`libs/contracts/src/veritas` and are imported as `@oshun/contracts/veritas`
through the workspace path map.

There is also a separate, much larger collection at **`libs/veritas/`** — about
**66** sub-libraries (`agents-fact-checking`, `agents-journalism`,
`bias-detection`, `claims`, `knowledge-graph`, `headline-service`, `nlp-core`,
`rag`, `recommendations`, and so on). That collection is _not_ the canonical V1
Veritas domain; it is a distinct body of fact-checking and journalism tooling.
When this page says "Veritas," it means `@oshun/domain-veritas` unless a
`libs/veritas/*` path is written out explicitly.

> A wiring caveat worth recording: `libs/contracts/veritas/` (no `src`) contains
> **only a `.gitkeep` file** — no `package.json`, no exports. It is _not_ a
> standalone published package. Veritas types are imported as
> `@oshun/contracts/veritas`, which resolves to `libs/contracts/src/veritas`
> through the path map. Earlier docs described a "standalone
> `libs/contracts/veritas` package for consumers"; that package does not exist.

### The two-tier adapter architecture

`@oshun/domain-veritas` ships **two** models side by side, and the docs have
historically described only one of them.

1. **The presentational article-feed adapter** (`types.ts`). This is the
   reader-facing news app surface: a `VeritasApiAdapter` with
   `getTrendingArticles`, `getArticleBrief`, `getContinueReading`,
   `getTopClaims`, `getClaimDetail`, `getTrendingTopics`, `getCategories`,
   `getSavedArticles`, `saveArticle` / `unsaveArticle`, `getFollowedTopics`,
   `followTopic` / `unfollowTopic`, and `getHealth`. Its vocabulary is
   presentational: `VeritasVerdict` (8 values — `verified`, `likely_true`,
   `disputed`, `misleading`, `mostly_false`, `false`, `unverifiable`,
   `unverified`), `VeritasCredibilityTier` (`high` · `medium` · `low` ·
   `unknown`), `VeritasClaimType` (7 values — `factual`, `opinion`,
   `prediction`, `quote`, `statistical`, `historical`, `scientific`), and
   `VeritasArticleContentType` / `VeritasFeedRecommendationReason`.

2. **The canonical editorial model** (the contracts in
   `@oshun/contracts/veritas` plus the editorial/source-quality/attestor/
   counterclaim/topic-hub/retraction/contradiction modules). This is the
   editor-facing trust machinery the rest of this page documents.

The two are intentionally distinct. The presentational `VeritasVerdict` /
`VeritasCredibilityTier` are _display_ vocabularies for a feed; the canonical
`ClaimConfidenceBand` / `SourceQualityBand` are the _governance_ vocabularies a
reviewer attests against. Do not conflate them.

### The shell read-adapter RBAC surface

A third, frequently missed surface is the read-adapter role model in
`adapter.ts`. `VERITAS_ADAPTER_READ_CAPABILITIES` enumerates **ten** read
capabilities — `contract_descriptor`, `metadata`, `availability`, `home_cards`,
`continue_items`, `search`, `launch`, `saved_articles`, `trending_topics`,
`bridge_contexts` — and `VERITAS_ADAPTER_ROLE_CAPABILITIES` gates them per role:

| Role        | Capabilities granted                                                                                                                                                 |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shell`     | all ten (`VERITAS_ADAPTER_READ_CAPABILITIES`)                                                                                                                        |
| `admin`     | `contract_descriptor`, `metadata`, `availability`, `continue_items`, `search`, `saved_articles`, `trending_topics` (no `home_cards`, `launch`, or `bridge_contexts`) |
| `assistant` | `contract_descriptor`, `metadata`, `availability`, `continue_items`, `search`, `launch`, `bridge_contexts` (no `home_cards`, `saved_articles`, or `trending_topics`) |

`createVeritasReadAdapterRegistry` returns `{ shell, admin, assistant }` typed
views over the same canonical adapter, and
`getVeritasAdapterReadCapabilities(role)` returns the capability list for a role
(or the full ten when no role is given). The admin view deliberately drops
`home_cards` and `launch` (an operator does not need the reader home rail); the
assistant view drops the personalized `saved_articles` / `trending_topics` and
keeps `launch` and `bridge_contexts` so it can deep-link into the domain. This
is a real RBAC seam, not decoration.

---

## Where Veritas lives in the product

### Consumer surfaces

The consumer hub is `/veritas`, with presentational depth rendered by the
broadsheet-style `VeritasRoom`. The real `apps/oshun/web/src/app/veritas/` route
tree contains **nine** subroutes:

- `claim` — claim detail with confidence band and supporting/counter sources
- `counterclaims` — the side-by-side competing-view surface
- `evidence` — evidence-pack inspection (the inspect-evidence affordance)
- `mobile` — the mobile briefing / explainer surface
- `provenance` — source-provenance and lineage inspection
- `retraction` — the retraction-banner and correction-notice surface
- `source` — source inspection (kind, publisher, license, quality factors)
- `story` — the broadsheet story page with the evidence sidebar
- `topic` — the topic-hub reader surface

> The original feature list at `V1/features.md` omitted `/veritas/evidence`,
> which exists as a real route directory and is the inspect-evidence surface
> distinct from `provenance` (source-level) and `source` (single-source).

### Power-user deep tools

The `apps/oshun/web/src/app/domains/veritas/` tree carries the journalism /
fact-checking deep tools that compose against the `libs/veritas/*` collection:
`articles`, `bias`, `claims` (plus `claims/[claimId]`), `fact-check`,
`headlines`, `knowledge-graph`, `newsletter`, `nlp`, `research`, `story` (plus
`story/[id]`), and `topics` (plus `topics/[topicId]`).

### What V1 Veritas includes

- Canonical models for stories, claims, sources, evidence, topics, timelines,
  counterclaims, unsupported claims, and trust/confidence display.
- Home and mobile briefing modules.
- Web story pages with evidence sidebars, claim detail pages, source inspection,
  counterclaims, topic hubs, and timelines.
- Mobile explainers and follow-up flows.
- Save/follow controls for stories, claims, and topics.
- Inspect-source and inspect-evidence interactions with Sophia-backed evidence
  bundles (see [Sophia Grounding](./sophia-grounding.md)).
- Cross-domain links to Nisaba source lineage, Tara "metabolize this
  information" practices, Arete next actions, and assistant explainers that
  expose source inspection.
- Evaluation coverage for grounded explainer correctness, unsupported claims,
  citation integrity, trust/confidence display, story-to-source navigation, and
  web/mobile evidence inspection.

---

## The canonical contract vocabulary

The schemas in `@oshun/contracts/veritas` (`libs/contracts/src/veritas/index.ts`
plus `attestor.ts`) are the load-bearing vocabulary. They are Zod schemas with
`superRefine` invariants, so they are _enforced_ at parse time, not just typed.

### Source, claim, and evidence enums

| Schema                             | Values                                                                                                                                                                                     |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `SourceKindSchema`                 | **15** kinds: `peer-review`, `primary`, `secondary`, `press-release`, `opinion`, `social`, `government`, `ngo`, `wire`, `dataset`, `court-record`, `transcript`, `image`, `video`, `audio` |
| `SourceQualityBandSchema`          | `high` · `mixed` · `low` · `contested`                                                                                                                                                     |
| `ClaimConfidenceBandSchema`        | `well-supported` · `supported` · `emerging` · `contested` · `unsupported` · `retracted`                                                                                                    |
| `ClaimClassSchema`                 | **9** classes: `factual`, `statistical`, `quote`, `causal`, `prediction`, `interpretation`, `comparison`, `legal`, `scientific`                                                            |
| `RetractionStateSchema`            | `active` · `under-review` · `retracted` · `superseded`                                                                                                                                     |
| `EvidenceStanceSchema`             | `supports` · `refutes` · `contextualizes` · `contested` · `neutral`                                                                                                                        |
| `EvidenceLocatorGranularitySchema` | **8** granularities: `document`, `section`, `paragraph`, `sentence`, `table`, `figure`, `dataset-row`, `timestamp`                                                                         |

> The earlier feature list (`V1/features.md`) presented the source `type` enum
> as the 8 values
> `peer-review/primary/secondary/press-release/opinion/social/government/NGO`.
> The real `SourceKindSchema` is a **superset of 15** — it also includes `wire`,
> `dataset`, `court-record`, `transcript`, `image`, `video`, `audio`. The extra
> kinds matter: the composite scorer infers peer-review status and primacy
> differently for each (a `court-record` is a primary `institutional-report`; an
> unauthored `image` is `unattributed`).

The richer enums are not cosmetic. `EvidenceStance` includes `contextualizes`
(not every piece of evidence supports or refutes — some merely _frames_);
`RetractionState` distinguishes `under-review` (flagged but not yet pulled) from
`superseded` (replaced by a corrected version) from outright `retracted`; and
`EvidenceLocatorGranularity` goes all the way down to a `timestamp` (for an
audio/video citation) so a claim can cite the exact moment, not just "the
recording."

### The three lifecycle vocabularies (and why they differ)

This is a genuine source of confusion in the docs, so it is worth naming
plainly: there are **three** distinct editorial-state vocabularies in play for
what is loosely "the story lifecycle."

1. **`StoryEditorialStateSchema`** (`contracts/src/veritas/index.ts:79-87`) —
   the _story record_ state stored on a `Story`: `draft`, `review`, `published`,
   `updated`, `corrected`, `retracted`, `archived`. It has **no verification
   states at all** — it is the coarse persisted state.
2. **`VeritasEditorialState`** (`editorial/state-machine.ts:1-13`) — the
   _runtime workflow_ state the editorial state machine drives, in
   **kebab-case**: `draft`, `in-review`, `verifying-sources`,
   `awaiting-attestation`, `contradicts-existing`, `counterclaim-pending`,
   `approved`, `scheduled`, `published`, `archived`, `corrected`, `retracted`.
3. **The prose / diagrams** in `V1/features.md` and `V1/ARCHITECTURE.md`, which
   historically used **snake_case** names (`verifying_sources`,
   `awaiting_attestation`, …) that match _neither_ of the code vocabularies.

The implemented machine is the kebab-case `VeritasEditorialState`. There is **no
snake_case state in the code.** Where this page documents the state machine, the
kebab-case identifiers are authoritative; the snake_case in older diagrams is
illustrative spec, not the implemented graph.

### Schema invariants worth knowing

The `superRefine` blocks encode editorial policy directly in the contract. A
selection:

- **Sources**: a retracted source must carry both `retractedAt` and a `reason`;
  a `peer-review` source cannot leave `peerReviewStatus` as `unknown`; an
  `archiveUrl` requires a canonical `url`.
- **Claims**: a `well-supported` claim requires **≥ 2** supporting sources; an
  `unsupported` claim **cannot** carry supporting sources; a `contested` claim
  requires ≥ 1 counter-source; and a `retracted` `retractionState` forces the
  `confidenceBand` to `retracted`.
- **Evidence packs**: every evidence item's `claimId` / `sourceId` must be
  declared in the pack's `claimIds` / `sourceIds`; `reviewed`/`published` packs
  require `reviewedAt`; incomplete `grounding` requires a `missingReason`.
- **Correction notes**: `priorPublicVersion.claimText` must equal `priorText`;
  `visibleAt` must be after the prior version's `publishedAt`; reader
  notifications must target `readers-who-saw-prior-version`.
- **Stories**: a `published` story requires an `evidencePackId`; a `corrected`
  story requires correction notes; a `retracted` story requires a
  `retractionCascadeId`.

These are real gates — a story cannot be persisted as `published` without an
evidence pack, full stop.

---

## Source-quality scoring algorithm

`SourceQualityBand` is the user-visible label
(`high · mixed · low · contested`); the underlying score is a **deterministic
composite** computed by `computeSourceQualityComposite` in
`source-quality/composite.ts`. Every source carries the full
`SourceQualityScoreVector` so editorial decisions, reviewer attestations, and
retraction cascades can reconstruct _why_ a source landed where it did.

### The nine factors

The scorer takes nine factors. Eight are ratings; the ninth is a **count**, not
a rating:

1. `peerReviewStatus` — `peer-reviewed` (1.0) / `preprint` (0.6) /
   `institutional-report` (0.65) / `journalism` (0.55) / `press-release` (0.35)
   / `opinion` (0.25) / `social` (0.15) / `unattributed` (0.1)
2. `primacy` — `primary` (1.0) / `secondary` (0.65) / `tertiary` (0.4)
3. `editorialStandards` — `FactorRating`
4. `retractionHistory` — `FactorRating`
5. `expertiseMatch` — `FactorRating`
6. `recency` — `FactorRating`
7. `rightsClarity` — `FactorRating`
8. `independence` — `FactorRating` (disclosed conflicts / funder declarations)
9. `crossCorroboration` — a **count** of independent peer sources, mapped
   through `corroborationFactor`: `0 → 0.2`, `1 → 0.45`, `2 → 0.65`, `3 → 0.8`,
   `4 → 0.9`, `≥5 → 1.0`

`FactorRating` is `positive` (1.0) / `mixed` (0.6) / `unknown` (0.4) /
`negative` (0.15). Every normalized factor is clamped to a **floor of
`FACTOR_FLOOR = 0.05`** — no factor can be exactly zero, because the composite
is a _geometric_ mean and a single zero would annihilate it.

### Weighted geometric mean

The composite is a **weighted geometric mean**, computed in log space:

```
contribution_i = weight_i · ln(normalized_i)
logMean        = Σ contribution_i / Σ weight_i
geometric      = exp(logMean)            // in [0, 1]
composite      = round(geometric · 1000) / 10   // mapped to [0, 100], one decimal
```

Geometric (not arithmetic) is the deliberate choice: it means **one terrible
factor cannot be silently averaged away** by several good ones. A source with
pristine recency, rights, and editorial standards but a `negative` (0.15)
retraction history is dragged down hard, exactly as an editor would want.

### Per-domain weight tables

Weights are published **per claim domain** in
`source-quality/domain-weights.ts`. The platform default is `general@1.0.0`;
specialist tables exist for `medical`, `scientific`, `legal`, `statistical`,
`journalism`, `prediction`, `quote`, `causal`, `interpretation`, `comparison`,
and `factual`. Each table is normalized to sum to 1.0 by `buildWeights`. The
differences are domain-true:

- **Medical** weights `peerReviewStatus` (0.22) and `crossCorroboration` (0.14)
  highest and de-emphasizes `recency` (0.05) — randomized-trial evidence holds
  value across years.
- **Legal** weights `primacy` (0.28, court records and statutes) highest and
  `peerReviewStatus` (0.04) almost to nothing — legal scholarship is rarely
  peer-reviewed in the medical sense.
- **Journalism** weights `editorialStandards` (0.22) and `retractionHistory`
  (0.14) highest.
- **Prediction** weights `expertiseMatch` and `recency` (0.20 each) — forecasts
  decay quickly without recent calibration.

`resolveDomainWeightTable` resolves by explicit `claimDomain`, else by
`claimClass`, else falls back to `general`; `rollbackDomainWeightTable` refuses
to roll back past an initial release (no `rollbackTargetVersion`).

### Banding and the hard overrides

`bandFromComposite` applies deterministic thresholds, then three **hard
overrides** that can move a band regardless of the numeric composite:

| Composite | Base band |
| --------- | --------- |
| `≥ 82`    | `high`    |
| `≥ 60`    | `mixed`   |
| `≥ 35`    | `low`     |
| `< 35`    | `low`     |

(The thresholds are also exported as `DEFAULT_BANDING_THRESHOLDS`:
`highMin: 82`, `mixedMin: 60`, `lowMin: 35`.)

Then, in order:

1. **`unattributed-capped-low`** — if `peerReviewStatus === 'unattributed'` and
   the base band is `high` or `mixed`, it is forced down to `low`.
2. **`recent-retraction-drops-one-band`** — if the source publisher issued a
   retraction within the last **12 months** (computed in `differenceInMonths` /
   `recentRetraction`), the band drops one tier (`high→mixed→low→contested`).
3. **`retracted-forces-contested`** — if the source itself is currently
   retracted, the band is forced to `contested` outright.

Each applied override is recorded in `hardOverridesApplied` on the score vector,
and softer score-shaping notes (`peer-review-status-unattributed`,
`no-cross-corroboration`, `reviewer-attestation-missing`,
`recent-retraction-within-12-months`) are recorded in `reasons`. This is the
audit trail that lets a reviewer reconstruct the band.

> **Aspirational, not yet grounded here**: the prose describes _drift
> re-computation on a weekly cadence_ and _per-tenant composite tightening_
> (e.g., a research-lab tenant requiring a `mixed` floor). The composite is
> deterministic and re-runs on demand, and a `tenant-floor.ts` module exists,
> but the _scheduled_ weekly drift sweep is not wired in this code — treat it as
> planned.

---

## Expert-attestation workflow

Sources, claims, and stories with empirical weight require **named** expert
attestation; anonymous "trust us" attestation is forbidden. The contract is
`ExpertAttestorSchema` and `AttestationSchema` (`attestor.ts`), and the workflow
modules live in `src/attestor/` (`audit`, `conflict`, `credential-verification`,
`disagreement`, `expiry`, `probation`, `revocation`, plus `file-audit`).

### The attestor record

`ExpertAttestorSchema` carries `id`, `displayName`, a `credentialsBundle` (array
of `AttestorCredential`), `declaredExpertiseDomains` (≥ 1, ≤ 20), `conflicts[]`,
a nullable `institutionalAffiliation`, an `attestationHistory[]` (up to 2000
lifecycle entries), a `suspensionState`, a nullable `probationUntil`,
`onboardedAt`, `lastReviewedAt`, and an `operatorReviewerId`.

Invariants encoded directly in the schema:

- an attestor in `probation` **must** have a `probationUntil` deadline (and only
  a probation attestor may set one);
- an `active` attestor **requires** an `operatorReviewerId` of record —
  onboarding is operator-reviewed, not self-serve;
- `credentialsBundle` ids must be unique.

`AttestorSuspensionStateSchema` is `active` · `probation` · `suspended` ·
`revoked`. `AttestorCredentialKindSchema` enumerates the proof kinds: `degree`,
`professional-license`, `institutional-email`, `peer-attestation`,
`institutional-affiliation`, `publication-record`, each with a status of
`pending` / `verified` / `rejected` / `expired` (a `verified` credential must
record `verifiedBy`; a `rejected` one must record a `rejectionReason`).

### Attestation scope, cadence, conflicts

`AttestationScopeKindSchema` is `source` · `claim` · `counterclaim`: an attestor
attests that a _full source_ is reliable, that a _specific claim from a source_
is well-supported, or that a _competing view_ is well-supported by an
alternative source. Each `Attestation` carries `issuedAt` / `validUntil`
(validity window — `validUntil` must be after `issuedAt`), a nullable
`renewedFromId`, a status (`pending-review`, `admitted`, `soft-degraded`,
`withdrawn`, `revoked`, `contested`), `conflictsAtIssuance`, a nullable
`bandImpact`, and a `requiresReviewerBoard` flag.

The conflict vocabulary (`AttestorConflictKindSchema`) is `funder`, `employer`,
`family`, `financial`, `ideological-organization`, `undisclosed-financial`,
`professional-rivalry`, `other`. A conflict incompatible with the claim domain
auto-flags the attestation: `requiresReviewerBoard` is set, and the schema
**refuses to admit** (`status: 'admitted'`) any attestation still requiring
board sign-off. Expired attestations soft-degrade the source band one tier until
re-attested (the `expiry` module); revocation (`revocation`) re-bands and fires
a retraction-cascade pass; disagreement between two attestors (`disagreement`)
records both, surfaces the claim as a contested band, and feeds the editorial
state machine a contradiction event. Every lifecycle event
(`issued`/`renewed`/`withdrawn`/`expired`/`overridden`/`rebanded`/
`flagged-for-review`) appends to the `attestationHistory` audit trail.

> **Aspirational**: the _credential round-trip proof artifacts_ (e.g., an
> automated institutional-email round-trip or a public-record cross-check) are
> represented in the schema (`AttestorCredential.artifactUri`, the
> `institutional-email` credential kind) but the _live verification_ of those
> artifacts is an operator-review step, not an automated runtime in this code.

---

## Counterclaim-balance rules

A counterclaim is a _competing view supported by its own sources_, not a denial.
Veritas surfaces counterclaims when the evidence warrants — never gratuitously,
never to manufacture false balance. The logic is `evaluateCounterclaimSurfacing`
in `counterclaim/surface.ts`, and the decision is a **four-way union**:

```
type CounterclaimSurfacingDecision =
  | { surface: 'co-equal';             reasons: string[] }
  | { surface: 'minority-view-expand'; reasons: string[] }
  | { surface: 'mandatory-surface';    reasons: string[] }
  | { surface: 'suppress';             reasons: string[] }
```

### The default policy

`DEFAULT_SURFACING_POLICY` (`surface.ts:50-62`):

| Field                | Value                                                                                                              |
| -------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `tenantBandFloor`    | `mixed`                                                                                                            |
| `consensusThreshold` | `0.85`                                                                                                             |
| `recencyWindowDays`  | `365`                                                                                                              |
| `publicSafetyTopics` | `public-health`, `product-safety`, `biosecurity`, `aviation-safety`, `nuclear-safety`, `natural-disaster-response` |

### How the decision is made

The evaluator computes four surfacing conditions: the counterclaim's `bandFloor`
is at or above the tenant floor (`isBandAtOrAbove`); its strongest source is
**independent** of the primary's (no shared author, no shared funder, not the
same source id — `isIndependent`); the disagreement is **substantive**
(`isSubstantive`, classifier-assisted); and it is **contemporary** (within the
recency window of _both_ the evaluation time and the primary publication —
`withinRecencyWindow`).

The decision order is deliberate:

1. **Mandatory-surface overrides everything.** `mandatorySurfacingReasons` fires
   on a retraction-class disagreement, a `pluralism` editorial stance, or a
   public-safety topic (either the primary's `editorialCategory` is
   `public-safety`, or a `topicTag` is in `publicSafetyTopics`). When any of
   these holds, the counterclaim surfaces regardless of band or recency.
2. **Suppress** if band-floor, independence, or substantiveness fails — or if it
   is not contemporary.
3. **Minority-view-expand (the false-balance brake)** —
   `shouldApplyFalseBalanceBrake` fires when the primary band is `high`, the
   counter band is `low`, _and_ the `consensusStrength` is at or above the
   `consensusThreshold` (0.85). Instead of co-equal presentation, the reader
   gets a one-click "minority view exists" affordance. **This is the rule that
   prevents Veritas from manufacturing balance on settled questions.**
4. **Co-equal** only when all conditions pass and the brake does not engage.

Every decision returns the rule(s) applied in `reasons` (e.g.
`false-balance-brake-engaged`, `mandatory-public-safety-topic`,
`counter-below-band-floor`), so the surfacing decision is fully auditable
(`counterclaim/audit.ts`, `file-audit.ts`). Per-reader preferences ("always show
counterclaims" / opt out of minority-view expands) live in
`counterclaim/reader-preferences.ts`.

---

## Topic-hub composition algorithm

A topic hub organizes stories, claims, sources, timelines, and counterclaims
around a topic anchor. `composeTopicHub` in `topic-hub/composer.ts` builds a hub
from candidate sets across **six sections**, in this order
(`TOPIC_HUB_SECTIONS`):

`latest` · `key-facts` · `under-dispute` · `corrections` · `background` ·
`timeline`

### Ranking

`DEFAULT_RANKING_WEIGHTS` (`composer.ts:94-100`):

| Weight        | Value  |
| ------------- | ------ |
| `freshness`   | `0.30` |
| `proximity`   | `0.25` |
| `bandQuality` | `0.20` |
| `engagement`  | `0.15` |
| `attestation` | `0.10` |

Each section ranks its own candidate kind with a section-appropriate score:

- **`latest`** ranks `published`/`updated` stories by freshness × proximity ×
  engagement × attestation. Freshness uses an exponential decay
  `exp(-ageDays / 14)` — a two-week half-life-ish curve.
- **`key-facts`** filters to claims at the `high` source-quality floor with a
  `well-supported`/`supported` confidence band, scored on band quality,
  attestation, proximity, and half-weighted freshness.
- **`under-dispute`** filters to `contested`/`emerging` claims.
- **`corrections`** ranks by severity weight (`notification-included` 3,
  `banner-only` 2, `silent` 1) plus recency.
- **`background`** ranks linked `nisaba-explainer` / `metis-lesson` /
  `veritas-explainer` items by proximity plus half-weighted freshness.
- **`timeline`** ranks events by significance weight (`high` 3, `medium` 2,
  `low` 1) plus recency.

Ranking is **deterministic**: ties break by `id.localeCompare`, so the same
candidate set always produces the same ordering — exactly what the determinism
test asserts. Editorial gating (`topic-hub/editorial-gating.ts`) ensures nothing
surfaces automatically — every proposed candidate passes through the editorial
state machine first, and operators can pin, demote, or block with rationale.
Refresh is event-triggered (`topic-hub/refresh.ts`).

> **Aspirational**: _localized hub variants_ (per-locale `latest`/`background`
> ordering) are represented by `topic-hub/locale-variant.ts` and the `locale`
> field threaded through composition, but full localized parity is planned, not
> shipped.

---

## Story update, correction, and retraction

`StoryUpdateKindSchema` enumerates the update events: `new-evidence`,
`retracted-source`, `contested-claim`, `new-counterclaim`, `corrected-claim` —
each captured with `timestamp`, `rationale`, and `reviewerId`, and with
kind-specific invariants (a `corrected-claim` requires a `correctionNoteId`; a
`retracted-source` requires a `retractionCascadeId`; etc.).

When a prior public claim changes, a `CorrectionNote` surfaces the change to the
right audience: `CorrectionReaderAudienceSchema` is
`readers-who-saw-prior-version` or `all-current-readers`, and the schema
enforces that reader _notifications_ target only those who saw the prior
version, with a `seenPriorVersionCutoffAt` cutoff. Story-update notifications
are per-user, per-tenant, per-followed-topic (`story-update-notifications.ts`,
`story-update-notification-dispatch.ts`).

### The retraction-cascade contract

When a source retracts, every dependent surface re-grounds. The plan is the
`RetractionCascade` contract (`RetractionCascadeSchema`,
`contracts/src/veritas/index.ts:549-767`). It tracks, separately:

- `affectedStoryIds`, `affectedClaimIds`, `affectedExplainerIds`,
  `affectedEvidencePackIds` — the in-Veritas fan-out;
- `downstreamMetisPackageIds` **and** `downstreamMetisLessonIds` — the
  cross-domain fan-out into [Metis](./domain-metis.md).

> Older prose said only "downstream Metis lessons re-validated." The schema
> tracks **two** Metis fan-outs: _package_ ids and _lesson_ ids. The
> `reGroundingJobs` carry an `action` enum
> (`RetractionCascadeJobActionSchema = 're-ground' | 'metis-revalidate'`), and a
> `superRefine` **forces** every `metis-lesson` job to `metis-revalidate` and
> every non-Metis job to `re-ground`. The schema also requires a re-grounding
> job for _every_ affected target (a missing job target is a validation error),
> requires `completedAt` on completed jobs and `failureReason` on failed jobs,
> and requires that a `completed` cascade have every job completed and (if a
> user notice was required) the notice surfaced.

### The execution / event seam

The planner is pure; the **executor** makes the cascade durable and dispatches
it. `executeVeritasSourceRetractionCascade` (`retraction-cascade-execution.ts`)
does **plan → persist → publish**, in that deliberate order: the cascade is
persisted _before_ the dispatch event is published, so a crash between the two
never emits a dispatch for a non-durable cascade. The pairing is at-least-once —
a recovery sweep can safely re-publish a persisted-but-unpublished cascade
because consumers key off the deterministic `cascadeId` / per-job `jobId`.

The dispatch event constant is
**`VERITAS_RETRACTION_CASCADE_DISPATCHED_EVENT = 'veritas.retraction.cascade.dispatched'`**
(`retraction-cascade-execution.ts:37`). The library stays infra-free: it depends
only on small injected ports (`RetractionCascadePersistencePort`,
`RetractionCascadeEventPort`), and the app boundary wires the real
`ContractPersistenceService` and `IEventBus`.

### The cascade worker

`@oshun/veritas-cascade-worker` is the event-bus composition root.
`subscribeRetractionCascadeWorker` subscribes an `IEventBus` (from
`@oshun/event-bus`) to the dispatch topic and routes each event's per-target
jobs through `consumeRetractionCascadeDispatch`. The downstream re-grounders are
real and **persistence-backed**:

- `createPersistenceBackedSophiaReGrounder` loads a grounded answer and its
  _current_ (post-retraction) source texts from the `DurableSnapshotStore`
  (`@oshun/persistence`), re-scores groundedness with the real
  `createSophiaReGrounder` from `@sophia/evaluation`, and records the verdict
  back. Store keys:
  `SOPHIA_GROUNDED_ANSWER_STORE_KEY = 'sophia-grounded-answer'`,
  `SOPHIA_REGROUNDING_VERDICT_STORE_KEY = 'sophia-regrounding-verdict'`.
- `createPersistenceBackedMetisRevalidator` does the same shape for Metis
  lessons via `createMetisLessonRevalidator` from `@metis/research`. Store keys:
  `METIS_LESSON_SOURCES_STORE_KEY = 'metis-lesson-sources'`,
  `METIS_REVALIDATION_VERDICT_STORE_KEY = 'metis-revalidation-verdict'`.

> **Honest end-to-end status**: the _ports and persistence-backed re-grounders
> exist and are unit/integration-tested_, but the triage/audit notes record the
> customer-facing retraction-cascade _journey_ (full fan-out, notebook and
> living-offering surfaces, per-user gating) as only **partial** end-to-end in
> e2e. The "downstream Metis lessons re-validated" runtime is wired at the
> deployable boundary, not exercised end-to-end here.

### Retraction-cascade UX

What the _customer_ experiences is built in `retraction-ux/banner.ts`. There are
four surface kinds (`RetractionSurfaceKind`): `story-page`, `saved-claim`,
`notebook`, `living-offering`. Severity is computed by `severityFromBandChange`
into three `RetractionSeverity` levels:

| Severity                | When                                              |
| ----------------------- | ------------------------------------------------- |
| `notification-included` | source retracted, **or** a band drop of ≥ 2 tiers |
| `banner-only`           | a band drop of exactly 1 tier                     |
| `silent`                | no band drop (copy edit)                          |

Per-surface builders attach the right affordances: `buildStoryPageBanner` and
`buildSavedClaimBanner` offer `remove-from-library`, `view-corrected-source`,
and `appeal-or-correct`; `buildNotebookNotice` offers `open-citation-drill-down`
instead of remove-from-library; `buildLivingOfferingBanner` offers a
`re-render-offer`. The kept-Veritas-Living-Offering case (a pinned Sophia source
later corrected) surfaces a banner and offers re-render under the corrected
source — see
[Living Scenes — Concept and Customer Promise](./living-scenes-overview.md).

> **Aspirational**: reader-notification _quiet-hours_ integration is described
> in the prose but is not wired into the banner builders here — the severity
> gate is real, the quiet-hours routing is planned. `retraction-ux/recourse.ts`
> and `tenant-variant.ts` carry the appeal-routing and tenant-variant seams.

---

## Contradiction-detection loop with Sophia

Sophia surfaces semantic contradictions; Veritas treats them as editorial
events. The probe is `runContradictionProbe` in `contradiction/probe.ts`.

The trigger vocabulary (`ContradictionTrigger`) is `new-claim-published`,
`source-update`, `attestor-disagreement`, `operator-flag`, `cadence-sweep`,
`on-refresh`. The probe calls an injected `SophiaProbeClient`, which returns a
`SophiaEvidencePack` (from `@oshun/evidence-sophia`); each evidence item is
mapped to a `ContradictionPair` with a stance (`supporting` / `contradicting` /
`neutral`), a confidence, and a relevance. Pairs are sorted by confidence then
relevance, and **triaged** to the reviewer queue when
`stance === 'contradicting'` and `confidence >= triageThreshold`.
`DEFAULT_PROBE_OPTIONS.triageThreshold` is **`0.6`**. Reviewer routing
(`contradiction/reviewer-routing.ts`), continuous monitoring (`monitoring.ts`),
and per-tenant time-boxing (`tenant-timebox.ts`) build on the probe.
Contradictions surface to the queue rather than silently mutating any reader
surface — that is the design invariant.

---

## The Veritas editorial state machine

The generic editorial lifecycle (draft → in-review → approved → scheduled →
published → archived/retracted) is extended for the news domain by the
implemented `VeritasEditorialState` machine in `editorial/state-machine.ts`.
`attemptTransition` validates a transition against `TRANSITION_RULES`,
`InMemoryTransitionLog` records each `TransitionRecord`, and `replayHistory`
re-derives the current state from a record stream (throwing if the chain is
broken).

### The real transition graph

The implemented states are the **kebab-case** `VeritasEditorialState` union
(`draft`, `in-review`, `verifying-sources`, `awaiting-attestation`,
`contradicts-existing`, `counterclaim-pending`, `approved`, `scheduled`,
`published`, `archived`, `corrected`, `retracted`). The implemented edges
(`TRANSITION_RULES`, lines 58-141) are:

| From                   | Event                    | To                     | Gate(s)                    |
| ---------------------- | ------------------------ | ---------------------- | -------------------------- |
| `draft`                | `submit-for-review`      | `in-review`            | rationale                  |
| `in-review`            | `sources-ingested`       | `verifying-sources`    | sources                    |
| `verifying-sources`    | `sources-verified`       | `awaiting-attestation` | sources + sources-verified |
| `verifying-sources`    | `contradiction-flagged`  | `contradicts-existing` | contradictions             |
| `awaiting-attestation` | `attestation-collected`  | `approved`             | attestations               |
| `awaiting-attestation` | `contradiction-flagged`  | `contradicts-existing` | contradictions             |
| `awaiting-attestation` | `counterclaim-required`  | `counterclaim-pending` | rationale                  |
| `contradicts-existing` | `contradiction-resolved` | `awaiting-attestation` | contradictions resolved    |
| `counterclaim-pending` | `counterclaim-resolved`  | `approved`             | counterclaims              |
| `approved`             | `counterclaim-required`  | `counterclaim-pending` | rationale                  |
| `approved`             | `schedule`               | `scheduled`            | rationale                  |
| `approved`             | `publish`                | `published`            | rationale                  |
| `scheduled`            | `publish`                | `published`            | rationale                  |
| `published`            | `archive`                | `archived`             | rationale                  |
| `published`            | `issue-correction`       | `corrected`            | rationale + cascade-scope  |
| `published`            | `retract`                | `retracted`            | rationale + cascade-scope  |
| `corrected`            | `retract`                | `retracted`            | rationale + cascade-scope  |

> **Two corrections to older docs.** (1) There is **no direct
> `awaiting-attestation → published` edge**; the path goes
> `awaiting-attestation --attestation-collected--> approved`, then
> `approved --publish--> published` (or via `scheduled`). The "golden path" in
> older prose skipped `approved`. (2) The `ARCHITECTURE.md` mermaid that routes
> verification branches into `in_review` is illustrative spec — the implemented
> machine routes `verifying-sources` to `awaiting-attestation` and
> `awaiting-attestation` to `approved`. The diagram and the code diverge in both
> node set and edges; the code is authoritative.

### The gates

Every transition runs the three universal gates — `requireReviewer`,
`requireRationale`, `requireOccurredAt` — plus its rule-specific gates:
`requireSources`, `requireSourcesVerified`, `requireAttestations` (also blocks
until `openAttestationCount === 0`), `requireContradictions`,
`requireOpenContradictionsResolved`, `requireCounterclaims` (blocks until
`openCounterclaimCount === 0`), and `requireCascadeScope` (a correction or
retraction must declare its cascade scope). A failed gate returns
`{ ok: false, gateFailures: [...] }` with the specific reasons (e.g.
`attestation-references-required`, `contradictions-still-open`,
`cascade-scope-required`) and does **not** append a record. This is why
`awaiting-attestation`, `contradicts-existing`, and `counterclaim-pending` are
genuine blocking states: there is no edge out of them to publication that does
not first satisfy their gate.

---

## Tests and what they guarantee

The test suites verify _domain correctness_, not just data flow:

- **Source-quality composite determinism** (`composite.test.ts`): same factor
  vector → same composite → same band; per-domain weight tables honored; the
  three hard overrides fire on their exact conditions.
- **Expert attestation** (`attestor.test.ts`): credential-verification gating,
  conflict-flag auto-route to the reviewer board, disagreement surfaces as a
  contested band, revocation fires re-banding.
- **Counterclaim surfacing** (`counterclaim-balance.test.ts`): golden-path
  substantive disagreement surfaces co-equal; the false-balance brake suppresses
  to minority-view-expand; mandatory-surface overrides fire; reader-preference
  toggles honored.
- **Topic-hub composition** (`topic-hub.test.ts`): candidate ranking
  determinism, editorial gating enforced, locale variant parity.
- **Retraction-cascade UX** (`retraction-ux.test.ts`): every dependent surface
  receives the right banner severity; appeals route correctly.
- **Contradiction loop** (`contradiction.test.ts`, `execution.test.ts`):
  contradictions at/above 0.6 reach the queue; per-tenant time-box enforced.
- **Editorial state machine** (`state-machine` tests): every gate enforced;
  blocking states block publication until resolved; `replayHistory` reconstructs
  state.
- **Cascade execution and worker** (`retraction-cascade-execution.test.ts`,
  `veritas-cascade-worker.integration.test.ts`,
  `persistence-regrounders.integration.test.ts`): plan → persist → publish
  ordering, event subscription, and persistence-backed re-grounding over a real
  durable store.

---

## Related

- [Sophia Grounding](./sophia-grounding.md) — the evidence/contradiction backend
  the contradiction probe and re-grounders depend on
- [Metis — Education and Tutoring](./domain-metis.md) — the downstream domain
  whose packages and lessons the retraction cascade re-validates
- [Nisaba — Scholarly Study](./domain-nisaba.md) — source-lineage and study
  prompts Veritas cross-links to
- [Tara — Rituals and Contemplative Practice](./domain-tara.md) — the
  "metabolize this information" practices Veritas bridges into
- [Arete — Goals, Habits, and Reflection](./domain-arete.md) — the next-actions
  Veritas surfaces
- [Living Scenes — Concept and Customer Promise](./living-scenes-overview.md)
  and
  [Keep, Share, Shareability, Takedown, and Lineage](./scene-sharing-distribution.md)
  — the Grounded Explainer Living Offering and re-render-under-corrected-source
  flow
- [Search, Discovery, Recommendations, and Knowledge Graph](./search-discovery-recommendations.md)
  — the `libs/veritas/*` journalism and knowledge-graph deep tools
- [Review, Compliance, and Trust & Safety](./review-trust-safety.md) — the
  editorial review queue and audit surfaces
- [Customer Domains](../architecture/customer-domains.md) — the companion
  runtime view of Veritas's contracts and surfaces
- [Subsystem Glossary](./glossary.md) — shared vocabulary
- [`../features.md`](../features.md) — feature hub
