feat(server): expose typed semantic transition review
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"""Refresh the closed semantic OpenAPI catalog from the frozen Rust leaf table.
|
||||
|
||||
Run from any directory with Python 3: semantic_schema.py [--check].
|
||||
This generates documentation only, never runtime selectors or model mutations.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
PATH = ROOT / "docs/openapi.json"
|
||||
spec = json.loads(PATH.read_text(encoding="utf-8"))
|
||||
schemas = spec["components"]["schemas"]
|
||||
types = (ROOT / "crates/oa-core/src/semantic/types.rs").read_text(encoding="utf-8")
|
||||
model = (ROOT / "crates/oa-core/src/model.rs").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def ref(name):
|
||||
return {"$ref": f"#/components/schemas/{name}"}
|
||||
|
||||
|
||||
def obj(properties, required=None):
|
||||
return {"type": "object", "properties": properties,
|
||||
"required": list(properties) if required is None else required,
|
||||
"additionalProperties": False}
|
||||
|
||||
|
||||
def array(item, maximum=None):
|
||||
result = {"type": "array", "items": item}
|
||||
if maximum is not None:
|
||||
result["maxItems"] = maximum
|
||||
return result
|
||||
|
||||
|
||||
def optional(item):
|
||||
return {"anyOf": [item, {"type": "null"}]}
|
||||
|
||||
|
||||
def enum(names):
|
||||
return {"type": "string", "enum": names}
|
||||
|
||||
|
||||
def variant(name, value):
|
||||
return obj({name: value})
|
||||
|
||||
|
||||
def typed(name):
|
||||
if name.startswith("Option<"):
|
||||
return optional(typed(name[7:-1]))
|
||||
if name.startswith("Vec<"):
|
||||
return array(typed(name[4:-1]))
|
||||
primitives = {"String": {"type": "string"}, "bool": {"type": "boolean"},
|
||||
"u32": {"type": "integer", "minimum": 0, "maximum": 4294967295},
|
||||
"i64": {"type": "integer", "minimum": -9223372036854775808,
|
||||
"maximum": 9223372036854775807}, "f64": {"type": "number"}}
|
||||
return primitives[name] if name in primitives else ref(name)
|
||||
|
||||
|
||||
def reviewed(name):
|
||||
key = "Reviewed" + re.sub(r"[^A-Za-z0-9]", "", name)
|
||||
observation = {"oneOf": [enum(["NotApplicable"]), variant("Value", typed(name))]}
|
||||
schemas[key] = obj({"oa": observation, "sa": copy.deepcopy(observation)})
|
||||
return ref(key)
|
||||
|
||||
|
||||
def keypair(name):
|
||||
key = "KeyPair" + name
|
||||
schemas[key] = obj({"oa": optional(typed(name)), "sa": optional(typed(name))}, [])
|
||||
return ref(key)
|
||||
|
||||
|
||||
for name in ["DataConstructor", "PortConstructor", "OccurrenceConstructor", "EventConstructor",
|
||||
"MessageConstructor", "ReferenceCollection", "OrderedCollection", "IncidentEnd",
|
||||
"ArtifactFacet", "ComponentPortKind", "ComponentPortOrientation", "PortDirection",
|
||||
"FragmentOperator", "StateActionKind", "ArtifactRelation"]:
|
||||
source = types if f"pub enum {name}" in types else model
|
||||
body = re.search(r"pub enum " + name + r"\s*\{([^}]+)\}", source).group(1)
|
||||
body = re.sub(r"//[^\n]*|#\[[^\]]*\]", "", body)
|
||||
names = [value.strip() for value in body.split(",") if value.strip()]
|
||||
assert all(re.fullmatch(r"[A-Za-z]+", value) for value in names), (name, names)
|
||||
schemas[name] = enum(names)
|
||||
|
||||
catalog = types.split("macro_rules! scalar_families", 1)[1].split("pub(super) use", 1)[0]
|
||||
families = re.findall(r"(\w+), (\w+), (\w+) \{([^}]+)\}", catalog)
|
||||
assert len(families) == 13
|
||||
selectors, reviews = [], []
|
||||
leaf_count = 0
|
||||
for family, selector, review, body in families:
|
||||
leaves = re.findall(r"(\w+): ([\w<>]+)", body)
|
||||
leaf_count += len(leaves)
|
||||
schemas[selector] = enum([leaf for leaf, _ in leaves])
|
||||
schemas[review] = {"oneOf": [variant(leaf, reviewed(value)) for leaf, value in leaves]}
|
||||
selectors.append(variant(family, ref(selector)))
|
||||
reviews.append(variant(family, ref(review)))
|
||||
assert leaf_count == 84
|
||||
schemas["AnnotationKey"] = obj({"path": array(ref("Id")), "field": ref("Id")})
|
||||
id_keys, text_keys, annotation_keys = keypair("Id"), keypair("String"), keypair("AnnotationKey")
|
||||
artifact_types = {"Presence": "bool", "Name": "String", "Description": "String", "Owner": "Id",
|
||||
"Relation": "ArtifactRelation", "Uri": "String", "Revision": "Option<String>",
|
||||
"Fragment": "Option<String>"}
|
||||
schemas["ArtifactReview"] = {"oneOf": [variant(k, reviewed(v)) for k, v in artifact_types.items()]}
|
||||
selectors += [variant("Member", obj({"collection": ref("ReferenceCollection"), "keys": id_keys})),
|
||||
variant("Order", ref("OrderedCollection")), variant("Literal", text_keys),
|
||||
variant("Annotation", annotation_keys), variant("Binding", id_keys), variant("Child", id_keys),
|
||||
variant("Incident", obj({"keys": id_keys, "end": ref("IncidentEnd")})),
|
||||
variant("Artifact", obj({"keys": id_keys, "facet": ref("ArtifactFacet")}))]
|
||||
reviews += [variant("Member", obj({"collection": ref("ReferenceCollection"), "keys": id_keys, "review": reviewed("bool")})),
|
||||
variant("Order", obj({"collection": ref("OrderedCollection"), "review": reviewed("Vec<Id>")})),
|
||||
variant("Literal", obj({"keys": text_keys, "review": reviewed("bool")})),
|
||||
variant("Annotation", obj({"keys": annotation_keys, "review": reviewed("Option<MessageValue>")})),
|
||||
variant("Binding", obj({"keys": id_keys, "review": reviewed("Option<LifelineBinding>")})),
|
||||
variant("Child", obj({"keys": id_keys, "review": reviewed("bool")})),
|
||||
variant("Incident", obj({"keys": id_keys, "end": ref("IncidentEnd"), "review": reviewed("bool")})),
|
||||
variant("Artifact", obj({"keys": id_keys, "review": ref("ArtifactReview")}))]
|
||||
schemas["FacetSelector"] = {"oneOf": selectors}
|
||||
schemas["FacetReview"] = {"oneOf": reviews}
|
||||
schemas["SemanticBaseline"] = obj({"facets": array(ref("FacetReview"))})
|
||||
schemas["TraceBaseline"]["properties"]["semantic"] = {**optional(ref("SemanticBaseline")), "default": None,
|
||||
"description": "Absent/null means Unreviewed. Historical typed facts never become live graph references."}
|
||||
schemas["Project"]["properties"]["schema_version"]["const"] = 5
|
||||
schemas["MigrationReport"]["properties"]["from_schema"]["enum"] = [1, 2, 3, 4]
|
||||
schemas["MigrationReport"]["properties"]["to_schema"]["const"] = 5
|
||||
schemas["MigrationReport"]["description"] = schemas["MigrationReport"]["description"].replace("V1/V2/V3 to V4", "V1/V2/V3/V4 to V5")
|
||||
spec["info"]["description"] = spec["info"]["description"].replace("schema-4", "schema-5")
|
||||
spec["paths"]["/api/v1/projects/import"]["post"]["description"] = "Schema-5 native JSON is described below. Schema-1/2/3/4 JSON and source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Prior prose reviews remain intact; absent semantic review stays Unreviewed. Export emits schema 5 / arcadia 5. See docs/language.rst."
|
||||
schemas["SourceBundle"]["description"] = schemas["SourceBundle"]["description"].replace("source4 workspace (source3 migrates explicitly)", "source5 workspace (source3/4 migrate explicitly)")
|
||||
schemas["Drift"] = enum(["Unreviewed", "Unchanged", "OaChanged", "SaChanged", "BothChanged"])
|
||||
schemas["FacetDrift"] = obj({"selector": ref("FacetSelector"), "current": ref("FacetReview"), "drift": ref("Drift")})
|
||||
schemas["SemanticDrift"] = obj({"own": ref("Drift"), "linked": ref("Drift"),
|
||||
"own_unreviewed": typed("bool"), "linked_unreviewed": typed("bool")})
|
||||
schemas["LinkedSemanticImpact"] = obj({"objects": id_keys, "traces": array(ref("Id")),
|
||||
"drift": ref("Drift"), "unreviewed": typed("bool")})
|
||||
for name, members in [("SemanticRecord", ["Element", "Relation", "Artifact"]),
|
||||
("ImpactRecord", ["Element", "Relation", "Artifact", "Diagram", "Trace"])]:
|
||||
schemas[name] = {"oneOf": [variant(n, ref("ArtifactLink" if n == "Artifact" else n)) for n in members]}
|
||||
schemas["SemanticImpact"] = obj({"object": ref("Id"), "before": optional(ref("ImpactRecord")), "after": optional(ref("ImpactRecord"))}, ["object"])
|
||||
schemas["SemanticDecision"] = enum(["KeepTarget", "TakeSource", "Candidate"])
|
||||
schemas["FacetResolution"] = obj({"facet": ref("FacetSelector"), "decision": ref("SemanticDecision")})
|
||||
schemas["SemanticReviewRequest"] = obj({"source": ref("Id"), "resolutions": array(ref("FacetResolution"), 5000),
|
||||
"reset": {**array(ref("FacetSelector"), 5000), "default": []}}, ["source", "resolutions"])
|
||||
|
||||
|
||||
def mappings(maximum):
|
||||
return {"type": "object", "propertyNames": {"format": "uuid"},
|
||||
"additionalProperties": ref("Id"), "maxProperties": maximum, "default": {}}
|
||||
|
||||
|
||||
schemas["SemanticTransitionRequest"] = obj({"transition": ref("TransitionRequest"), "traces": mappings(100),
|
||||
"references": mappings(5000), "reviews": {**array(ref("SemanticReviewRequest"), 100), "default": []},
|
||||
"candidates": {**array(ref("SemanticRecord"), 200), "default": []},
|
||||
"accepted_impacts": {**array(ref("SemanticImpact"), 200), "default": []}}, ["transition"])
|
||||
schemas["SemanticTransitionRequest"]["description"] = "Canonical OA-to-SA references include historical UUIDs. Traces map selected source IDs to trace IDs. Combined resolutions/reset limit: 5,000. Candidates must equal exactly the result of their selected facets; accepted_impacts explicitly acknowledge exact induced changes."
|
||||
schemas["SemanticReviewPreview"] = obj({"source": ref("Id"), "target": ref("Id"), "facets": array(ref("FacetDrift")),
|
||||
"acknowledged": array(ref("FacetSelector")), "retired": array(ref("FacetSelector")), "linked_impacts": array(ref("LinkedSemanticImpact"))})
|
||||
schemas["SemanticTransitionPlan"] = obj({"base_fingerprint": typed("String"), "request": ref("SemanticTransitionRequest"),
|
||||
"transition": ref("TransitionPlan"), "reviews": array(ref("SemanticReviewPreview")), "impacts": array(ref("SemanticImpact")),
|
||||
"conflicts": array(typed("String")), "commands": array(ref("Command"), 200), "prepared_fingerprint": optional(typed("String"))})
|
||||
schemas["SemanticTransitionPlan"]["required"].remove("prepared_fingerprint")
|
||||
schemas["SemanticTransitionEnvelope"] = obj({"base_revision": ref("Revision"), "plan": ref("SemanticTransitionPlan")})
|
||||
schemas["SemanticQueryRequest"] = obj({"traces": {**array(ref("Id"), 100), "minItems": 1, "uniqueItems": True},
|
||||
"references": mappings(5000), "revision": optional(ref("Revision"))}, ["traces"])
|
||||
schemas["SemanticQueryItem"] = obj({"trace": ref("Id"), "oa": ref("Id"), "sa": ref("Id"),
|
||||
"facets": array(ref("FacetDrift")), "drift": ref("SemanticDrift"), "linked_impacts": array(ref("LinkedSemanticImpact"))})
|
||||
schemas["SemanticQueryResult"] = obj({"revision": ref("Revision"), "items": array(ref("SemanticQueryItem"), 100)})
|
||||
for operation, request, result in [("query", "SemanticQueryRequest", "SemanticQueryResult"),
|
||||
("preview", "SemanticTransitionRequest", "SemanticTransitionEnvelope"),
|
||||
("apply", "SemanticTransitionEnvelope", "MutationResult")]:
|
||||
endpoint = copy.deepcopy(spec["paths"]["/api/v1/projects/{id}/transitions/" + ("apply" if operation == "apply" else "preview")])
|
||||
post = endpoint["post"]
|
||||
post["operationId"] = operation + "SemanticTransition"
|
||||
post["summary"] = {"query": "Query typed trace drift and linked impacts without mutation",
|
||||
"preview": "Preview and source-validate a typed semantic transition without writing",
|
||||
"apply": "Conditionally apply a prepared semantic candidate in one model/source revision"}[operation]
|
||||
post["requestBody"]["content"]["application/json"]["schema"] = ref(request)
|
||||
post["responses"]["200"]["content"]["application/json"]["schema"] = ref(result)
|
||||
post["description"] = "Uses the frozen core typed semantic contract. A supplied plan is a client-owned candidate, not a signed receipt; fingerprints check consistency, not original review authorship. Preview validates source writeback when conflict-free. Apply preserves preview-generated IDs and does not replan random identities."
|
||||
if operation == "query":
|
||||
post["parameters"] = [p for p in post["parameters"] if p["in"] != "header"]
|
||||
post["responses"].pop("412", None)
|
||||
post["responses"].pop("428", None)
|
||||
post["responses"]["409"] = {"$ref": "#/components/responses/Problem"}
|
||||
post["description"] = "Read one immutable snapshot. Optional revision mismatch returns409; missing trace returns404; duplicate/empty trace selection returns400. References are canonical OA-to-SA correspondence. No write, baseline refresh or external artifact fetch."
|
||||
spec["paths"]["/api/v1/projects/{id}/semantic-transitions/" + operation] = endpoint
|
||||
|
||||
|
||||
def check_refs(value):
|
||||
if isinstance(value, dict):
|
||||
if "$ref" in value and value["$ref"].startswith("#/components/schemas/"):
|
||||
assert value["$ref"].split("/")[-1] in schemas, value
|
||||
for child in value.values():
|
||||
check_refs(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
check_refs(child)
|
||||
|
||||
|
||||
check_refs(spec)
|
||||
rendered = json.dumps(spec, indent=2, ensure_ascii=False) + "\n"
|
||||
if "--check" in sys.argv:
|
||||
assert json.loads(PATH.read_text(encoding="utf-8")) == spec, "Run semantic_schema.py to refresh OpenAPI"
|
||||
else:
|
||||
PATH.write_text(rendered, encoding="utf-8")
|
||||
print(f"Closed catalog: {len(families)} families, {leaf_count} scalar leaves; {len(schemas)} schemas")
|
||||
Reference in New Issue
Block a user