Skip to content

Commit d9a7488

Browse files
committed
docs(spec): adopt JSON Merge Patch (RFC 7396) for x-gts-traits and add ADR-0004
- Replace prior trait-value merge rule in README §9.7 with chain-applied JSON Merge Patch (RFC 7396) along the $id chain: descendant-last-wins at leaves, nested objects merge recursively, arrays replace wholesale, `null` deletes the key. - Make standard JSON Schema `const` in x-gts-traits-schema the publisher's lock mechanism (no GTS-specific immutability rule); a descendant that attempts to override a const-locked value fails ordinary JSON Schema validation against the effective trait-schema. - Document the principal use of `null` as a fallback to the trait-schema's `default` via ADR-0003 materialization, including the failure mode when the deleted key is required-without-default. - Update OP#13 description to drop the obsolete immutability sentence and point at `const` as the lock mechanism. - Add ADR-0004 with drivers, alternatives (no-merge, shallow last-wins, shallow immutable-once-set, RFC 7396, per-property keyword), decision, worked examples (override / nested merge / const lock / null-to-default fallback), edge cases, and conformance expectations. Signed-off-by: Aviator 5 <ai.agent.tor@gmail.com>
1 parent 5bdc3ed commit d9a7488

2 files changed

Lines changed: 543 additions & 16 deletions

File tree

README.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,7 +1335,7 @@ Implementation notes:
13351335

13361336
### 9.7 - GTS Type Schema Traits (`x-gts-traits-schema` / `x-gts-traits`)
13371337

1338-
**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, for non-abstract types, all required trait properties in the effective trait-schema are resolved (via explicit value in the chain-merged `x-gts-traits` or via `default` in the effective trait-schema), and that trait values satisfy the effective trait-schema's other 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 be a valid JSON Schema subschema (object, `true`, or `false`). Uses the same validation endpoints (`/validate-type-schema`, `/validate-entity`).
1338+
**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, for non-abstract types, all required trait properties in the effective trait-schema are resolved (via explicit value in the chain-merged `x-gts-traits` or via `default` in the effective trait-schema), and that the chain-merged trait values satisfy the effective trait-schema's other constraints (including `const`, which a publisher uses to lock individual trait values across descendants — see §9.7.5). Both `x-gts-traits-schema` and `x-gts-traits` are schema-only keywords and MUST NOT appear in instances. `x-gts-traits-schema` MUST be a valid JSON Schema [subschema](https://json-schema.org/learn/glossary#subschema) (object, `true`, or `false`). Uses the same validation endpoints (`/validate-type-schema`, `/validate-entity`).
13391339

13401340
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:
13411341

@@ -1364,12 +1364,9 @@ A single schema MAY contain both keywords. This is explicitly allowed and useful
13641364

13651365
When `x-gts-traits-schema` is an object subschema, the **effective** trait-schema (after chain aggregation per §9.7.5) MUST constrain trait values to JSON objects.
13661366

1367-
Because `x-gts-traits-schema` is an ordinary JSON Schema subschema, standard JSON Schema applies inside it without GTS reinventing anything:
1367+
Because `x-gts-traits-schema` is an ordinary JSON Schema subschema, all standard JSON Schema constructs apply inside it with their normal semantics; implementations MUST NOT invent a custom reference mechanism for `$ref`.
13681368

1369-
- `$ref` resolves per ordinary JSON Schema `$ref` rules (base URI resolution + JSON Pointer fragments). Implementations MUST NOT invent a custom reference mechanism.
1370-
- `allOf` / `oneOf` / `anyOf` / `not` carry their normal JSON Schema composition semantics — `allOf` is a logical AND over its subschemas at validation time, as for any other JSON Schema use.
1371-
- `properties` / `required` / `additionalProperties` / numeric and string constraints / `const` / `enum` / etc. behave as in any other JSON Schema.
1372-
- The trait shape MAY be declared **inline**, **referenced** from a standalone trait-schema registered as an ordinary GTS Type via `$ref`, or **composed** via `allOf` of inline parts and references. The choice is an authoring decision — inline keeps the trait surface private to the host and inheriting the host's ACL; the `$ref`-to-registered-type form is appropriate when the trait surface should be a separately governed artifact.
1369+
The trait shape MAY be declared **inline**, **referenced** from a standalone trait-schema registered as an ordinary GTS Type via `$ref`, or **composed** via `allOf` of inline parts and references. The choice is an authoring decision — inline keeps the trait surface private to the host and inheriting the host's ACL; the `$ref`-to-registered-type form is appropriate when the trait surface should be a separately governed artifact.
13731370

13741371
**Inheritance along the host-type derivation chain happens at the registry level, not at the author level.** A descendant host type does NOT need to repeat the ancestor's `x-gts-traits-schema` inside its own — the registry composes all `x-gts-traits-schema` declarations encountered along the host's `$id` chain via JSON Schema `allOf` (see §9.7.5). A descendant MAY write an explicit `allOf` that includes a `$ref` to an ancestor's `x-gts-traits-schema`; doing so is redundant under chain aggregation but not invalid (consistent with the JSON Schema extension framing — see [`adr/0001-derivation-form.md`](adr/0001-derivation-form.md)).
13751372

@@ -1379,7 +1376,9 @@ See [`adr/0002-x-gts-traits-schema.md`](adr/0002-x-gts-traits-schema.md) for the
13791376

13801377
#### 9.7.2 Trait schema definition (`x-gts-traits-schema`)
13811378

1382-
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.
1379+
A type schema declares the trait shape — property names, types, constraints, and `default` values. Any type in the `$id` chain (base or descendant) MAY contribute its own `x-gts-traits-schema`; the registry composes all such declarations along the chain via JSON Schema `allOf` into a single effective trait-schema (see §9.7.5).
1380+
1381+
The same derivation compatibility principle that governs host body schemas (§3.1) applies to `x-gts-traits-schema`: every value valid against the descendant's effective trait-schema MUST also be valid against each ancestor's trait-schema. This is enforced naturally by the `allOf` composition — contradictions across the chain (e.g., conflicting types, narrowed constraints that don't overlap, or different `default`s for the same property) produce an unsatisfiable effective trait-schema and fail registration. Typically a base declares the initial trait shape and descendants **narrow** existing trait properties (tighten constraints, `const`, narrower enums). Descendants MAY also **extend** the trait surface by introducing new top-level properties — but only if no ancestor's `x-gts-traits-schema` declares `additionalProperties: false` (or another restriction that would reject the new property); otherwise the new property is treated as "additional" against that ancestor's branch in the `allOf` composition and validation fails, by the same mechanic as §3.1 governs for host bodies.
13831382

13841383
**Inline definition:**
13851384

@@ -1390,7 +1389,6 @@ A base schema declares the trait schema — the shape and defaults of all trait
13901389
"type": "object",
13911390
"x-gts-traits-schema": {
13921391
"type": "object",
1393-
"additionalProperties": false,
13941392
"properties": {
13951393
"topicRef": {
13961394
"description": "GTS ID of the topic/stream where events of this type are published.",
@@ -1506,23 +1504,27 @@ Given an inheritance chain `S₀ → S₁ → … → Sₙ`:
15061504
- **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.
15071505

15081506
- **Trait value merge**
1509-
- The registry MUST build an *effective traits object* by collecting all `x-gts-traits` objects encountered in the chain (left-to-right).
1510-
- **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).
1511-
- Defaults declared in the effective trait schema SHOULD be used as normal JSON Schema defaults to produce a complete effective traits object.
1507+
- The registry MUST build an *effective traits object* by walking the type's `$id` chain root → leaf and applying each layer's `x-gts-traits` as a [JSON Merge Patch (RFC 7396)](https://datatracker.ietf.org/doc/html/rfc7396) against the chain-merged object so far. Top-level scalar / array / `null` leaves are overwritten by the descendant (last-wins). Object-valued top-level traits merge **recursively** — fields of an ancestor's object trait that the descendant does not restate are preserved.
1508+
- **Arrays replace wholesale** at any depth (per RFC 7396). Authors who need item-level composability SHOULD model the data as a keyed object instead of an array.
1509+
- **`null` at any depth deletes that key** from the effective object (per RFC 7396). The principal use case is to revert an ancestor-set value and let the trait-schema's `default` re-apply via the materialization step described in the Completeness check below — that is, a descendant writes `"<key>": null` to "fall back to the schema default" without picking a specific value. If the deleted key is `required` and has no `default`, the completeness check fails registration for non-abstract types (the descendant must then either mark itself abstract or accept that "delete + required + no default" is an unresolvable contract). Authors who want `null` as an *intended* trait value cannot express it via this merge and must use a sentinel value documented as part of the trait shape.
1510+
- Defaults declared in the effective trait-schema MUST be materialized into the effective traits object before the Completeness check runs (per ADR-0003): for every property declared in the effective trait-schema with a `default` and not present in the chain-merged object, the registry MUST substitute the default value. The Completeness check below (OP#13) operates on the resulting *materialized* effective traits object.
1511+
- A publisher who wants a trait value to be **locked** across all descendants of a base type SHOULD declare `"const": <value>` for that property in `x-gts-traits-schema`. A descendant attempting to override the value will fail the standard JSON Schema validation that runs against the effective trait-schema (per the Completeness check below). No GTS-specific "immutability" rule is required — `const` is the mechanism.
1512+
- A descendant MAY redeclare a trait value with the same value the ancestor already declared (idempotent restatement).
1513+
- See [`adr/0004-x-gts-traits-merge-strategy.md`](adr/0004-x-gts-traits-merge-strategy.md) for the rationale.
15121514

15131515
- **Validation**
15141516
- **Completeness check** (registration-time): For types whose `x-gts-abstract` is not `true`, the registry MUST verify that the *materialized* effective traits object validates against the effective trait-schema using standard JSON Schema validation. "Materialized" means: defaults declared in the effective trait-schema for properties not present in the chain-merged effective traits object are substituted in before validation. If validation fails — in particular, if a `required` property of the effective trait-schema has no chain-assigned value and no default — registration MUST fail. For types with `x-gts-abstract: true`, this completeness check is skipped; descendants are expected to close any unresolved required traits. See [`adr/0003-x-gts-traits-completeness.md`](adr/0003-x-gts-traits-completeness.md) for the rationale.
15151517
- If the effective trait schema cannot be satisfied (e.g., contradictory constraints introduced across the chain), schema validation MUST fail.
1516-
- If a descendant attempts to override a trait value already set by an ancestor with a different value, schema validation MUST fail.
15171518

1518-
**Example — immutable trait override (failure):**
1519+
**Example — descendant override and `const` lock:**
15191520

15201521
Consider a 3-level chain: `base → audit_event → most_derived_event`.
15211522

1522-
- `audit_event` sets `x-gts-traits.topicRef` to `gts.x.core.events.topic.v1~x.core._.audit.v1`
1523-
- `most_derived_event` attempts to set `x-gts-traits.topicRef` to `gts.x.core.events.topic.v1~x.core._.notification.v1`
1523+
- `base.x-gts-traits-schema.properties.indexed.const = true` — the publisher locks `indexed`.
1524+
- `audit_event.x-gts-traits` sets `topicRef = gts.x.core.events.topic.v1~x.core._.audit.v1`.
1525+
- `most_derived_event.x-gts-traits` sets `topicRef = gts.x.core.events.topic.v1~x.core._.notification.v1`.
15241526

1525-
Validation of `most_derived_event` MUST fail because `topicRef` was already set by `audit_event` and the new value differs.
1527+
Effective traits for `most_derived_event`: `{ "indexed": <chain-derived true>, "topicRef": ".../notification.v1" }`. The override of `topicRef` is permitted (last-wins). If `most_derived_event` also tried to set `"indexed": false`, registration would fail — not because of a GTS-specific immutability rule, but because the materialized effective traits object would not satisfy the `const: true` constraint declared on `indexed` in the effective trait-schema.
15261528

15271529
These rules are intentionally aligned with existing JSON Schema composition semantics and GTS schema chaining practices.
15281530

0 commit comments

Comments
 (0)