Projection#

Turning models into triples and back.

The northbound projection: hiding protocol metadata the ontology declares.

A parameter node bundles two kinds of fact. What the value means — its magnitude, its unit, whether a peer may write it — is northbound content. How the middleware physically reaches the device — a broker address, a topic, an OPC-UA endpoint — is southbound only: a peer holding it could drive the machine directly and bypass every check the middleware performs.

They are not separable by asking for less. PropertySpec._resolve_effective_ranges merges the whole rdfs:subPropertyOf* chain and there is no merge-depth parameter anywhere in the OGM’s API, so the shape a consumer gets always contains everything every level declares. The middleware must therefore cut the bundle itself, and it does so on the shape, before any data is read.

What gets cut is decided by the ontology, not by the code. Walking upward from one parameter property:

level

contributes

verdict

the parameter property’s own range

value, unit

keep

protocol markers between it and the root

broker, topic, …

delete

the interface root’s own range

inf:accessMode

keep

Deriving the delete set from the registry instead — the union of what the registered binding descriptors declare — was tried and is wrong: it only knows the protocols this middleware happens to have code for. Measured, on a belt made reachable over both MQTT and OPC-UA with no OPC-UA binding registered, the registry-derived set removed the MQTT metadata and served inf:hasOPCUAEndpoint with its address. Asking the ontology finds it, because the ontology is authoritative about what a protocol parameter is whether or not anyone wrote a connector.

Recomputed at every startup. Consuming middlewares are decentralized and live in domain experts’ packages; the ontology may have grown a protocol since one of them last looked.

A keep-list — naming what is safe and dropping the rest — was considered and rejected. It reads as the safer construction, but it is a closed-world assertion in the serving path, and the architecture has exactly one closed-world moment by design – SHACL at admission. It would also hide new legitimate domain content by default, taxing twenty domain engineers to guard something the OGM write path already governs.

exception kapps_semantic_middleware.projection.ProjectionError[source]#

Bases: RuntimeError

The projection could not prove a payload safe, so nothing is served.

Raised rather than logged. A projection that cannot determine what to hide has exactly one safe behaviour, and continuing is not it: the failure would surface as a broker address on a public REST route.

kapps_semantic_middleware.projection.southbound_properties(ogm: Any, parameter_property: Any, *, interface_root: Any = IRI('https://www.sfb1574.kit.edu/ontologies/CrcInterfaces#isInterfaceAccessibleParameter')) → frozenset[source]#

The properties a protocol marker contributes to one parameter’s shape.

Empty for a property that is not interface-accessible at all, which is the ordinary answer for a plain object property like tu:hasConveyorBelt.

kapps_semantic_middleware.projection.prune_southbound(class_spec: Any, *, ogm: Any, interface_root: Any = IRI('https://www.sfb1574.kit.edu/ontologies/CrcInterfaces#isInterfaceAccessibleParameter'), cache: Dict[str, frozenset] | None = None) → Any[source]#

Return a copy of class_spec with each parameter’s protocol metadata removed.

Recurses, because a resource’s parameters hang off its components. Every nested spec is asked the ontology what its own property contributes southbound, so two parameters reached over different protocols are each pruned correctly.

The input is left untouched: the caller needs both shapes at once — the full one for the bindings to read broker addresses out of, the pruned one to serve.

Copying is deliberately shallow, per spec node, never copy.deepcopy. A spec’s property keys are IRI, which subclasses rdflib.URIRef, which subclasses str — deep-copying one reconstructs it through str.__reduce_ex__ and yields a plain URIRef, which still compares equal but has silently lost the lined accessor ClassSpec.to_pydantic_model needs.

cache maps property IRI to its southbound set. Pass one in to have it filled and reused — every entry is a live SPARQL round trip, and the caller usually needs the same answers again for the binding cross-check.

kapps_semantic_middleware.projection.carries_southbound(value: Any, southbound: Collection[str]) → Set[str][source]#

Which of the given protocol properties appear anywhere in a materialized payload.

The projection’s assertion, usable as a guard or in a test: a northbound payload must contain none of them. A generated model’s field names are IRI-mangled, so this matches the mangled form as well as the raw IRI — the former is what a served JSON body carries, the latter what a spec or a graph dump does.

Mangling goes through IRI.lined, the OGM’s own field-name derivation, rather than a local copy of the rule: a detector that mirrored it could drift and start reporting “clean” for a payload that leaks.

kapps_semantic_middleware.projection.cross_check(parameter_property: Any, ontology_declared: Collection[str], descriptor_declared: Collection[Any] | None) → None[source]#

Warn when a binding’s declared metadata and the ontology’s disagree.

The ontology decides what is hidden; a descriptor’s connection_metadata says what that binding reads. They should coincide, and where they do not, one of the two has drifted:

  • In the ontology only — the protocol contract grew a term this binding ignores. Safe northbound (it is hidden either way), but the connector may be missing configuration.

  • In the descriptor only — the code expects a term the ontology does not declare. That term will not survive a write and will not reach the connector, so the parameter may come up silently dead. This is the direction that costs debugging time.

Warned, never raised: a drift in either direction is a real deployment state, and the projection is already safe because it follows the ontology.