# Claims (/v1/docs/claims)
Records, Observables, and Origins describe and classify an observation. **Claims** are how the standard records *who said so*. Every evaluative statement — "this showed instantaneous acceleration," "this is best explained by an unknown craft" — is a **claim**: an attributed assertion with a rationale and pointers to the specific evidence that backs it.
The claim model ships inside [`@disclosureos/records`](/docs/records) (the `@disclosureos/records/shared` module) and is reused by [Observables](/docs/observables) and [Origins](/docs/origins). It is the attribution layer that threads through the others — Part 4 of the standard, not a separate package.
The [Claims page](/claims) in the Standard Explorer shows how attributed claims, evidence types, and evaluator disagreement fit together.
What every claim carries [#what-every-claim-carries]
Every claim, whatever it's about, shares the same structure from `@disclosureos/records/shared`: an **attribution** (who evaluated it, when, why) plus the in-record **evidence references** that justify it.
```ts
import { evidenceRef, type Claim } from '@disclosureos/records/shared';
// who / when / why + the evidence that backs the call
{
evaluatedBy: 'Scientific Coalition for UAP Studies',
evaluatedAt: '2026-03-01T00:00:00Z',
rationale: 'Radar tracked descent from ~80,000 ft to sea level in seconds.',
evidenceRefs: [evidenceRef('sensor', 'princeton-spy1-radar')],
}
```
The point of this structure is auditability: a claim without `evidenceRefs` is an opinion; a claim with them can be traced back to the exact sensor reading, testimony, or document in the record that supports it.
Evidence references [#evidence-references]
An evidence reference is a typed string — `:` — pointing at something that already lives in the record. There are four kinds:
| Kind | Points to |
| ----------- | -------------------------------------------------- |
| `media` | A media attachment (photo, video, audio, document) |
| `sensor` | A sensor reading in the record's sensor evidence |
| `physical` | A physical evidence item (forensic extension) |
| `testimony` | A testimony statement (forensic extension) |
```ts
evidenceRef('sensor', 'princeton-spy1-radar'); // → 'sensor:princeton-spy1-radar'
```
The evidence must exist in the record first, with a stable id. The CLI and `parseEnrichedObservation` warn when a claim cites an evidence reference that doesn't resolve.
The two claim shapes [#the-two-claim-shapes]
Two first-party shapes extend that shared structure, one per evaluative layer:
| Shape | Package | Adds on top of the shared structure |
| -------------------- | ------------------------------------------------ | ------------------------------------------------------------------------- |
| **Observable Claim** | [`@disclosureos/observables`](/docs/observables) | an assessment `level` (how strong the evidence is) for one observable |
| **Origin Claim** | [`@disclosureos/origins`](/docs/origins) | a `primaryHypothesis` from the OCS taxonomy, with `alternativeHypotheses` |
```ts
import { createObservableClaim } from '@disclosureos/observables';
import { createOriginClaim } from '@disclosureos/origins';
const observableClaim = createObservableClaim('measured', {
confidence: 0.7,
rationale: 'Doppler velocity profile shows acceleration inconsistent with known aircraft.',
evidenceRefs: [evidenceRef('sensor', 'princeton-spy1-radar')],
evaluatedBy: 'example-institution',
});
const originClaim = createOriginClaim('1.1.3', 0.4, {
rationale: 'Performance exceeds known aerospace capability.',
alternativeHypotheses: [{ nodeId: '1.1.1.2.1', confidence: 0.25, label: 'Classified program' }],
evidenceRefs: [evidenceRef('sensor', 'princeton-spy1-radar')],
});
```
Where claims live: the two slots [#where-claims-live-the-two-slots]
The evaluative slots that [Observables](/docs/observables) and [Origins](/docs/origins) add to an `Observation` each hold a **list** of claims, never a single value:
| Slot | Holds | Owner |
| ----------------------- | ------------------------------------- | --------------------------- |
| `observableAssessments` | `ObservableClaim[]` per observable id | `@disclosureos/observables` |
| `origin` | `OriginClaim[]` | `@disclosureos/origins` |
Holding lists is deliberate. It lets the record keep two distinct kinds of uncertainty straight.
Two axes of uncertainty [#two-axes-of-uncertainty]
Inter-claim: evaluators disagree [#inter-claim-evaluators-disagree]
When two institutions assess the same case differently, both claims sit in the slot side by side. Neither overwrites the other.
```ts
// instantaneous_acceleration, as assessed by two parties
[
createObservableClaim('measured', {
confidence: 0.85,
rationale: 'Radar tracked descent from ~80,000 ft to sea level in seconds.',
evaluatedBy: 'Navy aviator (firsthand)',
}),
createObservableClaim('not_indicated', {
confidence: 0.6,
rationale: 'Available sensor data judged insufficient to establish the maneuver.',
evaluatedBy: 'AARO (later review)',
}),
]
```
The disagreement *is* the data. [Scoring](/docs/scoring/compellingness) reads it and sets a `contested` flag when claims disagree in direction — it never averages the dispute away.
Intra-claim: one evaluator hedges [#intra-claim-one-evaluator-hedges]
A single origin claim can carry a primary hypothesis alongside weighted alternatives — one evaluator expressing their own internal uncertainty:
```ts
createOriginClaim('1.1.3', 0.4, {
rationale: 'Best fit given the kinematics, but data is incomplete.',
alternativeHypotheses: [{ nodeId: '1.1.1.2.1', confidence: 0.25, label: 'Classified program' }],
});
```
These two axes are kept distinct on purpose: a *list of claims* is disagreement between evaluators; `primaryHypothesis` + `alternativeHypotheses` is one evaluator's own hedging.
The record holds facts. Every interpretation is a claim, attributed to whoever made it. Endorsement, weighting, and the `contested` flag belong to [Scoring](/docs/scoring), not to the record itself.
Working with claims [#working-with-claims]
* [Claiming Observables](/docs/observables/claiming-observables) — the discipline for picking the observable, level, and confidence, and citing evidence.
* [Claiming Origins](/docs/origins/claiming-origins) — choosing OCS nodes and expressing alternatives honestly.
* [Compellingness](/docs/scoring/compellingness) — how claims and their evidence become a score, and how `contested` is derived.
# Overview (/v1/docs/cli)
`@disclosureos/cli` is the developer tool for the ecosystem. It's not a library — it wraps the foundation packages so you can work with observations from a terminal: generate templates, validate files, and browse the registries without writing code.
Run it [#run-it]
No install required:
```bash title="Terminal"
npx @disclosureos/cli --help
```
Or add it to a project:
npm
pnpm
yarn
bun
```bash
npm install -D @disclosureos/cli
```
```bash
pnpm add -D @disclosureos/cli
```
```bash
yarn add --dev @disclosureos/cli
```
```bash
bun add --dev @disclosureos/cli
```
Once installed, the binary is `disclosureos`.
Commands [#commands]
| Command | Does |
| -------------------------------- | -------------------------------------------------------------------------------------------------- |
| [`scaffold`](/docs/cli/scaffold) | Generate typed templates — observations, assessment maps, classifications |
| [`validate`](/docs/cli/validate) | Validate observation JSON files: core + slots + evidence-ref warnings |
| `completeness` | Measure how fully observations populate the schema — percentage, required coverage, missing fields |
| [`registry`](/docs/cli/registry) | Introspect the field, observable, and origin registries |
| [`info`](/docs/cli/info) | Quick reference for types, observables, and OCS nodes |
```bash title="Terminal"
disclosureos scaffold observation --full
disclosureos validate ./data/ --recursive
disclosureos completeness ./out/ --recursive
disclosureos registry origins --id 1.1.3
disclosureos info observable TO-3
```
Never drifts from the libraries [#never-drifts-from-the-libraries]
The CLI doesn't re-implement any checks. `validate` delegates to [`parseEnrichedObservation`](/docs/schema/parsing) from `@disclosureos/schema` — the exact validation pipeline library consumers use — then layers on CLI-only niceties like dangling-evidence-reference warnings. `registry` and `info` read the same exported constants and taxonomies the packages ship. What the CLI tells you is what the code will do.
A typical loop [#a-typical-loop]
```bash title="Terminal"
# 1. Start from a template
disclosureos scaffold observation --out sighting.json
# 2. Fill it in (look things up as you go)
disclosureos registry observables
disclosureos info origin 1.1.1
# 3. Validate — repeat until clean
disclosureos validate sighting.json --strict
```
For programmatic use, do the same things with the libraries — see the [golden path](/docs/platform#the-golden-path).
Migrating a dataset [#migrating-a-dataset]
When converting an existing catalog to observations, the CLI provides two key commands:
```bash title="Terminal"
# Validate the output
disclosureos validate ./out/ --recursive --json
# Measure what's missing
disclosureos completeness ./out/ --recursive --json
```
Both accept `--json` for structured output in CI and scripts. See the [onboarding workspace](/onboard) for the full guided workflow.
# info (/v1/docs/cli/info)
`disclosureos info` is the fast lookup: one type, one observable, or one origin node at a time, with the context you need to use it correctly.
Usage [#usage]
```bash title="Terminal"
disclosureos info [topic] [id]
```
Topics [#topics]
Ecosystem overview [#ecosystem-overview]
```bash title="Terminal"
disclosureos info
```
Prints the package map — records, observables, origins, scoring, cli — with live counts (schema fields, observables, OCS nodes) read from the packages themselves.
info observation [#info-observation]
The `Observation` type at a glance: required fields, the cross-package slots (`observableAssessments`, `origin`), the records-owned extension slots, and the total field-path count.
info observable [#info-observable-code]
Look up one observable by code or id:
```bash title="Terminal"
disclosureos info observable TO-3
disclosureos info observable isotopic_provenance
```
```text
Observable: Hypersonic Velocities Without Signatures
Code: TO-3
ID: hypersonic_no_signatures
Category: technology
Description: Object achieves hypersonic speeds (Mach 5+) without producing
sonic boom, heat signature, or plasma sheath…
```
Run it without a code to list all twelve.
info origin [#info-origin-nodeid]
Look up one OCS node, including its ancestry path and children:
```bash title="Terminal"
disclosureos info origin 1.1.2
```
```text
Origin: Cryptoterrestrial
ID: 1.1.2
Domain: physical
Testable: Yes
Aliases: CTH
Path:
Physical → Intradimensional → Cryptoterrestrial
Children (4):
• [1.1.2.1] Ancient / Proto-Human Survival
• [1.1.2.2] Breakaway Civilization
...
```
The ancestry path is the quick sanity check when [choosing claim depth](/docs/origins/taxonomy#choosing-the-right-depth) — it shows exactly what you're asserting by claiming a node.
info vs registry [#info-vs-registry]
* `info` — one thing, in depth, fast.
* [`registry`](/docs/cli/registry) — whole inventories: every field, every observable, the full tree.
# registry (/v1/docs/cli/registry)
`disclosureos registry` answers "what does the standard define?" without opening an editor — the field lexicon, the twelve observables, and the OCS taxonomy, straight from the packages' own exports.
Usage [#usage]
```bash title="Terminal"
disclosureos registry [options]
```
registry fields [#registry-fields]
The observation field inventory, derived from the records JSON Schema (the same `deriveFieldPaths()` that powers [completeness scoring](/docs/scoring/completeness)):
```bash title="Terminal"
disclosureos registry fields
# every schema-derived field path, grouped by top-level domain
disclosureos registry fields --category temporal
# one group, as a path/required table
```
registry observables [#registry-observables]
All twelve observables with codes, labels, and descriptions:
```bash title="Terminal"
disclosureos registry observables
disclosureos registry observables --technology # TO-1 … TO-6 only
disclosureos registry observables --biologics # BO-1 … BO-6 only
```
```text
Technology Observables (AATIP-derived)
TO-1 Anti-Gravity Lift
Object appears to overcome gravitational force without conventional propulsion…
TO-2 Instantaneous Acceleration
…
Assessment Levels: not_indicated → reported → documented → measured → confirmed
```
registry origins [#registry-origins]
The OCS taxonomy:
```bash title="Terminal"
disclosureos registry origins
# the full tree: 3 domains, subdomains, and categories
disclosureos registry origins --id 1.1.3
# one node in detail: label, domain, depth, testability, aliases, children
disclosureos registry origins --testable
# only hypotheses with scientificallyTestable: true
```
```text
ID: 1.1.3
Label: Extraterrestrial
Domain: physical
Testable: Yes
Aliases: ETH
Children (4):
• [1.1.3.1] Solar System
• [1.1.3.2] Interstellar
• [1.1.3.3] Intergalactic
• [1.1.3.4] Unknown Region of Space-Time
```
When to use it [#when-to-use-it]
* Writing a data mapping and need the exact field paths → `registry fields`
* Deciding which observable a signature falls under → `registry observables`
* Choosing the right OCS node and depth for a claim → `registry origins --id`
* Scoping which hypotheses your tool should surface as testable → `registry origins --testable`
For single-item lookups with ancestry paths, [`info`](/docs/cli/info) is the quicker tool.
# scaffold (/v1/docs/cli/scaffold)
`disclosureos scaffold` generates ready-to-fill templates for the standard's data structures — as JSON for data files, or as TypeScript with typed factory calls.
Usage [#usage]
```bash title="Terminal"
disclosureos scaffold [options]
```
Templates [#templates]
| Template | Generates |
| -------------------- | --------------------------------------------------------------- |
| `observation` | Minimal `Observation` — required fields only |
| `observation --full` | Complete `Observation` with the optional domains stubbed |
| `assessment` | An `ObservableAssessmentMap` with all 12 observables pre-listed |
| `classification` | An origin classification slot with one claim |
Options [#options]
| Option | Effect |
| ------------------ | ----------------------------------------------------------------- |
| `--out, -o ` | Write to a file instead of stdout |
| `--ts` | Output TypeScript (typed imports + factory calls) instead of JSON |
Examples [#examples]
Start a new data file:
```bash title="Terminal"
disclosureos scaffold observation --out sighting.json
```
Start a full record in code — the TS template uses `createObservation`, so ids and timestamps are generated and the result is parse-validated:
```bash title="Terminal"
disclosureos scaffold observation --full --ts --out src/sighting.ts
```
Stub a complete assessment pass:
```bash title="Terminal"
disclosureos scaffold assessment --ts
```
```ts
// Each observable maps to a *list* of claims, so competing verdicts coexist.
export const assessment: ObservableAssessmentMap = {
technology: {
antigravity_lift: [createObservableClaim('not_indicated')],
instantaneous_acceleration: [createObservableClaim('not_indicated')],
// ...all 12 observables
},
};
```
Note the assessment template defaults everything to `not_indicated` — an explicit "evaluated, found absent" — so a completed template documents a real evaluation pass, not just the anomalies. Delete the entries you didn't actually evaluate.
After scaffolding [#after-scaffolding]
Fill in the template, then validate it:
```bash title="Terminal"
disclosureos validate sighting.json --strict
```
# validate (/v1/docs/cli/validate)
`disclosureos validate` checks observation JSON files against the full standard. It runs the same [`parseEnrichedObservation`](/docs/schema/parsing) pipeline the libraries use, plus file handling and provenance warnings.
Usage [#usage]
```bash title="Terminal"
disclosureos validate [options]
```
Paths can be files, directories, or shell globs — mixed freely and deduped:
```bash title="Terminal"
disclosureos validate ./data/nimitz.json
disclosureos validate ./data/ --recursive
disclosureos validate ./imports/*.json ./curated/key-cases.json
```
A path that matches no JSON **fails the run** — a typo'd path can never masquerade as a clean validation.
Options [#options]
| Option | Effect |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `--recursive, -r` | Descend into directories |
| `--strict, -s` | Also warn on missing recommended fields (`summary`, `description`, `objectCharacteristics`, `sourceData`, `witnesses`) |
| `--json, -j` | Output results as structured JSON for CI and scripts |
What it checks [#what-it-checks]
**Errors** (fail the run, exit code 1):
* Required fields and type correctness for the core record (via records' validator)
* Observable assessment levels and ids, when `observableAssessments` is present
* OCS node ids and confidence ranges, when `origin` is present
* Unknown top-level keys — third-party data belongs under `extensions`
* Semantic sanity: non-negative durations, speeds, witness counts
**Warnings** (reported, never fail the run):
* **Dangling evidence refs** — a claim citing `sensor:radar-01` when the record has no sensor with that id, or a malformed ref that doesn't match `:`
* Missing recommended fields, with `--strict`
```text
data/case-0042.json
✗ origin[0].confidence: Too big: expected number to be <=1
⚠ observableAssessments.technology.transmedium_travel[0].evidenceRefs[0]:
dangling evidence ref "sensor:spy1" — no sensor with id "spy1" in record
1 error(s) across 3 file(s)
```
Why dangling refs are warnings, not errors [#why-dangling-refs-are-warnings-not-errors]
The schema deliberately keeps `evidenceRefs` as plain strings (for JSON Schema portability), so a dangling ref is structurally valid — but it's a broken provenance link the author should fix. The CLI surfaces it without `--strict` because an evidence-backed claim whose evidence can't be found undermines the whole point of the claim model.
Structured JSON output [#structured-json-output]
Use `--json` (or `-j`) for machine-readable output. The JSON includes per-file results (errors, warnings, validity) and aggregate totals:
```bash title="Terminal"
disclosureos validate ./data/ --recursive --json
```
```json
{
"files": [{ "file": "...", "valid": true, "errors": [], "warnings": [] }],
"emptyTargets": [],
"totals": { "files": 1, "errors": 0, "warnings": 0, "emptyTargets": 0 },
"valid": true
}
```
CI usage [#ci-usage]
Exit code is 0 only when every file is valid and every path matched. Wire it straight into CI:
```yaml title=".github/workflows/data.yml"
- name: Validate observation data
run: npx @disclosureos/cli validate ./data --recursive --strict
```
See also [#see-also]
* [`completeness`](/docs/cli) — measure which fields are missing after validation passes
* [Onboarding workspace](/onboard) — guided migration workflow with paste-and-validate playground
# Calibration (/v1/docs/instruments/calibration)
Most datasets reduce calibration to a boolean. The manifest replaces it with a **maturity gradient** that separates what an operator actually does today from the traceable reference it has identified:
| Status | Meaning |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `none` | No calibration is performed. Declared honestly, this is itself useful information. |
| `candidate_identified` | A traceable reference has been named but is not yet part of practice. |
| `in_practice` | A real calibration method is performed today, but no full uncertainty budget exists. |
| `documented` | The calibration method and its uncertainty budget are published. |
Alongside `status`, an entry can carry `currentMethod` (what is actually done), `traceableReference` and `referenceInUse` (the reference and whether it is practice or aspiration), `cadence`, `uncertaintyBudget`, and `notes`. All nullable — **honest nulls are first-class**, so calibration gaps stay visible and comparable across operators.
The scoring feedback loop [#the-scoring-feedback-loop]
`@disclosureos/scoring` exposes the gradient as a trust input. `calibrationTrust` builds an `evaluatorWeight` policy that credits claims whose sensor evidence resolves — through `SensorReading.sensorRef` — to a manifest entry with real calibration provenance:
```ts
import { score, calibrationTrust, CALIBRATION_TRUST_WEIGHTS } from '@disclosureos/scoring';
const result = score(observation, {
evaluatorWeight: calibrationTrust(observation, [manifest]),
});
CALIBRATION_TRUST_WEIGHTS;
// { unresolved: 1, none: 1, candidate_identified: 1.05, in_practice: 1.15, documented: 1.25 }
```
The default is deliberately conservative:
* **Nothing weighs below the baseline.** Sensors with no manifest entry (and manifests that declare `none`) keep the 1.0 baseline. Publishing provenance can only add weight.
* **Only the consensus point shifts.** Like every `evaluatorWeight` policy, the `range` and `contested` flag still report the honest spread of what evaluators actually asserted.
* **The gradient is overridable** per call, and composes multiplicatively with any base evaluator-trust policy.
# Overview (/v1/docs/instruments)
`@disclosureos/instruments` answers the sixth question the standard asks: **what hardware produced the data?**
It defines the **Sensor Manifest** — the standard's first standalone document type besides `Observation`. An organization that operates detection hardware publishes one manifest cataloging every sensor it runs: modality, timing provenance, measurement units and uncertainty, raw-data locators, and calibration status. Case records cite manifest entries from their sensor readings via `SensorReading.sensorRef` (`":"`).
The [Instruments page](/instruments) in the Standard Explorer shows the manifest anatomy, the calibration gradient, and ELDÆON's published 28-sensor manifest rendered live.
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/instruments
```
```bash
pnpm add @disclosureos/instruments
```
```bash
yarn add @disclosureos/instruments
```
```bash
bun add @disclosureos/instruments
```
A standalone document, not an extension slot [#a-standalone-document-not-an-extension-slot]
Unlike `observables` and `origins`, this package does not add a slot to `Observation`. A manifest is published once per organization and cited from many records:
```ts
import { createSensorEntry, createSensorManifest, validateSensorManifest } from '@disclosureos/instruments';
import { createSensorReading } from '@disclosureos/records/factories';
import { sensorRef } from '@disclosureos/records/shared';
const spy1 = createSensorEntry('princeton-spy1', 'AN/SPY-1B Passive Phased Array Radar', 'radio_frequency', {
timing: { timeSource: 'gps_disciplined' },
calibration: { status: 'documented', currentMethod: 'Navy maintenance and alignment program', referenceInUse: true },
});
const manifest = createSensorManifest('US Navy', 'us-navy', [spy1]);
validateSensorManifest(manifest); // [] — no issues
// In a record, the reading points at the entry:
const radar = createSensorReading('shipborne_radar', 'radar_phased_array', {
id: 'princeton-spy1-radar',
sensorRef: sensorRef(manifest.orgSlug, spy1.id), // "us-navy:princeton-spy1"
});
```
What each sensor declares [#what-each-sensor-declares]
Design commitments [#design-commitments]
* **Honest nulls are first-class.** Declaring "no calibration" or "uncertainty not yet characterized" is itself information a data consumer needs. Gaps stay visible and comparable across operators.
* **Spec-level disclosure, not sourcing-level.** Publishers may withhold specific make/model identities and publish class descriptors instead ("dual GM-tube counter" rather than a product name).
* **Proposed vocabulary is flagged, not smuggled.** A `recordsMapping` value outside the records enums must carry a `proposedSensorType` / `proposedDetectionMethod` flag — the validator enforces it.
* **Metadata-first.** `rawData.format` and `locatorPattern` let manifests circulate widely while raw data is pulled on demand.
Validate from the CLI [#validate-from-the-cli]
```bash
disclosureos manifest validate ./my-org-sensors.json
```
Checks manifest structure, unique kebab-case sensor ids, and that every records-enum mapping either exists in the enums or carries its proposed flag.
Provenance [#provenance]
The Sensor Manifest was proposed by [ELDÆON](https://github.com/disclosurefoundation/disclosureos/pull/5), the standard's first external contributor, and promoted into the framework as this package. Their manifest is the [worked example](https://github.com/disclosurefoundation/disclosureos/tree/main/examples/manifests) in the repository, alongside the community registry of published manifests.
# The Manifest Document (/v1/docs/instruments/manifest)
A manifest is one JSON document per organization. The top level identifies the publisher and holds the sensor catalog:
| Field | Type | Notes |
| ---------------- | ----------------- | --------------------------------------------------------------------------------- |
| `schemaVersion` | `string` | **Required.** SemVer of the manifest schema targeted. |
| `org` | `string` | **Required.** Display form ("ELDÆON"). |
| `orgSlug` | `string` | **Required.** Stable kebab-case slug ("eldaeon") — the org half of a `sensorRef`. |
| `orgUrl` | `string` | Publisher URL. |
| `generatedAt` | `string` | ISO date the manifest was generated. |
| `sensors` | `SensorEntry[]` | **Required, ≥ 1.** The catalog. |
| `futureUpgrades` | `FutureUpgrade[]` | Planned hardware declared ahead of deployment. |
SensorEntry [#sensorentry]
Each entry is one sensor. `id` must be kebab-case and unique within the manifest — it is the sensor half of a `sensorRef`.
* **Identity** — `id`, `name`, `modality` (one of six: `audio`, `electro_optical`, `radio_frequency`, `quantum`, `environmental`, `biometric`), `manufacturerModel` (class descriptors allowed), `inHouse`, `compute`.
* **`recordsMapping`** — how the sensor maps to the `SensorType` / `DetectionMethod` vocabulary in `@disclosureos/records`. Values outside those enums must be flagged `proposedSensorType: true` / `proposedDetectionMethod: true`; the validator rejects unflagged strangers. This is also the pipeline for growing the records enums — flagged proposals from real manifests are how the vocabulary expands.
* **`timing`** — `timeSource` (`atomic_clock`, `gps_disciplined`, `ntp`, `system`, `unknown`) plus `timeUncertaintyNs`. Timestamp error of even a few seconds confounds multi-station correlation, so the clock is declared, never assumed.
* **`rawData`** — `format`, `locatorPattern`, `sampleRateHz`, `bitDepth`. Supports metadata-first distribution: circulate manifests widely, pull raw data on demand.
* **`measurements`** — each measured quantity with a UCUM-style `unit` and GUM uncertainty (`u_c`, `U`, `k`, `type` A/B). Nullable until an uncertainty budget exists.
* **`calibration`** — see [Calibration](/docs/instruments/calibration).
* **`specs` / `specSource`** — free-form hardware spec sheet values and their provenance.
The DX quintet [#the-dx-quintet]
Like every foundation package, `instruments` ships constants (`MODALITIES`, `CALIBRATION_STATUSES`, …), labels (`MODALITY_LABELS`, `CALIBRATION_STATUS_DESCRIPTIONS`, …), guards (`isModality`, …), factories (`createSensorEntry`, `createSensorManifest`), formatters (`formatSensorManifest`, …), and the canonical validator `validateSensorManifest`.
Schema artifact [#schema-artifact]
The committed JSON Schema is emitted from the Zod source and drift-tested:
```
https://os.disclosure.org/schema/instruments/1.0.0/sensor-manifest.json
```
# Publishing a Manifest (/v1/docs/instruments/publishing-a-manifest)
Any operator — vendor, research group, or institution — can publish a manifest. Three steps:
1. Author it [#1-author-it]
Build the document with the factories (or write JSON directly against the [schema artifact](https://os.disclosure.org/schema/instruments/1.0.0/sensor-manifest.json)):
```ts
import { createSensorEntry, createSensorManifest } from '@disclosureos/instruments';
const magnetometer = createSensorEntry('site-a-fluxgate', 'Fluxgate Magnetometer', 'environmental', {
recordsMapping: { sensorType: 'fluxgate_magnetometer', detectionMethod: 'magnetic' },
timing: { timeSource: 'gps_disciplined', timeUncertaintyNs: null },
rawData: { format: 'CSV', locatorPattern: 'site-a/mag/YYYY/MM/DD/' },
measurements: [{ name: 'field_strength', unit: 'uT', uncertainty: { u_c: null, U: null, k: null, type: 'B' } }],
calibration: {
status: 'in_practice',
currentMethod: 'weekly zero-field chamber check',
traceableReference: 'USGS/INTERMAGNET',
referenceInUse: false,
},
});
const manifest = createSensorManifest('Example Observatory', 'example-observatory', [magnetometer], {
orgUrl: 'https://example.org',
});
```
Be honest about gaps — `null` uncertainty and `status: 'none'` are correct answers, not failures. Spec-level class descriptors are fine where you prefer not to disclose exact hardware.
2. Validate it [#2-validate-it]
```bash
disclosureos manifest validate ./example-observatory-sensors.json
```
Or programmatically with `validateSensorManifest(manifest)`, which returns a list of issues (empty = valid). Proposing a `sensorType` or `detectionMethod` that isn't in the records enums yet? Flag it with `proposedSensorType: true` / `proposedDetectionMethod: true` — that flag is the standard's vocabulary-growth pipeline.
3. Host it and register it [#3-host-it-and-register-it]
Host the JSON at a stable URL you control, then add your organization to the [community registry](https://github.com/disclosurefoundation/disclosureos/blob/main/examples/manifests/registry.json) via pull request:
```json
{
"orgSlug": "example-observatory",
"org": "Example Observatory",
"manifestUrl": "https://example.org/sensors.json"
}
```
Once registered, any record's `sensorRef` (`"example-observatory:site-a-fluxgate"`) resolves to your published spec — and claims backed by your calibrated instruments [earn scoring credit](/docs/instruments/calibration).
# Assessment (/v1/docs/observables/assessment)
This page is the reference for the assessment machinery: the evidentiary levels, the claim type, and the exact shape of the slot.
Assessment levels [#assessment-levels]
Five tiers, ordered by how strongly the signal is established:
| Level | Meaning |
| --------------- | ---------------------------------------------------------------------------------- |
| `not_indicated` | Evaluated and found absent — an explicit negative, not missing data |
| `reported` | Asserted by witnesses, but not independently documented |
| `documented` | Written records, photographs, or contemporaneous documentation of the signal exist |
| `measured` | Quantitative instrument data captured the signal — radar, FLIR, or other sensors |
| `confirmed` | Peer-reviewed analysis with independent verification establishes the signal |
```ts
import { ASSESSMENT_LEVELS, isAssessmentLevel, formatAssessmentLevel } from '@disclosureos/observables';
```
Two distinctions worth internalizing:
* **`not_indicated` vs. absence.** An observable missing from the map means *nobody evaluated it*. `not_indicated` means *somebody looked and found nothing* — which is real, scoreable information.
* **Level vs. confidence.** The level says what kind of evidence backs the claim; `confidence` says how sure the evaluator is of their own judgment. A radar-measured anomaly with ambiguous data is `measured` with low confidence — not `reported`.
The claim shape [#the-claim-shape]
`ObservableClaim` composes the shared [`Claim`](/docs/records/shared#claim) envelope with the level and confidence:
```ts
interface ObservableClaim {
level: AssessmentLevel;
confidence?: number; // [0, 1]
rationale?: string; // the evaluator's reasoning
evaluatedBy?: string; // who made it
evaluatedAt?: string; // ISO datetime (factory defaults to now)
evidenceRefs?: string[]; // "sensor:", "media:", ...
}
```
Build claims with the factory — it parses on construction, so out-of-range confidence throws:
```ts
import { createObservableClaim } from '@disclosureos/observables';
const claim = createObservableClaim('measured', {
confidence: 0.7,
rationale: 'Two independent radar tracks with consistent kinematics.',
evidenceRefs: ['sensor:radar-01', 'sensor:radar-02'],
evaluatedBy: 'example-institution',
});
```
Note there's no `observableId` field — the claim's position in the map supplies it.
The slot shape [#the-slot-shape]
```ts
interface ObservableAssessmentMap {
technology?: Partial>;
biologics?: Partial>;
}
```
```ts
observation.observableAssessments = {
technology: {
instantaneous_acceleration: [claimFromSCU, claimFromAARO], // arrays — always
low_observability: [
createObservableClaim('not_indicated', {
rationale: 'Object maintained continuous track on all sensors.',
}),
],
},
};
```
**Arrays are the point.** When two institutions assess the same observable differently, both claims stand, attributed. The [scoring layer](/docs/scoring/compellingness) reads the spread: agreement strengthens a case, contradiction flags it `contested`.
Standalone assessment type [#standalone-assessment-type]
`ObservableAssessment` is the claim plus an explicit `observableId` — useful when claims travel outside the map (queues, review tools, exports):
```ts
import type { ObservableAssessment } from '@disclosureos/observables';
// { observableId: string; level: AssessmentLevel; confidence?: number; ... }
```
Validation [#validation]
```ts
import { validateObservableAssessments } from '@disclosureos/observables';
const issues = validateObservableAssessments(observation.observableAssessments);
// ValidationIssue[] — unknown observable ids, bad levels, out-of-range confidence
```
This validates the slot in isolation. For the whole enriched record — including whether claim `evidenceRefs` resolve — use [`parseEnrichedObservation`](/docs/schema/parsing) and [`disclosureos validate`](/docs/cli/validate).
# Biologics Observables (/v1/docs/observables/biologics)
The Biologics framework defines what would make biological material or biological interaction anomalous — the criteria that claims about "anomalous biologics" must be tested against. Each observable is grounded in published science (assembly theory, isotopic analysis, phylogenetics) so that an extraordinary claim has an ordinary, checkable measurement path.
```ts
import { BIOLOGICS_OBSERVABLES, BIOLOGICS_OBSERVABLE_IDS } from '@disclosureos/observables';
```
The six observables [#the-six-observables]
| Code | Id | Label |
| ---- | --------------------------- | ----------------------------------- |
| BO-1 | `molecular_complexity` | Molecular Complexity Anomaly |
| BO-2 | `isotopic_provenance` | Non-Terrestrial Isotopic Provenance |
| BO-3 | `non_standard_biochemistry` | Non-Standard Biochemistry |
| BO-4 | `non_phylogenetic_genetics` | Non-Phylogenetic Genetic Signatures |
| BO-5 | `anomalous_morphology` | Anomalous Morphology |
| BO-6 | `anomalous_bio_interaction` | Anomalous Biological Interaction |
BO-1 — Molecular Complexity Anomaly [#bo-1--molecular-complexity-anomaly]
Material exhibits molecular assembly complexity requiring a guided process (life or technology) to produce — molecules with high assembly indices found in abundances impossible through abiotic chemistry. Grounded in assembly theory (Cronin et al.).
Assembly Theory is an active area of research and remains contested. Critics argue that its assembly index overlaps with established complexity and information measures and that it has not been independently validated as a definitive biosignature. Treat a high assembly index as suggestive rather than conclusive. The definition carries this as its `caveat` field, so any UI rendering the standard surfaces the qualification automatically.
BO-2 — Non-Terrestrial Isotopic Provenance [#bo-2--non-terrestrial-isotopic-provenance]
Isotope ratios inconsistent with terrestrial, solar-system, or known astrophysical formation processes. This is the workhorse test for "not from here" claims about recovered material — mass spectrometry gives it a clean measurement path.
BO-3 — Non-Standard Biochemistry [#bo-3--non-standard-biochemistry]
Biology using fundamentally different building blocks than terrestrial life — alternative nucleic acids, non-standard amino acid backbones, non-carbon scaffolding, or alternative solvent systems.
BO-4 — Non-Phylogenetic Genetic Signatures [#bo-4--non-phylogenetic-genetic-signatures]
Genetic material outside Earth's tree of life — no homology to any known organism, novel genetic codes, or information-storage systems incompatible with LUCA-derived biology. Sequencing plus comparative genomics is the method; the null hypothesis (contamination, degradation artifacts) must be excluded.
BO-5 — Anomalous Morphology [#bo-5--anomalous-morphology]
Body plans, organ systems, or cellular architectures outside all known phylogenetic trees — features that cannot be derived from any known evolutionary lineage. Note that *unusual* is not *anomalous*: terrestrial biology is strange, and this observable requires excluding known taxa, pathology, and hoax.
BO-6 — Anomalous Biological Interaction [#bo-6--anomalous-biological-interaction]
Agent-level biological interaction beyond simple energetic exposure — directed, purposeful, or communicative engagement implying intelligence or agency. Distinct from TO-6 (Biological Effects), which covers passive radiological or energetic injury; BO-6 is the home for the abduction/experiencer phenomenon and precision animal-tissue removal ("cattle mutilation"). The hardest to establish and the most dependent on multi-witness, multi-modal documentation.
Unlike the sensor- and laboratory-grounded observables, this one rests largely on witness and experiencer testimony, and the inference of directed intelligence behind an interaction is not established. Investigations of related phenomena — including the federally funded 1980 Operation Animal Mutilation report — have attributed many cases to mundane causes such as predation. Treat a reported interaction as a prompt for physical corroboration, not as confirmation of agency.
Honest levels matter most here [#honest-levels-matter-most-here]
Biologics claims attract the strongest scrutiny, so the [assessment levels](/docs/observables/assessment) carry the weight:
```ts
import { createObservableClaim } from '@disclosureos/observables';
observation.observableAssessments = {
biologics: {
isotopic_provenance: [
createObservableClaim('measured', {
confidence: 0.55,
rationale: 'Magnesium isotope ratios outside terrestrial range in two of three samples; third lab could not replicate.',
evidenceRefs: ['physical:sample-1976-frag'],
evaluatedBy: 'example-materials-lab',
}),
],
},
};
```
A `measured` claim with honest confidence and cited lab evidence is worth more to the ecosystem than a `confirmed` claim that can't survive an audit. Replication disagreements belong in the record — add a second claim rather than overwriting the first.
Companions [#companions]
```ts
import {
BIOLOGICS_OBSERVABLE_IDS,
BIOLOGICS_OBSERVABLE_LABELS,
isBiologicsObservableId,
formatBiologicsObservable,
} from '@disclosureos/observables';
```
# Claiming Observables (/v1/docs/observables/claiming-observables)
This guide is the workflow for adding observable claims to a record. The standard's credibility rests on claims being made honestly, so most of this guide is about restraint.
1. Make the evidence citable first [#1-make-the-evidence-citable-first]
A claim without `evidenceRefs` is an assertion; a claim with them is auditable. Before claiming, make sure the evidence lives in the record with a stable id:
```ts
import { createObservation, createSensorReading } from '@disclosureos/records/factories';
import { evidenceRef } from '@disclosureos/records/shared';
const radar = createSensorReading('military_radar', 'radar_doppler', {
id: 'redflag-range-radar',
operator: 'USAF',
});
const obs = createObservation({
// ...
sensorEvidence: { sensors: [radar] },
});
const RADAR = evidenceRef('sensor', radar.id);
```
Testimony and physical evidence are citable too — see [Extension Slots](/docs/records/extensions).
2. Pick the observable precisely [#2-pick-the-observable-precisely]
Read the definition before claiming — each observable has specific `detectionSignals`. "It moved fast" is not `instantaneous_acceleration`; a measured velocity change inconsistent with known aerospace capability is:
```ts
import { TECHNOLOGY_OBSERVABLES } from '@disclosureos/observables';
TECHNOLOGY_OBSERVABLES.instantaneous_acceleration.detectionSignals;
// ['Radar track showing extreme velocity change over minimal time', ...]
```
If no observable fits, claim nothing. The absence of claims is a valid, honest state for a record.
3. Choose the level the evidence supports [#3-choose-the-level-the-evidence-supports]
| The evidence is... | Level |
| -------------------------------------------------------------- | --------------- |
| Witness accounts only | `reported` |
| Written records, photographs, or contemporaneous documentation | `documented` |
| Quantitative instrument data — radar, FLIR, other sensors | `measured` |
| Peer-reviewed analysis with independent verification | `confirmed` |
| Evaluated and found absent | `not_indicated` |
Resist level inflation. `documented` with a solid paper trail outranks an unsupportable `confirmed` in every way that matters — and [compellingness scoring](/docs/scoring/compellingness) weighs evidence quality, not just the level word.
4. Set confidence separately [#4-set-confidence-separately]
Confidence is your certainty *in your own judgment*, not the strength of the evidence class:
```ts
import { createObservableClaim } from '@disclosureos/observables';
const claim = createObservableClaim('measured', {
confidence: 0.6, // the data is instrumented, but the calibration is debatable
rationale: 'Doppler velocity profile shows 40g lateral acceleration; sensor calibration records incomplete.',
evidenceRefs: [RADAR],
evaluatedBy: 'example-institution',
});
```
Always write the `rationale`. It's what lets another analyst — or Congress, or a journalist — understand the claim without calling you.
5. Attach without overwriting [#5-attach-without-overwriting]
Slots hold arrays. When re-assessing or disagreeing, **append**:
```ts
obs.observableAssessments ??= {};
obs.observableAssessments.technology ??= {};
(obs.observableAssessments.technology.instantaneous_acceleration ??= []).push(claim);
```
Never delete another evaluator's claim to make room for yours. Contradicting claims coexist — that's how the standard records a live dispute, and scoring will surface it as `contested`.
6. Validate the result [#6-validate-the-result]
```ts
import { parseEnrichedObservation } from '@disclosureos/schema';
const result = parseEnrichedObservation(obs);
```
Or from the terminal — which also warns on evidence refs that don't resolve:
```bash title="Terminal"
npx @disclosureos/cli validate observation.json
```
Worked end-to-end [#worked-end-to-end]
The [golden path](/docs/platform#the-golden-path) shows the full sequence on the Nimitz case, and [Scoring](/docs/scoring) shows what your claims feed into.
# Overview (/v1/docs/observables)
`@disclosureos/observables` answers the second question the standard asks: **what anomalous characteristics did it show?**
It defines twelve **observables** — specific, detectable anomaly signatures, each with a scientific basis, detection signals, and measurement methods — and the claim machinery for asserting them against an observation with evidence.
The [Observables page](/observables) in the Standard Explorer shows the Technology and Biologics frameworks, the assessment ladder, and the caution banners in one view.
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/observables
```
```bash
pnpm add @disclosureos/observables
```
```bash
yarn add @disclosureos/observables
```
```bash
bun add @disclosureos/observables
```
The two frameworks [#the-two-frameworks]
Each observable is a full definition, not just an enum value:
```ts
import { TECHNOLOGY_OBSERVABLES } from '@disclosureos/observables';
const def = TECHNOLOGY_OBSERVABLES.instantaneous_acceleration;
def.code; // 'TO-2'
def.label; // 'Instantaneous Acceleration'
def.scientificBasis; // why this is anomalous, with citations
def.detectionSignals; // what to look for
def.methods; // how to measure it
def.references; // published analyses
```
This is what makes the framework auditable: the criteria for "anomalous" are public, specific, and versioned — not vibes.
The slot [#the-slot]
Importing the package augments `Observation` with the `observableAssessments` slot:
```ts
import { createObservableClaim } from '@disclosureos/observables';
observation.observableAssessments = {
technology: {
instantaneous_acceleration: [
createObservableClaim('confirmed', {
confidence: 0.85,
rationale: 'Radar tracked descent from ~80,000 ft to sea level in seconds.',
evidenceRefs: ['sensor:princeton-spy1-radar'],
}),
],
},
};
```
Three things to notice:
1. **The key carries the observable id** — claims in the map don't repeat it.
2. **The value is an array** — multiple evaluators can claim the same observable, including in opposite directions. Disagreement is data.
3. **Claims cite evidence** — `evidenceRefs` point at sensors, media, testimony, or physical evidence *inside the record*.
Assessment levels [#assessment-levels]
Claims are tiered by how strongly the signal is established — see [Assessment](/docs/observables/assessment):
`not_indicated` → `reported` → `documented` → `measured` → `confirmed`
Validation [#validation]
```ts
import { validateObservableAssessments } from '@disclosureos/observables';
const issues = validateObservableAssessments(slotValue); // ValidationIssue[]
```
For whole enriched records, use [`parseEnrichedObservation`](/docs/schema/parsing).
In this section [#in-this-section]
* [Technology Observables](/docs/observables/technology) — TO-1 through TO-6 in full
* [Biologics Observables](/docs/observables/biologics) — BO-1 through BO-6 in full
* [Assessment](/docs/observables/assessment) — levels, claims, and the slot shape
* [Claiming Observables](/docs/observables/claiming-observables) — the working guide
# Technology Observables (/v1/docs/observables/technology)
The Technology framework descends from the "five observables" articulated by Luis Elizondo from AATIP's analysis, extended with a sixth for biological effects. Each is a specific, measurable signature — not a synonym for "weird."
```ts
import { TECHNOLOGY_OBSERVABLES, TECHNOLOGY_OBSERVABLE_IDS } from '@disclosureos/observables';
```
The six observables [#the-six-observables]
| Code | Id | Label |
| ---- | ---------------------------- | ---------------------------------------- |
| TO-1 | `antigravity_lift` | Anti-Gravity Lift |
| TO-2 | `instantaneous_acceleration` | Instantaneous Acceleration |
| TO-3 | `hypersonic_no_signatures` | Hypersonic Velocities Without Signatures |
| TO-4 | `low_observability` | Low Observability |
| TO-5 | `transmedium_travel` | Transmedium Travel |
| TO-6 | `biological_effects` | Biological Effects |
TO-1 — Anti-Gravity Lift [#to-1--anti-gravity-lift]
Object appears to overcome gravitational force without conventional propulsion — no wings, rotors, exhaust, or visible means of lift. Detection signals include sustained hover with no downwash, no thermal propulsion signature, and silent operation. Methods: FLIR/IR imaging, Doppler radar, ground-level pressure sensors, acoustic monitoring.
TO-2 — Instantaneous Acceleration [#to-2--instantaneous-acceleration]
Acceleration far beyond known aerospace capability — estimated hundreds to thousands of g with no apparent inertial effects (F-18 sustained max: \~9g; SCU's Nimitz analysis estimated accelerations from \~40g to hundreds of g, with some maneuvers reaching into the thousands). Signals: extreme velocity change over minimal time, hover-to-hypersonic in under a second, acute-angle turns at speed. Methods: multi-radar track correlation, frame-by-frame video analysis, Doppler measurements.
TO-3 — Hypersonic Velocities Without Signatures [#to-3--hypersonic-velocities-without-signatures]
Mach 5+ travel without sonic boom, heat signature, or plasma sheath — violating known atmospheric physics. Methods: acoustic sensor networks, IR imaging of the leading edge, radar velocity profiling.
TO-4 — Low Observability [#to-4--low-observability]
The object evades or confounds multiple sensor modalities simultaneously — radar, infrared, visual, and electromagnetic systems fail to maintain a consistent track. The signature is *cross-modal* inconsistency, not mere stealth.
TO-5 — Transmedium Travel [#to-5--transmedium-travel]
Transition between air, water, and/or space without structural reconfiguration, speed loss, or medium-transition effects. Signals: continuous radar/sonar track across the boundary, no splash or cavitation signature.
TO-6 — Biological Effects [#to-6--biological-effects]
Proximity causes measurable radiological, energetic, or physiological effects on organisms — burns, nausea, cellular damage, or other medical sequelae. Methods: medical examination, dosimetry, site radiological survey.
Using the definitions [#using-the-definitions]
Every entry carries its full definition — render them in UIs, cite them in reports:
```ts
const def = TECHNOLOGY_OBSERVABLES.transmedium_travel;
def.scientificBasis; // why this violates known physics
def.detectionSignals; // ['Continuous track across medium boundary', ...]
def.methods; // ['Combined radar + sonar correlation', ...]
def.references; // published analyses to read
```
Companions follow [the standard pattern](/docs/platform/concepts/patterns):
```ts
import {
TECHNOLOGY_OBSERVABLE_IDS, // ['antigravity_lift', ...]
TECHNOLOGY_OBSERVABLE_LABELS, // { antigravity_lift: 'Anti-Gravity Lift', ... }
isTechnologyObservableId,
formatTechnologyObservable, // id → 'Instantaneous Acceleration'
formatObservableCode, // id → 'TO-2'
} from '@disclosureos/observables';
```
Claiming a technology observable [#claiming-a-technology-observable]
```ts
import { createObservableClaim } from '@disclosureos/observables';
observation.observableAssessments = {
technology: {
transmedium_travel: [
createObservableClaim('documented', {
confidence: 0.6,
rationale: 'Subsurface radar returns reported on departure.',
evidenceRefs: ['sensor:princeton-spy1-radar'],
}),
],
},
};
```
See [Claiming Observables](/docs/observables/claiming-observables) for choosing levels and confidence honestly.
# Claiming Origins (/v1/docs/origins/claiming-origins)
This guide is the workflow for classifying an observation's origin. Origin claims are where credibility is won or lost, so the discipline matters more than the mechanics.
1. Survey the taxonomy first [#1-survey-the-taxonomy-first]
Before classifying, know what the candidate space looks like — especially the prosaic branch:
```ts
import { getChildren, searchNodes, getPath } from '@disclosureos/origins';
getChildren('1.1.1'); // the prosaic family: natural, human
searchNodes('balloon'); // find nodes by label/description/alias
getPath('1.1.1.2.4'); // 'Physical > Intradimensional > Prosaic > Human > Private / Commercial'
```
The strongest exotic claim is one made by an evaluator who demonstrably worked the prosaic branch first.
2. Claim at the depth the evidence supports [#2-claim-at-the-depth-the-evidence-supports]
```ts
import { createOriginClaim } from '@disclosureos/origins';
// Evidence excludes known craft but says nothing about locale:
createOriginClaim('1.1.3', 0.4, { /* ... */ }); // honest
// The same evidence does NOT support:
createOriginClaim('1.1.3.2', 0.4, { /* ... */ }); // interstellar? based on what?
```
See [choosing the right depth](/docs/origins/taxonomy#choosing-the-right-depth).
3. Weight your alternatives inside the claim [#3-weight-your-alternatives-inside-the-claim]
A serious analysis almost always entertains more than one hypothesis. Put them *in the claim*, weighted:
```ts
const claim = createOriginClaim('1.1.3', 0.4, {
rationale:
'Kinematics exceed known aerospace capability per radar data; ' +
'classified domestic program cannot be excluded; sensor artifact unlikely given multi-platform correlation.',
alternativeHypotheses: [
{ nodeId: '1.1.1.2.1', confidence: 0.25, label: 'Classified U.S. program' },
{ nodeId: '2.2.1', confidence: 0.1, label: 'Misinterpretation' },
],
evidenceRefs: ['sensor:princeton-spy1-radar'],
evaluatedBy: 'example-institution',
});
```
The confidences here sum to 0.75 — the remaining 0.25 is unassigned uncertainty, and that's the honest state. Don't inflate the primary to make the numbers look decisive.
4. Append to the slot — never overwrite [#4-append-to-the-slot--never-overwrite]
```ts
(observation.origin ??= []).push(claim);
```
The `origin` slot is the inter-evaluator axis. If a prior claim from another institution says `2.1.5` (hoax) and you conclude `1.1.3`, both belong in the list. Removing a competing claim doesn't strengthen yours — it just makes the record untrustworthy.
If you're *revising your own* earlier claim, add the new one and relate the records or note the supersession in `rationale`; the timestamps establish the sequence.
5. Cite evidence, especially for exotic nodes [#5-cite-evidence-especially-for-exotic-nodes]
The further from prosaic, the more the claim needs `evidenceRefs`. A `1.1.3` claim with no cited evidence will — correctly — be discounted by [compellingness scoring](/docs/scoring/compellingness) and by every serious reader.
6. Validate [#6-validate]
```ts
import { parseEnrichedObservation } from '@disclosureos/schema';
const result = parseEnrichedObservation(observation);
```
```bash title="Terminal"
npx @disclosureos/cli validate observation.json
# warns if a claim cites evidence the record doesn't contain
```
Anti-patterns [#anti-patterns]
| Anti-pattern | Instead |
| -------------------------------------------------- | -------------------------------------------------- |
| Claiming a leaf node on witness testimony alone | Claim the shallowest honest node |
| Confidence 0.95 on anything exotic | If it were that certain, it would be world news |
| Deleting a hoax claim you disagree with | Add your claim; let scoring surface the contest |
| Using `eventType` or tags for origin language | Origin lives in the `origin` slot, attributed |
| One "consensus" claim merged from several analysts | One claim per evaluator — attribution is the point |
# Classification (/v1/docs/origins/classification)
Classification is how an evaluator's judgment about origin attaches to a record. The types model the two axes of disagreement separately: alternatives *within* one evaluator's claim, and competing claims *between* evaluators.
OriginClaim [#originclaim]
One evaluator's verdict — the shared [`Claim`](/docs/records/shared#claim) envelope plus the hypothesis fields:
```ts
interface OriginClaim {
primaryHypothesis: string; // OCS node id, e.g. '1.1.3'
confidence: number; // [0, 1] — confidence in the primary
alternativeHypotheses?: HypothesisWeight[]; // also-entertained, weighted
rationale?: string;
evaluatedBy?: string;
evaluatedAt?: string;
evidenceRefs?: string[];
}
interface HypothesisWeight {
nodeId: string; // OCS node id
confidence: number; // [0, 1]
label?: string; // display convenience
}
```
Build with the factory — it asserts every node id exists in the taxonomy and parses confidence ranges:
```ts
import { createOriginClaim } from '@disclosureos/origins';
const claim = createOriginClaim('1.1.3', 0.4, {
rationale: 'Performance exceeds known aerospace capability; cannot exclude advanced terrestrial program.',
alternativeHypotheses: [
{ nodeId: '1.1.1.2.1', confidence: 0.25, label: 'Classified U.S. program' },
{ nodeId: '2.2.1', confidence: 0.1, label: 'Misinterpretation' },
],
evidenceRefs: ['sensor:princeton-spy1-radar'],
evaluatedBy: 'example-institution',
});
createOriginClaim('9.9.9', 0.5); // throws: Unknown OCS node ID
```
Confidences within a claim need not sum to 1 — the gap is honest unresolved uncertainty.
The origin slot [#the-origin-slot]
A flat array of claims. Push, never replace:
```ts
observation.origin = [claimFromInstitutionA, claimFromInstitutionB];
```
Institution A says `1.1.3` at 0.4; Institution B says `2.1.5` (hoax) at 0.7. Both stand, attributed and evidence-linked. [Compellingness scoring](/docs/scoring/compellingness) reads the spread and flags the case `contested` — which is exactly what a reader should know.
ConfidenceDistribution [#confidencedistribution]
For analyses that assign confidence across many hypotheses explicitly, with the remainder tracked:
```ts
import { createConfidenceDistribution } from '@disclosureos/origins';
const dist = createConfidenceDistribution([
{ nodeId: '1.1.1.1.2', confidence: 0.5, label: 'Celestial' },
{ nodeId: '1.1.1.2.4', confidence: 0.3, label: 'Private/commercial craft' },
]);
// dist.unresolved === 0.2 — computed, the distribution always accounts for 1.0
```
CategoryConfidence [#categoryconfidence]
A simplified eight-bucket distribution for quick, coarse classification — useful for triage UIs and bulk imports before detailed analysis:
```ts
interface CategoryConfidence {
conventional: number; // OCS 1.1.1
cryptoterrestrial: number; // OCS 1.1.2
extraterrestrial: number; // OCS 1.1.3
extradimensional: number; // OCS 1.2
interdimensional: number; // OCS 1.3
psychosocial: number; // OCS 2
metaphysical: number; // OCS 3
insufficientData: number; // cannot classify
}
```
`insufficientData` is a first-class answer. Most honest triage of historical records lands there.
Validation [#validation]
```ts
import { validateOriginClassification, isOriginClaim } from '@disclosureos/origins';
const issues = validateOriginClassification(observation.origin); // ValidationIssue[]
```
For the whole enriched record, use [`parseEnrichedObservation`](/docs/schema/parsing).
# Overview (/v1/docs/origins)
`@disclosureos/origins` answers the third question the standard asks: **what might explain it?**
It provides the **Origin Classification System (OCS)** — a descriptive taxonomy of origin hypotheses spanning prosaic to exotic — and the claim machinery for classifying an observation against it with attributed, weighted, evidence-backed hypotheses.
The [Origins page](/origins) in the Standard Explorer lets you browse the OCS tree and see how classifications become attributed claims.
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/origins
```
```bash
pnpm add @disclosureos/origins
```
```bash
yarn add @disclosureos/origins
```
```bash
bun add @disclosureos/origins
```
A taxonomy, not a verdict [#a-taxonomy-not-a-verdict]
The OCS organizes every proposed explanation for UAP into one tree with three domains:
| Domain | Root | Covers |
| ---------------- | ---- | ----------------------------------------------------------------------------------------------------------------- |
| **Physical** | `1` | Prosaic (natural phenomena, human craft), cryptoterrestrial, extraterrestrial, extradimensional, interdimensional |
| **Psychosocial** | `2` | Sociological (hoax, mass hysteria), psychological (misinterpretation), neurological |
| **Metaphysical** | `3` | Paranormal, occult, transcendental |
Inclusion in the taxonomy is **descriptive, not an endorsement**. "Hoax" (`2.1.5`) and "Extraterrestrial — Interstellar" (`1.1.3.2`) are both nodes because both are hypotheses people advance; the taxonomy's job is to make them addressable so claims about them can be compared, weighted, and tested. Each node carries a `scientificallyTestable` flag marking whether the hypothesis can be empirically falsified.
```ts
import { getNode } from '@disclosureos/origins';
const eth = getNode('1.1.3');
eth.label; // 'Extraterrestrial'
eth.aliases; // ['ETH']
eth.scientificallyTestable; // true
```
The slot [#the-slot]
Importing the package augments `Observation` with the `origin` slot — a **flat list of claims**, one per evaluator:
```ts
import { createOriginClaim } from '@disclosureos/origins';
observation.origin = [
createOriginClaim('1.1.3', 0.4, {
rationale: 'Performance exceeds known aerospace capability; cannot exclude advanced terrestrial program.',
alternativeHypotheses: [
{ nodeId: '1.1.1.2.1', confidence: 0.25, label: 'Classified U.S. program' },
],
evidenceRefs: ['sensor:princeton-spy1-radar'],
evaluatedBy: 'example-institution',
}),
];
```
Two axes of disagreement are modeled separately:
* **Within a claim** — one evaluator's primary hypothesis plus weighted `alternativeHypotheses` they also entertain.
* **Between claims** — different evaluators' verdicts coexist in the list. No claim is privileged; endorsement is a [scoring](/docs/scoring) concern.
The factory rejects unknown node ids, so typos can't silently corrupt the data.
Reference systems included [#reference-systems-included]
Hynek, Vallée, AARO, and GEIPAN classification systems ship as subpath imports for working with historical and institutional data — see [Reference Systems](/docs/origins/reference-systems).
In this section [#in-this-section]
* [Taxonomy](/docs/origins/taxonomy) — the OCS tree, traversal API, and node anatomy
* [Classification](/docs/origins/classification) — claims, hypothesis weights, confidence distributions
* [Reference Systems](/docs/origins/reference-systems) — Hynek, Vallée, AARO, GEIPAN
* [Claiming Origins](/docs/origins/claiming-origins) — the working guide
# Reference Systems (/v1/docs/origins/reference-systems)
Decades of UAP data are already classified under other systems. The reference systems make those classifications first-class data so historical and institutional records can be ingested without losing their original coding.
They are deliberately **off the root barrel** — the core import surface of origins is the OCS taxonomy. Reach them via subpaths:
```ts
import { HYNEK_SYSTEM, HYNEK_CATEGORIES } from '@disclosureos/origins/reference/hynek';
import { VALLEE_BEHAVIORS, VALLEE_INTERACTIONS } from '@disclosureos/origins/reference/vallee';
import { AARO_SYSTEM, AARO_CATEGORIES } from '@disclosureos/origins/reference/aaro';
import { GEIPAN_SYSTEM, GEIPAN_CATEGORIES } from '@disclosureos/origins/reference/geipan';
// or everything: '@disclosureos/origins/reference'
```
Hynek [#hynek]
J. Allen Hynek's close-encounter scale, extended with the proposed CE4–CE7 categories:
| Category | Label |
| ----------- | -------------------------------------------------- |
| `NL` | Nocturnal Light |
| `DD` | Daylight Disc |
| `RV` | Radar-Visual |
| `CE1`–`CE7` | Close Encounters of the First through Seventh Kind |
```ts
HYNEK_SYSTEM.RV;
// { category: 'RV', label: 'Radar-Visual',
// description: 'Anomaly confirmed on radar and simultaneously by visual observation.' }
```
Vallée [#vallée]
Jacques Vallée's two-axis system: a behavior code (`AN` anomaly, `FB` flyby, `MA` maneuver, `CE` close encounter) crossed with an interaction level (1–5), plus the SVP credibility rating (source/site visit/possible-explanation digits).
```ts
import type { ValleeClassification, ValleeSVP } from '@disclosureos/origins/reference/vallee';
```
AARO [#aaro]
The Pentagon's All-domain Anomaly Resolution Office resolution categories — the vocabulary modern U.S. government reporting resolves cases into:
`airborne_clutter` · `natural_atmospheric` · `usg_industry` · `foreign_adversary` · `other_scientific_discovery`
GEIPAN [#geipan]
The French space agency's case-classification grades — the longest-running official civilian UAP investigation:
`A` (identified) · `B` (probably identified) · `C` (insufficient data) · `D1`/`D2` (unidentified after rigorous analysis), with strangeness/consistency ratings.
Relationship to the OCS [#relationship-to-the-ocs]
Reference classifications and OCS claims answer different questions:
* **Hynek/Vallée** describe the *phenomenology* of the encounter (what kind of sighting it was) — not what caused it.
* **AARO/GEIPAN** record an *institutional resolution status*.
* **The OCS** organizes *causal hypotheses* with attributed confidence.
A record can carry all three: a Hynek `RV` coding from a 1960s catalog, a GEIPAN `D1` status, and a modern OCS claim — none overwrites the others. Keep reference codings in your ingest mapping or the `extensions` bag, and express causal judgments as [origin claims](/docs/origins/classification).
# Taxonomy (/v1/docs/origins/taxonomy)
The Origin Classification System is a tree of hypothesis nodes identified by numeric dot-paths. Depth encodes specificity: `1` (domain) → `1.1` (subdomain) → `1.1.3` (category) → `1.1.3.2` (leaf).
Node anatomy [#node-anatomy]
```ts
interface OCSNode {
id: string; // '1.1.2.1'
label: string; // 'Ancient / Proto-Human Survival'
description: string;
domain: 'physical' | 'psychosocial' | 'metaphysical';
depth: number; // 0=domain … 4=leaf
parentId: string | null;
children: string[]; // child ids, not nested nodes
scientificallyTestable: boolean; // can this be empirically falsified?
aliases?: string[]; // 'ETH', 'CTH', ...
}
```
The tree at a glance [#the-tree-at-a-glance]
```
1 Physical
├─ 1.1 Intradimensional
│ ├─ 1.1.1 Prosaic (natural phenomena, human craft — governmental,
│ │ extra-governmental, private)
│ ├─ 1.1.2 Cryptoterrestrial (ancient survival, breakaway civilization, …)
│ └─ 1.1.3 Extraterrestrial (solar system, interstellar, intergalactic, …)
├─ 1.2 Extradimensional (hyperspace entities, brane projections, …)
└─ 1.3 Interdimensional (multiverse, time travel, scale invariance)
2 Psychosocial
├─ 2.1 Sociological (hypnosis, mass hysteria, memetics, hoax)
├─ 2.2 Psychological (misinterpretation, screen memory, …)
└─ 2.3 Neurological (pharmacology, pathology, …)
3 Metaphysical
├─ 3.1 Paranormal (NDE/OBE, disembodied consciousness, …)
├─ 3.2 Occult
└─ 3.3 Transcendental
```
Prosaic explanations are first-class citizens: most observations *are* `1.1.1.x`, and a framework that couldn't say so wouldn't be credible when it says otherwise.
Traversal API [#traversal-api]
```ts
import {
getNode, getChildren, getAncestors, getSiblings,
getDomain, getPath, getLeafNodes, getTestableNodes, searchNodes,
} from '@disclosureos/origins';
getNode('1.1.3'); // the ETH node (undefined for unknown ids)
getChildren('1.1.3'); // solar system, interstellar, intergalactic, unknown region
getAncestors('1.1.3.2'); // [node '1', node '1.1', node '1.1.3'] — root to parent
getSiblings('1.1.2'); // prosaic, extraterrestrial
getDomain('psychosocial'); // every node in the psychosocial domain
getPath('1.1.3.2'); // 'Physical > Intradimensional > Extraterrestrial > Interstellar'
getTestableNodes(); // every node with scientificallyTestable: true
searchNodes('hoax'); // search labels, descriptions, aliases
```
Constants give you the id sets directly:
```ts
import {
ALL_OCS_IDS, LEAF_IDS, TESTABLE_IDS,
PHYSICAL_IDS, PSYCHOSOCIAL_IDS, METAPHYSICAL_IDS,
OCS_TAXONOMY, // the full OCSNode[] tree
} from '@disclosureos/origins';
```
Choosing the right depth [#choosing-the-right-depth]
Claim at the depth your evidence supports — no deeper:
| Evidence supports... | Claim |
| ------------------------------------------------------ | ----------------------------------- |
| "Not prosaic, physical craft, origin unknown" | `1.1` or `1` |
| "Consistent with non-human technology, locale unknown" | `1.1.3` |
| "Specifically interstellar" | `1.1.3.2` — and expect to defend it |
A confident claim at `1.1` is more useful than a speculative one at `1.1.3.2`. The depth of a node is a measure of *specificity*, not of merit.
Display companions [#display-companions]
```ts
import { OCS_LABELS, ORIGIN_DOMAIN_LABELS } from '@disclosureos/origins';
import { formatOCSCode, formatNodeLabel, formatDomainLabel, formatTestability } from '@disclosureos/origins';
```
# Quick Start (/v1/docs/platform)
These instructions cover the published v1 packages. Experimental v2 inspection
is available separately. See [Versions and release status](/docs/platform/versions)
before choosing a format or migration path.
import { Database, Radar, Network, Scale, Gauge, SatelliteDish, Braces, Terminal } from 'lucide-react';
Introduction [#introduction]
DisclosureOS is an open-source standard for structuring UAP evidence. It gives every tool and institution the same vocabulary for describing an observation, the same criteria for what makes it anomalous, the same taxonomy for what might explain it, and the same measures for how complete and compelling the case is.
The standard has six parts, each answering one question about an observation:
} title="Records" description="What was observed? The shared data dictionary — time, place, object, witnesses, and sensor evidence." href="/docs/records" />
} title="Observables" description="What anomalous characteristics did it show? Detection criteria across the Technology and Biologics frameworks." href="/docs/observables" />
} title="Origins" description="What might explain it? The Origin Classification System — a taxonomy of competing hypotheses." href="/docs/origins" />
} title="Claims" description="Who assessed it, and on what evidence? Attributed claims, with evaluator disagreement preserved." href="/docs/claims" />
} title="Scoring" description="How complete and compelling is the case? Multiple measures, not one reductive number." href="/docs/scoring" />
} title="Instruments" description="What hardware produced the data? Published sensor manifests — specs, timing, and calibration." href="/docs/instruments" />
Five of these ship as their own npm package; **Claims** is the attribution model that lives inside `@disclosureos/records` and threads through Observables and Origins. Two supporting packages complete the foundation:
} title="Schema" description="The portable contract that binds the package-backed parts into one enriched, validated Observation — TypeScript and JSON Schema." href="/docs/schema" />
} title="CLI" description="Scaffold, validate, and inspect observations from the command line." href="/docs/cli" />
Read [What is DisclosureOS?](/docs/platform/what-is-disclosureos) for the model, the architecture, and why an open standard matters.
Two starting paths [#two-starting-paths]
Installation [#installation]
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/records @disclosureos/observables @disclosureos/origins @disclosureos/instruments @disclosureos/scoring @disclosureos/schema
```
```bash
pnpm add @disclosureos/records @disclosureos/observables @disclosureos/origins @disclosureos/instruments @disclosureos/scoring @disclosureos/schema
```
```bash
yarn add @disclosureos/records @disclosureos/observables @disclosureos/origins @disclosureos/instruments @disclosureos/scoring @disclosureos/schema
```
```bash
bun add @disclosureos/records @disclosureos/observables @disclosureos/origins @disclosureos/instruments @disclosureos/scoring @disclosureos/schema
```
All packages are ESM-only and require Node 20+. See the [Installation](/docs/platform/installation) guide for subpath imports and TypeScript configuration.
The golden path [#the-golden-path]
One observation through every part of the standard — the USS Nimitz "Tic Tac" encounter (2004). This is the core workflow of the entire framework.
1. Records — describe what was observed [#1-records--describe-what-was-observed]
```ts title="golden-path.ts"
import { createObservation, createSensorReading } from '@disclosureos/records/factories';
import { evidenceRef } from '@disclosureos/records/shared';
// A sensor reading the claims below will cite as evidence. `sensorRef`
// points at a published manifest entry — created in step 2.
const radar = createSensorReading('shipborne_radar', 'radar_phased_array', {
id: 'princeton-spy1-radar',
platform: 'USS Princeton (CG-59), AN/SPY-1 phased array',
operator: 'US Navy',
sensorRef: 'us-navy:princeton-spy1',
});
const RADAR = evidenceRef('sensor', radar.id);
const base = createObservation(
{
temporal: { date: '2004-11-14', dateCertainty: 'exact', durationSeconds: 300 },
location: { name: 'Pacific Ocean, ~100mi SW of San Diego', country: 'United States', latitude: 31.5, longitude: -117.5, siteType: 'ocean' },
summary: 'Tic-Tac-shaped craft tracked on radar and intercepted by F/A-18 pilots.',
objectCharacteristics: { shape: 'tic_tac', sizeMeters: 12, color: 'white', numberObserved: 1 },
witnesses: { count: 4, categories: ['military_pilot', 'radar_operator'], militaryWitnesses: true, multipleIndependent: true },
sensorEvidence: { sensors: [radar] },
},
{ id: 'nimitz-tic-tac-2004', status: 'published' },
);
```
2. Instruments — publish the hardware behind the reading [#2-instruments--publish-the-hardware-behind-the-reading]
The reading above cites a manifest entry through `sensorRef` (`:`). A **sensor manifest** is a standalone document the operating organization publishes once — hardware specifications, timing sources, and calibration status — so every record that cites the instrument shares one source of truth.
```ts
import { createSensorEntry, createSensorManifest } from '@disclosureos/instruments';
const spy1 = createSensorEntry('princeton-spy1', 'AN/SPY-1B Passive Phased Array Radar', 'radio_frequency', {
recordsMapping: { sensorType: 'shipborne_radar', detectionMethod: 'radar_phased_array' },
timing: { timeSource: 'gps_disciplined' },
calibration: { status: 'documented', currentMethod: 'Navy maintenance and alignment program', referenceInUse: true },
});
const manifest = createSensorManifest('US Navy', 'us-navy', [spy1]);
```
3. Observables — claim what was anomalous [#3-observables--claim-what-was-anomalous]
Anomalies are recorded as **claims**: attributed, evidence-backed assertions. Multiple evaluators can claim the same observable — disagreement is first-class data.
```ts
import { createObservableClaim } from '@disclosureos/observables';
const observableAssessments = {
technology: {
instantaneous_acceleration: [
createObservableClaim('confirmed', {
confidence: 0.85,
rationale: 'Radar tracked descent from ~80,000 ft to sea level in seconds.',
evidenceRefs: [RADAR],
}),
],
},
};
```
4. Origins — claim what might explain it [#4-origins--claim-what-might-explain-it]
Origin hypotheses come from the [Origin Classification System](/docs/origins/taxonomy), a descriptive taxonomy of competing explanations. A claim can carry alternative hypotheses with their own confidence.
```ts
import { createOriginClaim } from '@disclosureos/origins';
const origin = [
createOriginClaim('1.1.3', 0.4, {
rationale: 'Performance exceeds known aerospace capability.',
alternativeHypotheses: [{ nodeId: '1.1.1.2.1', confidence: 0.25, label: 'Classified U.S. program' }],
evidenceRefs: [RADAR],
}),
];
```
5. Claims — preserve who said what [#5-claims--preserve-who-said-what]
The Observables and Origins examples above both produced claims. Claims are not a separate package; they are the shared attribution structure that records who made an assessment, why, and which evidence inside the record supports it. This is what lets two evaluators disagree without overwriting each other.
6. Schema — validate the enriched whole [#6-schema--validate-the-enriched-whole]
`parseEnrichedObservation` validates the core record **and** every package-owned slot in one call, without stripping anything.
```ts
import { parseEnrichedObservation } from '@disclosureos/schema';
const observation = { ...base, observableAssessments, origin };
const parsed = parseEnrichedObservation(observation);
if (!parsed.success) {
console.error(parsed.issues); // [{ path, message }, ...]
}
```
Never validate an enriched record with `ObservationSchema.parse()` — Zod will silently drop the `observableAssessments` and `origin` slots. Always use `parseEnrichedObservation`. Read more in [Type Safety & Validation](/docs/platform/concepts/type-safety).
7. Scoring — measure the case [#7-scoring--measure-the-case]
```ts
import { getCompleteness, score, calibrationTrust } from '@disclosureos/scoring';
const completeness = getCompleteness(observation); // field coverage of the record
const compellingness = score(observation); // signal from claims + evidence
// Optional: credit claims backed by instruments with published calibration.
const instrumentAware = score(observation, {
evaluatorWeight: calibrationTrust(observation, [manifest]),
});
console.log(completeness.percentage); // e.g. 38
console.log(compellingness.score); // e.g. 0.71
console.log(compellingness.contested); // do evaluators disagree?
```
Run the complete example from the monorepo:
```bash title="Terminal"
pnpm --filter @disclosureos/examples golden-path
```
Validate from the command line [#validate-from-the-command-line]
The CLI wraps the same validation pipeline:
```bash title="Terminal"
npx @disclosureos/cli validate observation.json
```
It checks the core record, every package-owned slot, and warns on evidence references that don't resolve. See the [CLI docs](/docs/cli) for `scaffold`, `registry`, and `info`.
FAQ [#faq]
TypeScript 5.0 or later with `"moduleResolution": "bundler"` (or `"node16"` / `"nodenext"`) and `"strict": true`. Packages ship with built-in types — no separate `@types` install needed.
Yes. `@disclosureos/records` depends only on Zod and works standalone. `observables`, `origins`, `instruments`, and `scoring` each depend on `records`. Install only the layers you need — but install `schema` whenever you use more than the core record, so validation covers every slot.
No. The v1 foundation packages are ESM-only and require Node 20+. See [Installation](/docs/platform/installation).
Because evidence evaluation is contested by nature. Each observable or origin slot holds a list of claims — one per evaluator — with attribution, confidence, and evidence references. Scoring surfaces disagreement instead of hiding it. See [Tour of the API](/docs/platform/concepts).
Yes. Every layer publishes JSON Schema (draft 2020-12), and `@disclosureos/schema` composes them into one enriched-observation contract usable from Python, Rust, or anything else. See [JSON Schema](/docs/schema/json-schema).
Learn more [#learn-more]
Understanding the model [#understanding-the-model]
Building on the standard [#building-on-the-standard]
# Installation (/v1/docs/platform/installation)
These instructions cover the published v1 packages. Experimental v2 inspection
is available separately. See [Versions and release status](/docs/platform/versions)
before choosing a format or migration path.
Requirements [#requirements]
* **Node.js 20 or later**
* **TypeScript 5.0+** (for TypeScript projects) with `"strict": true`
* An **ESM** project — the v1 packages do not ship CommonJS builds
Install the foundation [#install-the-foundation]
Install the foundation packages and the contract:
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/records @disclosureos/observables @disclosureos/origins @disclosureos/instruments @disclosureos/scoring @disclosureos/schema
```
```bash
pnpm add @disclosureos/records @disclosureos/observables @disclosureos/origins @disclosureos/instruments @disclosureos/scoring @disclosureos/schema
```
```bash
yarn add @disclosureos/records @disclosureos/observables @disclosureos/origins @disclosureos/instruments @disclosureos/scoring @disclosureos/schema
```
```bash
bun add @disclosureos/records @disclosureos/observables @disclosureos/origins @disclosureos/instruments @disclosureos/scoring @disclosureos/schema
```
Or only what you need — each package is independently useful:
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/records
```
```bash
pnpm add @disclosureos/records
```
```bash
yarn add @disclosureos/records
```
```bash
bun add @disclosureos/records
```
| Package | Depends on | Install when |
| --------------------------- | ----------------------------------- | -------------------------------------------------------------------------------- |
| `@disclosureos/records` | Zod only | Always — it is the shared record vocabulary |
| `@disclosureos/observables` | `records` | You assess anomalous characteristics |
| `@disclosureos/origins` | `records` | You classify origin hypotheses |
| `@disclosureos/scoring` | `records`, `observables`, `origins` | You measure completeness or compellingness |
| `@disclosureos/schema` | records, observables, origins | You validate enriched records (recommended whenever you use more than `records`) |
The CLI doesn't need installing — run it with `npx`:
```bash title="Terminal"
npx @disclosureos/cli validate observation.json
```
TypeScript configuration [#typescript-configuration]
```json title="tsconfig.json"
{
"compilerOptions": {
"strict": true,
"module": "esnext",
"moduleResolution": "bundler",
"target": "es2020"
}
}
```
`"moduleResolution": "node16"` or `"nodenext"` also work. All packages ship built-in type declarations — there are no `@types/*` packages to install.
The v1 packages publish ESM exclusively. If your project uses `require()`, you'll need to migrate to `import` (or dynamic `import()`) to use them.
Imports [#imports]
The root barrel of each package exports its full public API:
```ts
import { createObservation, validateObservation } from '@disclosureos/records';
import { createObservableClaim } from '@disclosureos/observables';
import { createOriginClaim } from '@disclosureos/origins';
import { parseEnrichedObservation } from '@disclosureos/schema';
import { getCompleteness, score } from '@disclosureos/scoring';
```
Subpath imports [#subpath-imports]
Packages also expose focused subpaths for tighter imports:
```ts
// Records: domain modules and companions
import { createObservation } from '@disclosureos/records/factories';
import { evidenceRef, type Claim } from '@disclosureos/records/shared';
import type { Observation } from '@disclosureos/records/observation';
// Observables: the two frameworks
import { TECHNOLOGY_OBSERVABLES } from '@disclosureos/observables/technology';
import { BIOLOGICS_OBSERVABLES } from '@disclosureos/observables/biologics';
// Origins: taxonomy and reference systems
import { getNode, searchNodes } from '@disclosureos/origins/taxonomy';
import { HYNEK_SYSTEM } from '@disclosureos/origins/reference/hynek';
// Scoring: one measure at a time
import { getCompleteness } from '@disclosureos/scoring/completeness';
import { score } from '@disclosureos/scoring/compellingness';
```
Side-effect imports and typing [#side-effect-imports-and-typing]
Importing `@disclosureos/observables` or `@disclosureos/origins` augments the `Observation` type with that layer's slot. Importing `@disclosureos/schema` registers both slots and gives you the `EnrichedObservation` convenience type:
```ts
import type { EnrichedObservation } from '@disclosureos/schema';
// observation.observableAssessments and observation.origin are now typed
```
JSON Schema artifacts [#json-schema-artifacts]
Every package commits its generated JSON Schema (draft 2020-12) and exposes it as a subpath:
```ts
import recordsSchema from '@disclosureos/records/schema' with { type: 'json' };
import enrichedSchema from '@disclosureos/schema/schema' with { type: 'json' };
```
The schemas are also hosted at `https://os.disclosure.org/schema/...` for non-JavaScript consumers. See [JSON Schema](/docs/schema/json-schema).
Next steps [#next-steps]
Walk the [golden path](/docs/platform#the-golden-path) — one observation through every layer — or jump into the [Tour of the API](/docs/platform/concepts).
# Versions and release status (/v1/docs/platform/versions)
Choose a version [#choose-a-version]
The portal opens on **v2 Beta** for new visitors. Use the version selector to switch to **v1**, the previous stable standard. The URL identifies the selected documentation; an explicit v1 link keeps opening v1.
| Surface | Status | Entry point |
| ---------------------- | ---------------------------------------------------------------- | ------------------------------------------------- |
| v2 portal | Public beta, using exact published `2.0.0-beta.0` packages | [Meet v2](/v2/docs) |
| v2 local inspector | Published records parser; structural and semantic checks | [Inspect a sample](/v2/onboard) |
| v1 reference | Previous stable contracts and package pins | [v1 installation](/v1/docs/platform/installation) |
| Legacy local inspector | Legacy migration preview and a pinned experimental source bridge | [Previous inspector](/v1/onboard) |
| Dataset viewer | Public partner preview with source limitations | [Explore datasets](/datasets) |
The six library packages and CLI have been published as `2.0.0-beta.0` under the npm `beta` tag. Their `latest` tags remain on v1. The public beta is the portal default, not a declaration of stable v2.
The [portal inventory](/releases/portal.json) records both dependency sets, source identities and immutable hosted schema hashes. The [beta publication receipt](/releases/2.0.0-beta.0.json) records artifact checks and the fresh-registry consumer verification.
Different kinds of version [#different-kinds-of-version]
**Package versions** identify npm releases. This beta coordinates all seven packages at `2.0.0-beta.0`.
**Schema versions** identify document contracts. Some existing schema URLs already contain `2.0.0` and belong to the v1 package set. That number alone does not identify the overall standard version. Existing schema URLs retain their original bytes.
**Source pins** identify a particular implementation snapshot. The v1 explorer and legacy local inspector retain their separate pins. The new v2 inspector resolves the published beta records package directly.
What the beta does not establish [#what-the-beta-does-not-establish]
Passing structural and semantic checks does not establish source authenticity, calibration accuracy or a scientific conclusion. Profile and external checks that have not run remain `not_checked`.
The ELDÆON page displays a bounded public visualization extract. The native archive and partner mapping approval are pending. Independent semantic exchange, real-data reproduction, qualified scientific review and governance remain stable-release gates. Making the beta easier to find does not imply those reviews are complete.
# What is DisclosureOS? (/v1/docs/platform/what-is-disclosureos)
The problem [#the-problem]
UAP data is scattered across government releases, archives, FOIA productions, civilian reporting networks, and research institutions — each with its own formats, vocabulary, and standards of rigor. Tools built on this data cannot interoperate, analyses cannot be compared, and the public questions that actually matter go unanswered:
1. **Is it technologically anomalous?**
2. **Is there anomalous biology?**
3. **What is its origin?**
Answering those questions credibly requires a shared structure: a common language for what was observed, explicit criteria for what counts as anomalous, a taxonomy of explanations that can compete on evidence, and measures of case quality that don't reduce everything to one number.
DisclosureOS is that structure — an open-source standard, free to build on, with every schema and scoring rule public and auditable.
The six parts [#the-six-parts]
The standard has six parts. Five are their own npm package. Claims live inside `@disclosureos/records` because attribution is shared by both Observables and Origins:
| Part | Package surface | Question |
| --------------- | ------------------------------ | ------------------------------------------- |
| **Records** | `@disclosureos/records` | What was observed? |
| **Observables** | `@disclosureos/observables` | What anomalous characteristics did it show? |
| **Origins** | `@disclosureos/origins` | What might explain it? |
| **Claims** | `@disclosureos/records/shared` | Who assessed it, why, and on what evidence? |
| **Scoring** | `@disclosureos/scoring` | How complete and compelling is the case? |
| **Instruments** | `@disclosureos/instruments` | What hardware produced the data? |
**[Claims](/docs/claims)** are the attribution layer. They are not a separate package; they are the shared structure that lets Observables and Origins record *who* assessed a case and *on what evidence*. More on that [below](#claims-contested-assessments-are-first-class).
Records is the shared vocabulary [#records-is-the-shared-vocabulary]
`Observation` is the core type of the entire standard — a structured record of time, place, object characteristics, movement, witnesses, sensor evidence, investigation status, media, and relations to other observations. Records also provides the shared primitives every other layer builds on: `Confidence`, `Attribution`, `Claim`, evidence references, and validation helpers.
Observables and Origins augment the record [#observables-and-origins-augment-the-record]
Importing `@disclosureos/observables` or `@disclosureos/origins` adds typed slots to `Observation` — `observableAssessments` and `origin` — through TypeScript module augmentation. The record stays one object; the layers enrich it without forking it.
Schema is the contract, not another part [#schema-is-the-contract-not-another-part]
`@disclosureos/schema` mirrors the compile-time augmentation at runtime. It composes the package-owned JSON Schemas into one enriched-observation contract and provides `parseEnrichedObservation` — validation of the whole record, slots intact. Because the contract is JSON Schema (draft 2020-12), the standard is portable to Python, Rust, or any other ecosystem.
Scoring consumes, never augments [#scoring-consumes-never-augments]
Scoring reads the enriched record and produces two orthogonal measures: **completeness** (how thoroughly the record is documented) and **compellingness** (how strong the anomaly signal is, given the claims and their evidence). There is deliberately no single headline score — flattening contested evidence into one number is how rigor dies.
Instruments is a standalone document [#instruments-is-a-standalone-document]
`@disclosureos/instruments` defines the **sensor manifest** — a document an organization publishes once to describe its instruments: hardware specifications, timing sources, calibration status, and raw-data locators. It is not another slot on `Observation`; sensor readings inside a record cite manifest entries through the `sensorRef` field, and scoring can consume manifests through the `calibrationTrust` hook to credit claims backed by instruments with published calibration provenance. Read the [Instruments docs](/docs/instruments) for the full model.
Claims: contested assessments are first-class [#claims-contested-assessments-are-first-class]
Evaluative statements — "this showed instantaneous acceleration," "this is best explained by an unknown advanced craft" — are recorded as **claims**: attributed assertions with confidence, rationale, and references to specific evidence in the record.
```ts
{
level: 'confirmed',
confidence: 0.85,
rationale: 'Radar tracked descent from ~80,000 ft to sea level in seconds.',
evidenceRefs: ['sensor:princeton-spy1-radar'],
evaluatedBy: 'Scientific Coalition for UAP Studies',
evaluatedAt: '2026-03-01T00:00:00Z',
}
```
Every slot holds a **list** of claims. Two institutions can assess the same case differently, and both assessments live in the record with their evidence. Scoring detects when claims disagree in direction and flags the case as `contested` — disagreement is surfaced, not averaged away.
Who it's for [#who-its-for]
* **Research institutions** structure their case analyses once and publish them in a format every other tool can read — with attribution preserved.
* **Tool builders** skip years of schema design and inherit validation, scoring, and a taxonomy that improves over time.
* **Data engineers** ingest government releases, archives, and FOIA productions into a common record shape that downstream analysis can rely on.
* **Anyone auditing a claim** can trace a score back through claims to the specific sensor readings and testimony that support it.
What exists today [#what-exists-today]
* The foundation packages — `records`, `observables`, `origins`, `instruments`, `scoring`, `schema`, and `cli` — are at v1 and ready to publish: ESM-only, Node 20+, MIT-licensed.
* A runnable [golden path](/docs/platform#the-golden-path) takes the USS Nimitz encounter through every layer.
* The [Disclosure Index](https://index.disclosure.org) — shareable case pages with observables, origins, and scores in one view — is the flagship application, being rebuilt on these packages.
Next steps [#next-steps]
Start with the [Quick Start](/docs/platform), then take the [Tour of the API](/docs/platform/concepts) to build the full mental model.
# Companions (/v1/docs/records/companions)
Every vocabulary in the lexicon ships [companions](/docs/platform/concepts/patterns) — at minimum a constants array and a type guard, plus labels and formatters where the values surface in a UI. This page is the reference for what records provides and where.
Subpaths [#subpaths]
| Subpath | Contents |
| ---------------------------------- | ----------------------------------------------------------------------------------------- |
| `@disclosureos/records/constants` | Value arrays: `SOURCE_TYPES`, `TIMES_OF_DAY`, `SITE_TYPES`, `OBJECT_SHAPES`, ... |
| `@disclosureos/records/guards` | Type guards: `isSourceType`, `isTimeOfDay`, `isObjectShape`, ... |
| `@disclosureos/records/factories` | `createObservation`, `createTemporalData`, `createMediaAttachment`, `createSensorReading` |
| `@disclosureos/records/formatters` | `formatTemporalData`, `formatFuzzyDate`, `formatDateRange`, `formatSourceType`, ... |
| `@disclosureos/records/labels` | Display maps: `SOURCE_TYPE_LABELS`, `TIME_OF_DAY_LABELS`, `OBJECT_SHAPE_LABELS`, ... |
| `@disclosureos/records/validators` | `validateObservation`, `isValidLatitude`, `isValidLongitude`, `isValidISODate` |
Everything is also re-exported from the package barrel.
Constants and guards [#constants-and-guards]
Constants are derived directly from the Zod schemas (`Schema.options`), so they can never drift from the types. Guards are built with `makeGuard(Schema)` — they narrow `unknown` to the schema's type:
```ts
import { SOURCE_CREDIBILITIES } from '@disclosureos/records/constants';
import { isSourceCredibility } from '@disclosureos/records/guards';
SOURCE_CREDIBILITIES;
// ['official', 'verified', 'credible', 'unverified', 'questionable', 'disputed', 'unknown']
if (isSourceCredibility(input)) {
// input: SourceCredibility
}
```
Covered vocabularies include source types and credibility, temporal certainty, date granularity and range types, time of day, media types, site/terrain types, airspace classes, location sensitivity, coordinate precision, object shapes, maneuver types, relation kinds, confidence levels, and publication status.
Factories [#factories]
Factories fill defaults, generate ids, and **parse on construction** — they throw on invalid input and their return values are guaranteed valid:
```ts
import {
createObservation,
createTemporalData,
createMediaAttachment,
createSensorReading,
} from '@disclosureos/records/factories';
const temporal = createTemporalData('2004-11-14', 'exact', { durationSeconds: 300 });
const photo = createMediaAttachment('image', 'https://example.org/frame.png');
// photo.id auto-generated — citable via evidenceRef('media', photo.id)
const radar = createSensorReading('shipborne_radar', 'radar_phased_array');
// radar.id auto-generated — citable via evidenceRef('sensor', radar.id)
```
`createMediaAttachment` and `createSensorReading` auto-generate stable `id`s precisely so that [claims can cite them](/docs/records/shared#evidence-references).
Formatters and labels [#formatters-and-labels]
Formatters turn structured values into prose; labels map enum values to display names:
```ts
import { formatTemporalData, formatFuzzyDate } from '@disclosureos/records/formatters';
import { OBJECT_SHAPE_LABELS } from '@disclosureos/records/labels';
formatTemporalData({ date: '2004-11-14', dateCertainty: 'approximate' });
// "2004-11-14 [Approximate]"
formatFuzzyDate({ value: '1947-07', granularity: 'month', certainty: 'approximate' });
// "July 1947"
OBJECT_SHAPE_LABELS['tic_tac']; // "Tic Tac"
```
The temporal formatters understand the full fuzzy-date model — exact dates, ranges, granularities, and relative dates — so UIs never reimplement date display logic.
Validators [#validators]
```ts
import {
validateObservation,
isValidLatitude,
isValidLongitude,
isValidISODate,
} from '@disclosureos/records/validators';
```
`validateObservation` is the package's record-level validator — see [Validating at Boundaries](/docs/records/guides/validating-at-boundaries) for usage patterns and the strip hazard.
# Extension Slots (/v1/docs/records/extensions)
Five optional forensic and clerical surfaces ship inside `@disclosureos/records`. They're plain optional fields on `Observation`, with their schemas exposed subpath-only to keep the core barrel lean:
```ts
import { ProvenanceSchema } from '@disclosureos/records/extensions/provenance';
import { CrossReferencesSchema } from '@disclosureos/records/extensions/identifiers';
import { TestimonyStatementSchema } from '@disclosureos/records/extensions/testimony';
import { PhysicalEvidenceItemSchema } from '@disclosureos/records/extensions/physical';
import { DocumentMetadataSchema } from '@disclosureos/records/extensions/document';
```
These are *records-owned* slots — part of `ObservationSchema`, validated by `validateObservation`. (Cross-package slots like `observableAssessments` attach through [module augmentation](/docs/records/observation#the-extension-seam) instead.)
provenance — chain of custody and digital integrity [#provenance--chain-of-custody-and-digital-integrity]
Where evidence came from and whether it has been altered:
* **Chain of custody** — ordered custody entries: who held the material, when, and what action they took
* **Digital provenance** — file hashes (`FileHash`, with algorithm), capture metadata, third-party verification results
Use it when the integrity of media or documents is itself part of the case — FOIA productions, leaked footage, archival scans.
identifiers — cross-references [#identifiers--cross-references]
External identifiers tying the record to other catalogs: case numbers in legacy databases, archive accession numbers, FOIA request ids. This is how the same event is recognized across NICAP, Blue Book, NUFORC, and modern datasets without merging records prematurely.
testimony — witness statements [#testimony--witness-statements]
An array of `TestimonyStatement` items: the statement text, the witness (with category and credibility assessment), when and to whom it was given. Each statement has a `statementId` — which makes it **citable evidence**:
```ts
import { evidenceRef } from '@disclosureos/records/shared';
const ref = evidenceRef('testimony', statement.statementId); // "testimony:"
```
physicalEvidence — material traces [#physicalevidence--material-traces]
An array of `PhysicalEvidenceItem`: trace material, ground effects, recovered objects — each with type, custody status, analysis results, and an evidence-quality assessment. Items carry an `id`, so claims can cite them with `evidenceRef('physical', item.id)`.
documents — document metadata [#documents--document-metadata]
Structured metadata for primary documents attached to a record: title, author, classification history, release authority, page count. Pairs with `media` attachments holding the files themselves.
When to reach for extensions [#when-to-reach-for-extensions]
| You're recording | Use |
| --------------------------------------------- | --------------------------------- |
| Basic facts of the sighting | The core domains — not extensions |
| Who has held a piece of footage since capture | `provenance` |
| "This is NICAP case #1961-04" | `identifiers` |
| A pilot's signed statement | `testimony` |
| Soil samples, radiation readings | `physicalEvidence` |
| A declassified memo's archival metadata | `documents` |
The extensions are deliberately off the hot path: most records won't have them, and tools can ignore them safely. But when an [observable claim](/docs/observables) needs to cite a witness statement or a lab analysis, these slots make that evidence addressable.
# Overview (/v1/docs/records)
`@disclosureos/records` answers the first question the standard asks: **what was observed?**
It provides the `Observation` type — the core record of the entire standard — and the shared primitives every other layer builds on. It depends only on Zod, so it works standalone in any TypeScript project.
The [Records page](/records) in the Standard Explorer shows how the Observation fields are grouped and rendered from the package schema.
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/records
```
```bash
pnpm add @disclosureos/records
```
```bash
yarn add @disclosureos/records
```
```bash
bun add @disclosureos/records
```
What's in the package [#whats-in-the-package]
| Module | Provides |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| [Observation](/docs/records/observation) | The core record: temporal, location, object, movement, witnesses, sensors, media, relations |
| [Shared primitives](/docs/records/shared) | `Confidence`, `Attribution`, `Claim`, evidence refs, validation helpers — the primitives the other parts reuse |
| [Extension slots](/docs/records/extensions) | Forensic and clerical surfaces: provenance, identifiers, testimony, physical evidence, documents |
| [Companions](/docs/records/companions) | Constants, guards, factories, formatters, labels for every vocabulary in the lexicon |
Sixty seconds of records [#sixty-seconds-of-records]
```ts
import { createObservation, createSensorReading } from '@disclosureos/records/factories';
import { validateObservation } from '@disclosureos/records';
const radar = createSensorReading('shipborne_radar', 'radar_phased_array', {
platform: 'USS Princeton (CG-59)',
});
const observation = createObservation(
{
temporal: { date: '2004-11-14', dateCertainty: 'exact' },
location: {
name: 'Pacific Ocean, SW of San Diego',
country: 'United States',
latitude: 31.5,
longitude: -117.5,
siteType: 'ocean',
},
summary: 'Tic-Tac-shaped craft tracked on radar.',
witnesses: { count: 4, militaryWitnesses: true, multipleIndependent: true },
sensorEvidence: { sensors: [radar] },
},
{ id: 'nimitz-tic-tac-2004', status: 'published' },
);
const issues = validateObservation(observation); // []
```
Factories parse their output against the schema, so what they return is guaranteed valid.
Records is the shared vocabulary [#records-is-the-shared-vocabulary]
The other layers don't just sit beside records — they're built from its primitives:
* `observables` and `origins` compose their claims from records' `Claim` and `Confidence`
* Their slots attach to `Observation` through the `ObservationExtensions` extension point records declares
* `scoring` derives its completeness field inventory from records' JSON Schema
* All validation across the standard returns records' `ValidationIssue` shape
This is why records has no DisclosureOS dependencies and everything else depends on it.
`validateObservation` checks the **core** record only — it ignores `observableAssessments` and `origin`. If your record carries slots, validate with [`parseEnrichedObservation`](/docs/schema/parsing) from `@disclosureos/schema`.
JSON Schema [#json-schema]
The full lexicon is published as JSON Schema (draft 2020-12):
```ts
import { recordsJsonSchema, RECORDS_SCHEMA_VERSION } from '@disclosureos/records';
```
The committed artifact ships at `@disclosureos/records/schema` and is hosted for non-JavaScript consumers. See [JSON Schema](/docs/schema/json-schema).
# Observation (/v1/docs/records/observation)
`Observation` is the single source of truth for the record lexicon. It's a plain JSON-serializable object validated by `ObservationSchema`.
Required fields [#required-fields]
| Field | Type | Notes |
| ------------------------- | ------------------- | ----------------------------------------------------------- |
| `id` | `string` | Stable unique identifier |
| `temporal` | `TemporalData` | Date (with certainty), optional time, duration |
| `location` | `LocationData` | Name, country, coordinates, site type |
| `status` | `PublicationStatus` | `draft` · `review` · `published` · `archived` · `retracted` |
| `createdAt` / `updatedAt` | `string` | ISO timestamps |
Everything else is optional — a sparse record is a valid record. [Completeness scoring](/docs/scoring/completeness) measures how much of the lexicon a record fills in; validity never depends on it.
Descriptive domains [#descriptive-domains]
| Field | Describes |
| ------------------------- | ----------------------------------------------------------------------------------------- |
| `summary` / `description` | Short and long prose accounts |
| `objectCharacteristics` | Shape, size, color, luminosity, number observed |
| `movement` | Maneuvers, speed, acceleration, departure |
| `witnesses` | Count, categories, military/independent flags |
| `sensorEvidence` | Sensor readings — each with an `id` claims can cite |
| `investigation` | Status, investigator, findings |
| `responseImpact` | Interception response, physical effects |
| `environment` | Weather, visibility, terrain |
| `aviation` | Airspace class, nearby facilities |
| `relations` | Typed edges to other observations (`RelationEdge`) |
| `sourceData` | Where the record came from: source type, credibility |
| `media` / `featuredMedia` | Attachments — each with an `id` claims can cite |
| `eventType` | Free-text descriptor (the word *classification* is reserved for [origins](/docs/origins)) |
```ts
import type { Observation } from '@disclosureos/records/observation';
import { createObservation } from '@disclosureos/records/factories';
```
`createObservation` fills in `id`, `status`, and timestamps, then parses — invalid input throws. See [Composing an Observation](/docs/records/guides/composing-an-observation).
The extension seam [#the-extension-seam]
`Observation` carries an empty interface that other packages augment:
```ts
export interface ObservationExtensions {}
export type Observation = z.infer & ObservationExtensions;
```
Importing `@disclosureos/observables` adds the `observableAssessments` slot; importing `@disclosureos/origins` adds `origin`. The slots are typed at compile time but are **not** part of `ObservationSchema` — which is why enriched records must be validated with [`parseEnrichedObservation`](/docs/schema/parsing), never `ObservationSchema.parse()`.
Two more escape hatches exist for data outside the standard:
* **`extensions`** — a free-form `Record` bag for your own pipeline metadata. Passed through validation untouched.
* **`schemaVersion` / `dataSourceId`** — clerical metadata for migrations and ingestion tracking.
Records-owned extension slots [#records-owned-extension-slots]
Five optional forensic surfaces ship inside records itself (subpath-only schemas, plain optional fields on the record): `provenance`, `identifiers`, `testimony`, `physicalEvidence`, and `documents`. See [Extension slots](/docs/records/extensions).
Validating [#validating]
```ts
import { validateObservation } from '@disclosureos/records';
const issues = validateObservation(record);
// ValidationIssue[] — [{ path: 'location.latitude', message: '...' }]
```
`validateObservation` accepts `unknown`, never mutates, and never strips. For records carrying slots, use the [schema contract](/docs/schema) instead.
# Shared Primitives (/v1/docs/records/shared)
`@disclosureos/records/shared` is the shared primitive module: the definitions written once so that `observables`, `origins`, `scoring`, and your own code share one vocabulary for confidence, attribution, and validation.
```ts
import {
type Confidence, type Claim, type ValidationIssue,
evidenceRef, parseEvidenceRef, createEnumGuard,
} from '@disclosureos/records/shared';
```
Confidence [#confidence]
A number in `[0, 1]`. `0` is no confidence, `1` is absolute.
```ts
import { isConfidence, assertConfidence, asConfidence } from '@disclosureos/records/shared';
isConfidence(0.85); // true — guard
assertConfidence(1.2); // throws — assertion
asConfidence(0.85); // 0.85, typed as Confidence — cast with check
```
A coarser five-step `ConfidenceLevel` enum also exists (with `CONFIDENCE_LEVELS` and `isConfidenceLevel`) for vocabularies that want named tiers instead of a continuous scale.
Attribution [#attribution]
The "who evaluated this, when, and why" mixin — every assessment surface in the standard composes these fields rather than inventing its own:
```ts
interface Attribution {
evaluatedAt?: string; // ISO datetime
evaluatedBy?: string; // person, institution, or pipeline
rationale?: string; // why this assessment was made
}
```
Claim [#claim]
A `Claim` is the unit of assessment in DisclosureOS: an `Attribution` plus the in-record evidence that justifies it.
```ts
interface Claim extends Attribution {
evidenceRefs?: string[]; // "sensor:", "media:", ...
}
```
Every evaluative slot in the standard — `observableAssessments`, `origin` — holds **lists** of claims, so contested verdicts coexist with full attribution. Opinion aggregation lives in the [scoring layer](/docs/scoring), never in the record.
Evidence references [#evidence-references]
Claims point at evidence *inside the record* using `:` strings:
| Kind | Resolves to |
| ----------- | --------------------------------------------- |
| `sensor` | `observation.sensorEvidence.sensors[].id` |
| `media` | `observation.media[].id` / `featuredMedia.id` |
| `testimony` | `observation.testimony[].statementId` |
| `physical` | `observation.physicalEvidence[].id` |
```ts
import { evidenceRef, isEvidenceRef, parseEvidenceRef } from '@disclosureos/records/shared';
const ref = evidenceRef('sensor', 'radar-01'); // "sensor:radar-01"
isEvidenceRef(ref); // true
parseEvidenceRef(ref); // { kind: 'sensor', id: 'radar-01' }
```
Refs stay plain strings in the schema so they survive JSON Schema portability; the convention is enforced by the helpers and checked by `disclosureos validate`, which warns when a claim cites evidence the record doesn't contain.
ValidationIssue [#validationissue]
The canonical error shape returned by every validator in every package:
```ts
interface ValidationIssue {
path: string; // 'temporal.date', 'origin[0].confidence'
message: string;
}
```
The helpers `issuesFrom` (Zod error → issues) and `validateWith` (schema → validator function) let your own validators return the same shape.
Guard builders [#guard-builders]
```ts
import { createEnumGuard, makeGuard } from '@disclosureos/records/shared';
const isStatus = createEnumGuard(['open', 'closed'] as const);
const isPercent = makeGuard(z.number().min(0).max(100));
```
These are the same builders the packages use for their own guards — see [The Companion Pattern](/docs/platform/concepts/patterns).
Brands [#brands]
`ObservationId` is a branded string for code that wants nominal typing of record ids:
```ts
import { asObservationId, type ObservationId } from '@disclosureos/records/shared';
const id: ObservationId = asObservationId('nimitz-tic-tac-2004');
```
# Overview (/v1/docs/schema)
`@disclosureos/schema` is **not another part of the standard**. It's the contract: the package that takes the records core and the registered slots and makes them one thing — in TypeScript, in JSON Schema, and at runtime.
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/schema
```
```bash
pnpm add @disclosureos/schema
```
```bash
yarn add @disclosureos/schema
```
```bash
bun add @disclosureos/schema
```
Install it whenever a record can carry more than the core lexicon. In practice: always, except in tools that only ever touch core records.
The three things it does [#the-three-things-it-does]
| Capability | Export | Solves |
| ------------------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------------- |
| [Non-stripping validation](/docs/schema/parsing) | `parseEnrichedObservation` | The strip hazard — validate core + slots in one call, nothing dropped |
| [Runtime slot registry](/docs/schema/registry) | `ExtensionRegistry`, `defaultRegistry` | TS module augmentation is compile-time only; the registry mirrors it at runtime |
| [Composed JSON Schema](/docs/schema/json-schema) | `composeObservationSchema` | One portable artifact for Python, Rust, validators, and docs |
One import, full contract [#one-import-full-contract]
Importing the package does three things at once:
```ts
import { parseEnrichedObservation, type EnrichedObservation } from '@disclosureos/schema';
```
1. **Types** — both satellite packages are imported, so `observableAssessments` and `origin` are typed on `Observation`. `EnrichedObservation` names that complete shape.
2. **Runtime** — both first-party slots are registered on `defaultRegistry`.
3. **Validation** — `parseEnrichedObservation` is ready to validate the whole enriched record.
```ts
const result = parseEnrichedObservation(record);
if (result.success) {
// result.data — the SAME object you passed in, slots intact
} else {
// result.issues — [{ path: 'origin[0].confidence', message: '...' }, ...]
}
```
Why a contract package exists [#why-a-contract-package-exists]
The records core schema can't know about the slots — `records` can't depend on its own satellites. That leaves a gap with two failure modes:
* **The strip hazard:** `ObservationSchema.parse()` silently drops slot data.
* **The portability gap:** TS module augmentation is invisible to JSON Schema and non-TS languages.
`@disclosureos/schema` closes both at the integration layer, by **delegation**: the core is validated by records' own validator, each slot by its owner package's validator, and the JSON Schemas are merged as plain data. No layer's authority is duplicated; the contract just composes them.
Design properties [#design-properties]
* **Registry-driven.** Validation and schema composition both walk the registry. A newly registered slot is validated and composed with no changes to either code path.
* **Closed root, open extension area.** Unknown top-level keys are rejected (typo protection); arbitrary third-party data still flows through the `extensions` bag.
* **Reference-equal on success.** `parseEnrichedObservation` returns your object, not a reconstruction — nothing is stripped because nothing is rebuilt.
* **Versioned artifact.** The composed schema is enveloped with `$id` and `x-schema-version` (`ENRICHED_OBSERVATION_SCHEMA_VERSION`), with drift tests guarding the committed artifact.
# JSON Schema (/v1/docs/schema/json-schema)
Everything in DisclosureOS is expressible as JSON Schema (draft 2020-12). This is what makes it a *standard* rather than a TypeScript library: a Python ingest pipeline, a Rust validator, and a TS app can all enforce the identical contract.
The artifacts [#the-artifacts]
Every package commits a versioned schema artifact and exposes it as a subpath:
| Artifact | Import |
| ------------------------------------- | ---------------------------------- |
| Records core | `@disclosureos/records/schema` |
| Observables slot | `@disclosureos/observables/schema` |
| Origins slot | `@disclosureos/origins/schema` |
| Scoring result shapes | `@disclosureos/scoring/schema` |
| Sensor manifest (standalone document) | `@disclosureos/instruments/schema` |
| **Enriched observation (composed)** | `@disclosureos/schema/schema` |
Each is wrapped in the same envelope:
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://os.disclosure.org/schema/schema/1.1.0/enriched-observation.json",
"x-schema-version": "1.1.0",
"$ref": "#/$defs/Observation",
"$defs": { "Observation": { /* ... */ } }
}
```
The `$id` URLs are canonical and versioned — non-JavaScript consumers can pin an exact contract version.
The composed schema [#the-composed-schema]
`composeObservationSchema()` folds the records core and every [registered slot](/docs/schema/registry) into one enriched `Observation` schema:
```ts
import { composeObservationSchema } from '@disclosureos/schema';
const schema = composeObservationSchema();
```
The composition is a plain-data merge with three properties worth knowing:
* **Slots become real properties.** `observableAssessments` and `origin` appear in `properties`, `$ref`-ing the `$defs` their owner packages emitted.
* **The root closes.** The records core schema is open (`additionalProperties: true`) because slots attach by augmentation; the composed schema tightens to `additionalProperties: false` — the JSON Schema equivalent of the unknown-key rejection in [`parseEnrichedObservation`](/docs/schema/parsing).
* **`$defs` stay disjoint.** Each layer owns its definition names; a collision throws instead of silently winning.
Using it outside TypeScript [#using-it-outside-typescript]
```python title="validate.py"
import json
import jsonschema
with open('node_modules/@disclosureos/schema/schema/enriched-observation.schema.json') as f:
schema = json.load(f)
jsonschema.validate(instance=observation, schema=schema)
```
Anything with a draft 2020-12 validator works the same way: Rust (`jsonschema`), Go (`santhosh-tekuri/jsonschema`), Java (`networknt`), or a `$ref`-aware code generator for typed models in your language.
Drift protection [#drift-protection]
The committed artifacts are guarded by drift tests: regenerating the schema in CI and diffing against the committed file. A layer schema change that alters the composed artifact fails the build until the artifact and its version are updated deliberately. When you see `x-schema-version` change, the contract changed — never silently.
Custom contracts [#custom-contracts]
Compose with a custom registry to emit a narrower or extended artifact for your own ecosystem:
```ts
import { ExtensionRegistry, composeObservationSchema } from '@disclosureos/schema';
const registry = new ExtensionRegistry();
// register only the slots your consortium uses...
const contract = composeObservationSchema(registry);
```
# Parsing (/v1/docs/schema/parsing)
`parseEnrichedObservation` is the validation entry point for any record that may carry slots. One call validates everything; nothing is stripped.
Usage [#usage]
```ts
import { parseEnrichedObservation } from '@disclosureos/schema';
const result = parseEnrichedObservation(record);
```
The result [#the-result]
```ts
interface EnrichedParseResult {
success: boolean;
data?: EnrichedObservation; // reference-equal to the input, when success
issues: ValidationIssue[]; // empty iff success
}
```
```ts
if (!result.success) {
for (const issue of result.issues) {
console.error(`${issue.path}: ${issue.message}`);
}
// temporal.date: Invalid input
// observableAssessments.technology.warp_drive: unknown observable id
// origin[0].confidence: Too big: expected number to be <=1
// discloureRating: unknown top-level key "discloureRating" — third-party data belongs under "extensions".
}
```
Slot-local issue paths are prefixed with the slot name, so every issue points at the exact field — ready for form mapping or import reports.
What one call checks [#what-one-call-checks]
1. **The records core** — via records' own `validateObservation`.
2. **Every present slot** — via the owner package's canonical validator (`validateObservableAssessments`, `validateOriginClassification`), discovered through the [registry](/docs/schema/registry). Absent slots are simply skipped — a core-only record passes.
3. **Top-level key allowlist** — any key that is neither a core property nor a registered slot is rejected with a pointer to the `extensions` bag.
Validation composes by **delegation**, not by re-parsing through one mega-schema. Each package validates its own slot with its own Zod instance; the contract just orchestrates. This is the same pipeline [`disclosureos validate`](/docs/cli/validate) runs against files.
Why it never strips [#why-it-never-strips]
On success the returned `data` is the same object reference you passed in. There is no `.parse()` round-trip constructing a new object from recognized keys — which is precisely how `ObservationSchema.parse()` loses slot data:
```ts
// ❌ The hazard this function exists to close:
const stripped = ObservationSchema.parse(enriched);
stripped.origin; // undefined — silently gone
// ✅ The contract way:
const result = parseEnrichedObservation(enriched);
result.data === enriched; // true
result.data?.origin; // intact
```
Unknown keys vs. the extensions bag [#unknown-keys-vs-the-extensions-bag]
The closed root protects against typos and stale field names — the difference between an error today and corrupted data discovered in two years:
```ts
parseEnrichedObservation({ ...base, sigthing_quality: 'high' });
// issue: unknown top-level key "sigthing_quality"
parseEnrichedObservation({
...base,
extensions: { myPipeline: { sightingQuality: 'high' } },
});
// ✅ valid — extensions content is yours, passed through untouched
```
EnrichedObservation [#enrichedobservation]
The convenience type for the complete shape — `Observation` with both slots typed (the package imports both satellites, so the augmentation is active):
```ts
import type { EnrichedObservation } from '@disclosureos/schema';
function publish(obs: EnrichedObservation) {
obs.observableAssessments; // typed
obs.origin; // typed
}
```
# Registry (/v1/docs/schema/registry)
TypeScript module augmentation makes slots *typed*, but types vanish at runtime and are invisible to JSON Schema, Python, and the CLI. The `ExtensionRegistry` records the same facts as data: which slots exist, who owns them, how to validate them, and what their JSON Schema is.
The default registry [#the-default-registry]
Importing `@disclosureos/schema` registers the first-party slots on `defaultRegistry` — the runtime counterpart of importing both satellite packages for their type augmentation:
```ts
import { defaultRegistry } from '@disclosureos/schema';
defaultRegistry.list();
// [
// { slot: 'observableAssessments', owner: '@disclosureos/observables', ... },
// { slot: 'origin', owner: '@disclosureos/origins', ... },
// ]
defaultRegistry.has('origin'); // true
defaultRegistry.get('origin'); // the full registration
```
What a registration carries [#what-a-registration-carries]
```ts
interface SlotRegistration {
slot: string; // property name on Observation
owner: string; // owning package
schemaId: string; // canonical, versioned $id of the slot's JSON Schema
version: string; // the slot's x-schema-version
jsonSchema: () => Record; // standalone JSON Schema artifact
validate: (value: unknown) => ValidationIssue[]; // the owner's canonical validator
}
```
Both consumers of the registry are fully driven by it:
* [`parseEnrichedObservation`](/docs/schema/parsing) walks `list()` and calls each `validate` on present slots.
* [`composeObservationSchema`](/docs/schema/json-schema) walks `list()` and folds each `jsonSchema()` into the enriched artifact.
Registering a slot makes it validated *and* composed — no edits to either code path.
Registering a third-party slot [#registering-a-third-party-slot]
Third-party registration works today, but the extensibility contract — how external slots version, compose, and resolve conflicts — is still settling and may change in a minor release. For most third-party data, the `extensions` bag is the stable extension area.
```ts
import { defaultRegistry } from '@disclosureos/schema';
import { z } from 'zod';
import { validateWith } from '@disclosureos/records/shared';
const AcousticSchema = z.object({
infrasoundDetected: z.boolean(),
peakFrequencyHz: z.number().optional(),
});
defaultRegistry.register({
slot: 'acousticAnalysis',
owner: '@my-org/disclosureos-acoustics',
schemaId: 'https://my-org.example/schema/acoustics/1.0.0/acoustics.json',
version: '1.0.0',
jsonSchema: () => z.toJSONSchema(AcousticSchema, { target: 'draft-2020-12' }),
validate: validateWith(AcousticSchema),
});
```
From that point, `parseEnrichedObservation` validates `acousticAnalysis` when present, and `composeObservationSchema` includes it in the enriched artifact. Pair it with your own module augmentation for compile-time typing:
```ts
declare module '@disclosureos/records' {
interface ObservationExtensions {
acousticAnalysis?: z.infer;
}
}
```
Rules enforced by the registry:
* **One owner per slot** — re-registering an existing slot name throws.
* **Disjoint `$defs`** — composition throws on a `$defs` name collision rather than silently overwriting.
Custom registries [#custom-registries]
`composeObservationSchema(registry)` accepts a custom `ExtensionRegistry` for composing a subset of slots — useful in tests or when emitting a deliberately narrower contract:
```ts
import { ExtensionRegistry, composeObservationSchema } from '@disclosureos/schema';
const coreOnly = new ExtensionRegistry();
const schema = composeObservationSchema(coreOnly); // records core, no slots
```
# Compellingness (/v1/docs/scoring/compellingness)
Compellingness answers: **how strongly does this case bear on the two public questions** — anomalous technology/biologics, and a non-mundane origin? It reads the claims in the `observableAssessments` and `origin` slots and reduces them to a transparent reference score.
Usage [#usage]
```ts
import { score } from '@disclosureos/scoring';
const result = score(observation);
```
The result [#the-result]
```ts
interface ScoreResult {
score: number; // 0–1 consensus point estimate
range: { low: number; high: number }; // spread across competing claims
contested: boolean; // claims disagree on direction
components: { // per-signal contributions, before weighting
technology: number;
biologics: number;
origin: number;
};
weights: CompellingnessWeights; // what produced this score
scoringVersion: string; // methodology version (e.g. '2.0.0')
}
```
Read all three headline values together: `score: 0.71` with `range: { low: 0.68, high: 0.74 }` is a strong consensus; `score: 0.4` with `range: { low: 0, high: 0.8 }, contested: true` is a live dispute — and presenting it as "0.4" alone would be a lie of omission.
How the signal is computed [#how-the-signal-is-computed]
**Observable claims** convert to anomaly magnitude via evidentiary tier × confidence:
| Level | Weight |
| --------------- | ------ |
| `not_indicated` | 0 |
| `reported` | 0.25 |
| `documented` | 0.5 |
| `measured` | 0.75 |
| `confirmed` | 1.0 |
Within each domain (technology, biologics) the scorer takes the **strongest established observable** — the "is there *any* anomaly?" reduction. Six weak signals don't outscore one confirmed one.
**Origin claims** contribute their confidence — unless the hypothesis is **prosaic** (the OCS `1.1.1` branch by default), in which case the magnitude is zero. A confident "it was a balloon" classification doesn't just fail to add anomaly signal; it pulls the consensus down and can flag the case `contested` against anomaly claims.
**Combination** is a weighted average of the three signals:
```ts
import { DEFAULT_WEIGHTS } from '@disclosureos/scoring';
// { technology: 0.45, biologics: 0.35, origin: 0.2 }
```
The default values are a reference methodology (marked `@experimental`) — pin your own `weights` if you need stable numbers across releases.
Options [#options]
```ts
const result = score(observation, {
weights: { technology: 0.5, biologics: 0.3, origin: 0.2 },
prosaicPrefixes: ['1.1.1', '2'], // also treat psychosocial as explained
evaluatorWeight: (claim) =>
claim.evidenceRefs?.length ? 1 : 0.5, // trust hook: discount uncited claims
});
```
The `evaluatorWeight` hook answers **who gets the most weight**: trust policies (weighting verified institutions higher, discounting anonymous claims) plug in here without touching the formula. It affects the consensus point only — `range` and `contested` always report the honest spread of what evaluators actually asserted.
Ranking a corpus [#ranking-a-corpus]
```ts
import { rankByCompellingness } from '@disclosureos/scoring';
const ranked = rankByCompellingness(observations);
for (const { observation, result } of ranked.slice(0, 10)) {
console.log(observation.id, result.score, result.contested ? '⚠ contested' : '');
}
```
This is the "one case that matters" workflow: across a large ingest, the multi-sensor, multi-witness cases float to the top — with their disputes visible.
Reading scores responsibly [#reading-scores-responsibly]
* A score is a function of the **claims**, not the event. No claims → score 0 — "unevaluated", not "mundane".
* Always display `contested` and `range` alongside `score`.
* Cite `scoringVersion` when publishing numbers; methodology versions matter.
* Completeness and compellingness travel together: a compelling-but-thin record is an investigation lead, not a conclusion.
# Completeness (/v1/docs/scoring/completeness)
Completeness answers: **is this record well-documented?** It measures how fully an observation populates the records schema — nothing more. It says nothing about whether the case is interesting; that's [compellingness](/docs/scoring/compellingness).
Usage [#usage]
```ts
import { getCompleteness } from '@disclosureos/scoring';
const result = getCompleteness(observation);
```
The result [#the-result]
```ts
interface CompletenessResult {
total: number; // leaf field paths defined by the records schema
present: number; // how many carry a value
percentage: number; // present / total, 0–100 integer
missing: string[]; // exactly which paths are absent or empty
requiredTotal: number; // schema-required leaf paths
requiredPresent: number;
requiredPercentage: number; // 0–100 integer
}
```
Two percentages, because they answer different questions:
* **`requiredPercentage`** — is this a structurally complete record? (100 for anything a factory produced.)
* **`percentage`** — how much of the full lexicon does it use? Most real records sit well under 50, and that's fine.
The `missing` array is the actionable part — it's a work list for researchers improving a record:
```ts
result.missing;
// ['movement.speedKmh', 'witnesses.descriptions', 'environment.weather', ...]
```
What counts as present [#what-counts-as-present]
A field is present when it carries a non-empty value: non-empty strings, non-empty arrays, objects with at least one key, and any number or boolean. `null`, `undefined`, `''`, `[]`, and `{}` count as missing.
Derived from the schema, not hardcoded [#derived-from-the-schema-not-hardcoded]
The field inventory is derived from the records JSON Schema at runtime via `deriveFieldPaths()` — when the lexicon grows, completeness automatically accounts for the new fields. You can inspect or override the inventory:
```ts
import { deriveFieldPaths, getCompleteness } from '@disclosureos/scoring';
const paths = deriveFieldPaths();
// [{ path: 'temporal.date', required: true }, { path: 'movement.speedKmh', required: false }, ...]
// Score against a custom subset — e.g. your project's "minimum publishable record"
const result = getCompleteness(observation, {
paths: paths.filter((p) => myMinimumSet.has(p.path)),
});
```
What completeness is not [#what-completeness-is-not]
* **Not a quality score.** A complete record of a streetlight sighting is still a streetlight.
* **Not validation.** An invalid record can be "complete" — validate first ([Validating at Boundaries](/docs/records/guides/validating-at-boundaries)).
* **Not about the slots.** Completeness measures the core record. Claims in `observableAssessments` and `origin` feed compellingness instead.
# Overview (/v1/docs/scoring)
`@disclosureos/scoring` answers the fifth question the standard asks: **how complete and compelling is the case?**
It is the consumer layer — it reads the enriched record produced by the other three and computes reference measures. It augments nothing and stores nothing on the record.
The [Scoring page](/scoring) in the Standard Explorer shows completeness, compellingness, contestedness, and evaluator weighting without collapsing them into one number.
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/scoring
```
```bash
pnpm add @disclosureos/scoring
```
```bash
yarn add @disclosureos/scoring
```
```bash
bun add @disclosureos/scoring
```
Two orthogonal measures [#two-orthogonal-measures]
The separation is deliberate. A thoroughly documented mundane sighting scores high on completeness and near zero on compellingness. A thin but extraordinary radar case scores the reverse. Collapsing those into one number would hide exactly what a reader needs to know — so the framework doesn't.
```ts
import { getCompleteness, score } from '@disclosureos/scoring';
const completeness = getCompleteness(observation);
completeness.percentage; // 0–100: coverage of all schema fields
completeness.requiredPercentage; // 0–100: coverage of required fields
completeness.missing; // exactly which paths are absent
const result = score(observation);
result.score; // 0–1 consensus point estimate
result.range; // { low, high } — spread across competing claims
result.contested; // do evaluators disagree on direction?
result.components; // per-signal breakdown: technology, biologics, origin
```
Design commitments [#design-commitments]
* **No single headline score.** Multiple measures, each answering one question.
* **Transparent methodology.** The weights are exported data (`DEFAULT_WEIGHTS`), the formula is open source, and every result is stamped with `scoringVersion`.
* **Disagreement is surfaced, not averaged away.** Competing claims produce a `range` and a `contested` flag alongside the point estimate.
* **Prosaic means explained.** A confident *prosaic* origin classification contributes zero anomaly signal — the scorer can't be gamed by enthusiastically classifying a case as a balloon.
* **Overridable, not configurable.** Weights, prosaic prefixes, and evaluator trust are plain function options. Institutions can publish alternative methodologies as data, not forks.
Ranking [#ranking]
```ts
import { rankByCompellingness } from '@disclosureos/scoring';
const ranked = rankByCompellingness(observations);
// most compelling first — the "one case that matters" surfaces
```
Subpaths [#subpaths]
```ts
import { getCompleteness } from '@disclosureos/scoring/completeness';
import { score, rankByCompellingness } from '@disclosureos/scoring/compellingness';
```
# Tour of the API (/v1/docs/platform/concepts)
Every package in DisclosureOS follows the same design. Once you understand four ideas — the Observation, slots, claims, and companions — you can predict how the entire API behaves.
The mental model [#the-mental-model]
1. One record: Observation [#1-one-record-observation]
The `Observation` type from [`@disclosureos/records`](/docs/records) is the unit of the entire standard. Everything else describes, enriches, validates, or measures it. It is a plain JSON-serializable object — no classes, no methods — so it survives databases, APIs, and language boundaries.
2. Slots: layers augment, never fork [#2-slots-layers-augment-never-fork]
Observables and Origins don't define their own record types. They add **slots** to `Observation` via TypeScript module augmentation:
```ts
import '@disclosureos/observables'; // Observation now has observableAssessments?
import '@disclosureos/origins'; // Observation now has origin?
```
The record stays one object. A tool that only knows `records` can still read, store, and forward an enriched observation — the slots ride along as data.
3. Claims: every assessment cites its evidence [#3-claims-every-assessment-cites-its-evidence]
Wherever the standard records an assessment — "this showed instantaneous acceleration", "this is best explained by X" — it uses a `Claim`: who evaluated, when, why, and pointers to the in-record evidence that justifies it.
```ts
import { evidenceRef } from '@disclosureos/records/shared';
createObservableClaim('measured', {
confidence: 0.7,
rationale: 'Two independent radar tracks with consistent kinematics.',
evaluatedBy: 'example-institution',
evidenceRefs: [evidenceRef('sensor', 'radar-01')],
});
```
Slots hold **arrays** of claims. Evaluators disagree; the standard records the disagreement instead of resolving it prematurely. The scoring layer decides what disagreement means (the `contested` flag) — the record just keeps the facts.
4. Companions: every enum brings its toolkit [#4-companions-every-enum-brings-its-toolkit]
Each package ships the same five companions for its vocabulary — constants, guards, factories, formatters, and labels. If a `TimeOfDay` type exists, then `TIMES_OF_DAY`, `isTimeOfDay`, and `TIME_OF_DAY_LABELS` exist too. Read [The Companion Pattern](/docs/platform/concepts/patterns) for the full convention.
Validation entry points [#validation-entry-points]
There are exactly two correct ways to validate, depending on what you hold:
| You have | Use | From |
| ------------------------------ | ------------------------------------------------------------- | ----------------------- |
| A core record (no slots) | `validateObservation(obs)` → `ValidationIssue[]` | `@disclosureos/records` |
| An enriched record (any slots) | `parseEnrichedObservation(obs)` → `{ success, data, issues }` | `@disclosureos/schema` |
Slot-specific validators also exist (`validateObservableAssessments`, `validateOriginClassification`) for validating a slot value in isolation. Sensor manifests are standalone documents, not slots — validate them with `validateSensorManifest` from `@disclosureos/instruments`.
Zod parsing strips unknown keys — including the slots. [Type Safety & Validation](/docs/platform/concepts/type-safety) explains the strip hazard in detail.
All validators return the same `ValidationIssue` shape — `{ path, message }` — so error handling is uniform across every layer.
When to use which layer [#when-to-use-which-layer]
| Goal | Reach for |
| ----------------------------------------------------------------------- | ---------------------------------- |
| Describe an observation: time, place, object, witnesses, sensors | [`records`](/docs/records) |
| Assert anomalous characteristics with evidence | [`observables`](/docs/observables) |
| Classify candidate explanations | [`origins`](/docs/origins) |
| Publish what your sensors are and how they are calibrated | [`instruments`](/docs/instruments) |
| Measure documentation coverage or case strength | [`scoring`](/docs/scoring) |
| Validate a full enriched record, or export JSON Schema | [`schema`](/docs/schema) |
| Validate files, scaffold templates, browse the registry from a terminal | [`cli`](/docs/cli) |
See it all at once [#see-it-all-at-once]
The [golden path](/docs/platform#the-golden-path) walks one real case — the USS Nimitz encounter — through every layer in about 60 lines of code.
# The Companion Pattern (/v1/docs/platform/concepts/patterns)
Every vocabulary in DisclosureOS follows the same pattern: a **type** plus five kinds of **companions**. Learning this once lets you use the entire API — the pattern holds in `records`, `observables`, and `origins` alike.
The five companions [#the-five-companions]
| Companion | Purpose | Example |
| -------------- | --------------------------------- | --------------------------------------------- |
| **Constants** | Valid values as arrays | `TIMES_OF_DAY` → `['dawn', 'morning', ...]` |
| **Guards** | Runtime type narrowing | `isTimeOfDay(value)` → `value is TimeOfDay` |
| **Factories** | Create with defaults + validation | `createTemporalData({ date: '2004-11-14' })` |
| **Formatters** | Human-readable strings | `formatTemporalData(temporal)` |
| **Labels** | Value → display name | `TIME_OF_DAY_LABELS['morning']` → `"Morning"` |
Worked example: TimeOfDay [#worked-example-timeofday]
```ts title="example.ts"
import {
TIMES_OF_DAY,
TIME_OF_DAY_LABELS,
isTimeOfDay,
createTemporalData,
formatTemporalData,
} from '@disclosureos/records';
// Constants — build a dropdown
const options = TIMES_OF_DAY.map((v) => ({
value: v,
label: TIME_OF_DAY_LABELS[v],
}));
// Guard — validate form or API input
const raw: unknown = formData.get('timeOfDay');
const timeOfDay = isTimeOfDay(raw) ? raw : undefined;
// Factory — create a validated temporal object
const temporal = createTemporalData('2004-11-14', 'exact', { timeOfDay });
// Formatter — display in UI
console.log(formatTemporalData(temporal));
```
The same pattern appears for source credibility, object shapes, location sensitivity, observable assessment levels, origin domains, and every other enum in the standard. Constants for options, guards for validation, factories for creation, formatters for display, labels for dropdowns and tables.
Claims have factories too [#claims-have-factories-too]
The pattern extends beyond enums. The evaluation layers ship claim factories that validate on construction:
```ts
import { createObservableClaim } from '@disclosureos/observables';
import { createOriginClaim } from '@disclosureos/origins';
const claim = createObservableClaim('measured', { confidence: 0.7 });
// throws if confidence is out of [0, 1]
const hypothesis = createOriginClaim('1.1.3', 0.4, {
rationale: 'Performance exceeds known aerospace capability.',
});
// throws if the OCS node id is unknown
```
Factories parse their output against the schema — what they return is guaranteed valid.
Barrel vs. subpath imports [#barrel-vs-subpath-imports]
You can import from the barrel or from subpaths:
```ts title="imports.ts"
// Barrel
import { isTimeOfDay, TIMES_OF_DAY } from '@disclosureos/records';
// Subpaths
import { isTimeOfDay } from '@disclosureos/records/guards';
import { TIMES_OF_DAY } from '@disclosureos/records/constants';
import { TIME_OF_DAY_LABELS } from '@disclosureos/records/labels';
```
Use subpaths when you want tighter imports and only need a subset of the API.
Building custom guards [#building-custom-guards]
The shared primitives module exports the helpers the packages use internally, so your own vocabularies can follow the same pattern:
```ts title="guards.ts"
import { createEnumGuard, makeGuard } from '@disclosureos/records/shared';
import { z } from 'zod';
// From a values array
const REVIEW_STATES = ['queued', 'in_review', 'done'] as const;
const isReviewState = createEnumGuard(REVIEW_STATES);
// From any Zod schema
const isPercent = makeGuard(z.number().min(0).max(100));
```
Next steps [#next-steps]
* [Type Safety & Validation](/docs/platform/concepts/type-safety) — compile-time types, runtime guards, and where to validate
* [Records — Companions](/docs/records/companions) — the full companion reference for the core lexicon
* [Observables](/docs/observables) — assessment levels and their companions
# Type Safety & Validation (/v1/docs/platform/concepts/type-safety)
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 [#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:
```ts
// Inside @disclosureos/observables:
declare module '@disclosureos/records' {
interface ObservationExtensions {
observableAssessments?: ObservableAssessmentMap;
}
}
```
This means typing follows your imports:
```ts
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:
```ts
import type { EnrichedObservation } from '@disclosureos/schema';
```
Runtime: guards and validators [#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:
```ts
import { isObjectShape, isSourceCredibility } from '@disclosureos/records/guards';
if (isObjectShape(raw)) {
// raw: ObjectShape
}
```
**Validators** check whole structures and return uniform issues:
```ts
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:
```ts
interface ValidationIssue {
path: string; // 'location.latitude', 'origin[0].confidence', ...
message: string;
}
```
The strip hazard [#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:
```ts
import { ObservationSchema } from '@disclosureos/records';
const parsed = ObservationSchema.parse(enrichedObservation);
// ❌ parsed.observableAssessments — GONE
// ❌ parsed.origin — GONE
// No error. No warning. Data silently destroyed.
```
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.
```ts
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 [#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 [#validating-at-api-boundaries]
The pattern for any ingestion surface:
```ts title="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 [#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:
```ts
const observation = {
...base,
extensions: {
myPipeline: { ingestBatch: '2026-06-01', sourceRow: 4821 },
},
};
```
Standard Schema [#standard-schema]
All schemas are Zod 4 schemas, which implement the [Standard Schema](https://standardschema.dev) interface — so they plug directly into libraries that accept Standard Schema validators (form libraries, RPC layers, agent frameworks) without adapters.
Next steps [#next-steps]
* [Schema — Parsing](/docs/schema/parsing) — `parseEnrichedObservation` internals
* [Records — Companions](/docs/records/companions) — the full guard catalog
* [The Companion Pattern](/docs/platform/concepts/patterns) — constants, guards, factories, formatters, labels
# Building on the Standard (/v1/docs/platform/guides/building-a-research-tool)
This guide builds a small but real research tool on the standard: ingest a dataset, structure it as observations, add evaluations, score the corpus, and expose it. It's the [golden path](/docs/platform#the-golden-path) at project scale.
What we're building [#what-were-building]
A "case triage" service: it ingests reports, validates them, and surfaces the most compelling cases — with their disagreements visible. The same skeleton extends to FOIA trackers, archive search, map apps, or analysis pipelines.
```bash title="Terminal"
mkdir case-triage && cd case-triage && npm init -y && npm pkg set type=module
npm install @disclosureos/records @disclosureos/observables @disclosureos/origins @disclosureos/scoring @disclosureos/schema
npm install -D typescript @types/node @disclosureos/cli
```
1. Ingest: source rows → observations [#1-ingest-source-rows--observations]
Write one mapper per source. Keep raw source data in `extensions`, mark everything `draft`, and let the factory validate:
```ts title="src/ingest.ts"
import { createObservation } from '@disclosureos/records/factories';
import type { Observation } from '@disclosureos/records';
export function fromReport(report: SourceReport): Observation {
return createObservation(
{
temporal: { date: report.date, dateCertainty: 'approximate' },
location: {
name: report.place,
country: report.country,
latitude: report.lat,
longitude: report.lng,
siteType: 'unknown',
},
summary: report.narrative.slice(0, 280),
description: report.narrative,
dataSourceId: report.sourceName,
extensions: { ingest: { raw: report.id, batch: process.env.BATCH_ID } },
},
{ id: `${report.sourceName}-${report.id}`, status: 'draft' },
);
}
```
The full honest-mapping playbook — fuzzy dates, enum guards, quarantining failures — is in [Data Migration](/docs/platform/guides/data-migration).
2. Evaluate: claims, attributed and cited [#2-evaluate-claims-attributed-and-cited]
Evaluation is a separate pass from ingestion, done by people (or pipelines) who sign their work:
```ts title="src/evaluate.ts"
import { createObservableClaim } from '@disclosureos/observables';
import { createOriginClaim } from '@disclosureos/origins';
import { evidenceRef } from '@disclosureos/records/shared';
import type { EnrichedObservation } from '@disclosureos/schema';
export function addRadarAssessment(obs: EnrichedObservation, sensorId: string): void {
const REF = evidenceRef('sensor', sensorId);
obs.observableAssessments ??= {};
obs.observableAssessments.technology ??= {};
(obs.observableAssessments.technology.instantaneous_acceleration ??= []).push(
createObservableClaim('measured', {
confidence: 0.7,
rationale: 'Track shows velocity change inconsistent with known aerospace capability.',
evidenceRefs: [REF],
evaluatedBy: 'case-triage-team',
}),
);
(obs.origin ??= []).push(
createOriginClaim('1.1', 0.5, {
rationale: 'Physical craft indicated by sensor track; insufficient data for deeper classification.',
evidenceRefs: [REF],
evaluatedBy: 'case-triage-team',
}),
);
}
```
The discipline that keeps this credible is covered in [Claiming Observables](/docs/observables/claiming-observables) and [Claiming Origins](/docs/origins/claiming-origins).
3. Validate: one gate, everywhere [#3-validate-one-gate-everywhere]
Every record entering storage passes the full contract:
```ts title="src/store.ts"
import { parseEnrichedObservation } from '@disclosureos/schema';
export function assertValid(record: unknown) {
const result = parseEnrichedObservation(record);
if (!result.success) {
throw new Error(result.issues.map((i) => `${i.path}: ${i.message}`).join('\n'));
}
return result.data!;
}
```
And the data directory is checked in CI:
```bash title="Terminal"
npx disclosureos validate ./data --recursive --strict
```
Storage layer: see [Supabase Integration](/docs/platform/guides/supabase) for the JSONB-with-promoted-columns pattern.
4. Triage: scoring with disagreement visible [#4-triage-scoring-with-disagreement-visible]
```ts title="src/triage.ts"
import { rankByCompellingness, getCompleteness } from '@disclosureos/scoring';
import type { EnrichedObservation } from '@disclosureos/schema';
export function triage(corpus: EnrichedObservation[]) {
return rankByCompellingness(corpus).map(({ observation, result }) => ({
id: observation.id,
summary: observation.summary,
score: result.score,
range: result.range, // always ship the range...
contested: result.contested, // ...and the contested flag
completeness: getCompleteness(observation).percentage,
scoringVersion: result.scoringVersion,
}));
}
```
A UI built on this shows: the strongest cases first, a visible band of disagreement, and a completeness meter telling researchers where work is needed. That's the case-file dashboard pattern — score, claims, and evidence, each one click apart.
5. Expose: the API is the contract [#5-expose-the-api-is-the-contract]
Because the wire format *is* the standard, your API needs no bespoke DTOs:
```ts title="src/api.ts"
export async function GET() {
const corpus = await loadPublished(); // EnrichedObservation[]
return Response.json(triage(corpus));
}
export async function POST(request: Request) {
const obs = assertValid(await request.json()); // 400 on contract violations
await save(obs);
return Response.json({ id: obs.id }, { status: 201 });
}
```
Any other tool in the ecosystem can consume your `GET` and contribute via your `POST` — including tools in other languages validating against the [JSON Schema artifact](/docs/schema/json-schema). That interoperability is the entire reason the standard exists.
Where this goes [#where-this-goes]
* **Add institutional attribution** — claims from verified institutions, surfaced with badges; the `evaluatorWeight` hook in [compellingness](/docs/scoring/compellingness) is the seam.
* **Embed a map** — observations carry coordinates, so the same records render geospatially with any mapping library.
* **Publish your dataset** — a directory of validated observation JSON *is* a publishable, interoperable dataset. Ship it with `disclosureos validate` in CI and a `schemaVersion` stamp.
# Contributing (/v1/docs/platform/guides/contributing)
DisclosureOS is an open standard — contributions of code, documentation, data, and design critique are all welcome. This page covers setup, the conventions the packages enforce, and what to expect from the process.
Where to contribute [#where-to-contribute]
* **The foundation packages** — [`records`](/docs/records), [`observables`](/docs/observables), [`origins`](/docs/origins), [`instruments`](/docs/instruments), [`scoring`](/docs/scoring), [`schema`](/docs/schema), and the [`cli`](/docs/cli), under `packages/` in the monorepo.
* **The taxonomy and observables definitions** — proposals to refine OCS nodes or observable criteria are *standards work*; open a discussion first so the change can be debated on the merits.
* **The Disclosure Index** — the flagship application built on the standard.
* **Documentation** — this site (`apps/disclosureos`) and package READMEs.
Development setup [#development-setup]
Prerequisites: **Node.js 20+** and **pnpm 9+**.
```bash title="Terminal"
git clone https://github.com/disclosurefoundation/disclosureos
cd disclosureos && pnpm install
# work on a package
pnpm --filter @disclosureos/records test
pnpm --filter @disclosureos/records build
# run the whole gate locally
pnpm test && pnpm build
# the end-to-end smoke test
pnpm --filter @disclosureos/examples golden-path
```
Conventions the packages enforce [#conventions-the-packages-enforce]
These aren't style preferences — CI checks them:
* **Schema-first.** Types are inferred from Zod schemas (`z.infer`), never written separately. If you add a field, you add it to the schema.
* **The companion pattern.** A new enum ships with its [five companions](/docs/platform/concepts/patterns) — constants from `Schema.options`, a guard via `makeGuard`, labels, and formatter coverage where display matters.
* **ESM-only, Node 20+.** No CommonJS builds, no `require` patterns.
* **Committed JSON Schema artifacts.** Each package commits its generated schema under `schema/`. **Drift tests** regenerate and diff in CI — if your change alters the artifact, regenerate it and bump `x-schema-version` deliberately. A failing drift test is the system working.
* **`ValidationIssue` everywhere.** Validators return `{ path, message }[]`; don't invent new error shapes.
* **Slots, not forks.** New evaluation surfaces attach through the records extension point and the [runtime registry](/docs/schema/registry) — never by editing `ObservationSchema` to know about satellites.
Changing the contract [#changing-the-contract]
Anything that alters the enriched observation shape — new fields, new slots, changed enums — is a contract change:
1. Open an issue or discussion **before** implementing; contract changes affect every consumer in every language.
2. Update the owning package's schema and regenerate its artifact.
3. Regenerate the composed artifact in `@disclosureos/schema` and update the drift tests.
4. Document the change in the package CHANGELOG (changesets drive versioning and publishing).
Additive optional fields are minor versions; anything that tightens or renames is major. When in doubt, ask in the issue.
Pull request process [#pull-request-process]
1. For large or breaking changes, open an issue first so maintainers can align with you.
2. Branch from the default branch; keep PRs focused.
3. Add or update tests — guards, validators, factories, and formatters all have unit-test patterns to follow.
4. Run `pnpm test && pnpm build` plus the golden path before requesting review.
5. Describe *why* in the PR body, link related issues, and address review feedback.
Bugs and proposals [#bugs-and-proposals]
* **Bugs** — open an issue with reproduction steps, expected vs. actual behavior, and environment (OS, Node version).
* **Standards proposals** — for taxonomy nodes, observable definitions, or scoring methodology, describe the use case and the evidence basis. These changes are deliberately slower than code changes; the standard's value is its stability.
Code of conduct [#code-of-conduct]
The project follows a Code of Conduct. Report unacceptable behavior to [os@disclosure.org](mailto:os@disclosure.org).
# Data Migration (/v1/docs/platform/guides/data-migration)
This guide turns an existing dataset — a CSV catalog, a legacy database, an API export — into valid DisclosureOS Observation records. The workflow is a loop: map your source fields, run validation, fix what fails, and rerun until clean.
Prerequisites [#prerequisites]
npm
pnpm
yarn
bun
```bash
npm install @disclosureos/records @disclosureos/scoring @disclosureos/schema @disclosureos/cli
```
```bash
pnpm add @disclosureos/records @disclosureos/scoring @disclosureos/schema @disclosureos/cli
```
```bash
yarn add @disclosureos/records @disclosureos/scoring @disclosureos/schema @disclosureos/cli
```
```bash
bun add @disclosureos/records @disclosureos/scoring @disclosureos/schema @disclosureos/cli
```
You also need a TypeScript runner. The examples below use `tsx`:
npm
pnpm
yarn
bun
```bash
npm install -D tsx
```
```bash
pnpm add -D tsx
```
```bash
yarn add --dev tsx
```
```bash
bun add --dev tsx
```
The migration loop [#the-migration-loop]
Every migration follows the same cycle:
```
┌─────────────────────────────────────────────┐
│ 1. Audit — understand your source schema │
│ 2. Map — write a row mapper function │
│ 3. Run — execute, write JSON to out/ │
│ 4. Validate — disclosureos validate out/ │
│ 5. Fix — update mapper, rerun from step 3 │
│ 6. Measure — score what you produced │
└─────────────────────────────────────────────┘
```
Below is each step in detail.
1. Audit your source [#1-audit-your-source]
Know your source's reality before writing any mapping code:
* Which fields are always present? Which are free text pretending to be structured?
* How are dates encoded — and are "1947-07-08" dates *actually* day-precise, or catalog conventions?
* Are coordinates real GPS fixes, or city-centroid lookups someone added later?
* What is the source's own id scheme? You will need it for `identifiers` and dedup.
The answers determine your mapper's honesty, which matters more than its completeness.
2. Write a row mapper [#2-write-a-row-mapper]
One function from source row to `ObservationInput`, with guards at every enum boundary.
File structure [#file-structure]
Use a consistent layout for every migration project:
```
my-migration/
source.csv # or source.json — your raw data
migrate.ts # the mapper script
out/ # validated output (one JSON file per record)
quarantine.json # rows that failed validation
```
The mapper [#the-mapper]
```ts title="migrate.ts"
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { createObservation } from '@disclosureos/records/factories';
import { isObjectShape } from '@disclosureos/records/guards';
import type { Observation } from '@disclosureos/records';
// ------------------------------------------------------------------
// Define your source row shape
// ------------------------------------------------------------------
interface SourceRow {
case_id: string;
date: string; // 'YYYY-MM-DD', sometimes 'YYYY-MM', sometimes 'unknown'
city: string;
state: string;
lat?: number;
lng?: number;
shape?: string;
duration_min?: number;
summary?: string;
witness_count?: number;
}
// ------------------------------------------------------------------
// Map one row to an Observation
// ------------------------------------------------------------------
function migrateRow(row: SourceRow): Observation {
return createObservation(
{
temporal: mapTemporal(row.date, row.duration_min),
location: {
name: `${row.city}, ${row.state}`,
country: 'United States',
latitude: row.lat ?? 0,
longitude: row.lng ?? 0,
siteType: 'unknown',
coordinatePrecision: row.lat ? 'locality' : 'region',
},
summary: row.summary,
objectCharacteristics: isObjectShape(row.shape)
? { shape: row.shape }
: undefined,
witnesses: row.witness_count ? { count: row.witness_count } : undefined,
dataSourceId: 'legacy-catalog-2019',
extensions: {
legacyCatalog: { rawRow: row.case_id, rawShape: row.shape },
},
},
{ id: `legacy-${row.case_id}`, status: 'draft' },
);
}
```
Key decisions in that mapper:
* **Stable, prefixed ids** — `legacy-${case_id}` keeps re-runs idempotent and provenance visible.
* **`status: 'draft'`** — migrated records are not published records; review is a separate step.
* **`dataSourceId` + `extensions`** — every record knows where it came from and carries the raw source for audit.
* **Unmappable enums degrade to undefined**, never to a wrong enum value.
3. Map dates with their real precision [#3-map-dates-with-their-real-precision]
The temporal model exists so you do not have to lie. `date` is always a full `YYYY-MM-DD` anchor (machine-sortable); the *real* precision is declared in `dateGranularity` and `dateCertainty`:
```ts
import type { TemporalData } from '@disclosureos/records';
function mapTemporal(date: string, durationMin?: number): TemporalData {
const base = durationMin ? { durationSeconds: durationMin * 60 } : {};
if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
return { ...base, date, dateCertainty: 'approximate' };
}
if (/^\d{4}-\d{2}$/.test(date)) {
return { ...base, date: `${date}-01`, dateCertainty: 'approximate', dateGranularity: 'month' };
}
if (/^\d{4}$/.test(date)) {
return { ...base, date: `${date}-01-01`, dateCertainty: 'estimated', dateGranularity: 'year' };
}
return { ...base, date: '1900-01-01', dateCertainty: 'unknown', dateGranularity: 'unknown' };
}
```
Reserve `dateCertainty: 'exact'` for records whose provenance actually establishes the date. Events spanning a period get a `dateRange`; dates known only relative to another event get a `relativeDate`.
4. Run the migration and write output [#4-run-the-migration-and-write-output]
```ts title="migrate.ts (continued)"
// ------------------------------------------------------------------
// Load, migrate, quarantine, write
// ------------------------------------------------------------------
const raw: SourceRow[] = JSON.parse(readFileSync('source.json', 'utf-8'));
const migrated: Observation[] = [];
const quarantine: { row: SourceRow; error: string }[] = [];
for (const row of raw) {
try {
migrated.push(migrateRow(row));
} catch (err) {
quarantine.push({ row, error: String(err) });
}
}
mkdirSync('out', { recursive: true });
for (const obs of migrated) {
writeFileSync(join('out', `${obs.id}.json`), JSON.stringify(obs, null, 2));
}
writeFileSync('quarantine.json', JSON.stringify(quarantine, null, 2));
console.log(`migrated ${migrated.length}, quarantined ${quarantine.length}`);
```
Run it:
```bash title="Terminal"
npx tsx migrate.ts
```
5. Validate and fix [#5-validate-and-fix]
Validate the output files independently as a second gate:
```bash title="Terminal"
npx @disclosureos/cli validate ./out --recursive
```
For CI and scripted workflows, add `--json` for machine-readable output:
```bash title="Terminal"
npx @disclosureos/cli validate ./out --recursive --json
```
When validation reports errors:
1. Read the error paths and messages.
2. Update your mapper to handle the failing case.
3. Re-run from step 4.
4. Repeat until `validate` is clean.
Never drop failures silently — the quarantine list is your data-quality report.
6. Do not fabricate evaluation [#6-do-not-fabricate-evaluation]
A row that says `explanation: "probably Venus"` is **not** an origin claim by you — it is the source's assessment. Either carry it in `extensions` for later review, or record it as an attributed claim *from the source*:
```ts
import { createOriginClaim } from '@disclosureos/origins';
const origin = [
createOriginClaim('1.1.1.1.2', 0.6, {
rationale: 'Catalog disposition: "probably Venus".',
evaluatedBy: 'legacy-catalog-2019',
evaluatedAt: '2019-01-01T00:00:00Z',
}),
];
```
The same goes for observables: migration produces *records*; claims come from evaluators. Resist the urge to bulk-generate `observableAssessments` from keyword matching — it poisons [scoring](/docs/scoring) for every downstream consumer.
7. Measure what you produced [#7-measure-what-you-produced]
```ts
import { getCompleteness } from '@disclosureos/scoring';
const coverage = migrated.map((o) => getCompleteness(o).percentage);
const mean = coverage.reduce((a, b) => a + b, 0) / coverage.length;
console.log(`mean completeness: ${mean.toFixed(1)}%`);
```
The missing-fields lists from `getCompleteness` tell you what a future enrichment pass should target.
Quick reference [#quick-reference]
| Convention | Value |
| ---------------- | ------------------------------- |
| Source file | `source.csv` or `source.json` |
| Mapper script | `migrate.ts` |
| Output directory | `out/` (one `.json` per record) |
| Quarantine file | `quarantine.json` |
| Record id scheme | `{source-prefix}-{source-id}` |
| Initial status | `draft` |
Checklist [#checklist]
* [ ] Stable, source-prefixed ids; re-runs are idempotent
* [ ] `dataSourceId` set; raw rows preserved in `extensions`
* [ ] Date/coordinate precision honestly declared
* [ ] Enum mappings guarded; unmappable values kept as text
* [ ] Failures quarantined and reported, not dropped
* [ ] Source dispositions attributed to the source, not to you
* [ ] `disclosureos validate` clean on the output
Runnable examples [#runnable-examples]
Two complete, working examples you can run locally:
**JSON source** — [`examples/migration-path.ts`](https://github.com/disclosurefoundation/disclosureos/tree/main/examples/migration-path.ts) converts three JSON rows end-to-end:
```bash title="Terminal"
pnpm --filter @disclosureos/examples migration-path
```
**CSV source** — [`examples/migration-csv-path.ts`](https://github.com/disclosurefoundation/disclosureos/tree/main/examples/migration-csv-path.ts) parses a real CSV file with `csv-parse` and produces Observations:
```bash title="Terminal"
pnpm --filter @disclosureos/examples migration-csv-path
```
See also [#see-also]
* [Field Mapping Reference](/docs/platform/guides/field-mapping-reference) — quick-lookup table for common source columns
* [Onboarding workspace](/onboard) — paste, validate, and search fields interactively
# Field Mapping Reference (/v1/docs/platform/guides/field-mapping-reference)
A quick-reference table for the most common source-data columns and their Observation targets. Use this alongside the [Data Migration guide](/docs/platform/guides/data-migration) when writing a row mapper, or search the [field explorer](/onboard) interactively.
Identifiers and provenance [#identifiers-and-provenance]
| Source column | Observation path | Notes |
| ---------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
| `case_id`, `report_id`, `event_id` | `id` | Prefix with a source tag: `nuforc-12345`. Keeps IDs unique and re-runs idempotent. |
| `source_name`, `catalog_name` | `dataSourceId` | A short, stable string identifying the dataset, not the individual row. |
| *(entire row)* | `extensions.migration.rawRow` | Preserve the original source row verbatim for audit. |
Dates and time [#dates-and-time]
| Source column | Observation path | Notes |
| ------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `date`, `date_reported`, `event_date` | `temporal.date` | Always a full `YYYY-MM-DD`. If the source only has month or year, pad with `-01` and declare the real precision below. |
| *(inferred from date format)* | `temporal.dateCertainty` | `exact` only when provenance proves the date. Otherwise `approximate`, `estimated`, or `unknown`. |
| *(inferred from date format)* | `temporal.dateGranularity` | `day`, `month`, `year`, or `unknown`. Must match what the source actually knows. |
| `duration_seconds`, `duration_min` | `temporal.durationSeconds` | Convert to seconds. Set to `undefined` when unknown (not zero). |
| `start_date` + `end_date` | `temporal.dateRange` | Use `{ start, end }` for events that span a period. |
| `time`, `time_utc` | `temporal.time` | Freeform string; pair with `timeZone` when available. |
Location [#location]
| Source column | Observation path | Notes |
| ---------------------------- | ------------------------------ | -------------------------------------------------------------------------------- |
| `city`, `state`, `region` | `location.name` | Composite: `"Phoenix, AZ"`. The human-readable label. |
| `country`, `country_code` | `location.country` | Full name or ISO code. |
| `lat`, `latitude` | `location.latitude` | Decimal degrees. Use `0` if absent (and mark `coordinatePrecision` accordingly). |
| `lng`, `longitude` | `location.longitude` | Decimal degrees. |
| *(inferred)* | `location.coordinatePrecision` | `gps_fix`, `locality`, `region`, `country`, or `unknown`. |
| `location_type`, `site_type` | `location.siteType` | Guard against the enum; fall back to `unknown`. |
| `altitude`, `elevation` | `location.elevation` | Meters above sea level. |
Object and shape [#object-and-shape]
| Source column | Observation path | Notes |
| -------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `shape`, `object_shape` | `objectCharacteristics.shape` | Must be a recognized enum value. Use `isObjectShape(value)` guard; carry unmappable shapes in `extensions`. |
| `size`, `apparent_size` | `objectCharacteristics.apparentSize` | Freeform string. |
| `color` | `objectCharacteristics.color` | Freeform string. |
| `count`, `number_observed` | `objectCharacteristics.numberObserved` | Integer >= 0. |
Witnesses [#witnesses]
| Source column | Observation path | Notes |
| -------------------------------- | ------------------------ | -------------------------- |
| `witness_count`, `num_witnesses` | `witnesses.count` | Integer >= 0. |
| `witness_type`, `credibility` | `witnesses.credibility` | Guard against enum values. |
| `witness_names` | `witnesses.descriptions` | Array of strings. |
Movement and behavior [#movement-and-behavior]
| Source column | Observation path | Notes |
| ----------------------------- | ------------------------- | ----------------------------------------------- |
| `speed`, `speed_kmh` | `movement.speedKmh` | Kilometers per hour, >= 0. |
| `altitude`, `altitude_meters` | `movement.altitudeMeters` | Flight altitude in meters, >= 0. |
| `flight_path`, `trajectory` | `movement.flightPath` | Freeform description string. |
| `maneuver` | `movement.maneuvers` | Array of strings describing observed maneuvers. |
Media and documents [#media-and-documents]
| Source column | Observation path | Notes |
| ------------------------- | ---------------- | ---------------------------------------------------------------------- |
| `photo_url`, `video_url` | `media[]` | Each entry needs `{ id, type, url }` at minimum. Generate a stable id. |
| `primary_photo` | `featuredMedia` | A single `MediaAttachment` promoted for display. |
| `document_url`, `pdf_url` | `documents[]` | Use `{ id, type, url }` for attached documents. |
Sensors [#sensors]
| Source column | Observation path | Notes |
| ----------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `radar_data`, `ir_data` | `sensorEvidence.sensors[]` | Each sensor reading needs `{ id, type }`. Guard `type` against the enum. If the instrument is cataloged in a published [sensor manifest](/docs/instruments), set `sensorRef` (`org-slug:sensor-id`). |
| `sensor_notes` | `sensorEvidence.notes` | Freeform string. |
Source dispositions and explanations [#source-dispositions-and-explanations]
| Source column | Observation path | Notes |
| ---------------------------- | --------------------------------- | ------------------------------------------------------- |
| `disposition`, `explanation` | **Not** a direct field; see below | Source dispositions are *their* assessments, not yours. |
Source dispositions should be attributed to the source as an origin claim, or carried in `extensions` for later review. Never map a catalog's `explanation: "probably Venus"` to your own origin claim. See [the migration guide on fabricated evaluation](/docs/platform/guides/data-migration#6-do-not-fabricate-evaluation).
Status and lifecycle [#status-and-lifecycle]
| Source column | Observation path | Notes |
| ---------------- | ------------------------ | -------------------------------------------------------------------------- |
| *(always set)* | `status` | Use `draft` for all migrated records. Promotion is a separate review step. |
| *(auto-set)* | `createdAt`, `updatedAt` | ISO timestamps. `createObservation` sets these automatically. |
| `schema_version` | `schemaVersion` | Set by the factory. Do not override unless deliberately pinning. |
Tips [#tips]
* **Guard enums**: use factory guards like `isObjectShape()` before assigning. Unmappable values should stay in `extensions`, not force-mapped to a close-but-wrong enum.
* **Preserve the raw row**: always carry the full source row in `extensions.migration.rawRow` so any mapping decision can be audited later.
* **Stable IDs**: prefix source IDs (`nuforc-12345`, `mufon-67890`) so re-runs are idempotent and provenance is visible.
* **Field search**: use the [onboarding workspace](/onboard) to search by field name, type, or enum value when mapping.
# Supabase Integration (/v1/docs/platform/guides/supabase)
Observations are JSON-serializable by design, which makes Postgres + JSONB a natural store. This guide shows the pattern we recommend: **the record as a JSONB document, with the fields you query promoted to generated columns.**
The table [#the-table]
```sql title="migrations/001_observations.sql"
create table observations (
id text primary key,
record jsonb not null,
-- promoted columns, generated from the document (always in sync)
status text generated always as (record->>'status') stored,
event_date date generated always as ((record->'temporal'->>'date')::date) stored,
latitude double precision generated always as ((record->'location'->>'latitude')::double precision) stored,
longitude double precision generated always as ((record->'location'->>'longitude')::double precision) stored,
country text generated always as (record->'location'->>'country') stored,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index observations_event_date_idx on observations (event_date);
create index observations_status_idx on observations (status);
create index observations_record_gin on observations using gin (record jsonb_path_ops);
```
Why this shape:
* **The document is the source of truth.** The standard's schema evolves by versioned releases; your table doesn't need a migration when an optional field is added.
* **Generated columns can't drift.** They're computed from the document on write — no dual-write bugs.
* **The GIN index** covers ad-hoc containment queries on everything you didn't promote.
Validate at the boundary, store what was validated [#validate-at-the-boundary-store-what-was-validated]
```ts title="lib/observations.ts"
import { createClient } from '@supabase/supabase-js';
import { parseEnrichedObservation, type EnrichedObservation } from '@disclosureos/schema';
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);
export async function saveObservation(input: unknown) {
const result = parseEnrichedObservation(input);
if (!result.success) {
return { error: result.issues };
}
const obs = result.data!;
const { error } = await supabase
.from('observations')
.upsert({ id: obs.id, record: obs, updated_at: new Date().toISOString() });
return { error };
}
```
The database stores only records that passed the full contract — core plus slots. Postgres never becomes the place where invalid data hides.
Querying [#querying]
Promoted columns for the hot paths:
```ts
const { data } = await supabase
.from('observations')
.select('id, record')
.eq('status', 'published')
.gte('event_date', '2004-01-01')
.order('event_date', { ascending: false });
const observations = (data ?? []).map((row) => row.record as EnrichedObservation);
```
JSONB containment for everything else — e.g. records with any origin claim, or military witnesses:
```ts
// any record claiming instantaneous acceleration
const { data } = await supabase
.from('observations')
.select('id, record')
.not('record->observableAssessments->technology->instantaneous_acceleration', 'is', null);
// containment via the GIN index
const { data: military } = await supabase
.from('observations')
.select('id, record')
.contains('record', { witnesses: { militaryWitnesses: true } });
```
Scoring on read [#scoring-on-read]
Scores are derived data — compute them in the app layer (and cache if needed) rather than storing them in the record:
```ts
import { rankByCompellingness } from '@disclosureos/scoring';
const ranked = rankByCompellingness(observations);
```
If you need scores queryable in SQL, store them in a separate `observation_scores` table stamped with `scoringVersion`, and treat it as a rebuildable cache — never as part of the record.
Row Level Security [#row-level-security]
Standard Supabase practice applies. A minimal published-only read policy:
```sql
alter table observations enable row level security;
create policy "public read of published"
on observations for select
using (status = 'published');
create policy "authenticated write"
on observations for insert to authenticated
with check (true);
```
`status` is a generated column, so the policy follows the document automatically.
Schema versioning [#schema-versioning]
Stamp records with the contract version on write (`schemaVersion` is a core field), so future migrations know exactly what they're reading:
```ts
import { ENRICHED_OBSERVATION_SCHEMA_VERSION } from '@disclosureos/schema';
const obs = { ...base, schemaVersion: ENRICHED_OBSERVATION_SCHEMA_VERSION };
```
For database-side validation in other languages or in Postgres functions, the [composed JSON Schema artifact](/docs/schema/json-schema) is the same contract this guide validates against in TypeScript.
# Composing an Observation (/v1/docs/records/guides/composing-an-observation)
This guide builds a real record from the ground up. The principle throughout: **record what you know, omit what you don't.** A sparse record is valid; certainty you don't have should never be invented to fill fields.
Start minimal [#start-minimal]
Only `temporal` and `location` are required as input — the factory supplies `id`, `status`, and timestamps:
```ts
import { createObservation } from '@disclosureos/records/factories';
const obs = createObservation({
temporal: { date: '1986-11-17', dateCertainty: 'exact' },
location: {
name: 'Eastern Alaska airspace',
country: 'United States',
latitude: 64.0,
longitude: -145.0,
siteType: 'wilderness',
},
});
// obs.status === 'draft', obs.id is a UUID
```
Express uncertainty honestly [#express-uncertainty-honestly]
The temporal model supports the reality of historical data — dates that are approximate, known only to a month or decade, or relative to another event:
```ts
temporal: {
date: '1947-07-01', // always a full YYYY-MM-DD anchor (sortable)
dateCertainty: 'approximate',
dateGranularity: 'month', // the real precision: "July 1947"
}
```
The same applies to location (`coordinatePrecision`, `locationSensitivity`) and witnesses. `dateCertainty: 'exact'` is an assertion; make it only when the record supports it.
Add descriptive domains [#add-descriptive-domains]
```ts
const obs = createObservation({
temporal: { date: '1986-11-17', time: '17:11', dateCertainty: 'exact' },
location: { /* ... */ },
summary: 'JAL 1628 crew reported two objects and a larger craft shadowing the 747.',
objectCharacteristics: { shape: 'other', numberObserved: 3 },
movement: { maneuvers: ['instant_acceleration', 'appear'], radarData: true },
witnesses: {
count: 3,
categories: ['commercial_pilot'],
aviationWitnesses: true,
},
sourceData: {
primarySource: {
sourceId: 'faa-jal1628-report',
type: 'official_report',
title: 'FAA report on JAL 1628 incident',
credibility: 'official',
},
},
});
```
Use guards when the values come from outside:
```ts
import { isObjectShape } from '@disclosureos/records/guards';
const shape = isObjectShape(row.shape) ? row.shape : undefined;
```
Attach evidence with citable ids [#attach-evidence-with-citable-ids]
Sensor readings and media get ids so that claims — yours or another institution's — can cite them later:
```ts
import { createSensorReading, createMediaAttachment } from '@disclosureos/records/factories';
import { evidenceRef } from '@disclosureos/records/shared';
const radar = createSensorReading('ground_radar', 'radar_primary', {
id: 'faa-anchorage-radar',
operator: 'FAA Anchorage Center',
});
const obs = createObservation({
// ...
sensorEvidence: { sensors: [radar] },
});
const RADAR = evidenceRef('sensor', radar.id); // ready for claims
```
Prefer explicit, stable ids (`'faa-anchorage-radar'`) over generated UUIDs when the record will be published — refs survive better when humans can read them.
Relate records, don't merge them [#relate-records-dont-merge-them]
The same event reported in two catalogs stays two records, linked:
```ts
relations: {
edges: [
{ kind: 'duplicate_of', targetId: 'nuforc-1986-11-17-jal' },
{ kind: 'corroborates', targetId: 'faa-anchorage-radar-1986-11-17' },
],
},
```
The edge vocabulary is typed: `corroborates`, `contradicts`, `duplicate_of`, `same_object`, `supersedes`, `superseded_by`, `re_analysis_of`, `part_of`. External catalog ids belong in the [`identifiers` extension slot](/docs/records/extensions).
Validate before storing [#validate-before-storing]
```ts
import { validateObservation } from '@disclosureos/records';
const issues = validateObservation(obs);
if (issues.length > 0) throw new Error(JSON.stringify(issues, null, 2));
```
When the record gains [observable](/docs/observables) or [origin](/docs/origins) claims, switch to `parseEnrichedObservation` — see [Validating at Boundaries](/docs/records/guides/validating-at-boundaries).
Next steps [#next-steps]
* [Claim anomalous characteristics](/docs/observables/claiming-observables) on the record
* [Classify origin hypotheses](/docs/origins/claiming-origins)
* [Score the case](/docs/scoring) for completeness and compellingness
# Validating at Boundaries (/v1/docs/records/guides/validating-at-boundaries)
Validate once, at the boundary where untrusted data enters — then trust the types inside. This guide covers the practical patterns.
The decision [#the-decision]
Pattern: API ingestion [#pattern-api-ingestion]
```ts title="api/observations/route.ts"
import { parseEnrichedObservation } from '@disclosureos/schema';
export async function POST(request: Request) {
const result = parseEnrichedObservation(await request.json());
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 });
}
```
Default to `parseEnrichedObservation` at ingestion boundaries even if today's payloads are core-only — the day a partner starts sending observable claims, you'll accept them instead of rejecting (or worse, storing records you never validated the slots of).
Pattern: form input [#pattern-form-input]
Guards narrow individual fields before you assemble the record:
```ts
import { isObjectShape, isTimeOfDay } from '@disclosureos/records/guards';
import { isConfidence } from '@disclosureos/records/shared';
const shape = isObjectShape(form.shape) ? form.shape : undefined;
const timeOfDay = isTimeOfDay(form.timeOfDay) ? form.timeOfDay : undefined;
```
Then let the factory be the final check — `createObservation` parses and throws with precise messages.
Pattern: batch import [#pattern-batch-import]
Collect issues per row instead of failing the batch:
```ts
import { validateObservation } from '@disclosureos/records';
const failures: { row: number; issues: ValidationIssue[] }[] = [];
for (const [i, record] of records.entries()) {
const issues = validateObservation(record);
if (issues.length > 0) failures.push({ row: i, issues });
}
```
For files, the [CLI](/docs/cli/validate) does this with formatted output:
```bash title="Terminal"
npx @disclosureos/cli validate ./import/*.json
```
Reading issues [#reading-issues]
Every validator returns the same shape:
```ts
[
{ path: 'temporal.date', message: 'Invalid input' },
{ path: 'location.latitude', message: 'Too big: expected number to be <=90' },
{ path: 'origin[0].confidence', message: 'Too big: expected number to be <=1' },
]
```
Paths use dot/index notation and include the slot name for enriched records, so errors can be mapped straight onto form fields or import-report columns.
What validation does not do [#what-validation-does-not-do]
* **It doesn't check completeness.** A record with only the required fields passes. Coverage is measured by [completeness scoring](/docs/scoring/completeness), not validity.
* **It doesn't resolve evidence refs.** `validateObservation` and `parseEnrichedObservation` check structure; dangling claim refs (a claim citing `sensor:x` when no such sensor exists) are surfaced as *warnings* by [`disclosureos validate`](/docs/cli/validate).
* **It doesn't judge plausibility.** The standard records claims and their evidence; it does not adjudicate them.
The one rule [#the-one-rule]
Zod strips unknown keys, so the slots vanish silently. `validateObservation` (non-mutating) and `parseEnrichedObservation` (slot-aware) exist precisely to avoid this. Full explanation: [Type Safety & Validation](/docs/platform/concepts/type-safety#the-strip-hazard).
# API Reference (/v1/docs/platform/explorer)
The interactive API Reference is available at this page. Use the package filter and type list to browse types from `@disclosureos/records`, `observables`, `origins`, `scoring`, and `schema`, then select a type to see its properties, allowed values and labels, and related companions (guards, formatters, factories, constants).