An official website of the Disclosure Foundation

Introduction

Quick StartWhat is DisclosureOS?Installation

Concepts

Tour of the APIThe Companion PatternType Safety & Validation

API

API Reference

Guides

Data MigrationField Mapping ReferenceSupabase IntegrationBuilding on the StandardContributing

Type Safety & Validation

Compile-time types, runtime validation, the extension point, and the strip hazard

DisclosureOS is built schema-first: every type is derived from a Zod schema, so the compile-time types and the runtime validators can never drift apart. This page explains the two halves and the one hazard you must understand.

Compile time: strict types and the extension point

All types are strict and literal. shape: 'tic_tac' compiles; shape: 'cigar-ish' does not. Optionality is explicit — if a field can be absent, its type says so.

The evaluation layers extend Observation through module augmentation rather than subclassing or wrapper types:

// Inside @disclosureos/observables:
declare module '@disclosureos/records' {
  interface ObservationExtensions {
    observableAssessments?: ObservableAssessmentMap;
  }
}

This means typing follows your imports:

import type { Observation } from '@disclosureos/records';
// observation.observableAssessments — not typed yet

import '@disclosureos/observables';
// observation.observableAssessments — now typed

If you work with fully enriched records, import the convenience type from the contract package — it registers both slots:

import type { EnrichedObservation } from '@disclosureos/schema';

Runtime: guards and validators

Compile-time types vanish at runtime. At system boundaries — form input, API payloads, files, database rows — use the runtime tools.

Guards narrow single values:

import { isObjectShape, isSourceCredibility } from '@disclosureos/records/guards';

if (isObjectShape(raw)) {
  // raw: ObjectShape
}

Validators check whole structures and return uniform issues:

import { validateObservation } from '@disclosureos/records';

const issues = validateObservation(payload); // ValidationIssue[]
if (issues.length > 0) {
  // [{ path: 'temporal.date', message: '...' }, ...]
}

Every validator in every package returns the same shape:

interface ValidationIssue {
  path: string;     // 'location.latitude', 'origin[0].confidence', ...
  message: string;
}

The strip hazard

This is the single most important rule in the framework.

ObservationSchema only knows the core record. When Zod parses an object, it silently drops unknown keys — which includes the observableAssessments and origin slots:

import { ObservationSchema } from '@disclosureos/records';

const parsed = ObservationSchema.parse(enrichedObservation);
// ❌ parsed.observableAssessments — GONE
// ❌ parsed.origin — GONE
// No error. No warning. Data silently destroyed.

Never Schema.parse() an enriched record

If a record may carry slots, validate it with parseEnrichedObservation from @disclosureos/schema. It validates the core and every registered slot, rejects unknown top-level keys instead of dropping them, and returns the same object — nothing stripped.

import { parseEnrichedObservation } from '@disclosureos/schema';

const result = parseEnrichedObservation(enrichedObservation);
if (result.success) {
  result.data.observableAssessments; // ✅ intact — same object reference
} else {
  result.issues; // ValidationIssue[], slot paths prefixed (e.g. 'origin[0].confidence')
}

Choosing a validation entry point

SituationUse
Core record only, want non-mutating checkvalidateObservation(obs) from records
Record may carry slotsparseEnrichedObservation(obs) from schema
One slot value in isolationvalidateObservableAssessments(v) / validateOriginClassification(v)
Single enum/scalar valueThe relevant guard from */guards

Validating at API boundaries

The pattern for any ingestion surface:

api/observations/route.ts
import { parseEnrichedObservation } from '@disclosureos/schema';

export async function POST(request: Request) {
  const body = await request.json();

  const result = parseEnrichedObservation(body);
  if (!result.success) {
    return Response.json({ errors: result.issues }, { status: 400 });
  }

  await db.observations.insert(result.data);
  return Response.json({ id: result.data.id }, { status: 201 });
}

Validate once at the boundary; trust the types inside. Re-validating already-validated data throughout your codebase adds noise, not safety.

Third-party data: the extensions bag

parseEnrichedObservation rejects unknown top-level keys — that's how it protects you from typos and stale field names. Data that isn't part of the standard belongs in the extensions bag, which is passed through untouched:

const observation = {
  ...base,
  extensions: {
    myPipeline: { ingestBatch: '2026-06-01', sourceRow: 4821 },
  },
};

Standard Schema

All schemas are Zod 4 schemas, which implement the Standard Schema interface — so they plug directly into libraries that accept Standard Schema validators (form libraries, RPC layers, agent frameworks) without adapters.

Next steps

  • Schema — Parsing — parseEnrichedObservation internals
  • Records — Companions — the full guard catalog
  • The Companion Pattern — constants, guards, factories, formatters, labels

The Companion Pattern

Deep dive into the five companion exports: constants, guards, factories, formatters, and labels. One pattern across every layer.

API Reference

Browse TypeScript types from the v1 foundation packages — records, observables, origins, scoring, and schema.

On this page

Compile time: strict types and the extension point
Runtime: guards and validators
The strip hazard
Choosing a validation entry point
Validating at API boundaries
Third-party data: the extensions bag
Standard Schema
Next steps