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 typedIf 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
| Situation | Use |
|---|---|
| Core record only, want non-mutating check | validateObservation(obs) from records |
| Record may carry slots | parseEnrichedObservation(obs) from schema |
| One slot value in isolation | validateObservableAssessments(v) / validateOriginClassification(v) |
| Single enum/scalar value | The relevant guard from */guards |
Validating at API boundaries
The pattern for any ingestion surface:
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 —
parseEnrichedObservationinternals - Records — Companions — the full guard catalog
- The Companion Pattern — constants, guards, factories, formatters, labels