diff --git a/schemas/V_gamma/CURIE_lookups_meta.json b/schemas/V_gamma/CURIE_lookups_meta.json index e556fc1..9fc1c22 100644 --- a/schemas/V_gamma/CURIE_lookups_meta.json +++ b/schemas/V_gamma/CURIE_lookups_meta.json @@ -3,7 +3,7 @@ "$id": "https://did-schema.example.org/meta/CURIE_lookups_meta.json", "title": "CURIE Registry for DID/NDI V_gamma", "description": "Registry mapping CURIE prefixes (used in ontology_term.node values and in field-level _ontology._node annotations) to their authoritative URI base and metadata. Consumer tooling uses this file to expand a CURIE such as 'uberon:0002436' into the full URI 'http://purl.obolibrary.org/obo/UBERON_0002436'. Prefixes are matched case-insensitively; by convention prefixes are written in lowercase inside documents and annotations.", - "_format_version": "1.0.0", + "_format_version": "1.1.0", "_prefixes": { "iao": { "_label": "Information Artifact Ontology", @@ -33,6 +33,20 @@ "_approximate": false, "_documentation": "Anatomical ontology for the developing mouse." }, + "mmusdv": { + "_label": "Mouse Developmental Stages", + "_uri_base": "http://purl.obolibrary.org/obo/MmusDv_", + "_uri_style": "obo_underscore", + "_approximate": false, + "_documentation": "Life-cycle / developmental-stage ontology for Mus musculus. Useful inside ontology_term values and in time_reference values whose referent_kind == 'ontology_stage' (e.g., 'mmusdv:0000037' postnatal stage)." + }, + "hsapdv": { + "_label": "Human Developmental Stages", + "_uri_base": "http://purl.obolibrary.org/obo/HsapDv_", + "_uri_style": "obo_underscore", + "_approximate": false, + "_documentation": "Life-cycle / developmental-stage ontology for Homo sapiens. Useful for time_reference values whose referent_kind == 'ontology_stage'." + }, "ncit": { "_label": "NCI Thesaurus", "_uri_base": "http://purl.obolibrary.org/obo/NCIT_", diff --git a/schemas/V_gamma/did_schema_meta.json b/schemas/V_gamma/did_schema_meta.json index d3c68f6..e86b87c 100644 --- a/schemas/V_gamma/did_schema_meta.json +++ b/schemas/V_gamma/did_schema_meta.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://did-schema.example.org/V_gamma/did_schema_meta.json", "title": "DID/NDI Schema Meta-Schema (V_gamma)", - "description": "Validates the structure of DID/NDI schema files for V_gamma. V_gamma introduces named composite types 'duration', 'ontology_term', 'volume', 'mass', 'length', 'voltage', 'current', and 'frequency', and a redesigned field-level '_ontology' annotation shape with keys '_node' (CURIE) and '_name' (label); the CURIE prefix is resolved against CURIE_lookups_meta.json. The six SI-dimensioned types share the same sub-field layout as 'duration' (a canonical-unit double, 'approximate' boolean, 'source_unit' char, 'source_value' double); only the canonical sub-field name and the allowed source units differ. NAMING CONVENTION: property names that are standard JSON Schema keywords (type, $schema, $id, $ref, $defs, properties, required, items, enum, const, pattern, description, etc.) are used as-is. Every property name introduced by this schema system — i.e. not part of the JSON Schema vocabulary — is prefixed with an underscore (_) to distinguish custom extensions from standard vocabulary.", + "description": "Validates the structure of DID/NDI schema files for V_gamma. V_gamma introduces named composite types 'duration', 'ontology_term', 'volume', 'mass', 'length', 'voltage', 'current', 'frequency', 'timeref_epochset', and 'time_reference', and a redesigned field-level '_ontology' annotation shape with keys '_node' (CURIE) and '_name' (label); the CURIE prefix is resolved against CURIE_lookups_meta.json. The seven SI-dimensioned types share the same sub-field layout as 'duration' (a canonical-unit double, 'approximate' boolean, 'source_unit' char, 'source_value' double); only the canonical sub-field name and the allowed source units differ. The 'time_reference' type expresses a time value as an offset from a chosen origin (a depended-on document, an acquisition-clock epoch, a UTC wall-clock timestamp, or an ontology-anchored stage); 'timeref_epochset' is the four-tuple sub-composite that names an acquisition-clock origin. NAMING CONVENTION: property names that are standard JSON Schema keywords (type, $schema, $id, $ref, $defs, properties, required, items, enum, const, pattern, description, etc.) are used as-is. Every property name introduced by this schema system — i.e. not part of the JSON Schema vocabulary — is prefixed with an underscore (_) to distinguish custom extensions from standard vocabulary.", "type": "object", "required": [ "_classname", @@ -215,7 +215,9 @@ "length", "voltage", "current", - "frequency" + "frequency", + "timeref_epochset", + "time_reference" ], "description": "Data type of the field. Uses the standard JSON Schema keyword 'type'; values are NDI-specific." }, diff --git a/schemas/V_gamma/session_occurrence.json b/schemas/V_gamma/session_occurrence.json new file mode 100644 index 0000000..d744858 --- /dev/null +++ b/schemas/V_gamma/session_occurrence.json @@ -0,0 +1,60 @@ +{ + "_classname": "session_occurrence", + "_class_version": "1.0.0", + "_maturity_level": "work_in_progress", + "_superclasses": [ + { + "_classname": "base", + "_schema": "$NDISCHEMAPATH/base/schema.json" + } + ], + "_depends_on": [ + { + "_name": "session_id", + "_mustBeNonEmpty": true, + "_documentation": "The session document this occurrence describes. Cascading delete: when the session is deleted, this occurrence is deleted too.", + "_must_refer_to_document_class": "session" + }, + { + "_name": "started_at_referent_id", + "_mustBeNonEmpty": false, + "_documentation": "Document that anchors started_at when started_at.referent_kind == 'document'. Empty otherwise.", + "_must_refer_to_document_class": "" + }, + { + "_name": "ended_at_referent_id", + "_mustBeNonEmpty": false, + "_documentation": "Document that anchors ended_at when ended_at.referent_kind == 'document'. Empty otherwise.", + "_must_refer_to_document_class": "" + } + ], + "_file": [], + "_fields": [ + { + "_name": "started_at", + "type": "time_reference", + "_blank_value": null, + "_default_value": null, + "_mustBeNonEmpty": false, + "_mustBeScalar": true, + "_mustNotHaveNaN": true, + "_queryable": true, + "_ontology": null, + "_documentation": "When the session started. Optional; leave null when start time is not recorded. Use referent_kind == 'unknown' to assert that the start time is intentionally not knowable rather than merely missing.", + "_constraints": {} + }, + { + "_name": "ended_at", + "type": "time_reference", + "_blank_value": null, + "_default_value": null, + "_mustBeNonEmpty": false, + "_mustBeScalar": true, + "_mustNotHaveNaN": true, + "_queryable": true, + "_ontology": null, + "_documentation": "When the session ended. Same shape and conventions as started_at.", + "_constraints": {} + } + ] +} diff --git a/schemas/V_gamma/subject_birth_event.json b/schemas/V_gamma/subject_birth_event.json new file mode 100644 index 0000000..ab8656f --- /dev/null +++ b/schemas/V_gamma/subject_birth_event.json @@ -0,0 +1,35 @@ +{ + "_classname": "subject_birth_event", + "_class_version": "1.0.0", + "_maturity_level": "work_in_progress", + "_superclasses": [ + { + "_classname": "base", + "_schema": "$NDISCHEMAPATH/base/schema.json" + } + ], + "_depends_on": [ + { + "_name": "subject_id", + "_mustBeNonEmpty": true, + "_documentation": "The subject document whose birth (or birth-equivalent origin) this document records. Cascading delete: when the subject tag document is deleted, this event is deleted too.", + "_must_refer_to_document_class": "subject" + } + ], + "_file": [], + "_fields": [ + { + "_name": "occurred_at", + "type": "time_reference", + "_blank_value": null, + "_default_value": null, + "_mustBeNonEmpty": true, + "_mustBeScalar": true, + "_mustNotHaveNaN": true, + "_queryable": true, + "_ontology": null, + "_documentation": "When the subject was born, expressed as a time_reference. For wild-caught animals or subjects without recorded DOB, use referent_kind == 'unknown'; for stage-only knowledge use referent_kind == 'ontology_stage' with a developmental-stage CURIE (e.g., 'mmusdv:0000037' postnatal stage). Document-kind references are unusual here but permitted (e.g., birth witnessed during another logged event).", + "_constraints": {} + } + ] +} diff --git a/schemas/V_gamma/treatment.json b/schemas/V_gamma/treatment.json index cb151d3..515ce19 100644 --- a/schemas/V_gamma/treatment.json +++ b/schemas/V_gamma/treatment.json @@ -1,6 +1,6 @@ { "_classname": "treatment", - "_class_version": "2.0.0", + "_class_version": "2.1.0", "_maturity_level": "work_in_progress", "_superclasses": [ { @@ -26,6 +26,12 @@ "_mustBeNonEmpty": false, "_documentation": "The document ID of the protocol associated with this treatment.", "_must_refer_to_document_class": "" + }, + { + "_name": "administered_at_referent_id", + "_mustBeNonEmpty": false, + "_documentation": "Document whose start, end, or midpoint is the origin for administered_at. Required when administered_at.referent_kind == 'document'; left empty otherwise. Cascading delete: if the referenced document is deleted, this treatment document is deleted too.", + "_must_refer_to_document_class": "" } ], "_file": [], @@ -68,6 +74,19 @@ "_ontology": null, "_documentation": "The character string value associated with the treatment.", "_constraints": {} + }, + { + "_name": "administered_at", + "type": "time_reference", + "_blank_value": null, + "_default_value": null, + "_mustBeNonEmpty": false, + "_mustBeScalar": true, + "_mustNotHaveNaN": true, + "_queryable": true, + "_ontology": null, + "_documentation": "When the treatment was administered, expressed as a time_reference. Optional — leave null when administration time is not recorded. When referent_kind == 'document', depends_on_name must equal 'administered_at_referent_id'.", + "_constraints": {} } ] } diff --git a/schemas/V_gamma/valid_interval.json b/schemas/V_gamma/valid_interval.json index 8b994fa..7f5a950 100644 --- a/schemas/V_gamma/valid_interval.json +++ b/schemas/V_gamma/valid_interval.json @@ -1,6 +1,6 @@ { "_classname": "valid_interval", - "_class_version": "1.0.0", + "_class_version": "2.0.0", "_maturity_level": "work_in_progress", "_superclasses": [ { @@ -14,226 +14,46 @@ "_mustBeNonEmpty": true, "_documentation": "The document ID of the element whose valid interval is marked.", "_must_refer_to_document_class": "" + }, + { + "_name": "t0_referent_id", + "_mustBeNonEmpty": false, + "_documentation": "Document whose start, end, or midpoint serves as the t0 origin. Required when t0_reference.referent_kind == 'document'; left empty otherwise. Cascading delete: if the referenced document is deleted, this valid_interval document is deleted too.", + "_must_refer_to_document_class": "" + }, + { + "_name": "t1_referent_id", + "_mustBeNonEmpty": false, + "_documentation": "Document whose start, end, or midpoint serves as the t1 origin. Required when t1_reference.referent_kind == 'document'; left empty otherwise.", + "_must_refer_to_document_class": "" } ], "_file": [], "_fields": [ { - "_name": "timeref_structt0", - "type": "structure", - "_blank_value": {}, - "_default_value": {}, - "_mustBeNonEmpty": true, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "Time reference for the start of the interval (t0). Specifies the epoch and clock against which t0 is measured.", - "_constraints": {}, - "_fields": [ - { - "_name": "referent_epochsetname", - "type": "char", - "_blank_value": "", - "_default_value": "", - "_mustBeNonEmpty": false, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "The name of the ndi.epoch.epochset object (e.g., device name, probe name, element name) that provides the time reference.", - "_constraints": { - "maxLength": 256 - } - }, - { - "_name": "referent_classname", - "type": "char", - "_blank_value": "", - "_default_value": "", - "_mustBeNonEmpty": false, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "The class name of the referent epochset object (e.g., 'element', 'probe', 'device').", - "_constraints": { - "maxLength": 256 - } - }, - { - "_name": "clocktypestring", - "type": "char", - "_blank_value": "", - "_default_value": "", - "_mustBeNonEmpty": false, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "String identifier of the ndi.time.clocktype used for this reference. Valid values: 'dev_local_time', 'dev_global_time', 'utc', 'approx_utc', 'exp_global_time', 'no_time'.", - "_constraints": { - "maxLength": 64, - "allowed_values": [ - "dev_local_time", - "dev_global_time", - "utc", - "approx_utc", - "exp_global_time", - "no_time" - ] - } - }, - { - "_name": "epoch", - "type": "char", - "_blank_value": "", - "_default_value": "", - "_mustBeNonEmpty": false, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "The epochid of the epoch in the referent epochset that provides the time origin.", - "_constraints": { - "maxLength": 256 - } - }, - { - "_name": "time", - "type": "double", - "_blank_value": 0.0, - "_default_value": 0.0, - "_mustBeNonEmpty": true, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "The time offset (in seconds) within the reference epoch that serves as the origin for t0.", - "_constraints": {} - } - ] - }, - { - "_name": "t0", - "type": "double", - "_blank_value": 0.0, - "_default_value": 0.0, + "_name": "t0_reference", + "type": "time_reference", + "_blank_value": null, + "_default_value": null, "_mustBeNonEmpty": true, "_mustBeScalar": true, - "_mustNotHaveNaN": false, + "_mustNotHaveNaN": true, "_queryable": true, "_ontology": null, - "_documentation": "Start time of the valid interval in seconds, measured relative to the origin defined by timeref_structt0.", + "_documentation": "Time reference for the start of the interval. Carries the kind of origin (document, acquisition_clock, utc, ontology_stage, unknown) and the offset_seconds past that origin. When referent_kind == 'document', the depends_on_name sub-field must equal 't0_referent_id'.", "_constraints": {} }, { - "_name": "timeref_structt1", - "type": "structure", - "_blank_value": {}, - "_default_value": {}, - "_mustBeNonEmpty": true, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "Time reference for the end of the interval (t1). Specifies the epoch and clock against which t1 is measured.", - "_constraints": {}, - "_fields": [ - { - "_name": "referent_epochsetname", - "type": "char", - "_blank_value": "", - "_default_value": "", - "_mustBeNonEmpty": false, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "The name of the ndi.epoch.epochset object (e.g., device name, probe name, element name) that provides the time reference.", - "_constraints": { - "maxLength": 256 - } - }, - { - "_name": "referent_classname", - "type": "char", - "_blank_value": "", - "_default_value": "", - "_mustBeNonEmpty": false, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "The class name of the referent epochset object (e.g., 'element', 'probe', 'device').", - "_constraints": { - "maxLength": 256 - } - }, - { - "_name": "clocktypestring", - "type": "char", - "_blank_value": "", - "_default_value": "", - "_mustBeNonEmpty": false, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "String identifier of the ndi.time.clocktype used for this reference. Valid values: 'dev_local_time', 'dev_global_time', 'utc', 'approx_utc', 'exp_global_time', 'no_time'.", - "_constraints": { - "maxLength": 64, - "allowed_values": [ - "dev_local_time", - "dev_global_time", - "utc", - "approx_utc", - "exp_global_time", - "no_time" - ] - } - }, - { - "_name": "epoch", - "type": "char", - "_blank_value": "", - "_default_value": "", - "_mustBeNonEmpty": false, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "The epochid of the epoch in the referent epochset that provides the time origin.", - "_constraints": { - "maxLength": 256 - } - }, - { - "_name": "time", - "type": "double", - "_blank_value": 0.0, - "_default_value": 0.0, - "_mustBeNonEmpty": true, - "_mustBeScalar": true, - "_mustNotHaveNaN": false, - "_queryable": false, - "_ontology": null, - "_documentation": "The time offset (in seconds) within the reference epoch that serves as the origin for t1.", - "_constraints": {} - } - ] - }, - { - "_name": "t1", - "type": "double", - "_blank_value": 0.0, - "_default_value": 0.0, + "_name": "t1_reference", + "type": "time_reference", + "_blank_value": null, + "_default_value": null, "_mustBeNonEmpty": true, "_mustBeScalar": true, - "_mustNotHaveNaN": false, + "_mustNotHaveNaN": true, "_queryable": true, "_ontology": null, - "_documentation": "End time of the valid interval in seconds, measured relative to the origin defined by timeref_structt1. Must be >= t0 when both references are the same clock.", + "_documentation": "Time reference for the end of the interval. Same shape as t0_reference. Must be greater than or equal to t0_reference when both resolve to the same origin.", "_constraints": {} } ] diff --git a/schemas/V_gamma/virus_injection.json b/schemas/V_gamma/virus_injection.json index fd616a6..8cb30ab 100644 --- a/schemas/V_gamma/virus_injection.json +++ b/schemas/V_gamma/virus_injection.json @@ -1,6 +1,6 @@ { "_classname": "virus_injection", - "_class_version": "1.0.0", + "_class_version": "1.1.0", "_maturity_level": "work_in_progress", "_superclasses": [ { @@ -14,6 +14,12 @@ "_mustBeNonEmpty": true, "_documentation": "The document ID of the subject that received this virus injection.", "_must_refer_to_document_class": "" + }, + { + "_name": "injected_at_referent_id", + "_mustBeNonEmpty": false, + "_documentation": "Document whose start, end, or midpoint is the origin for injected_at. Required when injected_at.referent_kind == 'document'; left empty otherwise. Cascading delete: if the referenced document is deleted, this virus_injection document is deleted too.", + "_must_refer_to_document_class": "" } ], "_file": [], @@ -118,6 +124,19 @@ "_constraints": { "maxLength": 256 } + }, + { + "_name": "injected_at", + "type": "time_reference", + "_blank_value": null, + "_default_value": null, + "_mustBeNonEmpty": false, + "_mustBeScalar": true, + "_mustNotHaveNaN": true, + "_queryable": true, + "_ontology": null, + "_documentation": "When the injection was performed, expressed as a time_reference. Optional. When referent_kind == 'document', depends_on_name must equal 'injected_at_referent_id'.", + "_constraints": {} } ] } diff --git a/schemas/V_gamma_SPEC.md b/schemas/V_gamma_SPEC.md index 4d92c15..e6f1d28 100644 --- a/schemas/V_gamma_SPEC.md +++ b/schemas/V_gamma_SPEC.md @@ -343,6 +343,8 @@ Relevant CURIEs for annotating fields: | `current` | Named composite: canonical amperes with unit provenance | `{ "minimum": number or null, "maximum": number or null, "allowed_units": array or null }` | Sub-fields: `amperes`, `approximate`, `source_unit`, `source_value`. `minimum`/`maximum` bound the canonical (`amperes`). | | `frequency` | Named composite: canonical hertz with unit provenance | `{ "minimum": number or null, "maximum": number or null, "allowed_units": array or null }` | Sub-fields: `hertz`, `approximate`, `source_unit`, `source_value`. `minimum`/`maximum` bound the canonical (`hertz`). | | `ontology_term` | Named composite: CURIE ontology reference with label snapshot | `{ "allowed_namespaces": array or null }` | Sub-fields: `node`, `name`. See "Named Composite Types". | +| `timeref_epochset` | Named composite: four-tuple naming an acquisition-clock origin (referent epochset name + class + epoch + clocktype) | `{ "allowed_classnames": array or null, "allowed_clocktypes": array or null }` | Sub-fields: `epochsetname`, `classname`, `epoch`, `clocktype`. See "Named Composite Types". | +| `time_reference` | Named composite: a time value expressed as an offset from a chosen origin (depended-on document, acquisition-clock epoch, UTC wall clock, ontology-anchored stage, or unknown) | `{ "allowed_kinds": array or null, "allowed_stage_namespaces": array or null }` | Sub-fields: `referent_kind`, `depends_on_name`, `anchor_point`, `epochset` (a `timeref_epochset`), `utc_timestamp`, `stage` (an `ontology_term`), `offset_seconds`, `approximate`, `notes`. See "Named Composite Types". | #### Semantics of validation flags by type @@ -364,6 +366,8 @@ Relevant CURIEs for annotating fields: | `current` | yes (amperes not null, source_unit non-empty, source_value not null) | yes — always true (single structured value) | yes (amperes and source_value not NaN) | | `frequency` | yes (hertz not null, source_unit non-empty, source_value not null) | yes — always true (single structured value) | yes (hertz and source_value not NaN) | | `ontology_term` | yes (node non-empty) | yes — always true | no — must be `false` | +| `timeref_epochset` | yes (all four sub-fields non-empty) | yes — always true | no — must be `false` | +| `time_reference` | yes (referent_kind non-empty; payload required by kind, see below) | yes — always true | yes (offset_seconds and any nested numeric not NaN) | --- @@ -391,7 +395,10 @@ specification — schema authors do NOT declare the sub-fields with ### Rules for named composites - Sub-field names are fixed by the spec; authors do not choose them. -- Sub-field types are primitive (`char`, `double`, `boolean`, etc.). +- Sub-field types are usually primitive (`char`, `double`, `boolean`, + etc.). A composite may nest another named composite as a sub-field + (e.g., `time_reference` embeds `timeref_epochset` and `ontology_term`); + the validator looks up the nested shape from this spec by type name. - Sub-field names are **not** underscore-prefixed — they are data field names, parallel to the field names inside a generic `structure`. - `_mustBeScalar` for a named composite value is always `true` (the value @@ -638,6 +645,175 @@ document was written in. If a particular study deliberately pins to a specific ontology release across time, that belongs in a study-level annotation, not on each term value. +### `timeref_epochset` (new in V_gamma) + +A `timeref_epochset` value is the four-tuple that identifies the origin of +an acquisition-clock time reference: which epochset (typically a device, +probe, or element) owns the clock, which of its epochs the time is measured +within, and which clock that epochset uses. + +It exists as its own named composite so it can be reused — currently only +inside `time_reference`, but available wherever else the same four fields +are needed (e.g., a future `epochclocktimes` rewrite). + +**Sub-fields (in document values):** + +| Sub-field | Type | Description | +|----------------|------|-------------| +| `epochsetname` | char | Name of the ndi epochset object (device / probe / element name). | +| `classname` | char | Class of that referent (e.g., `element`, `probe`, `device`). | +| `epoch` | char | The `epochid` within that epochset that defines the origin epoch. | +| `clocktype` | char | Which clock owned by the referent. One of: `dev_local_time`, `dev_global_time`, `exp_global_time`. UTC and approximate-UTC are not values here — they are their own `referent_kind` on `time_reference`. | + +**`_constraints` keys allowed:** +- `allowed_classnames` (array of strings or null) — restrict permissible + `classname` values for this field. +- `allowed_clocktypes` (array of strings or null) — restrict permissible + `clocktype` values. + +`_mustBeNonEmpty: true` requires all four sub-fields to be non-empty. + +### `time_reference` (new in V_gamma) + +A `time_reference` value expresses a time as `(origin, offset)` — a chosen +origin plus a number of seconds past it. The origin can be: + +- another document (referenced through the containing schema's `_depends_on` + array, so referential integrity and cascading deletes are inherited from + the existing dependency machinery), +- an acquisition-clock epoch (via a nested `timeref_epochset`), +- a UTC wall-clock timestamp (in which case there is no offset — the + timestamp *is* the time), +- an ontology-anchored developmental or life-cycle stage (via a nested + `ontology_term`), with an optional offset within the stage, +- explicitly `unknown` — a structurally valid placeholder for cases where + the time was not recorded (wild-caught animals with no DOB, treatments + delivered without a logged time, etc.). + +The discriminator is `referent_kind`. Different sub-fields are populated +for different kinds; unused sub-fields take their blank values. The +single value shape, with one discriminator, keeps the generic dot-path +query engine working unchanged (e.g., `started_at.utc_timestamp` or +`administered_at.offset_seconds`). + +**Sub-fields (in document values):** + +| Sub-field | Type | Used when `referent_kind` is | Description | +|-------------------|------------------|------------------------------|-------------| +| `referent_kind` | char (enum) | always | One of: `document`, `acquisition_clock`, `utc`, `ontology_stage`, `unknown`. | +| `depends_on_name` | char | `document` | Name of the `_depends_on` entry on the containing schema that resolves to the referent's did_uid. The actual document id lives in that depends_on slot, not in this value, so cascading deletes are inherited from the dependency mechanism. | +| `anchor_point` | char (enum) | `document` | Which point on the referenced document is the origin: `start` or `end`. | +| `epochset` | `timeref_epochset` | `acquisition_clock` | Nested composite naming the device / probe / element + epoch + clocktype that defines zero. | +| `utc_timestamp` | char (ISO 8601) | `utc` | Wall-clock time. The timestamp *is* the time — no offset applies. | +| `stage` | `ontology_term` | `ontology_stage` | Developmental / life-cycle stage CURIE + label. | +| `offset_seconds` | double | `document`, `acquisition_clock`, `ontology_stage` | Seconds past the origin. `0.0` means "the origin itself". Not present (or zero) for the `utc` and `unknown` kinds. | +| `approximate` | boolean | always | `true` for known-imprecise references (former `approx_utc` clock; reconstructed treatment times; rough stage assignments). | +| `notes` | char | always | Free-text qualifier (e.g., "DOB inferred from body mass"). May be empty. | + +**Required-by-kind table.** The validator must enforce, in addition to +the per-field validation flags: + +| `referent_kind` | required (non-blank) sub-fields | +|---------------------|--------------------------------------------------------------| +| `document` | `depends_on_name`, `anchor_point`, `offset_seconds` | +| `acquisition_clock` | `epochset`, `offset_seconds` | +| `utc` | `utc_timestamp` | +| `ontology_stage` | `stage`, `offset_seconds` | +| `unknown` | (none beyond `referent_kind` and `approximate`) | + +**`depends_on_name` cross-field invariant.** When `referent_kind == +"document"`, the value of `depends_on_name` must equal the `_name` of one +of the `_depends_on` entries declared on the *containing* schema, and that +slot in the actual document must hold a non-empty did_uid. When +`referent_kind != "document"`, `depends_on_name` is blank. This is what +makes referential integrity automatic: if the depended-on document is +deleted, the existing dependency cascade fires and the referencing +document is deleted too — there are no dangling time references. + +**Why no `midpoint` in `anchor_point`?** Two values cover the cases that +arise in practice (start of treatment, end of session, etc.) without +introducing a third option whose computation depends on knowing both +endpoints. If a future use case clearly needs midpoints, add them as a +spec change. + +**`_constraints` keys allowed:** +- `allowed_kinds` (array of strings or null) — restrict permissible + `referent_kind` values for this field (e.g., `["document", "utc"]` to + forbid stage- or unknown-kind references on a particular field). +- `allowed_stage_namespaces` (array of strings or null) — when + `ontology_stage` is permitted, restrict the CURIE prefixes accepted on + the nested `stage.node` (e.g., `["mmusdv", "hsapdv", "emapa"]`). + +**Composite-inside-composite.** `time_reference` is the first V_gamma +composite whose sub-fields include other named composites (`epochset` → +`timeref_epochset`; `stage` → `ontology_term`). The validator looks up +those sub-composites' shapes by name from this spec, the same way it +already does for top-level `duration`, `volume`, etc. Schema authors do +not redeclare the nested shapes. + +**Example values.** + +```json +// "2 hours after the start of the treatment that this doc depends on" +{ + "referent_kind": "document", + "depends_on_name": "treatment_id", + "anchor_point": "start", + "offset_seconds": 7200.0, + "approximate": false, + "notes": "" +} + +// "3.14 s into epoch e001 of probe vhlab_p1" +{ + "referent_kind": "acquisition_clock", + "epochset": { + "epochsetname": "vhlab_p1", + "classname": "probe", + "epoch": "e001", + "clocktype": "dev_local_time" + }, + "offset_seconds": 3.14, + "approximate": false, + "notes": "" +} + +// "2024-05-12T14:30:00.250Z" — wall clock, no offset applies +{ + "referent_kind": "utc", + "utc_timestamp": "2024-05-12T14:30:00.250Z", + "approximate": false, + "notes": "" +} + +// Approx-UTC (former 'approx_utc' clock, now just an approximate flag) +{ + "referent_kind": "utc", + "utc_timestamp": "2024-05-12T14:30:00Z", + "approximate": true, + "notes": "reconstructed from lab notebook" +} + +// "Postnatal day ~4 in mouse, exact time unknown" +{ + "referent_kind": "ontology_stage", + "stage": { + "node": "mmusdv:0000037", + "name": "postnatal stage" + }, + "offset_seconds": 345600.0, + "approximate": true, + "notes": "" +} + +// Wild-caught — DOB not recorded +{ + "referent_kind": "unknown", + "approximate": true, + "notes": "wild-caught; DOB not recorded" +} +``` + ### Future Candidate Composite Types The following shapes recur in scientific data and are plausible additions @@ -799,6 +975,16 @@ Checks that can be performed with only the document and its schema file(s): listed source unit is an exact ratio of the canonical unit). All unit conversions are constants, so no tolerance window applies. - `ontology_term`: `node` matches CURIE pattern `^[a-z][a-z0-9_]*:[^\s:]+$`. +- `timeref_epochset`: object with exactly the four sub-fields + (`epochsetname`, `classname`, `epoch`, `clocktype`) of correct primitive + types; `clocktype` is one of the allowed acquisition-clock values. +- `time_reference`: object whose `referent_kind` selects the required + payload sub-fields per the table in "Named Composite Types"; nested + `epochset` (a `timeref_epochset`) and `stage` (an `ontology_term`) are + validated against their own composite shapes; `utc_timestamp`, when + present, matches ISO 8601; `depends_on_name`, when `referent_kind == + "document"`, equals the `_name` of an entry in the containing schema's + `_depends_on` array. - Type-specific constraint checks in `_constraints`: on every SI-dimensioned type, `minimum` and `maximum` (if present) bound the canonical value; on `ontology_term`, `allowed_namespaces` restricts @@ -849,7 +1035,8 @@ The meta-schema must enforce: - Each field definition has all required keys with correct types. - `type` is one of: `did_uid`, `char`, `string`, `integer`, `double`, `matrix`, `timestamp`, `boolean`, `structure`, `duration`, `volume`, - `mass`, `length`, `voltage`, `current`, `frequency`, `ontology_term`. + `mass`, `length`, `voltage`, `current`, `frequency`, `ontology_term`, + `timeref_epochset`, `time_reference`. - `_ontology` is either `null` or an object with exactly `_node` (string) and `_name` (string). - `_mustBeNonEmpty`, `_mustBeScalar`, `_mustNotHaveNaN`, `_queryable` @@ -1106,8 +1293,9 @@ pytest the manifest.** 15. **Named composite types are spec-defined, not author-defined.** V_gamma ships `duration`, `volume`, `mass`, `length`, `voltage`, - `current`, `frequency`, and `ontology_term`. Adding a new composite - is a spec change, not a schema-author action. + `current`, `frequency`, `ontology_term`, `timeref_epochset`, and + `time_reference`. Adding a new composite is a spec change, not a + schema-author action. 16. **SI-dimensioned types use practical SI units as canonical.** One fixed canonical unit per dimension (`seconds`, `liters`, `grams`, `meters`, `volts`, `amperes`, `hertz`) keeps the generic numeric diff --git a/schemas/V_gamma_notes.md b/schemas/V_gamma_notes.md index c6477f1..b37d46a 100644 --- a/schemas/V_gamma_notes.md +++ b/schemas/V_gamma_notes.md @@ -8,7 +8,7 @@ over unchanged. V_gamma adds three related features on top of V_beta: -- **Named composite types.** Eight new entries in the field-definition +- **Named composite types.** Ten new entries in the field-definition `type` enum, each backed by a fixed sub-field layout in document values: - The SI-dimensioned family, all sharing the four-sub-field shape (`` double, `approximate` boolean, `source_unit` @@ -25,11 +25,32 @@ V_gamma adds three related features on top of V_beta: - `frequency`: canonical `hertz`. - `ontology_term`: `node` (char, a CURIE), `name` (char, label snapshot). Supported `_constraints` keys: `allowed_namespaces`. + - `timeref_epochset`: four-field tuple naming an acquisition-clock + origin — `epochsetname` (char), `classname` (char), `epoch` (char), + `clocktype` (char enum: `dev_local_time`, `dev_global_time`, + `exp_global_time`). Supported `_constraints` keys: `allowed_classnames`, + `allowed_clocktypes`. + - `time_reference`: a time value expressed as `(origin, offset_seconds)`, + where the origin is one of: a depended-on document (via the + containing schema's `_depends_on` array, so cascading deletes apply), + an acquisition-clock epoch (a nested `timeref_epochset`), a UTC + wall-clock timestamp (no offset applies), an ontology-anchored + developmental stage (a nested `ontology_term`), or `unknown`. Sub-fields: + `referent_kind` (enum), `depends_on_name`, `anchor_point` (`start` or + `end`), `epochset`, `utc_timestamp`, `stage`, `offset_seconds`, + `approximate`, `notes`. Supported `_constraints` keys: `allowed_kinds`, + `allowed_stage_namespaces`. See `V_gamma_SPEC.md` for the + required-by-kind table. These composite types let a single field carry what previously required several coordinated char/double fields. The canonical units of the SI-dimensioned family are practical SI (grams over kilograms, liters over cubic metres) so lab-scale values read naturally. + Composites may now nest other composites: `time_reference` embeds + `timeref_epochset` and `ontology_term` as sub-fields. The validator + resolves the nested shape from this spec by type name; schema authors + do not redeclare the sub-shape. + - **CURIE registry.** A new advisory file `CURIE_lookups_meta.json` maps CURIE prefixes (e.g., `iao`, `uberon`, `schema`, `allen_ccf_v3`) to their authoritative URI base and metadata. The registry is consumed by tooling @@ -61,6 +82,68 @@ the document-visible field layout changed. field is a comma-separated list of CURIEs (not a single composite), so it stays at `1.0.0`. +## `time_reference` migration of `valid_interval` (2.0.0) + +`valid_interval.json` previously carried two ad-hoc 5-sub-field structures +(`timeref_structt0`, `timeref_structt1`) plus two loose `t0` / `t1` +doubles. With the introduction of the `time_reference` composite type, +that schema is rewritten to: + +| Removed fields | Added fields | +|------------------------------------------------------------------|--------------------------------------------------------------------| +| `timeref_structt0`, `t0`, `timeref_structt1`, `t1` (4 fields) | `t0_reference` (`time_reference`), `t1_reference` (`time_reference`) | + +Two new optional `_depends_on` entries (`t0_referent_id`, +`t1_referent_id`) carry the document references when either side's +`referent_kind == 'document'`, so referential integrity (and cascading +delete) is inherited from the existing dependency machinery rather than +re-implemented. `_class_version` is bumped from `1.0.0` to `2.0.0` +because the document-visible field layout changed. + +The acquisition-clock case (the only one the old `timeref_struct*` shapes +could really express) round-trips through the new composite by setting +`referent_kind == 'acquisition_clock'` and populating the nested +`epochset` sub-composite (`epochsetname`, `classname`, `epoch`, +`clocktype`). + +## `time_reference` adoption on existing schemas (MINOR bumps) + +Two existing schemas gained a single optional `time_reference` field plus +a matching optional `_depends_on` entry: + +| Schema | Old version → new | Field added | Depends-on added | +|--------------------|-------------------|-------------------|-------------------------------| +| `treatment` | `2.0.0` → `2.1.0` | `administered_at` | `administered_at_referent_id` | +| `virus_injection` | `1.0.0` → `1.1.0` | `injected_at` | `injected_at_referent_id` | + +Both fields are `_mustBeNonEmpty: false`; documents written before the +bump remain valid. + +## New schemas: `session_occurrence`, `subject_birth_event` + +Two new tag-style companion documents were added rather than overloading +existing tag documents: + +- `session_occurrence` — depends on a `session` document; carries + optional `started_at` and `ended_at` `time_reference` fields. Created + separately so that `session.json` remains a stable identifier other + documents can `_depends_on` before its occurrence times are known. +- `subject_birth_event` — depends on a `subject` document; carries + required `occurred_at` (a `time_reference`). The `subject` document + remains a tag-only identifier, so DOB does not live there. For + wild-caught animals or subjects without recorded DOB, the + `occurred_at` value uses `referent_kind == 'unknown'`; for + stage-only knowledge, `referent_kind == 'ontology_stage'` with a + developmental-stage CURIE (e.g., `mmusdv:0000037`). + +## CURIE registry: `mmusdv`, `hsapdv` added (1.1.0) + +`CURIE_lookups_meta.json` `_format_version` bumped `1.0.0` → `1.1.0`. +Two new prefixes registered to support stage-anchored time references: + +- `mmusdv` — Mouse Developmental Stages (`http://purl.obolibrary.org/obo/MmusDv_`) +- `hsapdv` — Human Developmental Stages (`http://purl.obolibrary.org/obo/HsapDv_`) + ## `duration` constraint-key rename `duration` originally shipped with `_constraints` keys diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 7740cdd..d2b05db 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -16,12 +16,14 @@ } # V_gamma adds named composite types: the SI-dimensioned family (duration, -# volume, mass, length, voltage, current, frequency) plus ontology_term. +# volume, mass, length, voltage, current, frequency), ontology_term, plus +# the time-reference pair (timeref_epochset, time_reference). VALID_TYPES_BY_VERSION = { "V_beta": VALID_TYPES_BASE, "V_gamma": VALID_TYPES_BASE | { "duration", "volume", "mass", "length", "voltage", "current", "frequency", "ontology_term", + "timeref_epochset", "time_reference", }, }