1010def register (gts_id , schema_body , label = "register schema" ):
1111 """Register a schema via POST /entities."""
1212 body = {
13+ ** schema_body ,
1314 "$$id" : gts_id ,
1415 "$$schema" : "http://json-schema.org/draft-07/schema#" ,
15- ** schema_body ,
1616 }
1717 return Step (
1818 RunRequest (label )
@@ -28,7 +28,23 @@ def register_derived(gts_id, base_ref, overlay, label="register derived", top_le
2828
2929 top_level: optional dict of extra keys to add at schema top level
3030 (e.g. {"x-gts-final": True}) — these MUST NOT go inside allOf.
31+
32+ The document-level GTS keywords x-gts-traits / x-gts-traits-schema MUST
33+ appear at the schema top level, not nested inside an allOf entry
34+ (GTS spec §9.12). If a caller places them in the `overlay`, they are
35+ transparently hoisted to the top level so existing trait tests express
36+ the spec-correct placement without restating every call site.
3137 """
38+ overlay = dict (overlay )
39+ trait_kws = ("x-gts-traits" , "x-gts-traits-schema" )
40+ hoisted = {kw : overlay .pop (kw ) for kw in trait_kws if kw in overlay }
41+ if top_level :
42+ clobbered = [kw for kw in trait_kws if kw in top_level ]
43+ if clobbered :
44+ raise ValueError (
45+ "top_level must not contain trait keywords "
46+ f"{ clobbered } ; pass them in `overlay` so they are hoisted"
47+ )
3248 body = {
3349 "$$id" : gts_id ,
3450 "$$schema" : "http://json-schema.org/draft-07/schema#" ,
@@ -38,6 +54,7 @@ def register_derived(gts_id, base_ref, overlay, label="register derived", top_le
3854 overlay ,
3955 ],
4056 }
57+ body .update (hoisted )
4158 if top_level :
4259 body .update (top_level )
4360 return Step (
@@ -49,6 +66,58 @@ def register_derived(gts_id, base_ref, overlay, label="register derived", top_le
4966 )
5067
5168
69+ def register_derived_redeclared (
70+ gts_id , base_ref , body , label = "register derived (no allOf)" , top_level = None
71+ ):
72+ """Register a derived schema without allOf — caller restates parent fields directly.
73+
74+ Per ADR-0001 (GTS as a JSON Schema extension, dialect-agnostic; not a formal
75+ JSON Schema Dialect), derivation is established by the chained $id alone;
76+ the body MAY use any syntactically valid JSON Schema form.
77+ `base_ref` is accepted for parity with register_derived() and documents intent.
78+
79+ `body` is the entire schema body (caller is responsible for restating any
80+ parent fields that need to participate in OP#12 compatibility). The helper
81+ only injects $id and $schema; no allOf wrapping is added.
82+ top_level: optional dict merged into body at the top level.
83+ """
84+ full = {
85+ ** body ,
86+ "$$id" : gts_id ,
87+ "$$schema" : "http://json-schema.org/draft-07/schema#" ,
88+ }
89+ if top_level :
90+ full .update (top_level )
91+ return Step (
92+ RunRequest (label )
93+ .post ("/entities" )
94+ .with_json (full )
95+ .validate ()
96+ .assert_equal ("status_code" , 200 )
97+ )
98+
99+
100+ def register_abstract (gts_id , schema_body , label = "register abstract" ):
101+ """Register a schema marked x-gts-abstract: true.
102+
103+ Per ADR-0003, abstract types skip the trait-completeness check at
104+ /validate-type-schema time.
105+ """
106+ body = {
107+ ** schema_body ,
108+ "$$id" : gts_id ,
109+ "$$schema" : "http://json-schema.org/draft-07/schema#" ,
110+ "x-gts-abstract" : True ,
111+ }
112+ return Step (
113+ RunRequest (label )
114+ .post ("/entities" )
115+ .with_json (body )
116+ .validate ()
117+ .assert_equal ("status_code" , 200 )
118+ )
119+
120+
52121def register_instance (instance_body , label = "register instance" ):
53122 """Register an instance via POST /entities."""
54123 return Step (
0 commit comments