@disclosureos/schema 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@disclosureos/schema` are documented here. The format is
4
+ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 1.0.0
8
+
9
+ Initial public release — the portable contract.
10
+
11
+ ### Added
12
+ - `composeObservationSchema()` — merges the records core and every registered slot
13
+ schema into one enriched `Observation` JSON Schema, tightening the root to
14
+ `additionalProperties: false` while preserving the `extensions` bag.
15
+ - `parseEnrichedObservation()` — non-stripping validation of core + slots in one call,
16
+ closing the strip hazard at the integration layer.
17
+ - `ExtensionRegistry` / `defaultRegistry` — the runtime mirror of the TS augmentation.
18
+ - `EnrichedObservation` convenience type and the committed composed schema
19
+ (`schema/enriched-observation.schema.json`, draft 2020-12).
20
+ - ESM-only distribution.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Disclosure Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @disclosureos/schema
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ The **portable contract** for DisclosureOS. It composes the slot-bearing packages —
6
+ [`@disclosureos/records`](../disclosureos-records) (the core),
7
+ [`@disclosureos/observables`](../disclosureos-observables) (the
8
+ `observableAssessments` slot), and [`@disclosureos/origins`](../disclosureos-origins)
9
+ (the `origin` slot) — into one **enriched `Observation`** that means the same thing
10
+ in TypeScript, JSON Schema, and any other language.
11
+
12
+ DisclosureOS is a **five-part standard** (records → observables → origins → claims → scoring).
13
+ `schema` is the contract that *binds* records and the package-owned slots into one
14
+ portable artifact. It is a supporting package, not another part of the standard. (Scoring
15
+ consumes the enriched record, so it is not part of the composed schema.)
16
+
17
+ > ESM-only. CommonJS consumers can load it with a dynamic `import()`.
18
+
19
+ ## Why this package exists
20
+
21
+ In TypeScript, importing the satellite packages augments `Observation` with their
22
+ slots. That augmentation is compile-time only — invisible to JSON Schema or Python,
23
+ and `ObservationSchema.parse()` silently **drops** the slots. This package closes
24
+ that gap with three things:
25
+
26
+ - **`composeObservationSchema()`** — merges the records core and every registered
27
+ slot into one `enriched-observation.schema.json` (draft 2020-12). The root is
28
+ tightened to `additionalProperties: false`, so unknown top-level keys are
29
+ rejected while the `extensions` bag still carries third-party data.
30
+ - **`parseEnrichedObservation(value)`** — validates core + slots in one call
31
+ **without stripping** them (it delegates to each package's own validator).
32
+ - **`ExtensionRegistry` / `defaultRegistry`** — the runtime mirror of the TS
33
+ augmentation: a map of `slot → { owner, schemaId, version, jsonSchema, validate }`.
34
+ Importing this package registers the first-party slots. The registry drives **both**
35
+ `composeObservationSchema()` (JSON Schema) and `parseEnrichedObservation()`
36
+ (validation), so registering a slot once wires it into both — nothing is hard-coded.
37
+
38
+ > **See it end to end.** [`examples/golden-path.ts`](../../examples/golden-path.ts)
39
+ > takes one observation through every part — records → observables → origins → claims → scoring,
40
+ > validated as a whole by `@disclosureos/schema` — in a single type-checked file. Run it
41
+ > with `pnpm --filter @disclosureos/examples golden-path`.
42
+ >
43
+ > **Migrating an existing dataset?** See [`examples/migration-path.ts`](../../examples/migration-path.ts)
44
+ > and the [onboarding workspace](https://os.disclosure.org/onboard).
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pnpm add @disclosureos/schema @disclosureos/records @disclosureos/observables @disclosureos/origins zod
50
+ ```
51
+
52
+ ## Quickstart
53
+
54
+ ```ts
55
+ import { parseEnrichedObservation, type EnrichedObservation } from '@disclosureos/schema';
56
+
57
+ const observation: EnrichedObservation = {
58
+ id: 'nimitz-2004',
59
+ temporal: { date: '2004-11-14', dateCertainty: 'exact' },
60
+ location: { id: 'l', name: 'Pacific', country: 'US', longitude: -117, latitude: 32, siteType: 'ocean' },
61
+ status: 'published',
62
+ createdAt: '2020-01-01T00:00:00Z',
63
+ updatedAt: '2020-01-01T00:00:00Z',
64
+ observableAssessments: { technology: { antigravity_lift: [{ level: 'confirmed', confidence: 1 }] } },
65
+ origin: [{ primaryHypothesis: '1.1.3', confidence: 0.9 }],
66
+ };
67
+
68
+ const result = parseEnrichedObservation(observation);
69
+ if (!result.success) console.error(result.issues);
70
+ // result.data still has observableAssessments + origin — nothing stripped.
71
+ ```
72
+
73
+ ## The composed JSON Schema
74
+
75
+ `schema/enriched-observation.schema.json` is the committed, versioned artifact
76
+ (`$id` at `https://os.disclosure.org/schema/schema/<version>/enriched-observation.json`).
77
+ Regenerate it after any layer-schema change:
78
+
79
+ ```bash
80
+ pnpm --filter @disclosureos/schema emit:schema
81
+ ```
82
+
83
+ A drift test fails if the committed file diverges from the emitted one. Because the
84
+ composed schema depends on three layer schemas, bumping a layer schema may bump this
85
+ artifact too.
86
+
87
+ ## Standard Schema
88
+
89
+ `parseEnrichedObservation` delegates to each layer's own Zod validator, and those
90
+ schemas implement [Standard Schema v1](https://standardschema.dev) via their
91
+ `~standard` property (a guarantee of Zod 4). So the slot validators this package
92
+ composes are consumable directly by any Standard-Schema-compatible tool, with no
93
+ DisclosureOS-specific adapter.
94
+
95
+ ## License
96
+
97
+ MIT © [Disclosure Foundation](https://disclosure.org)
@@ -0,0 +1,117 @@
1
+ import { ValidationIssue } from '@disclosureos/records/shared';
2
+ import { Observation } from '@disclosureos/records';
3
+
4
+ /**
5
+ * `ExtensionRegistry` — the runtime mirror of the TypeScript module augmentation.
6
+ *
7
+ * In TS, importing `@disclosureos/observables` / `@disclosureos/origins` augments
8
+ * `Observation` with the `observableAssessments` / `origin` slots. That augmentation
9
+ * is compile-time only — invisible to JSON Schema, Python, or any non-TS consumer.
10
+ *
11
+ * This registry makes the same fact available at runtime: it maps each satellite
12
+ * slot to its owner package, canonical schema `$id`, version, and a function that
13
+ * emits the slot's standalone JSON Schema. {@link composeObservationSchema} walks
14
+ * the registry to fold every registered slot into one enriched `Observation` schema,
15
+ * so adding a new first-party (or third-party) slot is a single `register()` call —
16
+ * no edit to the composition logic OR the validation logic: {@link composeObservationSchema}
17
+ * walks the registry for JSON Schema and `parseEnrichedObservation` walks it for runtime
18
+ * validation, both driven entirely by what is registered.
19
+ */
20
+
21
+ /** One satellite slot on `Observation`, described for runtime composition. */
22
+ interface SlotRegistration {
23
+ /** Property name on `Observation` (e.g. `"observableAssessments"`, `"origin"`). */
24
+ slot: string;
25
+ /** Package that owns the slot (e.g. `"@disclosureos/observables"`). */
26
+ owner: string;
27
+ /** Canonical, versioned `$id` of the slot's standalone JSON Schema. */
28
+ schemaId: string;
29
+ /** Schema version of the slot (its `x-schema-version`). */
30
+ version: string;
31
+ /**
32
+ * Emit the slot's standalone JSON Schema (the same enveloped artifact the owner
33
+ * package commits). Called by {@link composeObservationSchema} during merge.
34
+ */
35
+ jsonSchema: () => Record<string, unknown>;
36
+ /**
37
+ * Validate the slot's value with the owner package's canonical, schema-backed
38
+ * validator. Called by `parseEnrichedObservation` for every present slot, so
39
+ * enriched validation stays brand-free (each package validates with its own Zod)
40
+ * and registry-driven (no slot is hard-coded in the parse path).
41
+ */
42
+ validate: (value: unknown) => ValidationIssue[];
43
+ }
44
+ /**
45
+ * A mutable map of `slot → registration`. Use the shared {@link defaultRegistry}
46
+ * for the first-party slots; construct your own only for isolated tests or to
47
+ * compose a custom subset of slots.
48
+ */
49
+ declare class ExtensionRegistry {
50
+ private readonly slots;
51
+ /**
52
+ * Register a slot. Throws on a duplicate slot name (one owner per slot).
53
+ *
54
+ * @experimental First-party slots (observables, origins) are registered for you
55
+ * on {@link defaultRegistry}. Registering *third-party* slots is supported but the
56
+ * extensibility contract (how external slots compose, version, and resolve) is
57
+ * still settling and may change in a minor release.
58
+ */
59
+ register(registration: SlotRegistration): this;
60
+ /** Look up a slot's registration, or `undefined` if not registered. */
61
+ get(slot: string): SlotRegistration | undefined;
62
+ /** Whether a slot is registered. */
63
+ has(slot: string): boolean;
64
+ /** All registrations, in insertion order. */
65
+ list(): SlotRegistration[];
66
+ }
67
+ /**
68
+ * The shared registry, pre-populated with the first-party satellite slots. Import
69
+ * sites that need the full enriched contract should use this; it is the runtime
70
+ * counterpart to importing both satellite packages for their TS augmentation.
71
+ */
72
+ declare const defaultRegistry: ExtensionRegistry;
73
+
74
+ /**
75
+ * Version of the emitted composed-schema artifact. Bump on any intentional change
76
+ * to the enriched shape — note that bumping a *layer* schema can also change this
77
+ * artifact, so the drift test guards both.
78
+ */
79
+ declare const ENRICHED_OBSERVATION_SCHEMA_VERSION = "1.0.0";
80
+ declare const ENRICHED_OBSERVATION_SCHEMA_ID: string;
81
+ /**
82
+ * Compose the enriched `Observation` JSON Schema (draft 2020-12) from the records
83
+ * core plus every slot in `registry` (defaults to the first-party
84
+ * {@link defaultRegistry}).
85
+ *
86
+ * The result is the standard enveloped artifact: a `$ref` to `#/$defs/Observation`,
87
+ * where `Observation` carries the records core properties plus one property per
88
+ * registered slot, and `additionalProperties: false` rejects unknown top-level keys.
89
+ */
90
+ declare function composeObservationSchema(registry?: ExtensionRegistry): Record<string, unknown>;
91
+
92
+ /**
93
+ * A complete observation: the records core plus the satellite slots. Because both
94
+ * satellite packages augment `Observation`, the slots are already part of the
95
+ * `Observation` type wherever `@disclosureos/schema` (which imports both) is loaded;
96
+ * this alias names that enriched shape explicitly as the public contract type.
97
+ */
98
+ type EnrichedObservation = Observation;
99
+ /** Outcome of {@link parseEnrichedObservation}: issues are empty iff `success`. */
100
+ interface EnrichedParseResult {
101
+ success: boolean;
102
+ /** The validated record (reference-equal to the input) when `success`. */
103
+ data?: EnrichedObservation;
104
+ /** Flat list of validation issues (empty when valid). */
105
+ issues: ValidationIssue[];
106
+ }
107
+ /**
108
+ * Validate an enriched observation (records core + `observableAssessments` + `origin`)
109
+ * in one call, WITHOUT stripping the satellite slots.
110
+ *
111
+ * Returns `{ success: true, data }` (where `data` is the same object you passed in)
112
+ * when valid, or `{ success: false, issues }` with a flat list of path-tagged issues.
113
+ * Unknown top-level keys are rejected; arbitrary data still belongs under `extensions`.
114
+ */
115
+ declare function parseEnrichedObservation(value: unknown): EnrichedParseResult;
116
+
117
+ export { ENRICHED_OBSERVATION_SCHEMA_ID, ENRICHED_OBSERVATION_SCHEMA_VERSION, type EnrichedObservation, type EnrichedParseResult, ExtensionRegistry, type SlotRegistration, composeObservationSchema, defaultRegistry, parseEnrichedObservation };
package/dist/index.js ADDED
@@ -0,0 +1,149 @@
1
+ import { recordsJsonSchema, validateObservation } from '@disclosureos/records';
2
+ import { schemaId, jsonSchemaEnvelope } from '@disclosureos/records/shared';
3
+ import { validateObservableAssessments, observablesJsonSchema, OBSERVABLES_SCHEMA_VERSION, OBSERVABLES_SCHEMA_ID } from '@disclosureos/observables';
4
+ import { validateOriginClassification, originsJsonSchema, ORIGINS_SCHEMA_VERSION, ORIGINS_SCHEMA_ID } from '@disclosureos/origins';
5
+
6
+ // src/registry.ts
7
+ var ExtensionRegistry = class {
8
+ constructor() {
9
+ this.slots = /* @__PURE__ */ new Map();
10
+ }
11
+ /**
12
+ * Register a slot. Throws on a duplicate slot name (one owner per slot).
13
+ *
14
+ * @experimental First-party slots (observables, origins) are registered for you
15
+ * on {@link defaultRegistry}. Registering *third-party* slots is supported but the
16
+ * extensibility contract (how external slots compose, version, and resolve) is
17
+ * still settling and may change in a minor release.
18
+ */
19
+ register(registration) {
20
+ if (this.slots.has(registration.slot)) {
21
+ const existing = this.slots.get(registration.slot);
22
+ throw new Error(
23
+ `Slot "${registration.slot}" is already registered by ${existing.owner}; cannot re-register it for ${registration.owner}.`
24
+ );
25
+ }
26
+ this.slots.set(registration.slot, registration);
27
+ return this;
28
+ }
29
+ /** Look up a slot's registration, or `undefined` if not registered. */
30
+ get(slot) {
31
+ return this.slots.get(slot);
32
+ }
33
+ /** Whether a slot is registered. */
34
+ has(slot) {
35
+ return this.slots.has(slot);
36
+ }
37
+ /** All registrations, in insertion order. */
38
+ list() {
39
+ return [...this.slots.values()];
40
+ }
41
+ };
42
+ var defaultRegistry = new ExtensionRegistry();
43
+ var ENRICHED_OBSERVATION_SCHEMA_VERSION = "1.0.0";
44
+ var ENRICHED_OBSERVATION_SCHEMA_ID = schemaId(
45
+ "schema",
46
+ "enriched-observation",
47
+ ENRICHED_OBSERVATION_SCHEMA_VERSION
48
+ );
49
+ defaultRegistry.register({
50
+ slot: "observableAssessments",
51
+ owner: "@disclosureos/observables",
52
+ schemaId: OBSERVABLES_SCHEMA_ID,
53
+ version: OBSERVABLES_SCHEMA_VERSION,
54
+ jsonSchema: observablesJsonSchema,
55
+ validate: validateObservableAssessments
56
+ }).register({
57
+ slot: "origin",
58
+ owner: "@disclosureos/origins",
59
+ schemaId: ORIGINS_SCHEMA_ID,
60
+ version: ORIGINS_SCHEMA_VERSION,
61
+ jsonSchema: originsJsonSchema,
62
+ validate: validateOriginClassification
63
+ });
64
+ function splitSlotSchema(schema) {
65
+ const { $schema, $id, "x-schema-version": _version, $defs, ...body } = schema;
66
+ return { body, defs: $defs ?? {} };
67
+ }
68
+ function composeObservationSchema(registry = defaultRegistry) {
69
+ const records = recordsJsonSchema();
70
+ const defs = structuredClone(records.$defs ?? {});
71
+ const observation = defs["Observation"];
72
+ if (!observation) {
73
+ throw new Error('records schema is missing the "Observation" $def; cannot compose.');
74
+ }
75
+ const slotProperties = {};
76
+ for (const registration of registry.list()) {
77
+ const { body, defs: slotDefs } = splitSlotSchema(registration.jsonSchema());
78
+ for (const [name, def] of Object.entries(slotDefs)) {
79
+ if (name in defs) {
80
+ throw new Error(
81
+ `$defs collision composing enriched schema: "${name}" is defined by both the records core and ${registration.owner}. Slots must own disjoint $defs.`
82
+ );
83
+ }
84
+ defs[name] = def;
85
+ }
86
+ slotProperties[registration.slot] = body;
87
+ }
88
+ observation["properties"] = {
89
+ ...observation["properties"],
90
+ ...slotProperties
91
+ };
92
+ observation["additionalProperties"] = false;
93
+ const composed = {
94
+ description: "A complete DisclosureOS observation: the records core enriched with observable assessments and origin classification. The portable contract across TypeScript, JSON Schema, and other languages.",
95
+ $ref: "#/$defs/Observation",
96
+ $defs: defs
97
+ };
98
+ return jsonSchemaEnvelope(composed, {
99
+ id: ENRICHED_OBSERVATION_SCHEMA_ID,
100
+ version: ENRICHED_OBSERVATION_SCHEMA_VERSION
101
+ });
102
+ }
103
+ function underSlot(slot, issues) {
104
+ return issues.map((issue) => ({
105
+ path: issue.path ? `${slot}.${issue.path}` : slot,
106
+ message: issue.message
107
+ }));
108
+ }
109
+ var allowedTopLevelKeys;
110
+ function getAllowedTopLevelKeys() {
111
+ if (!allowedTopLevelKeys) {
112
+ const composed = composeObservationSchema();
113
+ const observation = composed.$defs["Observation"];
114
+ const properties = observation?.["properties"] ?? {};
115
+ allowedTopLevelKeys = new Set(Object.keys(properties));
116
+ }
117
+ return allowedTopLevelKeys;
118
+ }
119
+ function parseEnrichedObservation(value) {
120
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
121
+ return { success: false, issues: [{ path: "", message: "expected an observation object" }] };
122
+ }
123
+ const record = value;
124
+ const issues = [];
125
+ issues.push(...validateObservation(record));
126
+ for (const registration of defaultRegistry.list()) {
127
+ const slotValue = record[registration.slot];
128
+ if (slotValue !== void 0) {
129
+ issues.push(...underSlot(registration.slot, registration.validate(slotValue)));
130
+ }
131
+ }
132
+ const allowed = getAllowedTopLevelKeys();
133
+ for (const key of Object.keys(record)) {
134
+ if (!allowed.has(key)) {
135
+ issues.push({
136
+ path: key,
137
+ message: `unknown top-level key "${key}" \u2014 third-party data belongs under "extensions".`
138
+ });
139
+ }
140
+ }
141
+ if (issues.length > 0) {
142
+ return { success: false, issues };
143
+ }
144
+ return { success: true, data: record, issues: [] };
145
+ }
146
+
147
+ export { ENRICHED_OBSERVATION_SCHEMA_ID, ENRICHED_OBSERVATION_SCHEMA_VERSION, ExtensionRegistry, composeObservationSchema, defaultRegistry, parseEnrichedObservation };
148
+ //# sourceMappingURL=index.js.map
149
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry.ts","../src/compose.ts","../src/parse.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `ExtensionRegistry` — the runtime mirror of the TypeScript module augmentation.\n *\n * In TS, importing `@disclosureos/observables` / `@disclosureos/origins` augments\n * `Observation` with the `observableAssessments` / `origin` slots. That augmentation\n * is compile-time only — invisible to JSON Schema, Python, or any non-TS consumer.\n *\n * This registry makes the same fact available at runtime: it maps each satellite\n * slot to its owner package, canonical schema `$id`, version, and a function that\n * emits the slot's standalone JSON Schema. {@link composeObservationSchema} walks\n * the registry to fold every registered slot into one enriched `Observation` schema,\n * so adding a new first-party (or third-party) slot is a single `register()` call —\n * no edit to the composition logic OR the validation logic: {@link composeObservationSchema}\n * walks the registry for JSON Schema and `parseEnrichedObservation` walks it for runtime\n * validation, both driven entirely by what is registered.\n */\nimport type { ValidationIssue } from '@disclosureos/records/shared';\n\n/** One satellite slot on `Observation`, described for runtime composition. */\nexport interface SlotRegistration {\n /** Property name on `Observation` (e.g. `\"observableAssessments\"`, `\"origin\"`). */\n slot: string;\n /** Package that owns the slot (e.g. `\"@disclosureos/observables\"`). */\n owner: string;\n /** Canonical, versioned `$id` of the slot's standalone JSON Schema. */\n schemaId: string;\n /** Schema version of the slot (its `x-schema-version`). */\n version: string;\n /**\n * Emit the slot's standalone JSON Schema (the same enveloped artifact the owner\n * package commits). Called by {@link composeObservationSchema} during merge.\n */\n jsonSchema: () => Record<string, unknown>;\n /**\n * Validate the slot's value with the owner package's canonical, schema-backed\n * validator. Called by `parseEnrichedObservation` for every present slot, so\n * enriched validation stays brand-free (each package validates with its own Zod)\n * and registry-driven (no slot is hard-coded in the parse path).\n */\n validate: (value: unknown) => ValidationIssue[];\n}\n\n/**\n * A mutable map of `slot → registration`. Use the shared {@link defaultRegistry}\n * for the first-party slots; construct your own only for isolated tests or to\n * compose a custom subset of slots.\n */\nexport class ExtensionRegistry {\n private readonly slots = new Map<string, SlotRegistration>();\n\n /**\n * Register a slot. Throws on a duplicate slot name (one owner per slot).\n *\n * @experimental First-party slots (observables, origins) are registered for you\n * on {@link defaultRegistry}. Registering *third-party* slots is supported but the\n * extensibility contract (how external slots compose, version, and resolve) is\n * still settling and may change in a minor release.\n */\n register(registration: SlotRegistration): this {\n if (this.slots.has(registration.slot)) {\n const existing = this.slots.get(registration.slot)!;\n throw new Error(\n `Slot \"${registration.slot}\" is already registered by ${existing.owner}; `\n + `cannot re-register it for ${registration.owner}.`,\n );\n }\n this.slots.set(registration.slot, registration);\n return this;\n }\n\n /** Look up a slot's registration, or `undefined` if not registered. */\n get(slot: string): SlotRegistration | undefined {\n return this.slots.get(slot);\n }\n\n /** Whether a slot is registered. */\n has(slot: string): boolean {\n return this.slots.has(slot);\n }\n\n /** All registrations, in insertion order. */\n list(): SlotRegistration[] {\n return [...this.slots.values()];\n }\n}\n\n/**\n * The shared registry, pre-populated with the first-party satellite slots. Import\n * sites that need the full enriched contract should use this; it is the runtime\n * counterpart to importing both satellite packages for their TS augmentation.\n */\nexport const defaultRegistry = new ExtensionRegistry();\n","/**\n * `composeObservationSchema()` — fold the records core schema and every registered\n * satellite slot schema into one enriched `Observation` JSON Schema.\n *\n * This is the **portable contract**: the single artifact that means the same thing\n * in TypeScript, JSON Schema, Python, and the docs. The records core schema is\n * deliberately OPEN (`additionalProperties: true`) because its satellite slots\n * attach via augmentation; here we fold those slots back in as real properties and\n * TIGHTEN the root to `additionalProperties: false` — unknown top-level keys are\n * rejected, while the `extensions` bag (a named property) still carries arbitrary\n * third-party data.\n *\n * Composition is a brand-free, plain-data merge: each layer's committed JSON Schema\n * (records + the slots from {@link defaultRegistry}) is merged by `$defs` name. The\n * three layers own disjoint `$defs` (records: `Observation`/`Temporal`/…;\n * observables: `ObservableAssessmentMap`/…; origins: `OriginClaim`/…), so the merge\n * needs no `$ref` rewriting; a collision throws loudly rather than silently winning.\n */\nimport { recordsJsonSchema } from '@disclosureos/records';\nimport { schemaId, jsonSchemaEnvelope } from '@disclosureos/records/shared';\nimport {\n observablesJsonSchema,\n validateObservableAssessments,\n OBSERVABLES_SCHEMA_ID,\n OBSERVABLES_SCHEMA_VERSION,\n} from '@disclosureos/observables';\nimport {\n originsJsonSchema,\n validateOriginClassification,\n ORIGINS_SCHEMA_ID,\n ORIGINS_SCHEMA_VERSION,\n} from '@disclosureos/origins';\nimport { defaultRegistry, ExtensionRegistry } from './registry';\n\n/**\n * Version of the emitted composed-schema artifact. Bump on any intentional change\n * to the enriched shape — note that bumping a *layer* schema can also change this\n * artifact, so the drift test guards both.\n */\nexport const ENRICHED_OBSERVATION_SCHEMA_VERSION = '1.0.0';\n\nexport const ENRICHED_OBSERVATION_SCHEMA_ID = schemaId(\n 'schema',\n 'enriched-observation',\n ENRICHED_OBSERVATION_SCHEMA_VERSION,\n);\n\n// Runtime mirror of the TS augmentation: register the first-party slots once, at\n// module load. Importing `@disclosureos/schema` is the runtime counterpart to\n// importing both satellite packages for their compile-time augmentation.\ndefaultRegistry\n .register({\n slot: 'observableAssessments',\n owner: '@disclosureos/observables',\n schemaId: OBSERVABLES_SCHEMA_ID,\n version: OBSERVABLES_SCHEMA_VERSION,\n jsonSchema: observablesJsonSchema,\n validate: validateObservableAssessments,\n })\n .register({\n slot: 'origin',\n owner: '@disclosureos/origins',\n schemaId: ORIGINS_SCHEMA_ID,\n version: ORIGINS_SCHEMA_VERSION,\n jsonSchema: originsJsonSchema,\n validate: validateOriginClassification,\n });\n\n/** Strip the standard envelope (`$schema`/`$id`/`x-schema-version`) and pull `$defs` off a slot schema. */\nfunction splitSlotSchema(schema: Record<string, unknown>): {\n body: Record<string, unknown>;\n defs: Record<string, unknown>;\n} {\n const { $schema, $id, 'x-schema-version': _version, $defs, ...body } = schema as Record<\n string,\n unknown\n > & { $defs?: Record<string, unknown> };\n void $schema;\n void $id;\n void _version;\n return { body, defs: $defs ?? {} };\n}\n\n/**\n * Compose the enriched `Observation` JSON Schema (draft 2020-12) from the records\n * core plus every slot in `registry` (defaults to the first-party\n * {@link defaultRegistry}).\n *\n * The result is the standard enveloped artifact: a `$ref` to `#/$defs/Observation`,\n * where `Observation` carries the records core properties plus one property per\n * registered slot, and `additionalProperties: false` rejects unknown top-level keys.\n */\nexport function composeObservationSchema(\n registry: ExtensionRegistry = defaultRegistry,\n): Record<string, unknown> {\n const records = recordsJsonSchema();\n const defs = structuredClone(records.$defs ?? {}) as Record<string, Record<string, unknown>>;\n\n const observation = defs['Observation'];\n if (!observation) {\n throw new Error('records schema is missing the \"Observation\" $def; cannot compose.');\n }\n\n const slotProperties: Record<string, unknown> = {};\n\n for (const registration of registry.list()) {\n const { body, defs: slotDefs } = splitSlotSchema(registration.jsonSchema());\n\n for (const [name, def] of Object.entries(slotDefs)) {\n if (name in defs) {\n throw new Error(\n `$defs collision composing enriched schema: \"${name}\" is defined by both `\n + `the records core and ${registration.owner}. Slots must own disjoint $defs.`,\n );\n }\n defs[name] = def as Record<string, unknown>;\n }\n\n slotProperties[registration.slot] = body;\n }\n\n // Fold the slots in as real properties and close the root: unknown top-level keys\n // are rejected; the `extensions` bag (a named property) keeps the third-party seam.\n observation['properties'] = {\n ...(observation['properties'] as Record<string, unknown> | undefined),\n ...slotProperties,\n };\n observation['additionalProperties'] = false;\n\n const composed = {\n description:\n 'A complete DisclosureOS observation: the records core enriched with observable '\n + 'assessments and origin classification. The portable contract across TypeScript, '\n + 'JSON Schema, and other languages.',\n $ref: '#/$defs/Observation',\n $defs: defs,\n };\n\n return jsonSchemaEnvelope(composed, {\n id: ENRICHED_OBSERVATION_SCHEMA_ID,\n version: ENRICHED_OBSERVATION_SCHEMA_VERSION,\n });\n}\n","/**\n * `parseEnrichedObservation()` — the non-stripping validation entry point.\n *\n * The records core `ObservationSchema` does not know about the satellite slots, so\n * a raw `ObservationSchema.parse(record)` SILENTLY DROPS `observableAssessments` and\n * `origin` (the A1 \"strip hazard\"). This function closes that hazard at the\n * integration layer: it validates the core and each present slot with the SAME\n * canonical validators each package ships, rejects unknown top-level keys, and\n * returns the caller's object untouched on success — slots intact.\n *\n * It composes by delegation (not by re-parsing through one mega-Zod-object), so it\n * is brand-free: each package validates its own slot with its own Zod instance,\n * exactly as the CLI does.\n */\nimport { validateObservation } from '@disclosureos/records';\nimport type { Observation } from '@disclosureos/records';\nimport type { ValidationIssue } from '@disclosureos/records/shared';\nimport { composeObservationSchema } from './compose';\nimport { defaultRegistry } from './registry';\n\n/**\n * A complete observation: the records core plus the satellite slots. Because both\n * satellite packages augment `Observation`, the slots are already part of the\n * `Observation` type wherever `@disclosureos/schema` (which imports both) is loaded;\n * this alias names that enriched shape explicitly as the public contract type.\n */\nexport type EnrichedObservation = Observation;\n\n/** Outcome of {@link parseEnrichedObservation}: issues are empty iff `success`. */\nexport interface EnrichedParseResult {\n success: boolean;\n /** The validated record (reference-equal to the input) when `success`. */\n data?: EnrichedObservation;\n /** Flat list of validation issues (empty when valid). */\n issues: ValidationIssue[];\n}\n\n/** Prefix slot-local issue paths with the slot name so errors point at the right field. */\nfunction underSlot(slot: string, issues: ValidationIssue[]): ValidationIssue[] {\n return issues.map((issue) => ({\n path: issue.path ? `${slot}.${issue.path}` : slot,\n message: issue.message,\n }));\n}\n\n// The composed schema is the single source of truth for which top-level keys are\n// allowed (records core properties + every registered slot). Computed once.\nlet allowedTopLevelKeys: Set<string> | undefined;\nfunction getAllowedTopLevelKeys(): Set<string> {\n if (!allowedTopLevelKeys) {\n const composed = composeObservationSchema();\n const observation = (composed.$defs as Record<string, Record<string, unknown>>)['Observation'];\n const properties = (observation?.['properties'] as Record<string, unknown>) ?? {};\n allowedTopLevelKeys = new Set(Object.keys(properties));\n }\n return allowedTopLevelKeys;\n}\n\n/**\n * Validate an enriched observation (records core + `observableAssessments` + `origin`)\n * in one call, WITHOUT stripping the satellite slots.\n *\n * Returns `{ success: true, data }` (where `data` is the same object you passed in)\n * when valid, or `{ success: false, issues }` with a flat list of path-tagged issues.\n * Unknown top-level keys are rejected; arbitrary data still belongs under `extensions`.\n */\nexport function parseEnrichedObservation(value: unknown): EnrichedParseResult {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return { success: false, issues: [{ path: '', message: 'expected an observation object' }] };\n }\n\n const record = value as Record<string, unknown>;\n const issues: ValidationIssue[] = [];\n\n issues.push(...validateObservation(record));\n\n // Validate every present slot with its owner's validator — driven by the registry,\n // so a newly registered slot is validated here without editing this function.\n for (const registration of defaultRegistry.list()) {\n const slotValue = record[registration.slot];\n if (slotValue !== undefined) {\n issues.push(...underSlot(registration.slot, registration.validate(slotValue)));\n }\n }\n\n const allowed = getAllowedTopLevelKeys();\n for (const key of Object.keys(record)) {\n if (!allowed.has(key)) {\n issues.push({\n path: key,\n message: `unknown top-level key \"${key}\" — third-party data belongs under \"extensions\".`,\n });\n }\n }\n\n if (issues.length > 0) {\n return { success: false, issues };\n }\n return { success: true, data: record as unknown as EnrichedObservation, issues: [] };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@disclosureos/schema",
3
+ "version": "1.0.0",
4
+ "description": "The DisclosureOS portable contract — composes the records core, observable assessments, and origin classification into one enriched Observation schema (TypeScript + JSON Schema), with a non-stripping parse",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/disclosurefoundation/disclosureos"
9
+ },
10
+ "homepage": "https://github.com/disclosurefoundation/disclosureos#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/disclosurefoundation/disclosureos/issues"
13
+ },
14
+ "keywords": [
15
+ "disclosureos",
16
+ "UAP",
17
+ "schema",
18
+ "json-schema",
19
+ "observation",
20
+ "typescript"
21
+ ],
22
+ "sideEffects": false,
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./schema": "./schema/enriched-observation.schema.json",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "main": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "files": [
38
+ "dist",
39
+ "schema",
40
+ "README.md",
41
+ "CHANGELOG.md",
42
+ "LICENSE"
43
+ ],
44
+ "dependencies": {
45
+ "zod": "^4.4.3",
46
+ "@disclosureos/records": "1.0.0",
47
+ "@disclosureos/observables": "1.0.0",
48
+ "@disclosureos/origins": "1.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20",
52
+ "tsup": "^8.0.0",
53
+ "tsx": "^4.21.0",
54
+ "typescript": "^5.7.2",
55
+ "vitest": "^4.0.18",
56
+ "@arethetypeswrong/cli": "^0.18.3",
57
+ "publint": "^0.3.21",
58
+ "@repo/typescript-config": "0.0.0"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "scripts": {
64
+ "build": "tsup",
65
+ "dev": "tsup --watch",
66
+ "lint": "eslint .",
67
+ "type-check": "tsc --noEmit",
68
+ "emit:schema": "tsx scripts/emit-schema.ts",
69
+ "clean": "rm -rf dist",
70
+ "test": "vitest",
71
+ "lint:publish": "publint --strict && attw --pack . --profile esm-only"
72
+ }
73
+ }