Oshun Platform · Features

Veritas — Grounded Stories and Claims

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

12sections23 minread7tables

On this page

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.

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), the goals domain (Arete — Goals, Habits, and Reflection), the scholarly-study domain (Nisaba — Scholarly Study), the education domain (Metis — Education and Tutoring), 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; cross-domain build ordering lives in ../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).
  • 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. peerReviewStatuspeer-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. primacyprimary (1.0) / secondary (0.65) / tertiary (0.4)
  3. editorialStandardsFactorRating
  4. retractionHistoryFactorRating
  5. expertiseMatchFactorRating
  6. recencyFactorRating
  7. rightsClarityFactorRating
  8. independenceFactorRating (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:

text
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:

text
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.

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.

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.