Source-system model validation

Validated with documented limitations.

1. The component

rfgen.scene.source_system is the immutable version-1 record boundary for a source system: named devices and their permitted directed links. It is not a population generator, event planner, trajectory resolver, or waveform generator. A topology is the graph of which device can communicate with which other device. The model records that graph before later components assign movement, emissions, or samples.

def namespace_hash(namespace: str, identifier: str) -> str: ...

class SourceDevice(BaseModel):
    device_id: str
    role: Literal["aircraft", "controller", "ground_station", "relay", "unknown"]
    pose: GeometryPose | None = None
    trajectory_ref: str | None = None

class SourceLink(BaseModel):
    link_id: str
    source_device: str
    target_device: str
    direction: Literal["uplink", "downlink", "peer"]

class SourceSystem(BaseModel):
    schema_version: Literal[1] = 1
    system_id: str
    devices: tuple[SourceDevice, ...]
    links: tuple[SourceLink, ...]

Field or operation

Contract

Purpose

namespace_hash

NFC (Unicode’s composed text form)-normalized UTF-8 (a text-to-byte encoding) namespace, a NUL (zero-byte) separator, and identifier are SHA-256 hashed

Produces a stable 64-character lowercase ID that is separated by namespace.

ID fields

Accept only a lowercase 64-hex digest representation; do not recompute a namespace/identifier preimage

Keep supplied system, device, and link IDs opaque while rejecting malformed representations.

SourceDevice geometry

Exactly one of pose or trajectory_ref

A device has either typed static geometry or an opaque reference to later trajectory ownership.

SourceLink.direction

uplink, downlink, or peer

States the directed relationship checked by the v1 role predicate.

SourceSystem

Frozen schema-version-1 graph

Rejects duplicate device/link IDs, missing endpoints, self-links, and incompatible roles.

The following creates the smallest directional system. A pose is a typed static location and orientation; the trajectory reference remains an opaque identifier owned by a later component.

controller = SourceDevice(
    device_id=namespace_hash("example", "controller"),
    role="controller",
    pose=GeometryPose(position_m=(0.0, 0.0, 0.0), orientation_rad=(0.0, 0.0, 0.0)),
)
aircraft = SourceDevice(
    device_id=namespace_hash("example", "aircraft"),
    role="aircraft",
    trajectory_ref="trajectory-reference-example-1",
)
uplink = SourceLink(
    link_id=namespace_hash("example", "uplink"),
    source_device=controller.device_id,
    target_device=aircraft.device_id,
    direction="uplink",
)
system = SourceSystem(
    system_id=namespace_hash("example", "system"),
    devices=(controller, aircraft),
    links=(uplink,),
)
assert system.schema_version == 1

2. What we validated

This validation establishes four load-bearing claims. Each is restated and supported by evidence in section 3.

  1. Canonical identifiers (§3.1): equivalent Unicode names have one stable ID representation.

  2. Immutable geometry boundary (§3.2): each device has one declared geometry reference.

  3. Directed role predicate (§3.3): every v1 role-pair/direction outcome is enforced.

  4. Closed graph integrity (§3.4): invalid graph references and duplicate IDs fail closed.

Limits and scope-bounded items appear in section 4; full citations are in section 5.

3. Evidence per claim

3.1 Canonical identifiers

Claim. namespace_hash returns one unambiguous, stable SHA-256 identifier for equivalent NFC, Unicode’s composed text form, inputs and rejects empty or NUL-containing components. Separately, the record ID fields accept only the lowercase 64-hex digest representation; they validate that format without recomputing a namespace/identifier preimage. NUL is a zero-byte separator.

Evidence. tests/unit/test_source_system.py::test_namespace_hash_uses_documented_nfc_utf8_separator_formula compares the result for decomposed cafe\u0301 with the independently computed SHA-256 digest of NFC café, a NUL separator, and device. UTF-8, the text-to-byte encoding used before hashing, gives that byte sequence a defined representation. The same test asserts equality for the composed and decomposed spellings. Its companion parameterized test rejects an empty namespace, an empty identifier, and a NUL inside a component. SHA-256 is the Secure Hash Algorithm 256-bit digest defined by FIPS 180-4 [1].

SourceDevice, SourceLink, and SourceSystem each validate supplied IDs as exactly 64 lowercase hexadecimal characters. That representation check is deliberately distinct from calling namespace_hash: it rejects malformed IDs but treats a well-formed supplied digest as opaque. The closed-vocabulary unit test rejects a raw device ID, while test_id_fields_accept_opaque_canonical_digests_without_preimage_provenance directly proves that unrelated well-formed digests are accepted without an available namespace/identifier preimage.

3.2 Immutable geometry boundary

Claim. A source device stores exactly one geometry reference and cannot be mutated after validation.

Evidence. tests/unit/test_source_system.py::test_source_system_serializes_v1_immutable_graph_with_static_and_opaque_geometry constructs one device with a GeometryPose and one with a nonblank opaque trajectory reference, then verifies the complete JSON-compatible schema-version-1 serialization. The test also attempts to replace a frozen model field and receives Pydantic’s validation error. test_device_requires_one_unresolved_geometry_reference rejects both the zero-reference and two-reference cases. This is a record boundary: the opaque reference has no required URI syntax and does not resolve a trajectory; the typed pose does not claim time-varying movement.

3.3 Directed role predicate

Claim. The v1 role predicate accepts all 25 peer role pairs, exactly one uplink pair (controller to aircraft), and exactly one downlink pair (aircraft to controller), while rejecting the other 48 of 75 outcomes.

Evidence. tests/validation/source_system/test_link_predicate_outcomes.py::test_link_predicate_has_the_documented_outcome_for_every_role_pair_and_direction is parameterized over the five roles, five target roles, and three directions: 5 × 5 × 3 = 75 independently constructed SourceSystem graphs. It accepts 27 graphs and asserts the structured source_link_invalid error, including the specific link ID, for the other 48. The companion cardinality test fixes the matrix at 75 outcomes, 27 accepted and 48 rejected. This test checks the public graph constructor rather than calling the private predicate directly.

3.4 Closed graph integrity

Claim. A system cannot contain duplicate identities, a self-link, or a link whose endpoint is not a member of that system.

Evidence. tests/unit/test_source_system.py::test_invalid_link_predicates_raise_structured_validation_error checks a self-link, a missing endpoint, and two reversed directional pairs; each returns the machine-readable source_link_invalid code and offending link ID. test_duplicate_device_and_link_ids_are_rejected_without_population_behaviour checks duplicate device and link IDs, including the separate source_device_invalid code for duplicate devices. These are identity and topology constraints only; they do not establish physical radio propagation.

4. Limits and what is not validated

  • The role predicate is policy, not a radio-protocol model. It records the deliberately narrow version-1 controller/aircraft direction convention and accepts all peer pairs; it does not establish that a protocol, spectrum allocation, antenna, or physical link is feasible.

  • SHA-256 namespace hashing is deterministic, not a global identity proof. It prevents ambiguity within the stated canonicalization rule but does not authenticate an origin, prevent a caller from reusing an identifier, or resolve entity identity across external registries.

  • A model-accepted digest does not prove its unseen preimage. The ID fields establish only the canonical lowercase 64-hex format. They cannot establish which namespace and identifier produced a supplied digest, whether it was produced by namespace_hash, or whether that unseen pair was authorized.

  • Geometry is descriptive only. Static poses and opaque trajectory references are validated for shape and exclusivity, not for coordinate-frame correctness, existence, kinematics, or temporal consistency.

  • The test matrix covers version-1 roles only. A future role or direction requires an explicit predicate and a revised exhaustive matrix; no outcome is inferred for values outside this closed vocabulary.

  • No signal-generation behavior is validated here. Population ownership, event planning, trajectory resolution, propagation, and waveform generation are outside this component boundary.

5. References

  1. National Institute of Standards and Technology, Secure Hash Standard (SHS), FIPS PUB 180-4, August 2015, FIPS 180-4 publication page. Defines SHA-256, the digest used by namespace_hash.

  2. Pydantic, PyPI distribution pydantic, installed version 2.13.3. Pydantic documentation. Supplies the frozen schema models and model-validation behavior exercised by the tests.

  3. pytest, PyPI distribution pytest, installed version 9.0.2. pytest documentation. Executes the 75-case exhaustive role-pair/direction validation and the unit tests.