@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 +20 -0
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/schema/enriched-observation.schema.json +2723 -0
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
|
+
[](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)
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|