diff --git a/README.md b/README.md
index e3171c6..e25dc88 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,7 @@ See the [Practical Benefits for Service and Platform Vendors](#51-practical-bene
- [3. Semantics and Capabilities](#3-semantics-and-capabilities)
- [3.1 Core Operations](#31-core-operations)
- [3.2 GTS Types Inheritance](#32-gts-types-inheritance)
+ - [3.2.1 Top-level composition rules for GTS Type Schemas](#321-top-level-composition-rules-for-gts-type-schemas)
- [3.3 Query Language](#33-query-language)
- [3.4 Attribute selector](#34-attribute-selector)
- [3.5 Access control with wildcards](#35-access-control-with-wildcards)
@@ -112,6 +113,7 @@ See the [Practical Benefits for Service and Platform Vendors](#51-practical-bene
| 0.9 | Add `x-gts-final` and `x-gts-abstract` schema modifiers; enforce final/abstract semantics in OP#6 and OP#12 |
| 0.10 | BREAKING: terminology unified around GTS Type / GTS Instance; rename API fields `schema_id` → `type_id` (also `old_schema_id`/`new_schema_id`/`to_schema_id`/`selected_schema_id_field`); rename API field `is_schema` → `is_type` (type-definition vs instance discriminator); `type_id` MUST be a GTS Type Identifier or `null` — no longer falls back to JSON Schema dialect URL; rename endpoints `/validate-schema` → `/validate-type`, `/schemas` → `/types`; rename OP#12 'Schema vs Schema Validation' → 'Type Derivation Validation'; rename OpenAPI components `ValidateSchemaRequest` → `ValidateTypeRequest`, `SchemaRegister` → `TypeRegister`; rename example directories `examples/**/schemas/` → `examples/**/types/` (file extensions `.schema.json` retained); add Terminology section |
| 0.11 | Introduce term **GTS Type Schema** as the canonical definition of a GTS Type; remove the standalone `Schema` term from Terminology; rewrite `GTS Type` entry to name the abstract registered entity; rename `GTS Type Registry` → `GTS Registry` (registry now scopes both Type Schemas and well-known Instances). **Conformance tests for reference implementations** also updated: rename API endpoints `/validate-type` → `/validate-type-schema` and `/types` → `/type-schemas`; rename OpenAPI components `TypeRegister` → `TypeSchemaRegister`, `ValidateTypeRequest` → `ValidateTypeSchemaRequest`; rename request field `TypeSchemaRegister.schema` → `TypeSchemaRegister.type_schema`; rename helper `validate_type` → `validate_type_schema`. |
+| 0.12 | BREAKING: introduce normative **top-level composition rules** (new §3.2.1, see [ADR-0002](./adr/0002-strict-allof-derivation-form.md)) — derived GTS Type Schemas MUST use the strict canonical form (single-item `allOf` with `$ref` to the immediate parent in the chained `$id`; overlay constraints at the top level); top-level `anyOf`/`oneOf`/`not` are forbidden on all GTS Type Schemas (nested usage unchanged). **Trait system redesign** (§9.7 full rewrite): `x-gts-traits-schema` value type changes from inline JSON Schema to a **single GTS Type URN string** referencing a registered trait-type; inline-schema, `$ref`, and composition forms removed (no deprecation); trait-types are first-class GTS Type Schemas subject to the same derivation rules; host-type and trait-type derivation chains run in parallel (a derived host's trait-type MUST be derived from the ancestor's trait-type); trait completeness made explicit — a non-abstract host with unresolved required trait fields is invalid; shallow + immutable-once-set merge semantics preserved. **OP#12** extended with structural validation per §3.2.1. **Existing examples and tests** (`examples/events/types/`, `test_op12_*`, `test_op13_*`) are rewritten to the new model. |
## Terminology
@@ -435,6 +437,82 @@ CREATE TABLE events (
4. **Full validation**: All events are validated against their registered GTS schema before insertion, ensuring data quality despite flexible storage
+### 3.2.1 Top-level composition rules for GTS Type Schemas
+
+GTS Type Schemas use JSON Schema composition (`allOf`) to express type derivation. To keep validators, linters, and codegen tooling simple, and to give authors one obvious place to declare each kind of constraint, the spec requires a **single canonical form** at the top level. The full rationale and rejected alternatives are recorded in [ADR-0002](./adr/0002-strict-allof-derivation-form.md).
+
+**Terminology used in this subsection:**
+
+- **Base GTS Type Schema** — a GTS Type Schema whose `$id` chain has exactly **one** segment, ending in `~` (e.g., `gts://gts.x.core.events.type.v1~`).
+- **Derived GTS Type Schema** — a GTS Type Schema whose `$id` chain has **two or more** segments separated by `~`, still ending in `~` (e.g., `gts://A~B~` or `gts://A~B~C~`).
+
+Both are *type* identifiers — both end in `~`. Instance identifiers (no trailing `~`) are not GTS Type Schemas and are not addressed by this subsection.
+
+#### Rules for derived schemas
+
+1. **`allOf` is required.** A derived GTS Type Schema MUST contain an `allOf` keyword at its top level.
+2. **`allOf` contains exactly one subschema.** That subschema MUST be `{"$ref": "gts://
"}`, where `
` is the **immediate** parent in the chained `$id`. The `$ref` value MUST be an exact string-prefix match against `$id` (for `$id = gts://A~B~C~` the `$ref` MUST be `gts://A~B~`). Skip-level `$ref`s (e.g., `gts://A~` from `$id = gts://A~B~C~`) and `$ref`s to types outside the `$id` chain are invalid.
+3. **Overlay at top level.** Every constraint the derived schema contributes — `type`, `properties`, `required`, narrowed constraints, `additionalProperties`, `x-gts-traits-schema`, `x-gts-traits`, `x-gts-final`, `x-gts-abstract`, and any other keyword — MUST appear at the top level of the schema, alongside `allOf`. The overlay MUST NOT be nested inside `allOf` as a second item.
+4. **No top-level union or negation.** `anyOf`, `oneOf`, and `not` MUST NOT appear at the top level of a derived GTS Type Schema.
+
+Forms that violate any of (1)–(4) are invalid GTS Type Schemas; registries and `/validate-type-schema` MUST reject them.
+
+#### Rules for base schemas
+
+5. **No `allOf`-based derivation.** A base GTS Type Schema does not inherit from anything and MUST NOT use `allOf` to express derivation. (Local `allOf` inside a property subschema, used for purely structural composition unrelated to GTS derivation, remains permitted — see rule 7.)
+6. **No top-level union or negation.** `anyOf`, `oneOf`, and `not` MUST NOT appear at the top level of a base GTS Type Schema either. The restriction is uniform with rule (4).
+
+#### Nested sub-schemas are unrestricted
+
+7. Inside `properties`, `definitions`, `items`, and any other nested context, `allOf`, `anyOf`, `oneOf`, and `not` remain valid Draft-07 constructs. Union types, discriminator patterns, and conditional sub-shapes are all still expressible — the restriction is purely top-level.
+
+#### Canonical example
+
+```jsonc
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~",
+ "type": "object",
+ "allOf": [
+ { "$ref": "gts://gts.x.core.events.type.v1~" }
+ ],
+ "required": ["payload"],
+ "properties": {
+ "typeId": { "const": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~" },
+ "payload": {
+ "type": "object",
+ "properties": {
+ "orderId": { "type": "string" },
+ "totalAmount": { "type": "number" }
+ },
+ "required": ["orderId", "totalAmount"]
+ }
+ }
+}
+```
+
+#### Invalid forms (rejected at registration)
+
+- `allOf` with two items (overlay inside `allOf` — the legacy "Form B"):
+ ```jsonc
+ { "$id": "gts://A~B~",
+ "allOf": [ { "$ref": "gts://A~" }, { "properties": { /* ... */ } } ] }
+ ```
+- `allOf` with three or more items.
+- `allOf` whose only item is an inline overlay (no `$ref` to the parent).
+- `allOf` with multiple `$ref` items (attempting multi-parent inheritance).
+- A **hybrid** schema that declares overlay constraints BOTH at the top level AND inside `allOf` as a second item. Under JSON Schema this is well-defined (all three subschemas combine via implicit AND), but the structure is opaque and the spec rejects it via rule (2) — `allOf` must contain exactly one subschema:
+ ```jsonc
+ { "$id": "gts://A~B~",
+ "allOf": [ { "$ref": "gts://A~" }, { "properties": { "x": { /* ... */ } } } ],
+ "properties": { "y": { /* ... */ } } }
+ ```
+- `$ref` that does not match the immediate parent — e.g., `$id = gts://A~B~C~` with `$ref = gts://A~` (skip-level), or `$ref` pointing to a type outside the chain.
+- Top-level `anyOf`, `oneOf`, or `not`.
+- A base schema (single-segment `$id`) declaring `allOf` for derivation, or declaring top-level `anyOf`/`oneOf`/`not`.
+
+OP#12 (Type Derivation Validation, §9.2) enforces these structural rules in addition to its existing constraint-compatibility checks.
+
### 3.3 Query Language
GTS Query Language is a compact predicate syntax, inspired by XPath/JSONPath, that lets you constrain results by attributes. Attach a square-bracketed clause to a GTS identifier containing name="value" pairs separated by commas. Example form: [ attr="value", other="value2" ].
@@ -1294,8 +1372,12 @@ Implement and expose all operations OP#1–OP#13 listed above and add appropriat
- **OP#9 - Version Casting**: Transform instances between compatible MINOR versions
- **OP#10 - Query Execution**: Filter identifier collections using the GTS query language
- **OP#11 - Attribute Access**: Retrieve property values and metadata using the attribute selector (`@`)
-- **OP#12 - Type Derivation Validation**: Validate that a derived type correctly extends its base chain. Today this includes JSON Schema-level constraint compatibility (derived schemas using `allOf` must conform to all constraints defined in their parent schemas throughout the inheritance hierarchy — `additionalProperties`, narrowing/widening, etc.) and trait inheritance from OP#13. This ensures type safety in extension and prevents constraint violations in multi-level type hierarchies. When validating derived types, if any base in the chain is marked `x-gts-final: true`, validation MUST fail (see section 9.11)
-- **OP#13 - Schema Traits Validation**: Validate schema traits (`x-gts-traits-schema` / `x-gts-traits`). See section 9.7 for full semantics and validation rules.
+- **OP#12 - Type Derivation Validation**: Validate that a derived type correctly extends its base chain. This includes:
+ - **Structural rules (§3.2.1)** — top-level form is the canonical single-item `allOf` with a `$ref` to the immediate parent in the chained `$id`; no top-level `anyOf`/`oneOf`/`not`; base schemas do not use `allOf` for derivation.
+ - **Constraint compatibility** — derived schemas conform to all constraints defined in their parent schemas throughout the inheritance hierarchy (`additionalProperties`, narrowing/widening, etc.).
+ - **Trait inheritance** — parallel trait-type derivation and trait completeness checks from OP#13.
+ - **Modifier enforcement** — if any base in the chain is marked `x-gts-final: true`, validation MUST fail (see §9.11).
+- **OP#13 - Schema Traits Validation**: Validate schema traits — `x-gts-traits-schema` (a URN-string reference to a registered trait-type) and `x-gts-traits` (a plain JSON object whose merged value across the host chain must be a valid instance of the host's effective trait-type). See §9.7 for full semantics and validation rules (parallel derivation, immutable-once-set merge, trait completeness for non-abstract host-types).
### 9.3 - GTS entities registration
@@ -1335,7 +1417,7 @@ Implementation notes:
### 9.7 - GTS Type Schema Traits (`x-gts-traits-schema` / `x-gts-traits`)
-**OP#13 - Schema Traits Validation**: Validate that `x-gts-traits` values in derived schemas conform to the `x-gts-traits-schema` defined in their base schemas. Verify that all trait properties are resolved (via direct value or `default`) and that trait values satisfy the trait schema constraints. Trait values set by an ancestor are immutable — descendants MUST NOT override them with a different value. Both `x-gts-traits-schema` and `x-gts-traits` are schema-only keywords and MUST NOT appear in instances. `x-gts-traits-schema` MUST have `"type": "object"`. Uses the same validation endpoints (`/validate-type-schema`, `/validate-entity`).
+**OP#13 - Schema Traits Validation**: Validate that `x-gts-traits` values on a host GTS Type Schema conform to the **trait-type** referenced by `x-gts-traits-schema` (resolved through the parallel trait-type chain), that the trait-type chain itself is a valid derivation, that traits set by an ancestor are not overridden by a descendant (immutable-once-set), and that every required trait field is resolved (concrete value or `default`) for any non-abstract host-type. Both `x-gts-traits-schema` and `x-gts-traits` are schema-only keywords and MUST NOT appear in instances. Uses the same validation endpoints (`/validate-type-schema`, `/validate-entity`).
A **schema trait** is a semantic annotation attached to a GTS Type Schema that describes **system behaviour** for processing instances of that type. Traits are not part of the object data model — they do not define instance properties. Instead, they configure cross-cutting concerns such as:
@@ -1343,180 +1425,204 @@ A **schema trait** is a semantic annotation attached to a GTS Type Schema that d
- **Processing directives** — how attributes should be handled (e.g., PII masking, indexing hints)
- **Association links** — linking schemas to related entities (e.g., associating an event type with its topic/stream)
+Traits are themselves **first-class GTS Type Schemas**, registered like any other type. A host type attaches one trait-type by URN; descendants of the host inherit that attachment and MAY refine it by attaching a *derived* trait-type (parallel derivation, §9.7.4).
+
#### 9.7.1 Keywords
Two JSON Schema annotation keywords are used together:
| Keyword | JSON type | Purpose | Typical location |
|---------|-----------|---------|------------------|
-| **`x-gts-traits-schema`** | JSON Schema (object) | Defines the **shape** of the trait — property names, types, constraints, and `default` values | Base / ancestor schemas |
-| **`x-gts-traits`** | Plain JSON object | Provides concrete **values** for the trait properties | Derived (leaf) schemas; may also appear alongside `x-gts-traits-schema` in the same schema |
+| **`x-gts-traits-schema`** | String — a GTS Type URN (e.g., `"gts://gts.x.core.traits.event_meta.v1~"`) | References the **trait-type** that defines the shape of trait values for instances of this host-type and its descendants | Base / ancestor host-types; derived host-types when they attach a derived trait-type |
+| **`x-gts-traits`** | Plain JSON object | Provides concrete **values** for the trait properties — an instance of the effective trait-type | Any host-type in the chain; values from all levels are merged top-down with immutable-once-set semantics (§9.7.5) |
**Schema-only keywords:** Both `x-gts-traits-schema` and `x-gts-traits` are **schema annotation keywords** and MUST only appear in JSON Schema documents (documents with `$schema`). They MUST NOT appear in instance documents. Implementations MUST reject instances that contain these keywords.
-A single schema MAY contain both keywords. This is explicitly allowed and useful when a mid-level schema defines new trait properties (`x-gts-traits-schema`) while also resolving traits inherited from its parent (`x-gts-traits`).
-
-**`x-gts-traits-schema`** MUST be a valid JSON Schema with `"type": "object"` at its top level. Implementations MUST reject trait schemas that declare a different type (e.g., `"type": "integer"`). It MAY be:
+**`x-gts-traits-schema` is a single string URN.** The inline-schema, `$ref`, and composition forms permitted by earlier versions of this spec are **removed**. Implementations MUST reject any `x-gts-traits-schema` whose value is not a string. The string value MUST be a valid GTS Type URN ending in `~`, and the referenced trait-type MUST already be registered (or registered in the same atomic operation) and MUST itself be a valid GTS Type Schema (subject to §3.2.1).
-- An **inline** schema object
-- A **`$ref`** to a standalone, reusable trait schema
-- A **composition** using `allOf`, `oneOf`, `anyOf`, etc.
+**`x-gts-traits` is a plain JSON object** containing concrete values. Each level of the host chain may provide a partial object; the registry merges values shallowly across the chain with immutable-once-set semantics (§9.7.5).
-Standard JSON Schema `$ref` resolution rules apply — implementations MUST NOT invent a custom reference mechanism.
+A single host schema MAY contain both keywords — common when a derived host-type attaches a derived trait-type (`x-gts-traits-schema`) and provides initial values for some of the newly-introduced fields (`x-gts-traits`).
-**`x-gts-traits`** is a plain JSON object of concrete values. Constraint keywords like `const` belong in `x-gts-traits-schema` (the trait schema), not in `x-gts-traits` (the trait values).
+#### 9.7.2 Trait-type definition
-#### 9.7.2 Trait schema definition (`x-gts-traits-schema`)
+A trait-type is a regular GTS Type Schema. It MUST follow all the same rules as any other GTS Type Schema — including the top-level composition rules of §3.2.1 (canonical form for derivation, no top-level `anyOf`/`oneOf`/`not`, single immediate-parent `$ref` in `allOf` for derived trait-types).
-A base schema declares the trait schema — the shape and defaults of all trait fields. This tells the system which traits exist and what values are acceptable.
+There is no special marker that identifies a type as "intended to be used as a trait" — any registered GTS Type Schema may be referenced as a trait-type. A vendor MAY group its trait-types under a shared base trait-type (so that all derived trait-types form one parallel-derivation family), or MAY register each trait-type independently. The spec does not mandate either organization.
-**Inline definition:**
+**Base trait-type example** (an `event_meta` trait-type that captures retention and topic-ref):
```json
{
- "$id": "gts://gts.x.core.events.type.v1~",
"$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.core.traits.event_meta.v1~",
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "topicRef": {
- "description": "GTS ID of the topic/stream where events of this type are published.",
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- "default": "gts.x.core.events.topic.v1~x.core._.default.v1"
- },
- "retention": {
- "description": "ISO 8601 duration for event retention.",
- "type": "string",
- "default": "P30D"
- }
+ "properties": {
+ "topicRef": {
+ "description": "GTS ID of the topic/stream where events of this type are published.",
+ "type": "string",
+ "x-gts-ref": "gts.x.core.events.topic.v1~",
+ "default": "gts.x.core.events.topic.v1~x.core._.default.v1"
+ },
+ "retention": {
+ "description": "ISO 8601 duration for event retention.",
+ "type": "string",
+ "default": "P30D"
}
- },
- "properties": { "..." : {} }
+ }
}
```
-**`$ref` to reusable trait schemas:**
-
-A platform MAY publish standalone, reusable trait schemas (e.g., `RetentionTrait`, `TopicTrait`, `PIITrait`). Base schemas reference them via standard `$ref`:
+**Derived trait-type example** (extending the base with an additional `auditRetention` field):
```json
{
- "$id": "gts://gts.x.core.events.type.v1~",
"$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.core.traits.event_meta.v1~x.core.audit.event_meta.v1~",
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "allOf": [
- { "$ref": "gts://gts.x.core.traits.retention.v1~" },
- { "$ref": "gts://gts.x.core.traits.topic.v1~" }
- ]
- },
- "properties": { "..." : {} }
+ "allOf": [
+ { "$ref": "gts://gts.x.core.traits.event_meta.v1~" }
+ ],
+ "properties": {
+ "auditRetention": {
+ "description": "Retention override for audit compliance.",
+ "type": "string",
+ "default": "P365D"
+ }
+ }
}
```
-Where each referenced trait schema is a standalone JSON Schema registered as a GTS entity:
+The derived trait-type follows the strict canonical form (§3.2.1): one-item `allOf` with `$ref` to the immediate parent, all new constraints (the additional `properties` entry) at the top level. OP#12 derivation rules apply to trait-types just as they do to host-types: a derived trait-type MUST be fully compatible with its parent, defaults declared in an ancestor MUST NOT change in a descendant, etc.
+
+#### 9.7.3 Attaching a trait-type to a host
+
+A host GTS Type Schema attaches a trait-type by setting `x-gts-traits-schema` to the trait-type's URN:
```json
{
- "$id": "gts://gts.x.core.traits.retention.v1~",
"$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.core.events.type.v1~",
"type": "object",
+ "x-gts-traits-schema": "gts://gts.x.core.traits.event_meta.v1~",
"properties": {
- "retention": {
- "description": "ISO 8601 duration for data retention.",
- "type": "string",
- "default": "P30D"
- }
- }
+ "id": { "type": "string" },
+ "typeId": { "type": "string" },
+ "occurredAt": { "type": "string", "format": "date-time" },
+ "payload": { "type": "object" }
+ },
+ "required": ["id", "typeId", "occurredAt", "payload"]
}
```
-#### 9.7.3 Trait values in derived schemas (`x-gts-traits`)
+A host-type may attach **at most one** trait-type. Multiple trait shapes are expressed by combining them into a single trait-type (with the relevant `properties`), not by attaching multiple trait-types to one host.
-Derived schemas **resolve** (configure) trait values by providing a plain JSON object via `x-gts-traits`. Trait values MUST be valid against the effective trait schema derived from the inheritance chain as defined below.
+Derived host-types provide trait values for the attached trait-type by adding `x-gts-traits` at the top level of the derived schema (alongside the canonical-form `allOf`):
```json
{
+ "$schema": "http://json-schema.org/draft-07/schema#",
"$id": "gts://gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~",
+ "type": "object",
"allOf": [
- { "$ref": "gts://gts.x.core.events.type.v1~" },
- {
- "x-gts-traits": {
- "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1",
- "retention": "P90D"
- }
+ { "$ref": "gts://gts.x.core.events.type.v1~" }
+ ],
+ "x-gts-traits": {
+ "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1",
+ "retention": "P90D"
+ },
+ "properties": {
+ "typeId": { "const": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~" },
+ "payload": {
+ "type": "object",
+ "properties": {
+ "orderId": { "type": "string" },
+ "totalAmount": { "type": "number" }
+ },
+ "required": ["orderId", "totalAmount"]
}
- ]
+ }
}
```
-#### 9.7.4 Both keywords in the same schema
+The derived host-type inherits `x-gts-traits-schema` from its base; it does not redeclare it. The `x-gts-traits` object is validated against the effective trait-type (§9.7.5).
-A mid-level schema MAY extend the trait schema while also providing values for inherited traits:
+#### 9.7.4 Parallel derivation
+
+Host-type inheritance and trait-type inheritance run on **two parallel chains**:
+
+- The **host chain** is the chained `$id` of the host (e.g., `gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~`).
+- The **trait-type chain** is the chained `$id` of whichever trait-type is in effect (e.g., `gts.x.core.traits.event_meta.v1~x.core.audit.event_meta.v1~`).
+
+For a host chain `H₀ → H₁ → … → Hₙ`:
+
+1. **Inheritance by default.** If `Hₖ` does not declare its own `x-gts-traits-schema`, it inherits the trait-type attached by the nearest ancestor that did declare one. If no ancestor declared one, the host-type has no traits at all (and `x-gts-traits` MUST NOT appear on it or any of its descendants).
+2. **Refinement by parallel derivation.** If `Hₖ` declares its own `x-gts-traits-schema = T_k` and some ancestor `Hⱼ` (j < k) declared `Tⱼ`, then `T_k`'s `$id`-chain MUST be derived from `Tⱼ`. In other words, `T_k`'s chain MUST extend `Tⱼ`'s chain (left-prefix match in trait-type space). Any other declaration on `Hₖ` is invalid.
+3. **First-time attachment.** If no ancestor of `Hₖ` declared a trait-type, `Hₖ` may attach any registered trait-type.
+
+This rule ensures that descending the host chain can only **refine** the trait shape — never substitute it for an unrelated one.
+
+**Example.** Suppose `gts.x.core.events.type.v1~` attaches `gts.x.core.traits.event_meta.v1~`. A derived audit-event host-type may attach a derived trait-type that adds an `auditRetention` field, **provided** that derived trait-type extends the base trait-type:
```json
{
+ "$schema": "http://json-schema.org/draft-07/schema#",
"$id": "gts://gts.x.core.events.type.v1~x.core.audit.event.v1~",
+ "type": "object",
"allOf": [
- { "$ref": "gts://gts.x.core.events.type.v1~" },
- {
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "auditRetention": {
- "description": "Retention override for audit compliance.",
- "type": "string",
- "default": "P365D"
- }
- }
- },
- "x-gts-traits": {
- "topicRef": "gts.x.core.events.topic.v1~x.core._.audit.v1"
- }
- }
- ]
+ { "$ref": "gts://gts.x.core.events.type.v1~" }
+ ],
+ "x-gts-abstract": true,
+ "x-gts-traits-schema": "gts://gts.x.core.traits.event_meta.v1~x.core.audit.event_meta.v1~",
+ "x-gts-traits": {
+ "topicRef": "gts.x.core.events.topic.v1~x.core._.audit.v1"
+ }
}
```
-#### 9.7.5 Trait merge and validation semantics (normative)
+The trait-type's chain (`event_meta.v1~ → audit.event_meta.v1~`) is parallel to the host's chain (`events.type.v1~ → audit.event.v1~`). Attaching, say, `gts.x.core.traits.retention.v1~` here — a trait-type unrelated to the base `event_meta` chain — would be invalid by rule (2).
+
+#### 9.7.5 Trait values resolution & merge (normative)
+
+For any host-type in a chain, the **effective trait-type** is the most-derived trait-type attached anywhere in the host chain at or above this level (inherited from the nearest ancestor that declared one, or declared at this level itself).
+
+The **effective trait values** are computed by **shallow merging** the `x-gts-traits` objects encountered along the host chain (root → leaf):
+
+- For each top-level key, the **first** schema in the chain to provide a concrete value for that key "wins". Descendants MAY restate the same value (idempotent) but MUST NOT change it. A schema that changes a concrete value set by an ancestor is **invalid** (immutable-once-set); the registry and OP#6 MUST reject it as a consequence.
+- Keys not provided in any `x-gts-traits` are resolved to the `default` declared in the effective trait-type for that key (if any).
+- A `default` is **not** a concrete assignment: it does not lock the key, and a descendant MAY provide a concrete value for a key that was previously only filled by a default.
+- The merge is **shallow** — only top-level keys are compared. Object-valued trait fields are replaced wholesale by the first concrete assignment; they are not deep-merged.
+
+The effective trait-type itself is a normal JSON Schema. The merged effective trait values MUST validate against it using standard JSON Schema validation. A host schema whose merged effective trait values fail validation — required field unresolved, type mismatch, constraint violation — is invalid.
+
+**Trait completeness rule (validity, not enforcement):** a GTS Type Schema is **invalid** if BOTH of the following hold:
+
+1. It is not marked `x-gts-abstract: true`.
+2. Its effective trait values do not satisfy the effective trait-type schema — i.e., at least one field that is `required` by the effective trait-type schema and not covered by a `default` has no concrete value assigned anywhere in the host chain.
-Traits MUST follow standard JSON Schema practices. The key rule is that **the registry MUST treat trait schemas as normal JSON Schemas** and MUST rely on standard JSON Schema composition and `$ref` semantics (especially `allOf`) rather than inventing a bespoke merge algorithm.
+Such a schema is invalid by definition. As a consequence:
-Given an inheritance chain `S₀ → S₁ → … → Sₙ`:
+- The registry MUST reject it at registration time (`/entities`, `/validate-type-schema`). This is the load-bearing check — once a concrete host-type is in the registry, it is guaranteed trait-complete.
+- OP#6 instance validation (`/validate-entity`) MUST also fail for any instance referencing such a schema, as defense in depth against registry corruption or implementations predating this rule.
-- **Trait schema merge**
- - The registry MUST build an *effective trait schema* by composing all encountered `x-gts-traits-schema` values using JSON Schema `allOf`.
- - Any `$ref` appearing inside `x-gts-traits-schema` MUST be resolved using standard JSON Schema `$ref` resolution rules (base URI resolution + JSON Pointer fragments).
- - Derived schemas MAY further constrain (narrow) traits by adding additional schema constraints in their `x-gts-traits-schema` (this is naturally enforced by `allOf`).
- - **Immutable defaults:** `default` values declared in an ancestor's `x-gts-traits-schema` MUST NOT be changed by a descendant's `x-gts-traits-schema`. If a descendant redeclares a trait property with a different `default`, schema validation MUST fail.
+A host-type that intentionally leaves trait values to be resolved by descendants MUST mark itself `x-gts-abstract: true`. There is no implicit "abstract because incomplete" state.
-- **Trait value merge**
- - The registry MUST build an *effective traits object* by collecting all `x-gts-traits` objects encountered in the chain (left-to-right).
- - **Immutable-once-set:** Once a trait key is assigned a concrete value by a schema in the chain, **no descendant may override it**. If a descendant's `x-gts-traits` provides a different value for a key already set by an ancestor, schema validation MUST fail. Providing the **same** value is permitted (idempotent).
- - Defaults declared in the effective trait schema SHOULD be used as normal JSON Schema defaults to produce a complete effective traits object.
+**Defaults are immutable across the trait-type chain.** A descendant trait-type MUST NOT change a `default` declared by an ancestor trait-type. A trait-type that does so is invalid; this is enforced through OP#12 on the trait-type chain itself (just like any other constraint narrowing/loosening rule).
-- **Validation**
- - The registry MUST validate the effective traits object against the effective trait schema using standard JSON Schema validation.
- - If the effective trait schema cannot be satisfied (e.g., contradictory constraints introduced across the chain), schema validation MUST fail.
- - If a trait is required by the effective trait schema (i.e., not covered by a default) but is not provided by any `x-gts-traits` in the chain, schema validation MUST fail for concrete (leaf) schemas.
- - If a descendant attempts to override a trait value already set by an ancestor with a different value, schema validation MUST fail.
+**Example — immutable trait override (failure).**
-**Example — immutable trait override (failure):**
+Consider a host chain: `base → audit_event → most_derived_event`.
-Consider a 3-level chain: `base → audit_event → most_derived_event`.
+- `audit_event` sets `x-gts-traits.topicRef` to `gts.x.core.events.topic.v1~x.core._.audit.v1`.
+- `most_derived_event` attempts to set `x-gts-traits.topicRef` to `gts.x.core.events.topic.v1~x.core._.notification.v1`.
-- `audit_event` sets `x-gts-traits.topicRef` to `gts.x.core.events.topic.v1~x.core._.audit.v1`
-- `most_derived_event` attempts to set `x-gts-traits.topicRef` to `gts.x.core.events.topic.v1~x.core._.notification.v1`
+`most_derived_event` is invalid: `topicRef` was already set concretely by `audit_event` and the new value differs.
-Validation of `most_derived_event` MUST fail because `topicRef` was already set by `audit_event` and the new value differs.
+#### 9.7.6 MAJOR version pinning of trait-types
-These rules are intentionally aligned with existing JSON Schema composition semantics and GTS schema chaining practices.
+`x-gts-traits-schema` references a trait-type at a specific **MAJOR version** (e.g., `v1`). Moving the host-type to a `v2` of the trait-type — `gts.x.core.traits.event_meta.v1~` → `gts.x.core.traits.event_meta.v2~` — is a **breaking change** for the host-type, equivalent to attaching a different trait family entirely. Authors who want continuous trait-shape evolution use MINOR versions of the same trait-type or derive new trait-types from the original via the canonical form (§3.2.1), preserving compatibility.
-See `./examples/events/types/` for complete examples demonstrating trait definition and resolution.
+See `./examples/events/types/` for complete worked examples demonstrating base trait-types, derived trait-types, host attachment, and parallel derivation.
### 9.8 - YAML support
@@ -1611,9 +1717,9 @@ When a schema declares `"x-gts-abstract": true`:
#### 9.11.4 Interaction with `x-gts-traits`
-- **Abstract types with traits**: An abstract base type MAY declare `x-gts-traits-schema`. Since abstract types cannot have direct instances, trait values (`x-gts-traits`) do not need to be fully resolved on the abstract type itself. Trait resolution completeness is only enforced on concrete (leaf) schemas (this is already the existing behavior from section 9.7.5).
+- **Abstract types with traits**: An abstract host-type MAY declare `x-gts-traits-schema`. Since abstract types cannot have direct instances, trait values (`x-gts-traits`) do not need to be fully resolved on the abstract type itself — descendants may close out remaining required fields. Trait completeness is enforced on any **non-abstract** host-type (see §9.7.5).
-- **Final types with traits**: A final type MAY declare `x-gts-traits` values. Since no derived types can exist, all trait values MUST be fully resolved on the final type itself. If the effective trait schema has required properties without defaults and the final type does not provide them via `x-gts-traits`, validation MUST fail.
+- **Final types with traits**: A final type MAY declare `x-gts-traits` values. Since no derived types can exist and a final type is non-abstract, all required trait fields MUST be resolved on the final type itself (concrete value or `default`). If the effective trait-type requires properties without defaults and the final type does not provide them via `x-gts-traits`, the schema is invalid.
#### 9.11.5 Registration enforcement
@@ -1702,6 +1808,7 @@ GTS Type Schemas are defined in terms of **JSON Schema Draft-07**. Implementatio
- The `$schema` field of every GTS Type Schema MUST be `http://json-schema.org/draft-07/schema#`.
- Implementations MAY accept other JSON Schema dialects for non-GTS schemas, but MUST NOT rely on keywords introduced after Draft-07 (such as `prefixItems`, `unevaluatedProperties`, `unevaluatedItems`, `$dynamicRef`/`$dynamicAnchor`, `dependentRequired`, `dependentSchemas`) when interpreting GTS Type Schemas.
- Reusable subschemas inside a GTS Type Schema SHOULD be placed under the Draft-07 canonical keyword `definitions`. Local JSON Pointer references such as `"$ref": "#/definitions/Foo"` are the recommended form.
+- `anyOf`, `oneOf`, and `not` are valid Draft-07 keywords and remain available inside nested sub-schemas (e.g., property sub-schemas, `definitions` entries, `items`). They MUST NOT appear at the **top level** of any GTS Type Schema — see §3.2.1 for the full top-level composition rules.
### 11.1 Global rules: schema vs instance, normalization, and document categories
diff --git a/adr/0002-strict-allof-derivation-form.md b/adr/0002-strict-allof-derivation-form.md
new file mode 100644
index 0000000..839a501
--- /dev/null
+++ b/adr/0002-strict-allof-derivation-form.md
@@ -0,0 +1,348 @@
+# ADR-0002: Single canonical form for derivation — strict 1-item `allOf` + top-level overlay
+
+- **Status:** Proposed
+- **Date:** 2026-05-21
+- **Deciders:** GTS spec maintainers
+- **Consulted:** —
+- **Informed:** Reference implementations (gts-go, gts-rust), gts-spec conformance test suite
+- **Supersedes:** —
+- **Superseded by:** —
+
+## Context and Problem Statement
+
+Up to this point the GTS specification has been silent on the **structural shape** of a derived GTS Type Schema. The semantic side (OP#12) validates derivation by checking *constraint compatibility* — that every valid instance of the derived schema is also a valid instance of every ancestor in the chain — but the spec does not say anything about how the derivation link itself is expressed in JSON Schema syntax.
+
+In practice every reference implementation, every example in `examples/**/types/`, and every test in `tests/test_op12_*` and `tests/test_op13_*` uses the same de-facto shape:
+
+```jsonc
+{
+ "$id": "gts://A~B~",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "allOf": [
+ { "$ref": "gts://A~" },
+ { "type": "object", "properties": { /* ... */ }, "required": [ /* ... */ ] }
+ ]
+}
+```
+
+This is convention, not contract. JSON Schema Draft-07 permits a long list of semantically-equivalent shapes for the same intent — properties at the top level instead of inside `allOf`; `allOf` with three or more items; multiple `$ref`s; an `$ref` to some type that is not the immediate parent in the chained `$id`; even substituting `allOf` for `anyOf` or `oneOf` and relying on a degenerate single-branch case. Some of these shapes are obviously bugs; others are legitimate ways an author might write the same derivation; all of them today must either be accepted or rejected on a case-by-case basis by every implementation, with no normative guidance.
+
+This costs us in three concrete ways.
+
+### Problem 1 — Validators and linters must handle every JSON-Schema-permissible variation
+
+Without a canonical shape, a registry that wants to enforce GTS-level rules ("the parent in `allOf` must be the immediate predecessor in the chained `$id`") has to first discover *which* of the many shapes it is looking at. Is the overlay at the top level or inside `allOf`? Are there one or two members in `allOf`? Is the `$ref` at index 0 or index 1? Linters and codegen face the same problem. Every cross-implementation interop bug we have seen in this area traces back to one tool accepting a shape that another tool rejects.
+
+### Problem 2 — Authors do not know where to put `properties`, `required`, `additionalProperties`, `x-gts-traits-schema`, `x-gts-final`, ...
+
+The top level of the schema and the second item of `allOf` are both syntactically valid homes for the overlay. Most ordinary keywords (`required`, `properties`, narrowed constraints) compose via `allOf` to produce the same effective schema regardless of placement; authors who write the same derivation two different ways generally get the same behaviour at instance-validation time.
+
+The notable exception is `additionalProperties`. In Draft-07 it is an in-place applicator and only "sees" properties declared in the **same schema object** as itself — never properties contributed via `$ref` or sibling `allOf` branches. So none of the placements works the way authors intuitively expect:
+- `additionalProperties: false` next to `properties: { tier: ... }` at the top level — rejects the base type's inherited `id`/`name` (they are not in top-level `properties`).
+- `additionalProperties: false` inside an `allOf[1]` overlay next to its own `properties` — that branch also rejects `id`/`name`.
+- `additionalProperties: false` at the top level when `properties` live only inside `allOf[1]` — rejects *every* field, including the derived `tier`.
+
+Authors repeatedly try all three and get three different broken behaviours. The right answer is "don't use `additionalProperties: false` for derivation closedness at all" (the subject of a separate discussion outside this ADR) — but until that's settled, a single canonical shape at least removes the placement choice as a degree of freedom.
+
+There is also a hybrid case to worry about: properties at the top level AND another overlay nested inside `allOf`. This is well-defined in JSON Schema (all three subschemas combine via implicit AND) but is opaque to read and to validate. A single canonical shape eliminates the placement question entirely and prevents the hybrid case.
+
+### Problem 3 — Codegen and registry tooling cannot rely on a single inspection path
+
+Tools that generate code from GTS Type Schemas, render documentation, or compute structural diffs between versions have to branch on the schema shape. A canonical shape collapses these branches and lets tools assume a single inspection path: read the top-level keys, follow the single `$ref` in `allOf` to the parent. Anything else is a malformed schema.
+
+## Decision Drivers
+
+- **Single-shape simplicity for validators, linters, and codegen.** One canonical form means one inspection path.
+- **Author clarity.** Properties, required, modifiers, and trait keywords should live in one obvious place.
+- **No new authoring burden.** The canonical form must be at least as natural as the current de-facto form, and ideally simpler.
+- **Acceptable migration cost.** We are inside a breaking release window already; a one-time rewrite of examples and tests is acceptable. We are not adding migration burden that would block the release.
+
+## Considered Options
+
+### Running example used in this section
+
+To keep the four options comparable, the worked examples below all express the same intent: a derived type `premium_user` that extends a base `user` type by adding a required `tier` field with an enum constraint. The base type is registered once and is the same across all options:
+
+```json
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.example.user.v1~",
+ "type": "object",
+ "required": ["id", "name"],
+ "properties": {
+ "id": { "type": "string" },
+ "name": { "type": "string" }
+ }
+}
+```
+
+Two instance payloads are referenced throughout:
+
+```json
+// Payload P-OK — valid against the derived premium_user
+{ "id": "u-1", "name": "Alice", "tier": "gold" }
+```
+
+```json
+// Payload P-BAD — missing the required `tier` field
+{ "id": "u-2", "name": "Bob" }
+```
+
+Under each option below, we show the derived schema in the shape(s) admitted by that option, plus which payloads validate and which do not.
+
+### Option 1 — Status quo (no normative form)
+
+The spec stays silent on top-level shape. Any JSON-Schema-permissible structure is admissible: `allOf` with 1 / 2 / 3+ items, `allOf` without a `$ref`, multiple `$ref`s, hybrid forms (overlay at the top level AND nested inside `allOf`), top-level `anyOf`/`oneOf`/`not`, and so on. The registry checks only the existing OP#12 semantic compatibility rules; it does not reject structural anomalies. This is the de-facto state of the spec before this ADR.
+
+Under Option 1 **all of the following shapes coexist** in the wild because no rule forbids any of them:
+
+```jsonc
+// Shape 1a — overlay at top level (Form A)
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.example.user.v1~vendor.premium_user.v1~",
+ "type": "object",
+ "allOf": [{ "$ref": "gts://gts.x.example.user.v1~" }],
+ "required": ["tier"],
+ "properties": { "tier": { "type": "string", "enum": ["gold", "platinum"] } }
+}
+```
+
+```jsonc
+// Shape 1b — overlay inside allOf (Form B)
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.example.user.v1~vendor.premium_user.v1~",
+ "allOf": [
+ { "$ref": "gts://gts.x.example.user.v1~" },
+ {
+ "type": "object",
+ "required": ["tier"],
+ "properties": { "tier": { "type": "string", "enum": ["gold", "platinum"] } }
+ }
+ ]
+}
+```
+
+```jsonc
+// Shape 1c — hybrid: overlay at top level AND nested inside allOf
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.example.user.v1~vendor.premium_user.v1~",
+ "type": "object",
+ "allOf": [
+ { "$ref": "gts://gts.x.example.user.v1~" },
+ { "properties": { "tier": { "type": "string", "enum": ["gold", "platinum"] } } }
+ ],
+ "required": ["tier"]
+}
+```
+
+```jsonc
+// Shape 1d — split overlay across two allOf items
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.example.user.v1~vendor.premium_user.v1~",
+ "allOf": [
+ { "$ref": "gts://gts.x.example.user.v1~" },
+ { "required": ["tier"] },
+ { "properties": { "tier": { "type": "string", "enum": ["gold", "platinum"] } } }
+ ]
+}
+```
+
+**Payload behavior under Option 1:** all four shapes happen to produce the same effective JSON Schema (`P-OK` validates, `P-BAD` fails because `tier` is required). But the equivalence is *incidental*. Any variant that adds `additionalProperties: false` — at the top level, inside an `allOf` branch, or both — silently rejects inherited properties from the base because the keyword does not "see" properties contributed through `$ref` (see Problem 2). So `P-OK` fails on every such variant, regardless of where the closing keyword is placed. Implementations have no normative rule that forbids any of these variations and they handle them inconsistently.
+
+### Option 2 — Strict Form A (`allOf` with exactly one `$ref` to parent; overlay at the top level) — **selected**
+
+Only one schema shape is accepted for the derived type:
+
+```jsonc
+// The only valid form under Option 2
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.example.user.v1~vendor.premium_user.v1~",
+ "type": "object",
+ "allOf": [{ "$ref": "gts://gts.x.example.user.v1~" }],
+ "required": ["tier"],
+ "properties": {
+ "tier": { "type": "string", "enum": ["gold", "platinum"] }
+ }
+}
+```
+
+The derived schema looks like a normal JSON Schema *plus* one extra `allOf` pointer back to its parent. Everything the derived schema contributes — properties, required, narrowed constraints, content-model keywords, GTS modifiers, trait keywords — lives at the top level of the schema object.
+
+**Payload behavior:**
+- `P-OK` (`{"id": "u-1", "name": "Alice", "tier": "gold"}`) validates — satisfies the parent (id, name present) and the derived top level (tier present and in enum).
+- `P-BAD` (`{"id": "u-2", "name": "Bob"}`) fails — derived schema requires `tier`.
+- All shapes 1b–1d from Option 1 (and any other variant) are rejected at registration regardless of payload.
+
+### Option 3 — Strict Form B (`allOf` with exactly two items: `$ref` + inline overlay)
+
+```jsonc
+// The only valid form under Option 3
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.example.user.v1~vendor.premium_user.v1~",
+ "allOf": [
+ { "$ref": "gts://gts.x.example.user.v1~" },
+ {
+ "type": "object",
+ "required": ["tier"],
+ "properties": {
+ "tier": { "type": "string", "enum": ["gold", "platinum"] }
+ }
+ }
+ ]
+}
+```
+
+This is the current de-facto convention in examples and tests. Migration cost is zero. The overlay is a self-contained sub-schema, which has minor advantages for tools that want to diff or extract the "delta" between a base and a derivation.
+
+**Payload behavior:**
+- `P-OK` validates — satisfies both `allOf` branches.
+- `P-BAD` fails — derived overlay requires `tier`.
+- Form-A-shaped schemas (overlay at top level) are rejected at registration regardless of payload.
+
+### Option 4 — Relaxed (Form A or Form B both valid; everything else rejected)
+
+A new normative rule with **two** canonical shapes instead of one. Form A and Form B are both accepted; every other shape — `allOf` with 3+ items, `allOf` without a `$ref`, multiple `$ref`s, hybrid forms (top-level overlay AND inside `allOf`), top-level `anyOf`/`oneOf`/`not` — is rejected at registration. Authors choose between Form A and Form B based on readability.
+
+This differs from Option 1 (Status quo) in that the registry *does* perform structural validation; it just permits two canonical shapes rather than one.
+
+Under Option 4, the SAME derived type can legitimately be written in two ways:
+
+```jsonc
+// 4a — Form A (overlay at top level)
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.example.user.v1~vendor.premium_user.v1~",
+ "type": "object",
+ "allOf": [{ "$ref": "gts://gts.x.example.user.v1~" }],
+ "required": ["tier"],
+ "properties": { "tier": { "type": "string", "enum": ["gold", "platinum"] } }
+}
+```
+
+```jsonc
+// 4b — Form B (overlay inside allOf)
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.example.user.v1~vendor.premium_user.v1~",
+ "allOf": [
+ { "$ref": "gts://gts.x.example.user.v1~" },
+ {
+ "type": "object",
+ "required": ["tier"],
+ "properties": { "tier": { "type": "string", "enum": ["gold", "platinum"] } }
+ }
+ ]
+}
+```
+
+**Payload behavior:** both shapes accept `P-OK` and reject `P-BAD` identically. Variants like Shape 1c (hybrid) or 1d (split across 3 allOf items) are rejected at registration. The downside is that one platform may publish Form A and another Form B for semantically identical types, forcing every consumer to handle both shapes.
+
+## Decision Outcome
+
+**Chosen option:** Option 2 — strict Form A. The derived GTS Type Schema MUST contain `allOf` with exactly one subschema, that subschema MUST be `{"$ref": "gts://"}`, and everything else lives at the top level of the derived schema alongside `allOf`. Form B, Form C, multi-item `allOf`, skip-level `$ref`s, references to unrelated types, and top-level `anyOf`/`oneOf`/`not` are all invalid and MUST be rejected at registration time.
+
+The same canonical form applies uniformly to host types and to trait types — a trait type is a regular registered GTS Type Schema, and the rules do not distinguish between them.
+
+### Normative changes to the spec (README.md)
+
+1. **New subsection 3.2.1 "Top-level composition rules for GTS Type Schemas"** (introduced in this release) — states the canonical form for derived schemas, the immediate-parent `$ref` rule, the prohibition on top-level `anyOf`/`oneOf`/`not`, and the rule that base schemas (single-segment chain) do not use `allOf` for derivation and also do not use top-level `anyOf`/`oneOf`/`not`. References this ADR for the rationale.
+
+2. **OP#12 description** — extended: structural validation now enforces the rules from 3.2.1 in addition to the existing constraint-compatibility checks.
+
+3. **Section 11.0 (JSON Schema Dialect)** — short addition noting that `anyOf`/`oneOf`/`not`, while valid Draft-07 keywords, are restricted at the top level of GTS Type Schemas per 3.2.1.
+
+### Example — desired final shape of a derived event type
+
+```jsonc
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "gts://gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~",
+ "type": "object",
+ "allOf": [
+ { "$ref": "gts://gts.x.core.events.type.v1~" }
+ ],
+ "required": ["payload"],
+ "properties": {
+ "typeId": { "const": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~" },
+ "payload": {
+ "type": "object",
+ "properties": {
+ "orderId": { "type": "string" },
+ "totalAmount": { "type": "number" }
+ },
+ "required": ["orderId", "totalAmount"]
+ }
+ },
+ "x-gts-traits": {
+ "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1",
+ "retention": "P90D"
+ }
+}
+```
+
+A few things to note about this shape:
+
+- `allOf` has exactly one item. That item is the `$ref` to the immediate parent (`gts.x.core.events.type.v1~`). Skip-level `$ref`s (referencing an ancestor higher up the chain) are not permitted.
+- `type: "object"` appears at the top level of the derived schema, not inside `allOf`. The same is true of `properties`, `required`, and the trait keywords.
+- Nested sub-schemas (e.g., `payload`) are unrestricted: `anyOf`, `oneOf`, and the rest of JSON Schema remain available at depth. The restriction is purely top-level.
+
+### Operations and conformance impact
+
+- **OP#12 (Type Derivation Validation)** is extended to enforce the top-level composition rules at registration time.
+- **OP#6 (Schema Validation of instances)** is unchanged — the canonical form does not affect instance validation, because `allOf` semantics in JSON Schema produce the same merged constraints regardless of where the overlay lives.
+- **`register_derived` test helper** (`tests/helpers/http_run_helpers.py`) changes shape — `allOf` becomes a single-item list and the overlay is spread at the top level. Existing call sites do not change.
+- **Existing example schemas** — every `examples/**/types/*.schema.json{,c}` that uses Form B is rewritten to Form A in the same release.
+- **Reference implementations** (gts-go, gts-rust) add a structural validator branch in their OP#12 pipeline. This is a new requirement, not an additive one — existing Form-B schemas in third-party registries become invalid and must be migrated.
+- **Backward compatibility of the spec:** this is a **breaking change**. It is bundled with the trait-system redesign (URN-string `x-gts-traits-schema`, parallel derivation) under one BREAKING version entry — see the README version log.
+
+## Pros and Cons of the Options
+
+### Option 1 — Status quo
+
+**Problems addressed:** P1 ❌ · P2 ❌ · P3 ❌
+
+- **+** No spec change required.
+- **−** Every implementation continues to invent its own shape recognition. Cross-implementation bugs persist.
+- **−** Authors continue to guess at placement; the `additionalProperties` + `allOf` footgun keeps catching them.
+- **−** Codegen and tooling stay branchy.
+
+### Option 2 — Strict Form A (selected)
+
+**Problems addressed:** P1 ✅ · P2 ✅ · P3 ✅
+
+- **+** Validators have one shape to recognise. Linters and codegen have one inspection path.
+- **+** Properties / required / modifiers / trait keywords always live at the top level — one place to look, one place to write them.
+- **+** The derived schema reads as a normal JSON Schema with one extra `allOf` pointer to its parent; consistent with how base schemas already look.
+- **+** Eliminates an entire class of placement-ambiguity bugs around `additionalProperties` by removing the "did the author put it on the right level?" question. (The underlying Draft-07 `additionalProperties` + `$ref` interaction is a separate concern, outside the scope of this ADR.)
+- **−** Migrates ~10 example files and the `register_derived` test helper; existing third-party Form-B schemas become invalid (one-time migration).
+- **−** Loses the minor "overlay is a self-contained sub-schema" property of Form B, which a small set of tools exploit for diffing.
+
+### Option 3 — Strict Form B
+
+**Problems addressed:** P1 ✅ · P2 ◐ · P3 ✅ (partially)
+
+- **+** Zero migration cost; matches existing examples and tests verbatim.
+- **+** Overlay is a self-contained sub-schema — convenient for tools that extract or diff "deltas".
+- **+** Single canonical shape, like Option 2, addresses P1 and P3.
+- **−** `type: "object"` either gets duplicated (top level *and* inside the overlay) or omitted at the wrong level. Either choice creates pedagogical friction.
+- **−** `additionalProperties: false` placement is still ambiguous in practice (top level vs inside `allOf`); authors place it wrongly more often than in Form A in our observed examples.
+- **−** Deeper nesting for the most common authoring path.
+
+### Option 4 — Relaxed (either form valid)
+
+**Problems addressed:** P1 ❌ · P2 ❌ · P3 ❌
+
+- **+** Author flexibility.
+- **−** Doubles the validation surface — every test must cover both forms; every validator must accept both.
+- **−** Splits the community: some teams adopt Form A, others Form B. Cross-vendor schemas mix and match. No single inspection path for tooling.
+- **−** Contradicts the "simpler validation" goal that motivated this ADR in the first place.
+
+## More Information
+
+This ADR is part of one consolidated BREAKING spec release (version 0.12) that also redesigns the trait system to reference trait-types by URN and removes inline-schema / composition forms of `x-gts-traits-schema`. See the README version log entry for the full set of changes that ship together.
diff --git a/examples/events/types/gts.x.core.events.type.v1~.schema.json b/examples/events/types/gts.x.core.events.type.v1~.schema.json
index 80afda5..8c60438 100644
--- a/examples/events/types/gts.x.core.events.type.v1~.schema.json
+++ b/examples/events/types/gts.x.core.events.type.v1~.schema.json
@@ -11,23 +11,7 @@
"tenantId",
"occurredAt"
],
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "topicRef": {
- "description": "GTS ID of the topic/stream where events of this type are published.",
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- "default": "gts.x.core.events.topic.v1~x.core._.default.v1"
- },
- "retention": {
- "description": "ISO 8601 duration for event retention.",
- "type": "string",
- "default": "P30D"
- }
- }
- },
+ "x-gts-traits-schema": "gts://gts.x.core.traits.event_meta.v1~",
"properties": {
"type": {
"description": "Identifier of the event type in GTS format.",
diff --git a/examples/events/types/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json b/examples/events/types/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json
index 0a32dde..47b8ff5 100644
--- a/examples/events/types/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json
+++ b/examples/events/types/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json
@@ -4,34 +4,31 @@
"title": "Event Instance Schema: order.placed",
"type": "object",
"allOf": [
- { "$ref": "gts://gts.x.core.events.type.v1~" },
- {
+ { "$ref": "gts://gts.x.core.events.type.v1~" }
+ ],
+ "required": ["type", "payload", "subjectType"],
+ "x-gts-traits": {
+ "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1",
+ "retention": "P90D"
+ },
+ "properties": {
+ "type": {
+ "const": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~",
+ "$comment": "The value is required only for IDE-side validation"
+ },
+ "payload": {
"type": "object",
- "required": ["type", "payload", "subjectType"],
- "x-gts-traits": {
- "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1",
- "retention": "P90D"
- },
+ "required": ["orderId", "customerId", "totalAmount", "items"],
"properties": {
- "type": {
- "const": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~",
- "$comment": "The value is required only for IDE-side validation"
- },
- "payload": {
- "type": "object",
- "required": ["orderId", "customerId", "totalAmount", "items"],
- "properties": {
- "orderId": { "type": "string", "format": "uuid" },
- "customerId": { "type": "string", "format": "uuid" },
- "totalAmount": { "type": "number" },
- "items": { "type": "array", "items": { "type": "object" } }
- }
- },
- "subjectType": {
- "type": "string",
- "x-gts-ref": "gts.x.commerce.orders.order.v1.0~"
- }
+ "orderId": { "type": "string", "format": "uuid" },
+ "customerId": { "type": "string", "format": "uuid" },
+ "totalAmount": { "type": "number" },
+ "items": { "type": "array", "items": { "type": "object" } }
}
+ },
+ "subjectType": {
+ "type": "string",
+ "x-gts-ref": "gts.x.commerce.orders.order.v1.0~"
}
- ]
+ }
}
diff --git a/examples/events/types/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json b/examples/events/types/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json
index aa5047a..660759d 100644
--- a/examples/events/types/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json
+++ b/examples/events/types/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json
@@ -4,35 +4,32 @@
"title": "Event Instance Schema: order.placed",
"type": "object",
"allOf": [
- { "$ref": "gts://gts.x.core.events.type.v1~" },
- {
+ { "$ref": "gts://gts.x.core.events.type.v1~" }
+ ],
+ "required": ["type", "payload", "subjectType"],
+ "x-gts-traits": {
+ "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1",
+ "retention": "P90D"
+ },
+ "properties": {
+ "type": {
+ "const": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~",
+ "$comment": "The value is required only for IDE-side validation"
+ },
+ "payload": {
"type": "object",
- "required": ["type", "payload", "subjectType"],
- "x-gts-traits": {
- "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1",
- "retention": "P90D"
- },
+ "required": ["orderId", "customerId", "totalAmount", "items"],
"properties": {
- "type": {
- "const": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~",
- "$comment": "The value is required only for IDE-side validation"
- },
- "payload": {
- "type": "object",
- "required": ["orderId", "customerId", "totalAmount", "items"],
- "properties": {
- "orderId": { "type": "string", "format": "uuid" },
- "customerId": { "type": "string", "format": "uuid" },
- "totalAmount": { "type": "number" },
- "items": { "type": "array", "items": { "type": "object" } },
- "new_field_in_v1_1": { "type": "string", "default": "some_value" }
- }
- },
- "subjectType": {
- "type": "string",
- "x-gts-ref": "gts.x.commerce.orders.order.v1.0~"
- }
+ "orderId": { "type": "string", "format": "uuid" },
+ "customerId": { "type": "string", "format": "uuid" },
+ "totalAmount": { "type": "number" },
+ "items": { "type": "array", "items": { "type": "object" } },
+ "new_field_in_v1_1": { "type": "string", "default": "some_value" }
}
+ },
+ "subjectType": {
+ "type": "string",
+ "x-gts-ref": "gts.x.commerce.orders.order.v1.0~"
}
- ]
+ }
}
diff --git a/examples/events/types/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json b/examples/events/types/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json
index f18f9d6..2063290 100644
--- a/examples/events/types/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json
+++ b/examples/events/types/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json
@@ -5,35 +5,32 @@
"description": "Event instance schema for the user.created event type",
"type": "object",
"allOf": [
- { "$ref": "gts://gts.x.core.events.type.v1~" },
- {
+ { "$ref": "gts://gts.x.core.events.type.v1~" }
+ ],
+ "required": ["type", "payload", "subjectType"],
+ "x-gts-traits": {
+ "topicRef": "gts.x.core.events.topic.v1~x.core.idp.contacts.v1",
+ "retention": "P365D"
+ },
+ "properties": {
+ "type": {
+ "const": "gts.x.core.events.type.v1~x.core.idp.contact_created.v1.0~",
+ "$comment": "The value is required only for IDE-side validation"
+ },
+ "payload": {
"type": "object",
- "required": ["type", "payload", "subjectType"],
- "x-gts-traits": {
- "topicRef": "gts.x.core.events.topic.v1~x.core.idp.contacts.v1",
- "retention": "P365D"
- },
+ "required": ["userId", "email", "name", "roles"],
+ "additionalProperties": false,
"properties": {
- "type": {
- "const": "gts.x.core.events.type.v1~x.core.idp.contact_created.v1.0~",
- "$comment": "The value is required only for IDE-side validation"
- },
- "payload": {
- "type": "object",
- "required": ["userId", "email", "name", "roles"],
- "additionalProperties": false,
- "properties": {
- "userId": { "type": "string", "format": "uuid" },
- "email": { "type": "string", "format": "email" },
- "name": { "type": "string" },
- "roles": { "type": "array", "items": { "type": "string" } }
- }
- },
- "subjectType": {
- "type": "string",
- "x-gts-ref": "gts.x.core.idp.contact.v1.0~"
- }
+ "userId": { "type": "string", "format": "uuid" },
+ "email": { "type": "string", "format": "email" },
+ "name": { "type": "string" },
+ "roles": { "type": "array", "items": { "type": "string" } }
}
+ },
+ "subjectType": {
+ "type": "string",
+ "x-gts-ref": "gts.x.core.idp.contact.v1.0~"
}
- ]
+ }
}
diff --git a/examples/events/types/gts.x.core.events.type_combined.v1~.schema.json b/examples/events/types/gts.x.core.events.type_combined.v1~.schema.json
index 6246333..b6dd322 100644
--- a/examples/events/types/gts.x.core.events.type_combined.v1~.schema.json
+++ b/examples/events/types/gts.x.core.events.type_combined.v1~.schema.json
@@ -9,17 +9,7 @@
"tenantId",
"occurredAt"
],
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "topicRef": {
- "description": "ID of the topic where events of this type are stored.",
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~"
- }
- }
- },
+ "x-gts-traits-schema": "gts://gts.x.core.traits.event_meta.v1~",
"properties": {
"$schema": {
"description": "Link to the event type schema.",
diff --git a/examples/events/types/gts.x.core.events.type_combined.v1~x.commerce.orders.order_placed.v1.0~.schema.json b/examples/events/types/gts.x.core.events.type_combined.v1~x.commerce.orders.order_placed.v1.0~.schema.json
index aeffedf..b9ad719 100644
--- a/examples/events/types/gts.x.core.events.type_combined.v1~x.commerce.orders.order_placed.v1.0~.schema.json
+++ b/examples/events/types/gts.x.core.events.type_combined.v1~x.commerce.orders.order_placed.v1.0~.schema.json
@@ -4,30 +4,30 @@
"title": "Event Instance Schema (Combined Anonymous Instance ID): order.placed",
"type": "object",
"allOf": [
- { "$ref": "gts://gts.x.core.events.type_combined.v1~" },
- {
+ { "$ref": "gts://gts.x.core.events.type_combined.v1~" }
+ ],
+ "required": ["payload", "subjectType"],
+ "x-gts-traits": {
+ "topicRef": "gts.x.core.events.topic.v1~x.commerce._.orders.v1"
+ },
+ "properties": {
+ "id": {
+ "type": "string",
+ "x-gts-ref": "gts.x.core.events.type_combined.v1~x.commerce.orders.order_placed.v1.0~*"
+ },
+ "payload": {
"type": "object",
- "required": ["payload", "subjectType"],
+ "required": ["orderId", "customerId", "totalAmount", "items"],
"properties": {
- "id": {
- "type": "string",
- "x-gts-ref": "gts.x.core.events.type_combined.v1~x.commerce.orders.order_placed.v1.0~*"
- },
- "payload": {
- "type": "object",
- "required": ["orderId", "customerId", "totalAmount", "items"],
- "properties": {
- "orderId": { "type": "string", "format": "uuid" },
- "customerId": { "type": "string", "format": "uuid" },
- "totalAmount": { "type": "number" },
- "items": { "type": "array", "items": { "type": "object" } }
- }
- },
- "subjectType": {
- "type": "string",
- "x-gts-ref": "gts.x.commerce.orders.order.v1.0~"
- }
+ "orderId": { "type": "string", "format": "uuid" },
+ "customerId": { "type": "string", "format": "uuid" },
+ "totalAmount": { "type": "number" },
+ "items": { "type": "array", "items": { "type": "object" } }
}
+ },
+ "subjectType": {
+ "type": "string",
+ "x-gts-ref": "gts.x.commerce.orders.order.v1.0~"
}
- ]
+ }
}
diff --git a/examples/events/types/gts.x.core.idp.contact.v1.0~x.core.idp.billing_contact.v1.0~.schema.json b/examples/events/types/gts.x.core.idp.contact.v1.0~x.core.idp.billing_contact.v1.0~.schema.json
index 189cbe8..a10a67b 100644
--- a/examples/events/types/gts.x.core.idp.contact.v1.0~x.core.idp.billing_contact.v1.0~.schema.json
+++ b/examples/events/types/gts.x.core.idp.contact.v1.0~x.core.idp.billing_contact.v1.0~.schema.json
@@ -4,21 +4,18 @@
"title": "User",
"type": "object",
"allOf": [
- { "$ref": "gts://gts.x.core.idp.contact.v1.0~" },
- {
+ { "$ref": "gts://gts.x.core.idp.contact.v1.0~" }
+ ],
+ "properties": {
+ "custom_properties": {
"type": "object",
+ "required": ["phone_number", "address", "currency"],
"properties": {
- "custom_properties": {
- "type": "object",
- "required": ["phone_number", "address", "currency"],
- "properties": {
- "phone_number": { "type": "string" },
- "address": { "type": "string" },
- "currency": { "type": "string" }
- },
- "additionalProperties": true
- }
- }
+ "phone_number": { "type": "string" },
+ "address": { "type": "string" },
+ "currency": { "type": "string" }
+ },
+ "additionalProperties": true
}
- ]
+ }
}
diff --git a/examples/events/types/gts.x.core.traits.event_meta.v1~.schema.json b/examples/events/types/gts.x.core.traits.event_meta.v1~.schema.json
new file mode 100644
index 0000000..9b6b1ef
--- /dev/null
+++ b/examples/events/types/gts.x.core.traits.event_meta.v1~.schema.json
@@ -0,0 +1,20 @@
+{
+ "$id": "gts://gts.x.core.traits.event_meta.v1~",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Event Meta Trait",
+ "type": "object",
+ "description": "Trait-type attached to event host-types. Carries cross-cutting metadata: where instances of the event are published (topicRef) and how long they are retained.",
+ "properties": {
+ "topicRef": {
+ "description": "GTS ID of the topic/stream where events of this type are published.",
+ "type": "string",
+ "x-gts-ref": "gts.x.core.events.topic.v1~",
+ "default": "gts.x.core.events.topic.v1~x.core._.default.v1"
+ },
+ "retention": {
+ "description": "ISO 8601 duration for event retention.",
+ "type": "string",
+ "default": "P30D"
+ }
+ }
+}
diff --git a/examples/mcp/types/gts.x.ai.mcp.tool.v1~x.ai.mcp.http_outbound.v1~.schema.jsonc b/examples/mcp/types/gts.x.ai.mcp.tool.v1~x.ai.mcp.http_outbound.v1~.schema.jsonc
index 8717b02..5c01f94 100644
--- a/examples/mcp/types/gts.x.ai.mcp.tool.v1~x.ai.mcp.http_outbound.v1~.schema.jsonc
+++ b/examples/mcp/types/gts.x.ai.mcp.tool.v1~x.ai.mcp.http_outbound.v1~.schema.jsonc
@@ -5,49 +5,46 @@
"title": "MCP Tool with HTTP Outbound Capability",
"type": "object",
"allOf": [
- { "$ref": "gts://gts.x.ai.mcp.tool.v1~" },
- {
+ { "$ref": "gts://gts.x.ai.mcp.tool.v1~" }
+ ],
+ "required": ["security", "capabilities"],
+ "additionalProperties": false,
+ "properties": {
+ "security": {
"type": "object",
"properties": {
- "security": {
- "type": "object",
- "properties": {
- "allow_network": { "const": true }, // Force true for HTTP outbound tools
- "allow_filesystem": { "const": false } // Force false for HTTP outbound tools
- },
- "required": ["allow_network", "allow_filesystem"],
- "additionalProperties": false
- },
- "capabilities": {
+ "allow_network": { "const": true }, // Force true for HTTP outbound tools
+ "allow_filesystem": { "const": false } // Force false for HTTP outbound tools
+ },
+ "required": ["allow_network", "allow_filesystem"],
+ "additionalProperties": false
+ },
+ "capabilities": {
+ "type": "object",
+ "properties": {
+ "http_outbound": {
"type": "object",
- "properties": {
- "http_outbound": {
- "type": "object",
- "properties": { // Specific tools instances will need to specify 'allowed_domains' and 'allowed_methods', etc
- "allowed_domains": {
- "type": "array",
- "minItems": 1,
- "uniqueItems": true,
- "items": { "type": "string", "pattern": "^[a-z0-9.-]+$" }
- },
- "allowed_methods": {
- "type": "array",
- "minItems": 1,
- "uniqueItems": true,
- "items": { "type": "string", "enum": ["GET", "HEAD", "POST"] }
- },
- "max_response_bytes": { "type": "integer", "minimum": 1024, "maximum": 10485760 }
- },
- "required": ["allowed_domains", "allowed_methods", "max_response_bytes"],
- "additionalProperties": false
- }
+ "properties": { // Specific tools instances will need to specify 'allowed_domains' and 'allowed_methods', etc
+ "allowed_domains": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": { "type": "string", "pattern": "^[a-z0-9.-]+$" }
+ },
+ "allowed_methods": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": { "type": "string", "enum": ["GET", "HEAD", "POST"] }
+ },
+ "max_response_bytes": { "type": "integer", "minimum": 1024, "maximum": 10485760 }
},
- "required": ["http_outbound"],
+ "required": ["allowed_domains", "allowed_methods", "max_response_bytes"],
"additionalProperties": false
}
},
- "required": ["security", "capabilities"],
+ "required": ["http_outbound"],
"additionalProperties": false
}
- ]
+ }
}
diff --git a/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~nutanix.ahv._.vm.v1~.schema.json b/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~nutanix.ahv._.vm.v1~.schema.json
index aab7cdb..3c69e95 100644
--- a/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~nutanix.ahv._.vm.v1~.schema.json
+++ b/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~nutanix.ahv._.vm.v1~.schema.json
@@ -5,72 +5,67 @@
"description": "Extends the base VM type with Nutanix AHV specific properties.",
"type": "object",
"allOf": [
- {
- "$ref": "gts://gts.x.infra.compute.vm.v1~"
+ { "$ref": "gts://gts.x.infra.compute.vm.v1~" }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "gts.x.infra.compute.vm.v1~nutanix.ahv._.vm.v1~",
+ "description": "GTS identifier of the VM type",
+ "x-gts-ref": "/$id"
},
- {
+ "metadata": {
"type": "object",
+ "description": "Nutanix-specific metadata",
"properties": {
- "type": {
+ "vmUuid": {
"type": "string",
- "const": "gts.x.infra.compute.vm.v1~nutanix.ahv._.vm.v1~",
- "description": "GTS identifier of the VM type",
- "x-gts-ref": "/$id"
+ "description": "Nutanix VM UUID"
},
- "metadata": {
+ "clusterUuid": {
+ "type": "string",
+ "description": "Nutanix cluster UUID"
+ },
+ "clusterName": {
+ "type": "string",
+ "description": "Nutanix cluster name"
+ },
+ "hostUuid": {
+ "type": "string",
+ "description": "Host UUID where VM is running"
+ },
+ "hostName": {
+ "type": "string",
+ "description": "Host name where VM is running"
+ },
+ "guestToolsStatus": {
+ "type": "string",
+ "description": "Status of Nutanix Guest Tools"
+ },
+ "protectionDomain": {
+ "type": "string",
+ "description": "Protection domain for DR"
+ },
+ "categories": {
"type": "object",
- "description": "Nutanix-specific metadata",
- "properties": {
- "vmUuid": {
- "type": "string",
- "description": "Nutanix VM UUID"
- },
- "clusterUuid": {
- "type": "string",
- "description": "Nutanix cluster UUID"
- },
- "clusterName": {
- "type": "string",
- "description": "Nutanix cluster name"
- },
- "hostUuid": {
- "type": "string",
- "description": "Host UUID where VM is running"
- },
- "hostName": {
- "type": "string",
- "description": "Host name where VM is running"
- },
- "guestToolsStatus": {
- "type": "string",
- "description": "Status of Nutanix Guest Tools"
- },
- "protectionDomain": {
- "type": "string",
- "description": "Protection domain for DR"
- },
- "categories": {
- "type": "object",
- "description": "Nutanix categories for organization",
- "additionalProperties": {
- "type": "string"
- }
- },
- "storageContainer": {
- "type": "string",
- "description": "Storage container name"
- },
- "haPriority": {
- "type": "string",
- "description": "High availability priority"
- },
- "backupSchedule": {
- "type": "string",
- "description": "Backup schedule identifier"
- }
+ "description": "Nutanix categories for organization",
+ "additionalProperties": {
+ "type": "string"
}
+ },
+ "storageContainer": {
+ "type": "string",
+ "description": "Storage container name"
+ },
+ "haPriority": {
+ "type": "string",
+ "description": "High availability priority"
+ },
+ "backupSchedule": {
+ "type": "string",
+ "description": "Backup schedule identifier"
}
}
}
- ]
+ }
}
diff --git a/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~vmware.esxi._.vm.v1~.schema.json b/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~vmware.esxi._.vm.v1~.schema.json
index 225a8fb..2b468ef 100644
--- a/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~vmware.esxi._.vm.v1~.schema.json
+++ b/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~vmware.esxi._.vm.v1~.schema.json
@@ -5,69 +5,64 @@
"description": "Extends the base VM type with VMWare ESXi-specific properties.",
"type": "object",
"allOf": [
- {
- "$ref": "gts://gts.x.infra.compute.vm.v1~"
+ { "$ref": "gts://gts.x.infra.compute.vm.v1~" }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "gts.x.infra.compute.vm.v1~vmware.esxi._.vm.v1~",
+ "description": "GTS identifier of the VM type",
+ "x-gts-ref": "/$id"
},
- {
+ "metadata": {
"type": "object",
+ "description": "VMWare ESXi-specific metadata",
"properties": {
- "type": {
+ "vmId": {
"type": "string",
- "const": "gts.x.infra.compute.vm.v1~vmware.esxi._.vm.v1~",
- "description": "GTS identifier of the VM type",
- "x-gts-ref": "/$id"
+ "description": "VMWare VM identifier"
},
- "metadata": {
- "type": "object",
- "description": "VMWare ESXi-specific metadata",
- "properties": {
- "vmId": {
- "type": "string",
- "description": "VMWare VM identifier"
- },
- "instanceUuid": {
- "type": "string",
- "description": "VMWare instance UUID"
- },
- "bios": {
- "type": "string",
- "description": "BIOS UUID"
- },
- "hostId": {
- "type": "string",
- "description": "ESXi host identifier"
- },
- "hostName": {
- "type": "string",
- "description": "ESXi host name"
- },
- "datastore": {
- "type": "string",
- "description": "Datastore name"
- },
- "cluster": {
- "type": "string",
- "description": "vSphere cluster name"
- },
- "resourcePool": {
- "type": "string",
- "description": "Resource pool name"
- },
- "folder": {
- "type": "string",
- "description": "VM folder path"
- },
- "vmwareToolsStatus": {
- "type": "string",
- "description": "VMWare Tools installation status"
- },
- "guestFamily": {
- "type": "string",
- "description": "Guest OS family"
- }
- }
+ "instanceUuid": {
+ "type": "string",
+ "description": "VMWare instance UUID"
+ },
+ "bios": {
+ "type": "string",
+ "description": "BIOS UUID"
+ },
+ "hostId": {
+ "type": "string",
+ "description": "ESXi host identifier"
+ },
+ "hostName": {
+ "type": "string",
+ "description": "ESXi host name"
+ },
+ "datastore": {
+ "type": "string",
+ "description": "Datastore name"
+ },
+ "cluster": {
+ "type": "string",
+ "description": "vSphere cluster name"
+ },
+ "resourcePool": {
+ "type": "string",
+ "description": "Resource pool name"
+ },
+ "folder": {
+ "type": "string",
+ "description": "VM folder path"
+ },
+ "vmwareToolsStatus": {
+ "type": "string",
+ "description": "VMWare Tools installation status"
+ },
+ "guestFamily": {
+ "type": "string",
+ "description": "Guest OS family"
}
}
}
- ]
+ }
}
diff --git a/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~vz.vz._.vm.v1~.schema.json b/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~vz.vz._.vm.v1~.schema.json
index 0df69c6..4a4d3bd 100644
--- a/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~vz.vz._.vm.v1~.schema.json
+++ b/examples/typespec/vms/types/gts.x.infra.compute.vm.v1~vz.vz._.vm.v1~.schema.json
@@ -5,68 +5,63 @@
"description": "Extends the base VM type with Virtuozzo properties.",
"type": "object",
"allOf": [
- {
- "$ref": "gts://gts.x.infra.compute.vm.v1~"
+ { "$ref": "gts://gts.x.infra.compute.vm.v1~" }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "gts.x.infra.compute.vm.v1~vz.vz._.vm.v1~",
+ "description": "GTS identifier of the VM type",
+ "x-gts-ref": "/$id"
},
- {
+ "metadata": {
"type": "object",
+ "description": "Virtuozzo-specific metadata",
"properties": {
- "type": {
+ "ctid": {
"type": "string",
- "const": "gts.x.infra.compute.vm.v1~vz.vz._.vm.v1~",
- "description": "GTS identifier of the VM type",
- "x-gts-ref": "/$id"
+ "description": "Container ID"
},
- "metadata": {
- "type": "object",
- "description": "Virtuozzo-specific metadata",
- "properties": {
- "ctid": {
- "type": "string",
- "description": "Container ID"
- },
- "uuid": {
- "type": "string",
- "description": "Container UUID"
- },
- "veid": {
- "type": "string",
- "description": "Virtual environment ID"
- },
- "nodeName": {
- "type": "string",
- "description": "Physical node name"
- },
- "osTemplate": {
- "type": "string",
- "description": "OS template used"
- },
- "layout": {
- "type": "string",
- "description": "Filesystem layout (ploop/simfs)"
- },
- "ioLimit": {
- "type": "integer",
- "description": "I/O operations limit"
- },
- "diskSpace": {
- "type": "string",
- "description": "Disk space allocation"
- },
- "ipAddress": {
- "type": "array",
- "description": "Assigned IP addresses",
- "items": {
- "type": "string"
- }
- },
- "hostname": {
- "type": "string",
- "description": "Container hostname"
- }
+ "uuid": {
+ "type": "string",
+ "description": "Container UUID"
+ },
+ "veid": {
+ "type": "string",
+ "description": "Virtual environment ID"
+ },
+ "nodeName": {
+ "type": "string",
+ "description": "Physical node name"
+ },
+ "osTemplate": {
+ "type": "string",
+ "description": "OS template used"
+ },
+ "layout": {
+ "type": "string",
+ "description": "Filesystem layout (ploop/simfs)"
+ },
+ "ioLimit": {
+ "type": "integer",
+ "description": "I/O operations limit"
+ },
+ "diskSpace": {
+ "type": "string",
+ "description": "Disk space allocation"
+ },
+ "ipAddress": {
+ "type": "array",
+ "description": "Assigned IP addresses",
+ "items": {
+ "type": "string"
}
+ },
+ "hostname": {
+ "type": "string",
+ "description": "Container hostname"
}
}
}
- ]
+ }
}
diff --git a/tests/helpers/http_run_helpers.py b/tests/helpers/http_run_helpers.py
index 1a5e2ab..a4fd2c4 100644
--- a/tests/helpers/http_run_helpers.py
+++ b/tests/helpers/http_run_helpers.py
@@ -24,20 +24,69 @@ def register(gts_id, schema_body, label="register schema"):
def register_derived(gts_id, base_ref, overlay, label="register derived", top_level=None):
- """Register a derived schema that uses allOf with a $$ref.
+ """Register a derived schema that uses the strict canonical form (§3.2.1).
- top_level: optional dict of extra keys to add at schema top level
- (e.g. {"x-gts-final": True}) — these MUST NOT go inside allOf.
+ Emits a derived GTS Type Schema with:
+ - top-level `allOf: [{"$ref": base_ref}]` (exactly one item, the parent ref)
+ - the `overlay` dict spread at the top level alongside `allOf`
+ - optional extra `top_level` keys (e.g. {"x-gts-final": True})
+
+ All of `properties`, `required`, narrowed constraints, GTS modifiers, and trait
+ keywords belong at the top level — never inside `allOf`.
+ """
+ body = {
+ "$$id": gts_id,
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [{"$$ref": base_ref}],
+ }
+ if overlay:
+ body.update(overlay)
+ if top_level:
+ body.update(top_level)
+ return Step(
+ RunRequest(label)
+ .post("/entities")
+ .with_json(body)
+ .validate()
+ .assert_equal("status_code", 200)
+ )
+
+
+def register_trait_type(trait_id, schema_body, label="register trait type"):
+ """Register a trait-type — a regular GTS Type Schema published under a
+ trait-namespaced `$id` (e.g. `gts://gts.x.core.traits.event_meta.v1~`).
+
+ Thin wrapper around `register` kept for readability in OP#13 tests.
+ """
+ return register(trait_id, schema_body, label)
+
+
+def register_host_with_trait_ref(
+ gts_id, base_ref, trait_urn, overlay=None, traits=None,
+ label="register host with trait ref", top_level=None,
+):
+ """Register a derived host-type that attaches a trait-type by URN.
+
+ Always emits strict Form A:
+ - `allOf: [{"$ref": base_ref}]` at top level
+ - `x-gts-traits-schema: trait_urn` (string) at top level
+ - optional `x-gts-traits: traits` (plain object) at top level
+ - `overlay` and `top_level` dicts spread at the top level
+
+ Use `register_trait_type` to publish the trait-type itself first.
"""
body = {
"$$id": gts_id,
"$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "allOf": [
- {"$$ref": base_ref},
- overlay,
- ],
+ "allOf": [{"$$ref": base_ref}],
+ "x-gts-traits-schema": trait_urn,
}
+ if traits is not None:
+ body["x-gts-traits"] = traits
+ if overlay:
+ body.update(overlay)
if top_level:
body.update(top_level)
return Step(
diff --git a/tests/test_op12_type_derivation_validation.py b/tests/test_op12_type_derivation_validation.py
index fd75821..9e7729d 100644
--- a/tests/test_op12_type_derivation_validation.py
+++ b/tests/test_op12_type_derivation_validation.py
@@ -3523,5 +3523,568 @@ def test_start(self):
]
+# ---------------------------------------------------------------------------
+# Structural top-level composition rules (§3.2.1, ADR-0002)
+#
+# These tests exercise the strict canonical form for derived GTS Type Schemas:
+# - allOf at top level with exactly ONE subschema
+# - that subschema is {"$ref": "gts://"}
+# - everything else (properties, required, modifiers, traits, ...) at top level
+# - top-level anyOf/oneOf/not forbidden on all GTS Type Schemas
+# - base types do not use allOf for derivation
+#
+# Each test posts a malformed derived schema directly to /entities (bypassing
+# the helper, which always emits strict Form A) and asserts that
+# /validate-type-schema reports ok=false.
+# ---------------------------------------------------------------------------
+
+
+def _post_schema_raw(body, label):
+ """Post a custom schema body to /entities. Used for malformed structural cases."""
+ return Step(
+ RunRequest(label)
+ .post("/entities")
+ .with_json(body)
+ .validate()
+ .assert_equal("status_code", 200)
+ )
+
+
+_STRUCT_NS = "gts.x.test12struct"
+
+
+class TestCaseOp12_Struct_CanonicalForm_Ok(HttpRunner):
+ """OP#12 §3.2.1: canonical form (allOf: [{$ref: parent}] + props at top level) passes."""
+
+ config = Config("OP#12 - canonical Form A accepted").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.canonical.base.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register base",
+ ),
+ _register_derived(
+ f"gts://{_STRUCT_NS}.canonical.base.v1~x.test12struct._.canonical_ok.v1~",
+ f"gts://{_STRUCT_NS}.canonical.base.v1~",
+ {"properties": {"b": {"type": "string"}}},
+ "register derived in canonical Form A",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.canonical.base.v1~x.test12struct._.canonical_ok.v1~",
+ True,
+ "validate canonical form should pass",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_AllOfTwoItems_Rejected(HttpRunner):
+ """OP#12 §3.2.1: legacy Form B (allOf with $ref + inline overlay) is rejected."""
+
+ config = Config("OP#12 - allOf with overlay inside (Form B) rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.formb.base.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register base",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.formb.base.v1~x.test12struct._.form_b_overlay.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [
+ {"$$ref": f"gts://{_STRUCT_NS}.formb.base.v1~"},
+ {
+ "type": "object",
+ "properties": {"b": {"type": "string"}},
+ },
+ ],
+ },
+ "register derived with overlay nested inside allOf (Form B)",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.formb.base.v1~x.test12struct._.form_b_overlay.v1~",
+ False,
+ "validate should fail - Form B is not the canonical form",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_HybridFormAB_Rejected(HttpRunner):
+ """OP#12 §3.2.1: hybrid form — overlay BOTH at top level AND inside allOf — rejected.
+
+ Under JSON Schema this is well-defined (parent + nested overlay + top-level overlay
+ all combine via implicit AND), but the structure is opaque to readers and tools.
+ Rule (2) — allOf MUST contain exactly one subschema — rejects this shape.
+ """
+
+ config = Config("OP#12 - hybrid Form A+B rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.hybrid.base.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register base",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.hybrid.base.v1~x.test12struct._.hybrid_a_b.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [
+ {"$$ref": f"gts://{_STRUCT_NS}.hybrid.base.v1~"},
+ {"properties": {"b_in_allof": {"type": "string"}}},
+ ],
+ "properties": {"c_top_level": {"type": "integer"}},
+ },
+ "register derived with overlays in BOTH allOf[1] and top level",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.hybrid.base.v1~x.test12struct._.hybrid_a_b.v1~",
+ False,
+ "validate should fail - allOf has 2 items even though top-level also has properties",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_AllOfThreeItems_Rejected(HttpRunner):
+ """OP#12 §3.2.1: allOf with 3+ items at top level is rejected."""
+
+ config = Config("OP#12 - allOf with 3 items rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.three.base.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register base",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.three.base.v1~x.test12struct._.three_items.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [
+ {"$$ref": f"gts://{_STRUCT_NS}.three.base.v1~"},
+ {"properties": {"b": {"type": "string"}}},
+ {"properties": {"c": {"type": "string"}}},
+ ],
+ },
+ "register derived with allOf of 3 items",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.three.base.v1~x.test12struct._.three_items.v1~",
+ False,
+ "validate should fail - allOf has 3 items",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_AllOfNoRef_Rejected(HttpRunner):
+ """OP#12 §3.2.1: allOf without a $ref to parent is rejected (no derivation pointer)."""
+
+ config = Config("OP#12 - allOf without $ref rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.noref.base.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register base",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.noref.base.v1~x.test12struct._.no_ref.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [
+ {"properties": {"b": {"type": "string"}}},
+ ],
+ },
+ "register derived with allOf containing only an inline overlay",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.noref.base.v1~x.test12struct._.no_ref.v1~",
+ False,
+ "validate should fail - allOf has no $ref to parent",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_AllOfMultipleRefs_Rejected(HttpRunner):
+ """OP#12 §3.2.1: multi-parent inheritance attempt (2+ $refs in allOf) is rejected."""
+
+ config = Config("OP#12 - allOf with multiple $refs rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.multiref.parent_a.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register parent A",
+ ),
+ _register(
+ f"gts://{_STRUCT_NS}.multiref.parent_b.v1~",
+ {"type": "object", "properties": {"b": {"type": "string"}}},
+ "register parent B",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.multiref.parent_a.v1~x.test12struct._.multi_ref.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [
+ {"$$ref": f"gts://{_STRUCT_NS}.multiref.parent_a.v1~"},
+ {"$$ref": f"gts://{_STRUCT_NS}.multiref.parent_b.v1~"},
+ ],
+ },
+ "register derived with two $ref items (multi-parent)",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.multiref.parent_a.v1~x.test12struct._.multi_ref.v1~",
+ False,
+ "validate should fail - multi-parent inheritance not allowed",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_AllOfRefSkipsLevel_Rejected(HttpRunner):
+ """OP#12 §3.2.1: $ref must point to the immediate parent; skip-level $ref rejected.
+
+ Chain A~B~C~ with $ref = A~ (skipping B) is invalid.
+ """
+
+ config = Config("OP#12 - $ref skipping a level rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.skip.a.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register A (base)",
+ ),
+ _register_derived(
+ f"gts://{_STRUCT_NS}.skip.a.v1~x.test12struct._.b.v1~",
+ f"gts://{_STRUCT_NS}.skip.a.v1~",
+ {"properties": {"b": {"type": "string"}}},
+ "register B from A",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": (
+ f"gts://{_STRUCT_NS}.skip.a.v1~x.test12struct._.b.v1~"
+ "x.test12struct._.c_skip.v1~"
+ ),
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [
+ {"$$ref": f"gts://{_STRUCT_NS}.skip.a.v1~"}
+ ],
+ "properties": {"c": {"type": "string"}},
+ },
+ "register C with $ref to A (skipping B)",
+ ),
+ _validate_type_schema(
+ (
+ f"{_STRUCT_NS}.skip.a.v1~x.test12struct._.b.v1~"
+ "x.test12struct._.c_skip.v1~"
+ ),
+ False,
+ "validate should fail - $ref skips immediate parent",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_AllOfRefWrongParent_Rejected(HttpRunner):
+ """OP#12 §3.2.1: $ref must match the chained $id; referencing an unrelated type rejected."""
+
+ config = Config("OP#12 - $ref to unrelated type rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.wrongp.expected.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register expected parent",
+ ),
+ _register(
+ f"gts://{_STRUCT_NS}.wrongp.unrelated.v1~",
+ {"type": "object", "properties": {"u": {"type": "string"}}},
+ "register unrelated type",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.wrongp.expected.v1~x.test12struct._.wrong_parent.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [
+ {"$$ref": f"gts://{_STRUCT_NS}.wrongp.unrelated.v1~"}
+ ],
+ "properties": {"b": {"type": "string"}},
+ },
+ "register derived with $ref to an unrelated type",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.wrongp.expected.v1~x.test12struct._.wrong_parent.v1~",
+ False,
+ "validate should fail - $ref does not match immediate parent in chain",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_TopLevelAnyOf_Rejected(HttpRunner):
+ """OP#12 §3.2.1: anyOf at top level of a derived schema is rejected."""
+
+ config = Config("OP#12 - top-level anyOf in derived rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.tlany.base.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register base",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.tlany.base.v1~x.test12struct._.top_anyof.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [{"$$ref": f"gts://{_STRUCT_NS}.tlany.base.v1~"}],
+ "anyOf": [
+ {"properties": {"b1": {"type": "string"}}},
+ {"properties": {"b2": {"type": "string"}}},
+ ],
+ },
+ "register derived with top-level anyOf",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.tlany.base.v1~x.test12struct._.top_anyof.v1~",
+ False,
+ "validate should fail - top-level anyOf forbidden",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_TopLevelOneOf_Rejected(HttpRunner):
+ """OP#12 §3.2.1: oneOf at top level of a derived schema is rejected."""
+
+ config = Config("OP#12 - top-level oneOf in derived rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.tloneof.base.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register base",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.tloneof.base.v1~x.test12struct._.top_oneof.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [{"$$ref": f"gts://{_STRUCT_NS}.tloneof.base.v1~"}],
+ "oneOf": [
+ {"properties": {"b1": {"type": "string"}}},
+ {"properties": {"b2": {"type": "string"}}},
+ ],
+ },
+ "register derived with top-level oneOf",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.tloneof.base.v1~x.test12struct._.top_oneof.v1~",
+ False,
+ "validate should fail - top-level oneOf forbidden",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_TopLevelNot_Rejected(HttpRunner):
+ """OP#12 §3.2.1: `not` at top level of a derived schema is rejected."""
+
+ config = Config("OP#12 - top-level not in derived rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.tlnot.base.v1~",
+ {"type": "object", "properties": {"a": {"type": "string"}}},
+ "register base",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.tlnot.base.v1~x.test12struct._.top_not.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [{"$$ref": f"gts://{_STRUCT_NS}.tlnot.base.v1~"}],
+ "not": {"required": ["forbidden_field"]},
+ },
+ "register derived with top-level not",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.tlnot.base.v1~x.test12struct._.top_not.v1~",
+ False,
+ "validate should fail - top-level not forbidden",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_BaseTypeWithAllOf_Rejected(HttpRunner):
+ """OP#12 §3.2.1: base types (1-segment $id) MUST NOT use allOf for derivation."""
+
+ config = Config("OP#12 - base type with allOf for derivation rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.basederiv.other.v1~",
+ {"type": "object", "properties": {"o": {"type": "string"}}},
+ "register an unrelated type to reference",
+ ),
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.basederiv.bad_base.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "allOf": [
+ {"$$ref": f"gts://{_STRUCT_NS}.basederiv.other.v1~"}
+ ],
+ "properties": {"x": {"type": "string"}},
+ },
+ "register a 1-segment base $id with allOf (derivation pointer)",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.basederiv.bad_base.v1~",
+ False,
+ "validate should fail - base type cannot use allOf for derivation",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_BaseTypeWithTopLevelAnyOf_Rejected(HttpRunner):
+ """OP#12 §3.2.1: base types also forbid top-level anyOf/oneOf/not."""
+
+ config = Config("OP#12 - base type with top-level anyOf rejected").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _post_schema_raw(
+ {
+ "$$id": f"gts://{_STRUCT_NS}.baseunion.bad.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "anyOf": [
+ {"properties": {"a": {"type": "string"}}},
+ {"properties": {"b": {"type": "integer"}}},
+ ],
+ },
+ "register a 1-segment base $id with top-level anyOf",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.baseunion.bad.v1~",
+ False,
+ "validate should fail - base type with top-level anyOf forbidden",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_NestedAnyOfInProperty_Ok(HttpRunner):
+ """OP#12 §3.2.1: anyOf inside a property sub-schema is allowed (union types at depth)."""
+
+ config = Config("OP#12 - nested anyOf in property accepted").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.nestany.base.v1~",
+ {
+ "type": "object",
+ "properties": {
+ "id": {"type": "string"},
+ "value": {
+ "anyOf": [
+ {"type": "string"},
+ {"type": "integer"},
+ ]
+ },
+ },
+ },
+ "register base with anyOf nested under properties.value",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.nestany.base.v1~",
+ True,
+ "validate should pass - anyOf is nested, not top-level",
+ ),
+ ]
+
+
+class TestCaseOp12_Struct_NestedOneOfInDefinitions_Ok(HttpRunner):
+ """OP#12 §3.2.1: oneOf inside `definitions` is allowed."""
+
+ config = Config("OP#12 - nested oneOf in definitions accepted").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register(
+ f"gts://{_STRUCT_NS}.nestdef.base.v1~",
+ {
+ "type": "object",
+ "definitions": {
+ "StringOrInt": {
+ "oneOf": [
+ {"type": "string"},
+ {"type": "integer"},
+ ]
+ }
+ },
+ "properties": {
+ "v": {"$$ref": "#/definitions/StringOrInt"}
+ },
+ },
+ "register base with oneOf nested under definitions",
+ ),
+ _validate_type_schema(
+ f"{_STRUCT_NS}.nestdef.base.v1~",
+ True,
+ "validate should pass - oneOf is nested under definitions",
+ ),
+ ]
+
+
if __name__ == "__main__":
TestCaseTestOp12TypeDerivationValidation_DerivedSchemaFullyMatches().test_start()
diff --git a/tests/test_op13_schema_traits_validation.py b/tests/test_op13_schema_traits_validation.py
index 2090f9a..2a4f951 100644
--- a/tests/test_op13_schema_traits_validation.py
+++ b/tests/test_op13_schema_traits_validation.py
@@ -1,2087 +1,1718 @@
+"""OP#13 — Schema Traits Validation tests for the URN-string trait-type model.
+
+In this model:
+- `x-gts-traits-schema` value is a single string URN referencing a registered
+ trait-type (a regular GTS Type Schema).
+- A host-type attaches one trait-type by URN. Descendants inherit by default
+ and MAY refine the trait-type via parallel derivation (the descendant's
+ trait-type must be derived from the ancestor's trait-type).
+- `x-gts-traits` is a plain JSON object — an instance of the effective
+ trait-type. Values from all levels of the host chain are merged shallowly
+ with immutable-once-set semantics.
+
+See README.md §9.7 and ADR-0002 for the full spec.
+"""
+
from .conftest import get_gts_base_url
from .helpers.http_run_helpers import (
register as _register,
register_derived as _register_derived,
+ register_trait_type as _register_trait_type,
+ register_host_with_trait_ref as _register_host_with_trait_ref,
validate_entity as _validate_entity,
validate_type_schema as _validate_type_schema,
)
-from httprunner import HttpRunner, Config
+from httprunner import HttpRunner, Config, Step, RunRequest
+
+
+# ---------------------------------------------------------------------------
+# Helpers and constants
+# ---------------------------------------------------------------------------
+
+
+TOPIC_REF_DEFAULT = "gts.x.core.events.topic.v1~x.core._.default.v1"
+TOPIC_REF_ORDERS = "gts.x.core.events.topic.v1~x.test13._.orders.v1"
+TOPIC_REF_AUDIT = "gts.x.core.events.topic.v1~x.test13._.audit.v1"
+TOPIC_REF_NOTIF = "gts.x.core.events.topic.v1~x.test13._.notif.v1"
+
+
+def _trait_event_meta(trait_id, with_topic_default=True, with_retention_default=True,
+ additional=None, required=None):
+ """Build a basic event_meta trait-type body (topicRef + retention)."""
+ topic_prop = {
+ "type": "string",
+ "x-gts-ref": "gts.x.core.events.topic.v1~",
+ }
+ if with_topic_default:
+ topic_prop["default"] = TOPIC_REF_DEFAULT
+ retention_prop = {"type": "string"}
+ if with_retention_default:
+ retention_prop["default"] = "P30D"
+ body = {
+ "type": "object",
+ "properties": {
+ "topicRef": topic_prop,
+ "retention": retention_prop,
+ },
+ }
+ if additional:
+ body["properties"].update(additional)
+ if required:
+ body["required"] = required
+ return body
+
+
+def _post_schema_raw(body, label):
+ """Direct /entities POST for malformed bodies that helpers can't build."""
+ return Step(
+ RunRequest(label)
+ .post("/entities")
+ .with_json(body)
+ .validate()
+ .assert_equal("status_code", 200)
+ )
# ---------------------------------------------------------------------------
-# Tests
+# Value-validation cases (merged effective values vs effective trait-type)
# ---------------------------------------------------------------------------
class TestCaseOp13_TraitsValid_AllResolved(HttpRunner):
- """OP#13 - Traits: derived schema provides all trait values.
+ """OP#13 - derived host provides every trait value concretely. Passes."""
- Validation passes.
- """
config = Config("OP#13 - All Traits Resolved").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.allres.v1~",
+ _trait_event_meta("allres", with_topic_default=False, with_retention_default=False),
+ "register trait-type (no defaults)",
+ ),
_register(
- "gts://gts.x.test13.traits.event.v1~",
+ "gts://gts.x.test13.h.allres.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "topicRef": {
- "type": "string",
- "description": "Topic reference",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- },
- "retention": {
- "type": "string",
- "description": "Retention period",
- },
- },
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.allres.v1~",
"required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "properties": {"id": {"type": "string"}},
},
- "register base with traits-schema (no defaults)",
+ "register base host attaching trait-type",
),
_register_derived(
- "gts://gts.x.test13.traits.event.v1~x.test13._.order_event.v1~",
- "gts://gts.x.test13.traits.event.v1~",
+ "gts://gts.x.test13.h.allres.event.v1~x.test13._.order_event.v1~",
+ "gts://gts.x.test13.h.allres.event.v1~",
{
- "type": "object",
"x-gts-traits": {
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.test13._.orders.v1"
- ),
+ "topicRef": TOPIC_REF_ORDERS,
"retention": "P90D",
},
},
- "register derived with all traits resolved",
+ "register derived host with all traits resolved",
),
_validate_type_schema(
- "gts.x.test13.traits.event.v1~x.test13._.order_event.v1~",
+ "gts.x.test13.h.allres.event.v1~x.test13._.order_event.v1~",
True,
- "validate derived - all traits resolved",
+ "validate - all traits resolved",
),
]
class TestCaseOp13_TraitsValid_DefaultsUsed(HttpRunner):
- """OP#13 - Traits: base provides defaults, derived omits them - passes"""
- config = Config("OP#13 - Traits Defaults Used").base_url(
- get_gts_base_url()
- )
+ """OP#13 - trait-type provides defaults for all fields; derived host omits values.
+
+ Defaults fill in, so a non-abstract concrete derived host is still trait-complete.
+ """
+
+ config = Config("OP#13 - Defaults cover all traits").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.dfl.v1~",
+ _trait_event_meta("dfl"),
+ "register trait-type (all fields default)",
+ ),
_register(
- "gts://gts.x.test13.dfl.event.v1~",
+ "gts://gts.x.test13.h.dfl.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "topicRef": {
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- "default": (
- "gts.x.core.events.topic.v1~"
- "x.core._.default.v1"
- ),
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.dfl.v1~",
"required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "properties": {"id": {"type": "string"}},
},
- "register base with traits-schema (all defaults)",
+ "register base host",
),
_register_derived(
- "gts://gts.x.test13.dfl.event.v1~x.test13._.simple_event.v1~",
- "gts://gts.x.test13.dfl.event.v1~",
- {
- "type": "object",
- },
- "register derived with no x-gts-traits (rely on defaults)",
+ "gts://gts.x.test13.h.dfl.event.v1~x.test13._.simple_event.v1~",
+ "gts://gts.x.test13.h.dfl.event.v1~",
+ {},
+ "register derived host with no x-gts-traits",
),
_validate_type_schema(
- "gts.x.test13.dfl.event.v1~x.test13._.simple_event.v1~",
+ "gts.x.test13.h.dfl.event.v1~x.test13._.simple_event.v1~",
True,
- "validate derived - defaults fill all traits",
+ "validate - defaults satisfy all required fields",
),
]
class TestCaseOp13_TraitsInvalid_MissingRequired(HttpRunner):
- """OP#13 - Traits: trait property has no default.
-
- Derived omits it - fails.
+ """OP#13 - trait-type field is `required` and has no default; nothing in the host
+ chain resolves it. Concrete derived host is invalid.
"""
- config = Config("OP#13 - Missing Required Trait").base_url(
- get_gts_base_url()
- )
+
+ config = Config("OP#13 - Missing required trait field").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.miss.v1~",
+ _trait_event_meta("miss", with_topic_default=False, required=["topicRef"]),
+ "register trait-type (topicRef required, no default)",
+ ),
_register(
- "gts://gts.x.test13.miss.event.v1~",
+ "gts://gts.x.test13.h.miss.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "topicRef": {
- "type": "string",
- "description": "Required - no default",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.miss.v1~",
"required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "properties": {"id": {"type": "string"}},
},
- "register base with one trait without default",
+ "register base host",
),
_register_derived(
- "gts://gts.x.test13.miss.event.v1~x.test13._.incomplete.v1~",
- "gts://gts.x.test13.miss.event.v1~",
- {
- "type": "object",
- "x-gts-traits": {
- "retention": "P90D",
- },
- },
- "register derived missing topicRef trait",
+ "gts://gts.x.test13.h.miss.event.v1~x.test13._.incomplete.v1~",
+ "gts://gts.x.test13.h.miss.event.v1~",
+ {"x-gts-traits": {"retention": "P90D"}},
+ "register concrete derived without topicRef",
),
_validate_type_schema(
- "gts.x.test13.miss.event.v1~x.test13._.incomplete.v1~",
+ "gts.x.test13.h.miss.event.v1~x.test13._.incomplete.v1~",
False,
- "validate should fail - topicRef not resolved",
+ "validate should fail - topicRef unresolved on non-abstract host",
),
]
class TestCaseOp13_TraitsInvalid_WrongType(HttpRunner):
- """OP#13 - Traits: trait value violates trait schema type - fails"""
- config = Config("OP#13 - Trait Value Wrong Type").base_url(
- get_gts_base_url()
- )
+ """OP#13 - trait value has the wrong JSON type for the trait-type schema. Invalid."""
+
+ config = Config("OP#13 - Trait value wrong type").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.wtype.v1~",
+ _trait_event_meta("wtype"),
+ "register trait-type",
+ ),
_register(
- "gts://gts.x.test13.wtype.event.v1~",
+ "gts://gts.x.test13.h.wtype.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "maxRetries": {
- "type": "integer",
- "minimum": 0,
- "default": 3,
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.wtype.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register base with integer trait",
+ "register base host",
),
_register_derived(
- "gts://gts.x.test13.wtype.event.v1~x.test13._.bad_type.v1~",
- "gts://gts.x.test13.wtype.event.v1~",
- {
- "type": "object",
- "x-gts-traits": {
- "maxRetries": "not_a_number",
- "retention": "P90D",
- },
- },
- "register derived with wrong type for maxRetries",
+ "gts://gts.x.test13.h.wtype.event.v1~x.test13._.wrong_type.v1~",
+ "gts://gts.x.test13.h.wtype.event.v1~",
+ {"x-gts-traits": {"retention": 30}}, # should be string
+ "register derived with int instead of string for retention",
),
_validate_type_schema(
- "gts.x.test13.wtype.event.v1~x.test13._.bad_type.v1~",
+ "gts.x.test13.h.wtype.event.v1~x.test13._.wrong_type.v1~",
False,
- "validate should fail - maxRetries is not integer",
+ "validate should fail - retention is not a string",
),
]
class TestCaseOp13_TraitsInvalid_UnknownProperty(HttpRunner):
- """OP#13 - Traits: trait value includes unknown property.
+ """OP#13 - trait value provides a key not present in the effective trait-type.
- additionalProperties false - fails.
+ The trait-type has `additionalProperties: false`, so the unknown key is invalid.
"""
- config = Config("OP#13 - Unknown Trait Property").base_url(
- get_gts_base_url()
- )
+
+ config = Config("OP#13 - Unknown trait property").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.unk.event.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.unkn.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
+ "additionalProperties": False,
"properties": {
- "id": {"type": "string"},
+ "retention": {"type": "string", "default": "P30D"},
},
},
- "register base with closed traits-schema",
+ "register trait-type with additionalProperties=false",
),
- _register_derived(
- "gts://gts.x.test13.unk.event.v1~x.test13._.extra_trait.v1~",
- "gts://gts.x.test13.unk.event.v1~",
+ _register(
+ "gts://gts.x.test13.h.unkn.event.v1~",
{
"type": "object",
- "x-gts-traits": {
- "retention": "P90D",
- "unknownTrait": "some_value",
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.unkn.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register derived with unknown trait property",
+ "register base host",
+ ),
+ _register_derived(
+ "gts://gts.x.test13.h.unkn.event.v1~x.test13._.unknown_prop.v1~",
+ "gts://gts.x.test13.h.unkn.event.v1~",
+ {"x-gts-traits": {"retention": "P90D", "mysteryKey": "x"}},
+ "register derived with unknown trait key",
),
_validate_type_schema(
- "gts.x.test13.unk.event.v1~x.test13._.extra_trait.v1~",
+ "gts.x.test13.h.unkn.event.v1~x.test13._.unknown_prop.v1~",
False,
- "validate should fail - unknownTrait not in schema",
+ "validate should fail - mysteryKey is not in trait-type",
),
]
class TestCaseOp13_TraitsValid_PartialOverride(HttpRunner):
- """OP#13 - Traits: derived overrides one trait.
+ """OP#13 - descendant provides a concrete value for a field that the ancestor
+ only had as `default`. This is allowed (default is not a concrete assignment)."""
- Other uses default - passes.
- """
- config = Config("OP#13 - Partial Override With Defaults").base_url(
- get_gts_base_url()
- )
+ config = Config("OP#13 - Partial override of defaulted ancestor").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.part.v1~",
+ _trait_event_meta("part"),
+ "register trait-type (defaults present)",
+ ),
_register(
- "gts://gts.x.test13.part.event.v1~",
+ "gts://gts.x.test13.h.part.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "topicRef": {
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- "default": (
- "gts.x.core.events.topic.v1~"
- "x.core._.default.v1"
- ),
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.part.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register base with all defaults",
+ "register base host (no x-gts-traits — relies on defaults)",
),
_register_derived(
- "gts://gts.x.test13.part.event.v1~x.test13._.partial.v1~",
- "gts://gts.x.test13.part.event.v1~",
- {
- "type": "object",
- "x-gts-traits": {
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.test13._.custom.v1"
- ),
- },
- },
- "register derived overriding only topicRef",
+ "gts://gts.x.test13.h.part.event.v1~x.test13._.override_default.v1~",
+ "gts://gts.x.test13.h.part.event.v1~",
+ {"x-gts-traits": {"topicRef": TOPIC_REF_ORDERS}},
+ "register derived overriding the default topicRef with a concrete value",
),
_validate_type_schema(
- "gts.x.test13.part.event.v1~x.test13._.partial.v1~",
+ "gts.x.test13.h.part.event.v1~x.test13._.override_default.v1~",
True,
- "validate - topicRef overridden, retention uses default",
+ "validate - first concrete assignment wins; defaults aren't 'set'",
),
]
-class TestCaseOp13_TraitsValid_BothKeywordsInSameSchema(HttpRunner):
- """OP#13 - Traits: mid-level schema has both x-gts-traits-schema.
+class TestCaseOp13_TraitsValid_BaseConcreteSomeFields(HttpRunner):
+ """OP#13 - base host sets some trait values concretely; derived adds the rest.
- And x-gts-traits.
+ (Replaces the old TraitsValid_BothKeywordsInSameSchema test — in the new model
+ `x-gts-traits-schema` lives only on the base, but `x-gts-traits` can appear at
+ any level. The mid-level can both attach a derived trait-type AND set values.)
"""
- config = Config("OP#13 - Both Keywords Same Schema").base_url(
- get_gts_base_url()
- )
+
+ config = Config("OP#13 - Concrete values distributed across chain").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.both.event.v1~",
- {
- "type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "topicRef": {
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
- },
- "register base with traits-schema",
+ _register_trait_type(
+ "gts://gts.x.test13.t.distr.v1~",
+ _trait_event_meta("distr", with_topic_default=False, with_retention_default=False),
+ "register trait-type (no defaults)",
),
- _register_derived(
- "gts://gts.x.test13.both.event.v1~x.test13._.audit.v1~",
- "gts://gts.x.test13.both.event.v1~",
+ _register(
+ "gts://gts.x.test13.h.distr.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "auditRetention": {
- "type": "string",
- "default": "P365D",
- },
- },
- },
- "x-gts-traits": {
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.test13._.audit.v1"
- ),
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.distr.v1~",
+ "x-gts-traits": {"topicRef": TOPIC_REF_DEFAULT},
+ "x-gts-abstract": True,
+ "properties": {"id": {"type": "string"}},
},
- "register mid-level with both keywords",
- ),
- _validate_type_schema(
- "gts.x.test13.both.event.v1~x.test13._.audit.v1~",
- True,
- "validate mid-level - topicRef resolved, retention has default",
+ "register base host with topicRef set (abstract until retention is set)",
),
_register_derived(
- (
- "gts://gts.x.test13.both.event.v1~"
- "x.test13._.audit.v1~"
- "x.test13._.login_audit.v1~"
- ),
- "gts://gts.x.test13.both.event.v1~x.test13._.audit.v1~",
- {
- "type": "object",
- "x-gts-traits": {
- "auditRetention": "P730D",
- },
- },
- "register leaf resolving auditRetention",
+ "gts://gts.x.test13.h.distr.event.v1~x.test13._.complete.v1~",
+ "gts://gts.x.test13.h.distr.event.v1~",
+ {"x-gts-traits": {"retention": "P90D"}},
+ "register derived adding retention value",
),
_validate_type_schema(
- (
- "gts.x.test13.both.event.v1~"
- "x.test13._.audit.v1~"
- "x.test13._.login_audit.v1~"
- ),
+ "gts.x.test13.h.distr.event.v1~x.test13._.complete.v1~",
True,
- "validate leaf - all traits resolved across chain",
+ "validate - merged values resolve all fields",
),
]
class TestCaseOp13_TraitsInvalid_3Level_MissingInLeaf(HttpRunner):
- """OP#13 - Traits: 3-level chain.
-
- Leaf missing trait from mid-level schema - fails.
+ """OP#13 - 3-level chain; intermediate is abstract and underspecified;
+ leaf is concrete but also underspecified. Leaf is invalid.
"""
- config = Config("OP#13 - 3-Level Missing Trait In Leaf").base_url(
- get_gts_base_url()
- )
+
+ config = Config("OP#13 - 3-level chain missing field in leaf").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.l3miss.v1~",
+ _trait_event_meta("l3miss", with_topic_default=False, with_retention_default=False),
+ "register trait-type (no defaults)",
+ ),
_register(
- "gts://gts.x.test13.l3miss.event.v1~",
+ "gts://gts.x.test13.h.l3miss.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.l3miss.v1~",
+ "x-gts-abstract": True,
+ "properties": {"id": {"type": "string"}},
},
- "register base",
+ "register abstract base host",
),
_register_derived(
- "gts://gts.x.test13.l3miss.event.v1~x.test13._.mid.v1~",
- "gts://gts.x.test13.l3miss.event.v1~",
+ "gts://gts.x.test13.h.l3miss.event.v1~x.test13._.mid.v1~",
+ "gts://gts.x.test13.h.l3miss.event.v1~",
{
- "type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "priority": {
- "type": "string",
- "description": "No default - must be resolved",
- },
- },
- },
+ "x-gts-traits": {"retention": "P60D"},
+ "x-gts-abstract": True,
},
- "register mid-level adding priority trait (no default)",
+ "register abstract mid (sets retention only)",
),
_register_derived(
- (
- "gts://gts.x.test13.l3miss.event.v1~"
- "x.test13._.mid.v1~"
- "x.test13._.leaf_missing.v1~"
- ),
- "gts://gts.x.test13.l3miss.event.v1~x.test13._.mid.v1~",
- {
- "type": "object",
- "x-gts-traits": {
- "retention": "P90D",
- },
- },
- "register leaf missing priority trait",
+ "gts://gts.x.test13.h.l3miss.event.v1~x.test13._.mid.v1~x.test13._.leaf.v1~",
+ "gts://gts.x.test13.h.l3miss.event.v1~x.test13._.mid.v1~",
+ {},
+ "register concrete leaf without topicRef",
),
_validate_type_schema(
- (
- "gts.x.test13.l3miss.event.v1~"
- "x.test13._.mid.v1~"
- "x.test13._.leaf_missing.v1~"
- ),
+ "gts.x.test13.h.l3miss.event.v1~x.test13._.mid.v1~x.test13._.leaf.v1~",
False,
- "validate should fail - priority not resolved",
+ "validate should fail - topicRef unresolved at concrete leaf",
),
]
class TestCaseOp13_TraitsInvalid_OverrideInChain(HttpRunner):
- """OP#13 - Traits: descendant cannot override ancestor trait value."""
- config = Config("OP#13 - Override In Chain").base_url(get_gts_base_url())
+ """OP#13 - immutable-once-set: descendant cannot change ancestor's concrete trait value."""
+
+ config = Config("OP#13 - Override of concrete ancestor value").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.ovr.event.v1~",
- {
- "type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "retention": {
- "type": "string",
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
- },
- "register base",
+ _register_trait_type(
+ "gts://gts.x.test13.t.ovr.v1~",
+ _trait_event_meta("ovr"),
+ "register trait-type (defaults present)",
),
- _register_derived(
- "gts://gts.x.test13.ovr.event.v1~x.test13._.mid_ovr.v1~",
- "gts://gts.x.test13.ovr.event.v1~",
+ _register(
+ "gts://gts.x.test13.h.ovr.event.v1~",
{
"type": "object",
- "x-gts-traits": {
- "retention": "P30D",
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.ovr.v1~",
+ "x-gts-traits": {"retention": "P30D"},
+ "properties": {"id": {"type": "string"}},
},
- "register mid-level setting retention=P30D",
- ),
- _validate_type_schema(
- "gts.x.test13.ovr.event.v1~x.test13._.mid_ovr.v1~",
- True,
- "validate mid-level",
+ "register base host concretely setting retention=P30D",
),
_register_derived(
- (
- "gts://gts.x.test13.ovr.event.v1~"
- "x.test13._.mid_ovr.v1~"
- "x.test13._.leaf_ovr.v1~"
- ),
- "gts://gts.x.test13.ovr.event.v1~x.test13._.mid_ovr.v1~",
- {
- "type": "object",
- "x-gts-traits": {
- "retention": "P365D",
- },
- },
- "register leaf overriding retention=P365D",
+ "gts://gts.x.test13.h.ovr.event.v1~x.test13._.try_override.v1~",
+ "gts://gts.x.test13.h.ovr.event.v1~",
+ {"x-gts-traits": {"retention": "P90D"}},
+ "register derived attempting to override retention",
),
_validate_type_schema(
- (
- "gts.x.test13.ovr.event.v1~"
- "x.test13._.mid_ovr.v1~"
- "x.test13._.leaf_ovr.v1~"
- ),
+ "gts.x.test13.h.ovr.event.v1~x.test13._.try_override.v1~",
False,
- "validate should fail - trait override not allowed",
+ "validate should fail - immutable-once-set violated",
),
]
class TestCaseOp13_TraitsInvalid_OverrideTopicRef3Level(HttpRunner):
- """OP#13 - Traits: 3-level chain, leaf overrides topicRef.
+ """OP#13 - 3-level chain; mid sets topicRef; leaf tries to change it. Leaf invalid."""
- Mid-level sets topicRef to audit topic, leaf tries notification
- topic. Validation must fail (immutable-once-set).
- """
- config = Config(
- "OP#13 - Override TopicRef 3-Level"
- ).base_url(get_gts_base_url())
+ config = Config("OP#13 - 3-level override of topicRef").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.ovr3.v1~",
+ _trait_event_meta("ovr3"),
+ "register trait-type (defaults present)",
+ ),
_register(
- "gts://gts.x.test13.ovt.event.v1~",
+ "gts://gts.x.test13.h.ovr3.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "topicRef": {
- "type": "string",
- "x-gts-ref": (
- "gts.x.core.events.topic.v1~"
- ),
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.ovr3.v1~",
+ "x-gts-abstract": True,
+ "properties": {"id": {"type": "string"}},
},
- "register base with topicRef + retention traits",
+ "register abstract base",
),
_register_derived(
- (
- "gts://gts.x.test13.ovt.event.v1~"
- "x.test13._.audit_evt.v1~"
- ),
- "gts://gts.x.test13.ovt.event.v1~",
+ "gts://gts.x.test13.h.ovr3.event.v1~x.test13._.audit.v1~",
+ "gts://gts.x.test13.h.ovr3.event.v1~",
{
- "type": "object",
- "x-gts-traits": {
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.core._.audit.v1"
- ),
- },
+ "x-gts-traits": {"topicRef": TOPIC_REF_AUDIT},
+ "x-gts-abstract": True,
},
- "register mid-level setting topicRef=audit",
- ),
- _validate_type_schema(
- (
- "gts.x.test13.ovt.event.v1~"
- "x.test13._.audit_evt.v1~"
- ),
- True,
- "validate mid-level - topicRef set",
+ "register abstract mid concretely setting topicRef=audit",
),
_register_derived(
- (
- "gts://gts.x.test13.ovt.event.v1~"
- "x.test13._.audit_evt.v1~"
- "x.test13._.most_derived.v1~"
- ),
- (
- "gts://gts.x.test13.ovt.event.v1~"
- "x.test13._.audit_evt.v1~"
- ),
- {
- "type": "object",
- "x-gts-traits": {
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.core._.notification.v1"
- ),
- },
- },
- "register leaf overriding topicRef=notification",
+ "gts://gts.x.test13.h.ovr3.event.v1~x.test13._.audit.v1~x.test13._.notif_leaf.v1~",
+ "gts://gts.x.test13.h.ovr3.event.v1~x.test13._.audit.v1~",
+ {"x-gts-traits": {"topicRef": TOPIC_REF_NOTIF}},
+ "register leaf attempting to change topicRef",
),
_validate_type_schema(
- (
- "gts.x.test13.ovt.event.v1~"
- "x.test13._.audit_evt.v1~"
- "x.test13._.most_derived.v1~"
- ),
+ "gts.x.test13.h.ovr3.event.v1~x.test13._.audit.v1~x.test13._.notif_leaf.v1~",
False,
- "validate should fail - topicRef override not allowed",
+ "validate should fail - leaf overrides mid's concrete topicRef",
),
]
class TestCaseOp13_TraitsInvalid_ChangeDefaultInMid(HttpRunner):
- """OP#13 - Traits: mid-level changes default set by base.
+ """OP#13 - changing a `default` declared in an ancestor trait-type is forbidden.
- Base sets retention default=P30D. Mid-level redeclares
- retention default=P90D. Validation must fail - defaults
- set by ancestor are immutable.
+ Expressed via parallel trait-type derivation: a derived trait-type that redeclares
+ a property with a different `default` is invalid (OP#12 on the trait-type chain).
"""
- config = Config(
- "OP#13 - Change Default In Mid-Level"
- ).base_url(get_gts_base_url())
+
+ config = Config("OP#13 - Default override in derived trait-type").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.chdfl.event.v1~",
- {
- "type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- "topicRef": {
- "type": "string",
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
- },
- "register base with retention default=P30D",
+ _register_trait_type(
+ "gts://gts.x.test13.t.chgdfl.v1~",
+ _trait_event_meta("chgdfl"),
+ "register base trait-type (retention default=P30D)",
),
- _register_derived(
- (
- "gts://gts.x.test13.chdfl.event.v1~"
- "x.test13._.chdfl_mid.v1~"
- ),
- "gts://gts.x.test13.chdfl.event.v1~",
+ # derived trait-type changing the default for retention to P365D — invalid
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.chgdfl.v1~x.test13._.bad.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "retention": {
- "type": "string",
- "default": "P90D",
- },
- },
- },
- "x-gts-traits": {
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.test13._.orders.v1"
- ),
+ "allOf": [{"$$ref": "gts://gts.x.test13.t.chgdfl.v1~"}],
+ "properties": {
+ "retention": {"type": "string", "default": "P365D"},
},
},
- "register mid changing retention default to P90D",
+ "register derived trait-type changing retention default",
),
_validate_type_schema(
- (
- "gts.x.test13.chdfl.event.v1~"
- "x.test13._.chdfl_mid.v1~"
- ),
+ "gts.x.test13.t.chgdfl.v1~x.test13._.bad.v1~",
False,
- "validate should fail - default override not allowed",
+ "validate should fail - default redeclared with different value",
),
]
class TestCaseOp13_TraitsInvalid_ConstraintViolation(HttpRunner):
- """OP#13 - Traits: trait value violates enum constraint.
+ """OP#13 - trait value violates a JSON Schema constraint declared in the trait-type."""
- In trait schema - fails.
- """
- config = Config("OP#13 - Trait Constraint Violation").base_url(
- get_gts_base_url()
- )
+ config = Config("OP#13 - Trait value constraint violation").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.enum.event.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.cstr.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "priority": {
- "type": "string",
- "enum": ["low", "medium", "high", "critical"],
- "default": "medium",
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
"properties": {
- "id": {"type": "string"},
+ "retentionDays": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 3650,
+ "default": 30,
+ },
},
},
- "register base with enum-constrained trait",
+ "register trait-type (retentionDays 1..3650)",
),
- _register_derived(
- "gts://gts.x.test13.enum.event.v1~x.test13._.bad_enum.v1~",
- "gts://gts.x.test13.enum.event.v1~",
+ _register(
+ "gts://gts.x.test13.h.cstr.event.v1~",
{
"type": "object",
- "x-gts-traits": {
- "priority": "ultra_high",
- "retention": "P90D",
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.cstr.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register derived with invalid enum value",
+ "register base host",
+ ),
+ _register_derived(
+ "gts://gts.x.test13.h.cstr.event.v1~x.test13._.too_long.v1~",
+ "gts://gts.x.test13.h.cstr.event.v1~",
+ {"x-gts-traits": {"retentionDays": 99999}},
+ "register derived with retentionDays out of range",
),
_validate_type_schema(
- "gts.x.test13.enum.event.v1~x.test13._.bad_enum.v1~",
+ "gts.x.test13.h.cstr.event.v1~x.test13._.too_long.v1~",
False,
- "validate should fail - priority not in enum",
+ "validate should fail - 99999 exceeds maximum",
),
]
-class TestCaseOp13_TraitsValid_ValidateEntity(HttpRunner):
- """OP#13 - Traits: validate-entity endpoint also checks traits"""
- config = Config("OP#13 - Validate Entity With Traits").base_url(
- get_gts_base_url()
- )
+class TestCaseOp13_TraitsInvalid_MinimumViolation(HttpRunner):
+ """OP#13 - integer trait value below the minimum constraint."""
+
+ config = Config("OP#13 - Minimum violation").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.ent.event.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.mn.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "topicRef": {
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
"properties": {
- "id": {"type": "string"},
+ "retentionDays": {
+ "type": "integer",
+ "minimum": 7,
+ "default": 30,
+ },
},
},
- "register base",
+ "register trait-type",
),
- _register_derived(
- "gts://gts.x.test13.ent.event.v1~x.test13._.good_ent.v1~",
- "gts://gts.x.test13.ent.event.v1~",
+ _register(
+ "gts://gts.x.test13.h.mn.event.v1~",
{
"type": "object",
- "x-gts-traits": {
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.test13._.orders.v1"
- ),
- "retention": "P90D",
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.mn.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register derived with traits",
+ "register base host",
),
- _validate_entity(
- "gts.x.test13.ent.event.v1~x.test13._.good_ent.v1~",
- True,
- "validate-entity should pass",
+ _register_derived(
+ "gts://gts.x.test13.h.mn.event.v1~x.test13._.too_short.v1~",
+ "gts://gts.x.test13.h.mn.event.v1~",
+ {"x-gts-traits": {"retentionDays": 1}},
+ "register derived with retentionDays below minimum",
+ ),
+ _validate_type_schema(
+ "gts.x.test13.h.mn.event.v1~x.test13._.too_short.v1~",
+ False,
+ "validate should fail - 1 < minimum 7",
),
]
-class TestCaseOp13_TraitsInvalid_ValidateEntity_MissingTrait(HttpRunner):
- """OP#13 - Traits: validate-entity catches missing trait"""
- config = Config("OP#13 - Validate Entity Missing Trait").base_url(
- get_gts_base_url()
- )
+# ---------------------------------------------------------------------------
+# Trait-type derivation via parallel inheritance (replaces "ref-based" /
+# "narrowing" / "AP-blocks-extension" composition tests of the old model)
+# ---------------------------------------------------------------------------
- def test_start(self):
+
+class TestCaseOp13_TraitTypeDerivation_Narrowing_Valid(HttpRunner):
+ """OP#13 - derived trait-type may narrow constraints (compatible refinement).
+
+ Replaces TraitsValid_NarrowingInDerived from the old model.
+ """
+
+ config = Config("OP#13 - Trait-type narrowing accepted").base_url(get_gts_base_url())
+
+ def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.entm.event.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.narrow.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "topicRef": {
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- },
- "retention": {
- "type": "string",
- },
- },
- },
- "required": ["id"],
"properties": {
- "id": {"type": "string"},
+ "retentionDays": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 3650,
+ "default": 30,
+ },
},
},
- "register base (no defaults)",
+ "register base trait-type",
),
- _register_derived(
- "gts://gts.x.test13.entm.event.v1~x.test13._.bad_ent.v1~",
- "gts://gts.x.test13.entm.event.v1~",
+ # derived trait-type tightening the range
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.narrow.v1~x.test13._.shortlived.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits": {
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.test13._.orders.v1"
- ),
+ "allOf": [{"$$ref": "gts://gts.x.test13.t.narrow.v1~"}],
+ "properties": {
+ "retentionDays": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 30,
+ "default": 30,
+ },
},
},
- "register derived missing retention",
+ "register derived trait-type narrowing maximum to 30",
),
- _validate_entity(
- "gts.x.test13.entm.event.v1~x.test13._.bad_ent.v1~",
- False,
- "validate-entity should fail - retention not resolved",
+ _validate_type_schema(
+ "gts.x.test13.t.narrow.v1~x.test13._.shortlived.v1~",
+ True,
+ "validate - narrowing is a valid OP#12 derivation",
),
]
-class TestCaseOp13_TraitsValid_BaseSchemaNoTraits(HttpRunner):
- """OP#13 - Traits: base has no traits-schema.
+class TestCaseOp13_TraitTypeDerivation_LooseningViolation(HttpRunner):
+ """OP#13 - derived trait-type cannot loosen a constraint of its parent.
- Derived has no traits - passes.
+ Replaces TraitsInvalid_NarrowingViolation.
"""
- config = Config("OP#13 - No Traits At All").base_url(get_gts_base_url())
+
+ config = Config("OP#13 - Trait-type loosening rejected").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.notr.event.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.loose.v1~",
{
"type": "object",
- "required": ["id"],
"properties": {
- "id": {"type": "string"},
+ "retentionDays": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 30,
+ "default": 30,
+ },
},
},
- "register base without traits-schema",
+ "register base trait-type (max=30)",
),
- _register_derived(
- "gts://gts.x.test13.notr.event.v1~x.test13._.plain.v1~",
- "gts://gts.x.test13.notr.event.v1~",
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.loose.v1~x.test13._.longer.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
+ "allOf": [{"$$ref": "gts://gts.x.test13.t.loose.v1~"}],
"properties": {
- "extra": {"type": "string"},
+ "retentionDays": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 3650,
+ "default": 30,
+ },
},
},
- "register derived without traits",
+ "register derived trait-type loosening max to 3650",
),
_validate_type_schema(
- "gts.x.test13.notr.event.v1~x.test13._.plain.v1~",
- True,
- "validate - no traits to check, should pass",
+ "gts.x.test13.t.loose.v1~x.test13._.longer.v1~",
+ False,
+ "validate should fail - loosening violates OP#12",
),
]
-class TestCaseOp13_TraitsInvalid_MinimumViolation(HttpRunner):
- """OP#13 - Traits: integer trait violates minimum constraint - fails"""
- config = Config("OP#13 - Trait Minimum Violation").base_url(
- get_gts_base_url()
- )
+class TestCaseOp13_TraitTypeDerivation_ExtendField_Valid(HttpRunner):
+ """OP#13 - derived trait-type can add new fields (replaces RefBasedTraitSchema).
+
+ A derived trait-type adds an `auditRetention` field; host hierarchy resolves it.
+ """
+
+ config = Config("OP#13 - Trait-type extends with new field").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.minv.event.v1~",
- {
- "type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "maxRetries": {
- "type": "integer",
- "minimum": 0,
- "maximum": 10,
- "default": 3,
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
- },
- "register base with integer trait with min/max",
+ _register_trait_type(
+ "gts://gts.x.test13.t.ext.v1~",
+ _trait_event_meta("ext"),
+ "register base trait-type",
),
- _register_derived(
- "gts://gts.x.test13.minv.event.v1~x.test13._.neg_retry.v1~",
- "gts://gts.x.test13.minv.event.v1~",
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.ext.v1~x.test13._.audited.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits": {
- "maxRetries": -1,
+ "allOf": [{"$$ref": "gts://gts.x.test13.t.ext.v1~"}],
+ "properties": {
+ "auditRetention": {"type": "string", "default": "P365D"},
},
},
- "register derived with negative maxRetries",
+ "register derived trait-type adding auditRetention",
),
_validate_type_schema(
- "gts.x.test13.minv.event.v1~x.test13._.neg_retry.v1~",
- False,
- "validate should fail - maxRetries below minimum",
+ "gts.x.test13.t.ext.v1~x.test13._.audited.v1~",
+ True,
+ "validate derived trait-type",
),
]
-class TestCaseOp13_TraitsValid_RefBasedTraitSchema(HttpRunner):
- """OP#13 - Traits: base uses $ref to standalone reusable trait schemas"""
- config = Config(
- "OP#13 - Ref-Based Trait Schema"
- ).base_url(get_gts_base_url())
+class TestCaseOp13_TraitTypeDerivation_DefaultsInherited_Valid(HttpRunner):
+ """OP#13 - defaults declared in a base trait-type are visible through derivation.
+
+ Replaces TraitsValid_DefaultsFromRefSchema.
+ """
+
+ config = Config("OP#13 - Defaults from base trait-type cover derived").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- # Register standalone reusable trait schema: RetentionTrait
- _register(
- "gts://gts.x.test13.traits.retention.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.dflinh.v1~",
+ _trait_event_meta("dflinh"),
+ "register base trait-type (defaults)",
+ ),
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.dflinh.v1~x.test13._.audited.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
+ "allOf": [{"$$ref": "gts://gts.x.test13.t.dflinh.v1~"}],
"properties": {
- "retention": {
- "description": "ISO 8601 retention duration.",
- "type": "string",
- "default": "P30D",
- },
+ "auditRetention": {"type": "string", "default": "P365D"},
},
},
- "register standalone RetentionTrait schema",
+ "register derived trait-type adding auditRetention (default)",
),
- # Register standalone reusable trait schema: TopicTrait
_register(
- "gts://gts.x.test13.traits.topic.v1~",
+ "gts://gts.x.test13.h.dflinh.event.v1~",
{
"type": "object",
- "properties": {
- "topicRef": {
- "description": "Topic reference.",
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- },
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.dflinh.v1~x.test13._.audited.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register standalone TopicTrait schema",
+ "register host attaching the derived trait-type",
),
- # Register base that composes traits via $ref + allOf
- _register(
- "gts://gts.x.test13.ref.event.v1~",
+ _validate_type_schema(
+ "gts.x.test13.h.dflinh.event.v1~",
+ True,
+ "validate - all defaults inherited, host is trait-complete",
+ ),
+ ]
+
+
+class TestCaseOp13_TraitsInvalid_RefBasedMissingTrait(HttpRunner):
+ """OP#13 - trait-type required field with no default; derived host doesn't set it.
+
+ Replaces TraitsInvalid_RefBasedMissingTrait under the new model.
+ """
+
+ config = Config("OP#13 - Required field in trait-type unresolved").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.reqf.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "allOf": [
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".traits.retention.v1~"
- ),
- },
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".traits.topic.v1~"
- ),
- },
- ],
- },
- "required": ["id"],
+ "required": ["topicRef"],
"properties": {
- "id": {"type": "string"},
+ "topicRef": {"type": "string"},
+ "retention": {"type": "string", "default": "P30D"},
},
},
- "register base with $ref trait schemas",
+ "register trait-type with required topicRef and no default",
),
- # Derived provides all trait values
- _register_derived(
- (
- "gts://gts.x.test13.ref.event.v1~"
- "x.test13._.ref_leaf.v1~"
- ),
- "gts://gts.x.test13.ref.event.v1~",
+ _register(
+ "gts://gts.x.test13.h.reqf.event.v1~",
{
"type": "object",
- "x-gts-traits": {
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.test13._.orders.v1"
- ),
- "retention": "P90D",
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.reqf.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register derived resolving $ref traits",
+ "register base host (no traits provided)",
),
_validate_type_schema(
- (
- "gts.x.test13.ref.event.v1~"
- "x.test13._.ref_leaf.v1~"
- ),
- True,
- "validate - $ref traits resolved",
+ "gts.x.test13.h.reqf.event.v1~",
+ False,
+ "validate should fail - concrete host with unresolved required field",
),
]
-class TestCaseOp13_TraitsInvalid_RefBasedMissingTrait(HttpRunner):
- """OP#13 - Traits: $ref trait schema, derived missing required trait"""
- config = Config(
- "OP#13 - Ref-Based Missing Trait"
- ).base_url(get_gts_base_url())
+# ---------------------------------------------------------------------------
+# Parallel derivation (host-type and trait-type chains run in parallel)
+# ---------------------------------------------------------------------------
+
+
+class TestCaseOp13_ParallelDerivation_DescendantTraitTypeDerivedFromAncestor_Ok(HttpRunner):
+ """OP#13 §9.7.4 - descendant host's trait-type is derived from ancestor's. Valid."""
+
+ config = Config("OP#13 - Parallel derivation OK").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.traits.retention.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.pard.base.v1~",
+ _trait_event_meta("pardbase"),
+ "register base trait-type",
+ ),
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.pard.base.v1~x.test13._.audit_meta.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
+ "allOf": [{"$$ref": "gts://gts.x.test13.t.pard.base.v1~"}],
"properties": {
- "retention": {
- "description": (
- "ISO 8601 retention duration."
- ),
- "type": "string",
- "default": "P30D",
- },
+ "auditRetention": {"type": "string", "default": "P365D"},
},
},
- "register standalone RetentionTrait schema",
+ "register derived trait-type adding auditRetention",
),
_register(
- "gts://gts.x.test13.traits.topic.v1~",
+ "gts://gts.x.test13.h.pard.event.v1~",
{
"type": "object",
- "properties": {
- "topicRef": {
- "description": "Topic reference.",
- "type": "string",
- "x-gts-ref": (
- "gts.x.core.events.topic.v1~"
- ),
- },
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.pard.base.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register standalone TopicTrait schema",
+ "register host base attaching base trait-type",
+ ),
+ _register_derived(
+ "gts://gts.x.test13.h.pard.event.v1~x.test13._.audit_evt.v1~",
+ "gts://gts.x.test13.h.pard.event.v1~",
+ {
+ "x-gts-traits-schema": "gts://gts.x.test13.t.pard.base.v1~x.test13._.audit_meta.v1~",
+ },
+ "register derived host attaching derived trait-type (parallel chain)",
+ ),
+ _validate_type_schema(
+ "gts.x.test13.h.pard.event.v1~x.test13._.audit_evt.v1~",
+ True,
+ "validate - parallel derivation is valid",
+ ),
+ ]
+
+
+class TestCaseOp13_ParallelDerivation_DescendantTraitTypeNotDerived_Rejected(HttpRunner):
+ """OP#13 §9.7.4 - descendant attaches a trait-type that is NOT derived from
+ the ancestor's trait-type. Parallel-derivation rule violated.
+ """
+
+ config = Config("OP#13 - Parallel derivation: unrelated trait-type rejected").base_url(
+ get_gts_base_url()
+ )
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.parX.familyA.v1~",
+ _trait_event_meta("parXa"),
+ "register trait-type family A",
+ ),
+ _register_trait_type(
+ "gts://gts.x.test13.t.parX.familyB.v1~",
+ _trait_event_meta("parXb"),
+ "register trait-type family B (unrelated to A)",
),
_register(
- "gts://gts.x.test13.refm.event.v1~",
+ "gts://gts.x.test13.h.parX.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "allOf": [
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".traits.retention.v1~"
- ),
- },
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".traits.topic.v1~"
- ),
- },
- ],
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.parX.familyA.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register base with $ref trait schemas",
+ "register host base attaching trait-type family A",
),
- # Derived only provides retention, missing topicRef
_register_derived(
- (
- "gts://gts.x.test13.refm.event.v1~"
- "x.test13._.ref_incomplete.v1~"
- ),
- "gts://gts.x.test13.refm.event.v1~",
+ "gts://gts.x.test13.h.parX.event.v1~x.test13._.swap.v1~",
+ "gts://gts.x.test13.h.parX.event.v1~",
{
- "type": "object",
- "x-gts-traits": {
- "retention": "P90D",
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.parX.familyB.v1~",
},
- "register derived missing topicRef from $ref trait",
+ "register derived host attaching unrelated trait-type family B",
),
_validate_type_schema(
- (
- "gts.x.test13.refm.event.v1~"
- "x.test13._.ref_incomplete.v1~"
- ),
+ "gts.x.test13.h.parX.event.v1~x.test13._.swap.v1~",
False,
- "validate should fail - topicRef not resolved",
+ "validate should fail - trait-type not in parent's chain",
),
]
-class TestCaseOp13_TraitsValid_NarrowingInDerived(HttpRunner):
- """OP#13 - Traits: derived narrows trait schema (adds constraints)"""
- config = Config(
- "OP#13 - Trait Schema Narrowing"
- ).base_url(get_gts_base_url())
+class TestCaseOp13_ParallelDerivation_DescendantOverridesDefaulted_Ok(HttpRunner):
+ """OP#13 - ancestor relied on `default`; descendant provides a concrete value.
+
+ This is allowed (default is not a concrete assignment).
+ """
+
+ config = Config("OP#13 - Default override is first concrete assignment").base_url(
+ get_gts_base_url()
+ )
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.parovr.v1~",
+ _trait_event_meta("parovr"),
+ "register trait-type (defaults)",
+ ),
_register(
- "gts://gts.x.test13.narrow.event.v1~",
+ "gts://gts.x.test13.h.parovr.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "priority": {
- "type": "string",
- "description": "Processing priority.",
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.parovr.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register base with open priority trait",
+ "register base host (no traits set; defaults apply)",
),
- # Mid-level narrows priority to enum
_register_derived(
- (
- "gts://gts.x.test13.narrow.event.v1~"
- "x.test13._.mid_narrow.v1~"
- ),
- "gts://gts.x.test13.narrow.event.v1~",
- {
- "type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "priority": {
- "type": "string",
- "enum": [
- "low", "medium",
- "high", "critical",
- ],
- },
- },
- },
- "x-gts-traits": {
- "priority": "high",
- },
- },
- "register mid-level narrowing priority to enum",
+ "gts://gts.x.test13.h.parovr.event.v1~x.test13._.concrete.v1~",
+ "gts://gts.x.test13.h.parovr.event.v1~",
+ {"x-gts-traits": {"retention": "P365D"}},
+ "register derived providing concrete retention",
),
_validate_type_schema(
- (
- "gts.x.test13.narrow.event.v1~"
- "x.test13._.mid_narrow.v1~"
- ),
+ "gts.x.test13.h.parovr.event.v1~x.test13._.concrete.v1~",
True,
- "validate - narrowed trait with valid value",
+ "validate - defaults are not 'set'; descendant may assign",
),
- # Leaf provides value within narrowed enum
- _register_derived(
- (
- "gts://gts.x.test13.narrow.event.v1~"
- "x.test13._.mid_narrow.v1~"
- "x.test13._.leaf_narrow.v1~"
- ),
- (
- "gts://gts.x.test13.narrow.event.v1~"
- "x.test13._.mid_narrow.v1~"
- ),
+ ]
+
+
+# ---------------------------------------------------------------------------
+# Abstract / concrete trait completeness (validity-by-definition rule)
+# ---------------------------------------------------------------------------
+
+
+class TestCaseOp13_AbstractHost_RequiredTraitUnresolved_Ok(HttpRunner):
+ """OP#13 §9.7.5 - x-gts-abstract:true host is exempt from completeness checks."""
+
+ config = Config("OP#13 - Abstract host with unresolved traits OK").base_url(
+ get_gts_base_url()
+ )
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.absok.v1~",
{
"type": "object",
- "x-gts-traits": {
- "priority": "critical",
+ "required": ["topicRef"],
+ "properties": {
+ "topicRef": {"type": "string"},
},
},
- "register leaf with valid narrowed priority",
+ "register trait-type with required topicRef (no default)",
+ ),
+ _register(
+ "gts://gts.x.test13.h.absok.event.v1~",
+ {
+ "type": "object",
+ "x-gts-traits-schema": "gts://gts.x.test13.t.absok.v1~",
+ "x-gts-abstract": True,
+ "properties": {"id": {"type": "string"}},
+ },
+ "register abstract base host (no x-gts-traits)",
),
_validate_type_schema(
- (
- "gts.x.test13.narrow.event.v1~"
- "x.test13._.mid_narrow.v1~"
- "x.test13._.leaf_narrow.v1~"
- ),
+ "gts.x.test13.h.absok.event.v1~",
True,
- "validate leaf - priority within narrowed enum",
+ "validate - abstract types are exempt from completeness",
),
]
-class TestCaseOp13_TraitsInvalid_NarrowingViolation(HttpRunner):
- """OP#13 - Traits: leaf value violates narrowed enum from mid-level"""
- config = Config(
- "OP#13 - Narrowing Violation"
- ).base_url(get_gts_base_url())
+class TestCaseOp13_AbstractToConcrete_DescendantResolves_Ok(HttpRunner):
+ """OP#13 - abstract base with gaps + concrete descendant that closes them."""
+
+ config = Config("OP#13 - Concrete descendant resolves abstract gaps").base_url(
+ get_gts_base_url()
+ )
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.nv.event.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.a2c.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "priority": {
- "type": "string",
- },
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
+ "required": ["topicRef"],
"properties": {
- "id": {"type": "string"},
+ "topicRef": {"type": "string"},
+ "retention": {"type": "string", "default": "P30D"},
},
},
- "register base",
+ "register trait-type (topicRef required, no default)",
),
- _register_derived(
- (
- "gts://gts.x.test13.nv.event.v1~"
- "x.test13._.mid_nv.v1~"
- ),
- "gts://gts.x.test13.nv.event.v1~",
+ _register(
+ "gts://gts.x.test13.h.a2c.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "priority": {
- "type": "string",
- "enum": [
- "low", "medium",
- "high", "critical",
- ],
- },
- },
- },
- "x-gts-traits": {
- "priority": "high",
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.a2c.v1~",
+ "x-gts-abstract": True,
+ "properties": {"id": {"type": "string"}},
},
- "register mid-level narrowing priority",
+ "register abstract base host",
),
_register_derived(
- (
- "gts://gts.x.test13.nv.event.v1~"
- "x.test13._.mid_nv.v1~"
- "x.test13._.leaf_bad_nv.v1~"
- ),
- (
- "gts://gts.x.test13.nv.event.v1~"
- "x.test13._.mid_nv.v1~"
- ),
- {
- "type": "object",
- "x-gts-traits": {
- "priority": "ultra_high",
- },
- },
- "register leaf with value outside narrowed enum",
+ "gts://gts.x.test13.h.a2c.event.v1~x.test13._.concrete.v1~",
+ "gts://gts.x.test13.h.a2c.event.v1~",
+ {"x-gts-traits": {"topicRef": TOPIC_REF_ORDERS}},
+ "register concrete derived providing topicRef",
),
_validate_type_schema(
- (
- "gts.x.test13.nv.event.v1~"
- "x.test13._.mid_nv.v1~"
- "x.test13._.leaf_bad_nv.v1~"
- ),
- False,
- "validate should fail - priority not in enum",
+ "gts.x.test13.h.a2c.event.v1~x.test13._.concrete.v1~",
+ True,
+ "validate - concrete descendant resolves required field",
),
]
-class TestCaseOp13_TraitsValid_DefaultsFromRefSchema(HttpRunner):
- """OP#13 - Traits: defaults from $ref trait schema fill values"""
- config = Config(
- "OP#13 - Defaults From Ref Schema"
- ).base_url(get_gts_base_url())
+class TestCaseOp13_AbstractToConcrete_DescendantDoesNotResolve_Rejected(HttpRunner):
+ """OP#13 - abstract base with gaps; concrete descendant ALSO leaves gaps. Invalid."""
+
+ config = Config("OP#13 - Concrete descendant leaves required gap").base_url(
+ get_gts_base_url()
+ )
def test_start(self):
super().test_start()
teststeps = [
- # Use the standalone RetentionTrait (default P30D)
- # and TopicTrait (no default) registered earlier
- _register(
- "gts://gts.x.test13.refd.event.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.a2cbad.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "allOf": [
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".traits.retention.v1~"
- ),
- },
- ],
- },
- "required": ["id"],
+ "required": ["topicRef"],
"properties": {
- "id": {"type": "string"},
+ "topicRef": {"type": "string"},
+ "retention": {"type": "string", "default": "P30D"},
},
},
- "register base with $ref retention trait only",
+ "register trait-type (topicRef required, no default)",
),
- # Derived provides no traits - retention default fills
- _register_derived(
- (
- "gts://gts.x.test13.refd.event.v1~"
- "x.test13._.default_ref.v1~"
- ),
- "gts://gts.x.test13.refd.event.v1~",
+ _register(
+ "gts://gts.x.test13.h.a2cbad.event.v1~",
{
"type": "object",
+ "x-gts-traits-schema": "gts://gts.x.test13.t.a2cbad.v1~",
+ "x-gts-abstract": True,
+ "properties": {"id": {"type": "string"}},
},
- "register derived with no traits (rely on $ref default)",
+ "register abstract base host",
+ ),
+ _register_derived(
+ "gts://gts.x.test13.h.a2cbad.event.v1~x.test13._.still_open.v1~",
+ "gts://gts.x.test13.h.a2cbad.event.v1~",
+ {},
+ "register concrete derived without filling topicRef",
),
_validate_type_schema(
- (
- "gts.x.test13.refd.event.v1~"
- "x.test13._.default_ref.v1~"
- ),
- True,
- "validate - retention default from $ref schema fills",
+ "gts.x.test13.h.a2cbad.event.v1~x.test13._.still_open.v1~",
+ False,
+ "validate should fail - non-abstract concrete host has unresolved required",
),
]
-class TestCaseOp13_TraitsInvalid_APBlocksExtension(HttpRunner):
- """OP#13 - Traits: base additionalProperties=false blocks extension."""
- config = Config(
- "OP#13 - Traits additionalProperties Blocks Extension"
- ).base_url(get_gts_base_url())
+# ---------------------------------------------------------------------------
+# MAJOR version pinning of trait-types
+# ---------------------------------------------------------------------------
+
+
+class TestCaseOp13_MajorVersionPin_V1ToV2_Rejected(HttpRunner):
+ """OP#13 §9.7.6 - host pins MAJOR v1 of trait-type; descendant trying to attach v2
+ is a different family, not a valid parallel-derivation chain. Rejected.
+ """
+
+ config = Config("OP#13 - MAJOR version switch rejected").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.major.v1~",
+ _trait_event_meta("majorV1"),
+ "register trait-type v1",
+ ),
+ _register_trait_type(
+ "gts://gts.x.test13.t.major.v2~",
+ _trait_event_meta("majorV2"),
+ "register trait-type v2 (separate family)",
+ ),
_register(
- "gts://gts.x.test13.ap.event.v1~",
+ "gts://gts.x.test13.h.major.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "retention": {"type": "string"},
- },
- },
- "required": ["id"],
+ "x-gts-traits-schema": "gts://gts.x.test13.t.major.v1~",
"properties": {"id": {"type": "string"}},
},
- "register base with traits-schema additionalProperties=false",
+ "register host attaching trait-type v1",
),
_register_derived(
- "gts://gts.x.test13.ap.event.v1~x.test13._.ap_mid.v1~",
- "gts://gts.x.test13.ap.event.v1~",
+ "gts://gts.x.test13.h.major.event.v1~x.test13._.v2_attempt.v1~",
+ "gts://gts.x.test13.h.major.event.v1~",
{
- "type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "topicRef": {
- "type": "string",
- "x-gts-ref": "gts.x.core.events.topic.v1~",
- },
- },
- },
- "x-gts-traits": {
- "retention": "P30D",
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.test13._.orders.v1"
- ),
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.major.v2~",
},
- "register mid-level that extends trait schema with topicRef",
+ "register derived host attaching v2 trait-type",
),
_validate_type_schema(
- "gts.x.test13.ap.event.v1~x.test13._.ap_mid.v1~",
+ "gts.x.test13.h.major.event.v1~x.test13._.v2_attempt.v1~",
False,
- (
- "validate should fail - base additionalProperties=false "
- "blocks topicRef"
- ),
+ "validate should fail - v2 is not derived from v1",
),
]
-class TestCaseOp13_TraitsInvalid_DerivedHasTraitsButNoTraitSchema(HttpRunner):
- """OP#13 - Traits: derived provides x-gts-traits.
+# ---------------------------------------------------------------------------
+# Validation via /validate-entity (instance-side)
+# ---------------------------------------------------------------------------
- No x-gts-traits-schema exists.
- """
- config = Config(
- "OP#13 - Derived Traits Without Trait Schema"
- ).base_url(get_gts_base_url())
+
+class TestCaseOp13_TraitsValid_ValidateEntity(HttpRunner):
+ """OP#13 - validate a registered host type schema via /validate-entity."""
+
+ config = Config("OP#13 - validate-entity passes for valid host").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.vent.v1~",
+ _trait_event_meta("vent"),
+ "register trait-type",
+ ),
_register(
- "gts://gts.x.test13.nt0.event.v1~",
+ "gts://gts.x.test13.h.vent.event.v1~",
{
"type": "object",
- "required": ["id"],
+ "x-gts-traits-schema": "gts://gts.x.test13.t.vent.v1~",
"properties": {"id": {"type": "string"}},
},
- "register base without traits-schema",
+ "register base host",
),
_register_derived(
- (
- "gts://gts.x.test13.nt0.event.v1~"
- "x.test13._.derived_has_traits.v1~"
- ),
- "gts://gts.x.test13.nt0.event.v1~",
+ "gts://gts.x.test13.h.vent.event.v1~x.test13._.complete.v1~",
+ "gts://gts.x.test13.h.vent.event.v1~",
+ {"x-gts-traits": {"topicRef": TOPIC_REF_ORDERS, "retention": "P90D"}},
+ "register complete derived host",
+ ),
+ _validate_entity(
+ "gts.x.test13.h.vent.event.v1~x.test13._.complete.v1~",
+ True,
+ "validate-entity should pass for trait-complete host schema",
+ ),
+ ]
+
+
+class TestCaseOp13_TraitsInvalid_ValidateEntity_MissingTrait(HttpRunner):
+ """OP#13 - validate-entity fails for a host schema with unresolved required trait."""
+
+ config = Config("OP#13 - validate-entity fails for incomplete host").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.ventm.v1~",
{
"type": "object",
- "x-gts-traits": {"retention": "P30D"},
+ "required": ["topicRef"],
+ "properties": {
+ "topicRef": {"type": "string"},
+ },
},
- "register derived with x-gts-traits but no traits-schema",
+ "register trait-type (topicRef required, no default)",
),
- _validate_type_schema(
- "gts.x.test13.nt0.event.v1~x.test13._.derived_has_traits.v1~",
+ _register(
+ "gts://gts.x.test13.h.ventm.event.v1~",
+ {
+ "type": "object",
+ "x-gts-traits-schema": "gts://gts.x.test13.t.ventm.v1~",
+ "properties": {"id": {"type": "string"}},
+ },
+ "register base host (concrete, incomplete)",
+ ),
+ _validate_entity(
+ "gts.x.test13.h.ventm.event.v1~",
False,
- "validate should fail - trait values have no trait schema",
+ "validate-entity should fail - unresolved required trait field",
),
]
-class TestCaseOp13_TraitsInvalid_BaseHasTraitsButNoTraitSchema(HttpRunner):
- """OP#13 - Traits: base provides x-gts-traits.
+# ---------------------------------------------------------------------------
+# Edge cases — base schema without traits, instance with trait keywords, etc.
+# ---------------------------------------------------------------------------
- No x-gts-traits-schema exists.
- """
- config = Config(
- "OP#13 - Base Traits Without Trait Schema"
- ).base_url(get_gts_base_url())
+
+class TestCaseOp13_TraitsValid_BaseSchemaNoTraits(HttpRunner):
+ """OP#13 - a host-type that does NOT attach a trait-type is fine. No completeness check."""
+
+ config = Config("OP#13 - Host without trait-type OK").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
_register(
- "gts://gts.x.test13.nt1.event.v1~",
+ "gts://gts.x.test13.h.notraits.event.v1~",
{
"type": "object",
- "x-gts-traits": {"retention": "P30D"},
- "required": ["id"],
"properties": {"id": {"type": "string"}},
},
- "register base with x-gts-traits but no traits-schema",
+ "register base host with no x-gts-traits-schema",
),
_validate_type_schema(
- "gts.x.test13.nt1.event.v1~",
- False,
- "validate should fail - x-gts-traits without x-gts-traits-schema",
+ "gts.x.test13.h.notraits.event.v1~",
+ True,
+ "validate - no traits attached, nothing to validate",
),
]
-class TestCaseOp13_TraitsInvalid_ConstNarrowingViolationInLeaf(HttpRunner):
- """OP#13 - Traits: mid-level narrows retention to const.
+class TestCaseOp13_TraitsInvalid_DerivedHasTraitsButNoTraitSchema(HttpRunner):
+ """OP#13 - derived host provides `x-gts-traits` but no trait-type is in the chain.
- Leaf tries different value.
+ This is a meaningless declaration and is rejected.
"""
- config = Config(
- "OP#13 - Const Narrowing Violation"
- ).base_url(get_gts_base_url())
+
+ config = Config("OP#13 - x-gts-traits without trait-type rejected").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
_register(
- "gts://gts.x.test13.const.event.v1~",
+ "gts://gts.x.test13.h.notrt.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {"retention": {"type": "string"}},
- },
- "required": ["id"],
"properties": {"id": {"type": "string"}},
},
- "register base with retention trait",
+ "register base host (no trait-type attached)",
),
_register_derived(
- "gts://gts.x.test13.const.event.v1~x.test13._.mid_const.v1~",
- "gts://gts.x.test13.const.event.v1~",
+ "gts://gts.x.test13.h.notrt.event.v1~x.test13._.has_traits.v1~",
+ "gts://gts.x.test13.h.notrt.event.v1~",
+ {"x-gts-traits": {"random": "value"}},
+ "register derived with x-gts-traits but no trait-type in chain",
+ ),
+ _validate_type_schema(
+ "gts.x.test13.h.notrt.event.v1~x.test13._.has_traits.v1~",
+ False,
+ "validate should fail - x-gts-traits used without trait-type in chain",
+ ),
+ ]
+
+
+class TestCaseOp13_TraitsInvalid_TraitsSchemaNotAString(HttpRunner):
+ """OP#13 - x-gts-traits-schema is not a string. Invalid value type.
+
+ Replaces TraitsInvalid_TraitsSchemaNotObject — value type changed in the new model.
+ """
+
+ config = Config("OP#13 - x-gts-traits-schema must be a string").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.h.bad_ts.event.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {"retention": {"const": "P30D"}},
- },
- "x-gts-traits": {"retention": "P30D"},
+ "x-gts-traits-schema": {"type": "object"}, # old-style inline schema
+ "properties": {"id": {"type": "string"}},
},
- "register mid-level narrowing retention to const P30D",
+ "register host with inline-object x-gts-traits-schema",
),
_validate_type_schema(
- "gts.x.test13.const.event.v1~x.test13._.mid_const.v1~",
- True,
- "validate mid-level - const narrowing",
+ "gts.x.test13.h.bad_ts.event.v1~",
+ False,
+ "validate should fail - x-gts-traits-schema is not a string URN",
),
- _register_derived(
- (
- "gts://gts.x.test13.const.event.v1~"
- "x.test13._.mid_const.v1~"
- "x.test13._.leaf_bad_const.v1~"
- ),
- "gts://gts.x.test13.const.event.v1~x.test13._.mid_const.v1~",
+ ]
+
+
+class TestCaseOp13_TraitsInvalid_TraitsSchemaNotValidUrn(HttpRunner):
+ """OP#13 - x-gts-traits-schema string is not a valid GTS Type URN."""
+
+ config = Config("OP#13 - x-gts-traits-schema invalid URN").base_url(get_gts_base_url())
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.h.bad_urn.event.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits": {"retention": "P90D"},
+ "x-gts-traits-schema": "not-a-gts-urn",
+ "properties": {"id": {"type": "string"}},
},
- "register leaf overriding retention to P90D",
+ "register host with malformed trait-type URN",
),
_validate_type_schema(
- (
- "gts.x.test13.const.event.v1~"
- "x.test13._.mid_const.v1~"
- "x.test13._.leaf_bad_const.v1~"
- ),
+ "gts.x.test13.h.bad_urn.event.v1~",
False,
- "validate should fail - leaf violates const retention=P30D",
+ "validate should fail - URN is not a valid GTS Type identifier",
),
]
-class TestCaseOp13_TraitsValid_ConstNarrowingLeafMatches(HttpRunner):
- """OP#13 - Traits: mid-level narrows retention to const.
+class TestCaseOp13_TraitsInvalid_TraitsInInstance(HttpRunner):
+ """OP#13 - x-gts-traits / x-gts-traits-schema MUST NOT appear in instance documents."""
- Leaf provides same value.
- """
- config = Config(
- "OP#13 - Const Narrowing Leaf Match"
- ).base_url(get_gts_base_url())
+ config = Config("OP#13 - Trait keywords in instance rejected").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.test13.t.inst.v1~",
+ _trait_event_meta("inst"),
+ "register trait-type",
+ ),
_register(
- "gts://gts.x.test13.constm.event.v1~",
+ "gts://gts.x.test13.h.inst.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {"retention": {"type": "string"}},
+ "x-gts-traits-schema": "gts://gts.x.test13.t.inst.v1~",
+ "properties": {
+ "id": {"type": "string"},
},
- "required": ["id"],
- "properties": {"id": {"type": "string"}},
},
- "register base with retention trait",
+ "register host",
),
- _register_derived(
- "gts://gts.x.test13.constm.event.v1~x.test13._.mid_constm.v1~",
- "gts://gts.x.test13.constm.event.v1~",
+ _post_schema_raw(
{
- "type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {"retention": {"const": "P30D"}},
- },
- "x-gts-traits": {"retention": "P30D"},
+ "id": "gts.x.test13.h.inst.event.v1~x.test13._.bad_inst.v1",
+ "x-gts-traits": {"topicRef": TOPIC_REF_ORDERS},
},
- "register mid-level narrowing retention to const P30D",
+ "register an instance with x-gts-traits leaked into it",
),
- _validate_type_schema(
- "gts.x.test13.constm.event.v1~x.test13._.mid_constm.v1~",
- True,
- "validate mid-level - const narrowing",
+ _validate_entity(
+ "gts.x.test13.h.inst.event.v1~x.test13._.bad_inst.v1",
+ False,
+ "validate should fail - x-gts-traits in instance is illegal",
),
- _register_derived(
- (
- "gts://gts.x.test13.constm.event.v1~"
- "x.test13._.mid_constm.v1~"
- "x.test13._.leaf_ok_constm.v1~"
- ),
- "gts://gts.x.test13.constm.event.v1~x.test13._.mid_constm.v1~",
+ ]
+
+
+class TestCaseOp13_TraitsInvalid_BaseHasTraitsButNoTraitSchema(HttpRunner):
+ """OP#13 - a base host declares x-gts-traits without declaring x-gts-traits-schema."""
+
+ config = Config("OP#13 - Base x-gts-traits without trait-type rejected").base_url(
+ get_gts_base_url()
+ )
+
+ def test_start(self):
+ super().test_start()
+
+ teststeps = [
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.h.lone_traits.event.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits": {"retention": "P30D"},
+ "x-gts-traits": {"random": "value"},
+ "properties": {"id": {"type": "string"}},
},
- "register leaf with retention matching const P30D",
+ "register base host with x-gts-traits but no x-gts-traits-schema",
),
_validate_type_schema(
- (
- "gts.x.test13.constm.event.v1~"
- "x.test13._.mid_constm.v1~"
- "x.test13._.leaf_ok_constm.v1~"
- ),
- True,
- "validate leaf - retention matches const",
+ "gts.x.test13.h.lone_traits.event.v1~",
+ False,
+ "validate should fail - x-gts-traits requires a trait-type in chain",
),
]
-class TestCaseOp13_TraitsInvalid_CyclingRef_SelfRef(HttpRunner):
- """OP#13 - Traits: x-gts-traits-schema refs itself."""
- config = Config(
- "OP#13 - Traits Self-Referencing Ref"
- ).base_url(get_gts_base_url())
+# ---------------------------------------------------------------------------
+# `const` narrowing in trait values (carried over)
+# ---------------------------------------------------------------------------
+
+
+class TestCaseOp13_ConstNarrowing_LeafMatches_Ok(HttpRunner):
+ """OP#13 - mid sets const via derived trait-type; leaf provides that exact value. OK."""
+
+ config = Config("OP#13 - const narrowing leaf matches").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.cyc.selfref.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.const.base.v1~",
{
"type": "object",
"properties": {
- "retention": {
- "type": "string",
- },
+ "channel": {"type": "string"},
},
},
- "register standalone trait schema",
+ "register base trait-type",
),
- _register(
- "gts://gts.x.test13.cyc.selfevt.v1~",
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.const.base.v1~x.test13._.audit_only.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "allOf": [
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".cyc.selfref.v1~"
- ),
- },
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".cyc.selfref.v1~"
- ),
- },
- ],
- },
- "required": ["id"],
+ "allOf": [{"$$ref": "gts://gts.x.test13.t.const.base.v1~"}],
"properties": {
- "id": {"type": "string"},
+ "channel": {"type": "string", "const": "audit"},
},
},
- "register base with self-cycling trait ref",
+ "register derived trait-type narrowing channel to const 'audit'",
),
- _register_derived(
- (
- "gts://gts.x.test13.cyc.selfevt.v1~"
- "x.test13._.cyc_self_leaf.v1~"
- ),
- "gts://gts.x.test13.cyc.selfevt.v1~",
+ _register(
+ "gts://gts.x.test13.h.const.event.v1~",
{
"type": "object",
- "x-gts-traits": {
- "retention": "P30D",
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.const.base.v1~x.test13._.audit_only.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register derived with traits",
+ "register host attaching the narrowed trait-type",
+ ),
+ _register_derived(
+ "gts://gts.x.test13.h.const.event.v1~x.test13._.match.v1~",
+ "gts://gts.x.test13.h.const.event.v1~",
+ {"x-gts-traits": {"channel": "audit"}},
+ "register derived providing matching const value",
),
_validate_type_schema(
- (
- "gts.x.test13.cyc.selfevt.v1~"
- "x.test13._.cyc_self_leaf.v1~"
- ),
- False,
- "validate should fail - cycling ref in traits-schema",
+ "gts.x.test13.h.const.event.v1~x.test13._.match.v1~",
+ True,
+ "validate - leaf value matches const constraint",
),
]
-class TestCaseOp13_TraitsInvalid_CyclingRef_TwoNode(HttpRunner):
- """OP#13 - Traits: trait schema A refs B, B refs A."""
- config = Config(
- "OP#13 - Traits Two-Node Ref Cycle"
- ).base_url(get_gts_base_url())
+class TestCaseOp13_ConstNarrowing_LeafViolation_Rejected(HttpRunner):
+ """OP#13 - mid declares const via derived trait-type; leaf supplies a different value."""
+
+ config = Config("OP#13 - const narrowing leaf violation").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.cyc2.trait_a.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.constv.base.v1~",
{
"type": "object",
- "allOf": [
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".cyc2.trait_b.v1~"
- ),
- },
- ],
"properties": {
- "retention": {"type": "string"},
+ "channel": {"type": "string"},
},
},
- "register trait schema A referencing B",
+ "register base trait-type",
),
- _register(
- "gts://gts.x.test13.cyc2.trait_b.v1~",
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.constv.base.v1~x.test13._.audit_only.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "allOf": [
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".cyc2.trait_a.v1~"
- ),
- },
- ],
+ "allOf": [{"$$ref": "gts://gts.x.test13.t.constv.base.v1~"}],
"properties": {
- "topicRef": {
- "type": "string",
- "x-gts-ref": (
- "gts.x.core.events.topic.v1~"
- ),
- },
+ "channel": {"type": "string", "const": "audit"},
},
},
- "register trait schema B referencing A",
+ "register derived trait-type narrowing channel to const 'audit'",
),
_register(
- "gts://gts.x.test13.cyc2.event.v1~",
+ "gts://gts.x.test13.h.constv.event.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "allOf": [
- {
- "$$ref": (
- "gts://gts.x.test13"
- ".cyc2.trait_a.v1~"
- ),
- },
- ],
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.constv.base.v1~x.test13._.audit_only.v1~",
+ "properties": {"id": {"type": "string"}},
},
- "register base with cycling trait refs",
+ "register host",
),
_register_derived(
- (
- "gts://gts.x.test13.cyc2.event.v1~"
- "x.test13._.cyc2_leaf.v1~"
- ),
- "gts://gts.x.test13.cyc2.event.v1~",
- {
- "type": "object",
- "x-gts-traits": {
- "retention": "P30D",
- "topicRef": (
- "gts.x.core.events.topic.v1~"
- "x.test13._.orders.v1"
- ),
- },
- },
- "register derived with traits",
+ "gts://gts.x.test13.h.constv.event.v1~x.test13._.mismatch.v1~",
+ "gts://gts.x.test13.h.constv.event.v1~",
+ {"x-gts-traits": {"channel": "notification"}},
+ "register derived with channel value not matching const",
),
_validate_type_schema(
- (
- "gts.x.test13.cyc2.event.v1~"
- "x.test13._.cyc2_leaf.v1~"
- ),
+ "gts.x.test13.h.constv.event.v1~x.test13._.mismatch.v1~",
False,
- "validate should fail - two-node cycle in trait refs",
+ "validate should fail - leaf value violates const",
),
]
-class TestCaseOp13_TraitsInvalid_TraitsSchemaNotObject(HttpRunner):
- """OP#13 - Traits: x-gts-traits-schema with type=integer.
- Must fail - trait schema must have type=object.
+# ---------------------------------------------------------------------------
+# Cycle detection (trait-type cycles via x-gts-traits-schema recursion)
+# ---------------------------------------------------------------------------
+
+
+class TestCaseOp13_CycleDetection_SelfRef_Rejected(HttpRunner):
+ """OP#13 - a trait-type attaches itself as its own trait-type (self-cycle).
+
+ The spec permits recursion in principle, but registries MUST detect cycles
+ of finite depth that would otherwise loop forever during effective-type
+ resolution.
"""
- config = Config(
- "OP#13 - Traits Schema Not Object"
- ).base_url(get_gts_base_url())
+
+ config = Config("OP#13 - self-referential trait-type rejected").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.tsnobj.event.v1~",
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.selfref.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits-schema": {
- "type": "integer",
- },
- "required": ["id"],
+ "x-gts-traits-schema": "gts://gts.x.test13.t.selfref.v1~",
"properties": {
- "id": {"type": "string"},
+ "anything": {"type": "string"},
},
},
- "register base with type=integer trait schema",
- ),
- _register_derived(
- (
- "gts://gts.x.test13.tsnobj.event.v1~"
- "x.test13._.tsnobj_leaf.v1~"
- ),
- "gts://gts.x.test13.tsnobj.event.v1~",
- {
- "type": "object",
- },
- "register derived",
+ "register a trait-type that references itself",
),
_validate_type_schema(
- (
- "gts.x.test13.tsnobj.event.v1~"
- "x.test13._.tsnobj_leaf.v1~"
- ),
+ "gts.x.test13.t.selfref.v1~",
False,
- "validate should fail - trait schema type is integer",
+ "validate should fail - self-cycle detected",
),
]
-class TestCaseOp13_TraitsInvalid_TraitsInInstance(HttpRunner):
- """OP#13 - Traits: x-gts-traits in an instance document.
+class TestCaseOp13_CycleDetection_TwoNodeCycle_Rejected(HttpRunner):
+ """OP#13 - trait-type A attaches B as its trait-type; B attaches A. Cycle."""
- Trait keywords are schema-only. Instance with x-gts-traits
- must fail entity validation.
- """
- config = Config(
- "OP#13 - Traits In Instance"
- ).base_url(get_gts_base_url())
+ config = Config("OP#13 - two-node trait-type cycle rejected").base_url(get_gts_base_url())
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.tinst.event.v1~",
+ # In a registry with single-pass registration, A would have to forward-ref B.
+ # Some implementations require both to exist; we register A first as a bare type,
+ # then register B attaching A, then update A to attach B.
+ # For an interop-friendly test, we use _post_schema_raw to upsert both as cycle.
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.cycleA.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
- "properties": {
- "id": {"type": "string"},
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.cycleB.v1~",
+ "properties": {"a": {"type": "string"}},
},
- "register base schema with traits",
+ "register A pointing at B (forward-ref)",
),
- _register_derived(
- (
- "gts://gts.x.test13.tinst.event.v1~"
- "x.test13._.tinst_leaf.v1~"
- ),
- "gts://gts.x.test13.tinst.event.v1~",
+ _post_schema_raw(
{
+ "$$id": "gts://gts.x.test13.t.cycleB.v1~",
+ "$$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
- "x-gts-traits": {
- "retention": "P90D",
- },
+ "x-gts-traits-schema": "gts://gts.x.test13.t.cycleA.v1~",
+ "properties": {"b": {"type": "string"}},
},
- "register derived with traits",
+ "register B pointing at A (closes the cycle)",
),
_validate_type_schema(
- (
- "gts.x.test13.tinst.event.v1~"
- "x.test13._.tinst_leaf.v1~"
- ),
- True,
- "validate derived schema - ok",
- ),
- _validate_entity(
- (
- "gts.x.test13.tinst.event.v1~"
- "x.test13._.tinst_leaf.v1~"
- ),
+ "gts.x.test13.t.cycleA.v1~",
False,
- "validate entity should fail - traits in instance",
+ "validate should fail - 2-node trait-type cycle",
),
]
-class TestCaseOp13_TraitsInvalid_TraitsSchemaInInstance(HttpRunner):
- """OP#13 - Traits: x-gts-traits-schema in an instance document.
+# ---------------------------------------------------------------------------
+# Instance validation (defense-in-depth for OP#6)
+# ---------------------------------------------------------------------------
+
+
+class TestCaseOp13_InstanceValidation_TypeWithUnresolvedTraits_Rejected(HttpRunner):
+ """OP#13 / OP#6 - validate-entity on an instance whose type is trait-incomplete.
- Trait keywords are schema-only. Instance with
- x-gts-traits-schema must fail entity validation.
+ Even if a registry somehow accepted such a type, OP#6 MUST reject instance
+ validation as defense-in-depth.
"""
- config = Config(
- "OP#13 - Traits Schema In Instance"
- ).base_url(get_gts_base_url())
+
+ config = Config("OP#13 - instance of trait-incomplete type rejected").base_url(
+ get_gts_base_url()
+ )
def test_start(self):
super().test_start()
teststeps = [
- _register(
- "gts://gts.x.test13.tsinst.event.v1~",
+ _register_trait_type(
+ "gts://gts.x.test13.t.inccpl.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "retention": {
- "type": "string",
- "default": "P30D",
- },
- },
- },
- "required": ["id"],
+ "required": ["topicRef"],
"properties": {
- "id": {"type": "string"},
+ "topicRef": {"type": "string"},
},
},
- "register base schema with traits-schema",
+ "register trait-type (topicRef required, no default)",
+ ),
+ _register(
+ "gts://gts.x.test13.h.inccpl.event.v1~",
+ {
+ "type": "object",
+ "x-gts-traits-schema": "gts://gts.x.test13.t.inccpl.v1~",
+ "properties": {"id": {"type": "string"}},
+ },
+ "register concrete host without resolving topicRef",
),
_validate_entity(
- "gts.x.test13.tsinst.event.v1~",
+ "gts.x.test13.h.inccpl.event.v1~",
False,
- "validate entity should fail - traits-schema in instance",
+ "validate-entity should fail - host type is trait-incomplete",
),
]
+
+
+if __name__ == "__main__":
+ TestCaseOp13_TraitsValid_AllResolved().test_start()
diff --git a/tests/test_refimpl_x_gts_final_abstract.py b/tests/test_refimpl_x_gts_final_abstract.py
index e02a057..c699a53 100644
--- a/tests/test_refimpl_x_gts_final_abstract.py
+++ b/tests/test_refimpl_x_gts_final_abstract.py
@@ -13,6 +13,7 @@
register as _register,
register_derived as _register_derived,
register_instance as _register_instance,
+ register_trait_type as _register_trait_type,
validate_entity as _validate_entity,
validate_instance as _validate_instance,
validate_type_schema as _validate_type_schema,
@@ -833,30 +834,33 @@ def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.testfa.finaltrait.traits.v1~",
+ {
+ "type": "object",
+ "properties": {
+ "retention": {"type": "string", "default": "P30D"},
+ "priority": {"type": "integer"},
+ },
+ "required": ["priority"],
+ },
+ "register trait-type with required priority (no default)",
+ ),
_register(
"gts://gts.x.testfa.finaltrait.base.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "retention": {"type": "string", "default": "P30D"},
- "priority": {"type": "integer"},
- },
- "required": ["priority"],
- },
+ "x-gts-traits-schema": "gts://gts.x.testfa.finaltrait.traits.v1~",
+ "x-gts-abstract": True,
"properties": {"name": {"type": "string"}},
},
- "register base with trait schema",
+ "register abstract base attaching the trait-type",
),
_register_derived(
"gts://gts.x.testfa.finaltrait.base.v1~x.testfa._.leaf.v1~",
"gts://gts.x.testfa.finaltrait.base.v1~",
{
- "type": "object",
- "x-gts-traits": {
- "priority": 5,
- },
+ "x-gts-traits": {"priority": 5},
},
"register final derived with traits resolved",
top_level={"x-gts-final": True},
@@ -882,26 +886,31 @@ def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.testfa.finalmiss.traits.v1~",
+ {
+ "type": "object",
+ "properties": {
+ "priority": {"type": "integer"},
+ },
+ "required": ["priority"],
+ },
+ "register trait-type (priority required, no default)",
+ ),
_register(
"gts://gts.x.testfa.finalmiss.base.v1~",
{
"type": "object",
- "x-gts-traits-schema": {
- "type": "object",
- "properties": {
- "priority": {"type": "integer"},
- },
- "required": ["priority"],
- },
+ "x-gts-traits-schema": "gts://gts.x.testfa.finalmiss.traits.v1~",
+ "x-gts-abstract": True,
"properties": {"name": {"type": "string"}},
},
- "register base with required trait (no default)",
+ "register abstract base attaching trait-type",
),
_register_derived(
"gts://gts.x.testfa.finalmiss.base.v1~x.testfa._.leaf.v1~",
"gts://gts.x.testfa.finalmiss.base.v1~",
{
- "type": "object",
# x-gts-traits intentionally omitted — priority not resolved
},
"register final derived without resolving traits",
@@ -928,19 +937,24 @@ def test_start(self):
super().test_start()
teststeps = [
+ _register_trait_type(
+ "gts://gts.x.testfa.abstrait.traits.v1~",
+ {
+ "type": "object",
+ "additionalProperties": False,
+ "properties": {
+ "priority": {"type": "integer"},
+ },
+ "required": ["priority"],
+ },
+ "register trait-type (priority required, no default)",
+ ),
_register(
"gts://gts.x.testfa.abstrait.base.v1~",
{
"type": "object",
"x-gts-abstract": True,
- "x-gts-traits-schema": {
- "type": "object",
- "additionalProperties": False,
- "properties": {
- "priority": {"type": "integer"},
- },
- "required": ["priority"],
- },
+ "x-gts-traits-schema": "gts://gts.x.testfa.abstrait.traits.v1~",
"properties": {"name": {"type": "string"}},
},
"register abstract base with required trait (no default)",