Composing an Observation
Build a complete record step by step — from minimal facts to citable evidence
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
Only temporal and location are required as input — the factory supplies id, status, and timestamps:
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 UUIDExpress 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:
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
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:
import { isObjectShape } from '@disclosureos/records/guards';
const shape = isObjectShape(row.shape) ? row.shape : undefined;Attach evidence with citable ids
Sensor readings and media get ids so that claims — yours or another institution's — can cite them later:
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 claimsPrefer 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
The same event reported in two catalogs stays two records, linked:
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.
Validate before storing
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 or origin claims, switch to parseEnrichedObservation — see Validating at Boundaries.
Next steps
- Claim anomalous characteristics on the record
- Classify origin hypotheses
- Score the case for completeness and compellingness