"""Refresh the closed semantic OpenAPI catalog from the frozen Rust leaf table. Run from any directory with Python 3: semantic_schema.py [--check] [--validate]. --validate additionally runs source-6 structural fixtures with jsonschema. 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") views = (ROOT / "crates/oa-core/src/view_options.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): name = name.replace("crate::view_options::", "") 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): name = name.replace("crate::view_options::", "") 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", "ExchangeFamily", "Coverage", "ExchangeLabels", "Kind", "RelationKind"]: source = next(s for s in [types, model, views] if f"pub enum {name}" in s) 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) == 14 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 == 85 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", "Fragment": "Option"} 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")})), variant("Literal", obj({"keys": text_keys, "review": reviewed("bool")})), variant("Annotation", obj({"keys": annotation_keys, "review": reviewed("Option")})), variant("Binding", obj({"keys": id_keys, "review": reviewed("Option")})), 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"] = 6 schemas["MigrationReport"]["properties"]["from_schema"]["enum"] = [1, 2, 3, 4, 5] schemas["MigrationReport"]["properties"]["to_schema"]["const"] = 6 schemas["MigrationReport"]["description"] = re.sub(r"V1(?:/V[2-5])+ to V[4-6]", "V1/V2/V3/V4/V5 to V6", schemas["MigrationReport"]["description"]) spec["info"]["description"] = re.sub(r"schema-[45]", "schema-6", spec["info"]["description"]) spec["paths"]["/api/v1/projects/import"]["post"]["description"] = "Schema-6 native JSON is described below. Schema-1/2/3/4/5 JSON and source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Earlier versions reject source-6 constructors and saved view fields. Prior prose and typed reviews remain intact; absent semantic review stays Unreviewed. Export emits schema 6 / arcadia 6. See docs/language.rst." schemas["SourceBundle"]["description"] = re.sub(r"source[45] workspace \(source[^)]+\)", "source6 workspace (source3/4/5 migrate explicitly)", schemas["SourceBundle"]["description"]) # Source-6 additions are closed serde records; selectors with an empty payload # remain explicit selections rather than reverting to their omitted defaults. schemas["EdgeKind"] = {"oneOf": [enum(["Trace"]), variant("Relation", ref("RelationKind"))]} schemas["RelationKinds"] = {"oneOf": [enum(["All"]), variant("Only", array(ref("EdgeKind")))]} for selection in ["CategorySelection", "ContextSelection"]: schemas[selection] = {"oneOf": [enum(["All"]), variant("Any", array(ref("Id")))]} schemas["ViewOptions"] = obj({ "hidden_edges": {**array(ref("Id")), "default": []}, "relation_kinds": {**ref("RelationKinds"), "default": "All"}, "work_categories": {**ref("CategorySelection"), "default": "All"}, "participant_categories": {**ref("CategorySelection"), "default": "All"}, "coverage": {**ref("Coverage"), "default": "All"}, "work_contexts": {**ref("ContextSelection"), "default": "All"}, "exchange_labels": {**ref("ExchangeLabels"), "default": "Name"}}, []) schemas["ViewOptions"]["description"] = "Presentation only. Independent predicates intersect; category members union within a family. Only([]) hides all relations and traces. Any([]) remains an explicit empty selection. Traceability permits both-layer hidden/category/context references; ordinary projections require the diagram layer. Filtering preserves unfiltered automatic port positions." schemas["Diagram"]["properties"]["view_options"] = {**ref("ViewOptions"), "default": {}} def upsert_variant(variants, name, value): variants[:] = [v for v in variants if name not in v.get("properties", {})] variants.append(variant(name, value)) upsert_variant(schemas["RelationBody"]["oneOf"], "CommunicationMean", obj({ "source": ref("Id"), "target": ref("Id"), "oriented": typed("bool"), "items": array(ref("Id"))})) upsert_variant(schemas["SharedBody"]["oneOf"][1]["oneOf"], "ExchangeCategory", obj({ "family": ref("ExchangeFamily"), "exchanges": array(ref("Id"))})) schemas["CommunicationPortChoice"] = {"oneOf": [ variant("Reuse", obj({"port": ref("Id")})), variant("Create", obj({"id": ref("Id"), "alias": typed("String"), "name": typed("String"), "description": typed("String"), "kind": ref("ComponentPortKind"), "orientation": ref("ComponentPortOrientation")}))]} schemas["CommunicationEndpointDecision"] = obj({"source": ref("Id"), "source_port": ref("CommunicationPortChoice"), "target_port": ref("CommunicationPortChoice")}) schemas["TransitionRequest"]["properties"]["communication_endpoints"] = { **array(ref("CommunicationEndpointDecision"), 200), "default": [], "description": "Explicit SA component-port pair per selected OA CommunicationMean. Reuse or Create only. Owners follow reviewed participant mappings; no participant-to-port substitution or automatic interface allocation."} 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) if "--validate" in sys.argv: from jsonschema import Draft202012Validator, FormatChecker for schema in schemas.values(): Draft202012Validator.check_schema(schema) fixture_count = 0 def accepts(name, value, expected=True): global fixture_count validator = Draft202012Validator({**ref(name), "components": spec["components"]}, format_checker=FormatChecker()) errors = list(validator.iter_errors(value)) assert (not errors) == expected, (name, value, [e.message for e in errors]) fixture_count += 1 a, b = "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" mean = {"source": a, "target": b, "oriented": False, "items": []} accepts("RelationBody", {"CommunicationMean": mean}) accepts("RelationBody", {"CommunicationMean": {**mean, "typo": True}}, False) for field in mean: accepts("RelationBody", {"CommunicationMean": {k: v for k, v in mean.items() if k != field}}, False) category = {"family": "Participant", "exchanges": [a]} accepts("SharedBody", {"ExchangeCategory": category}) accepts("SharedBody", {"ExchangeCategory": {**category, "family": "Future"}}, False) accepts("SharedBody", {"ExchangeCategory": {**category, "typo": []}}, False) accepts("ViewOptions", {}) options = {"hidden_edges": [a], "relation_kinds": {"Only": ["Trace", {"Relation": "CommunicationMean"}]}, "work_categories": {"Any": []}, "participant_categories": {"Any": [b]}, "coverage": "OutsideBehavior", "work_contexts": {"Any": []}, "exchange_labels": "NameAndItems"} accepts("ViewOptions", options) accepts("ViewOptions", {**options, "relation_kinds": {"Only": []}}) for field, value in [("typo", []), ("coverage", "Unknown"), ("hidden_edges", ["not-an-id"]), ("relation_kinds", {"Only": [], "typo": []}), ("relation_kinds", {"Only": [{"Relation": "Future"}]}), ("work_contexts", {"Any": [], "typo": []}), ("exchange_labels", "Names")]: accepts("ViewOptions", {**options, field: value}, False) create = {"id": a, "alias": "radio", "name": "Radio", "description": "", "kind": "Standard", "orientation": "Unset"} accepts("CommunicationPortChoice", {"Create": create}) accepts("CommunicationPortChoice", {"Reuse": {"port": a}}) accepts("CommunicationPortChoice", "Auto", False) accepts("CommunicationPortChoice", {"Reuse": {"port": a, "owner": b}}, False) for field in create: accepts("CommunicationPortChoice", {"Create": {k: v for k, v in create.items() if k != field}}, False) pair = {"source": a, "source_port": {"Create": create}, "target_port": {"Reuse": {"port": b}}} accepts("CommunicationEndpointDecision", pair) accepts("CommunicationEndpointDecision", {k: v for k, v in pair.items() if k != "target_port"}, False) request = {"direction": "OaToSa", "sources": [a], "scope": "SelectedOnly", "mappings": [], "fields": [], "resolutions": [], "destination_diagram": None} accepts("TransitionRequest", request) accepts("TransitionRequest", {**request, "communication_endpoints": [pair]}) accepts("TransitionRequest", {**request, "communication_endpoints": [pair] * 200}) accepts("TransitionRequest", {**request, "communication_endpoints": [pair] * 201}, False) accepts("FacetSelector", {"Category": "Family"}) accepts("FacetSelector", {"Category": "Unknown"}, False) accepts("FacetReview", {"Category": {"Family": {"oa": {"Value": "Work"}, "sa": {"Value": "Participant"}}}}) accepts("FacetSelector", {"Member": {"collection": "CategoryExchanges", "keys": {"oa": a, "sa": b}}}) assert schemas["Project"]["properties"]["schema_version"]["const"] == 6 assert schemas["MigrationReport"]["properties"]["from_schema"]["enum"] == [1, 2, 3, 4, 5] print(f"Source-6 schema fixtures: {fixture_count} passed; all {len(schemas)} component schemas well-formed") 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")