Verification Check Catalog¶
GDS runs 13 verification checks across two registries to validate both structural topology and domain semantics. This page is the complete reference for every check.
Overview¶
Verification answers the question: is this specification well-formed? It does not simulate or solve — it validates structure.
There are two independent check registries:
| Registry | Checks | Operates on | What it validates |
|---|---|---|---|
| Generic | G-001 through G-006 | SystemIR |
Structural topology — port matching, acyclicity, dangling references |
| Semantic | SC-001 through SC-007 | GDSSpec |
Domain properties — completeness, determinism, type safety, canonical form |
Generic checks run on the compiled IR (after compile_system()). Semantic checks
run on the specification (the GDSSpec registry). Both produce Finding objects
with a check ID, severity, message, and pass/fail status.
When to run verification¶
- After building a SystemIR — run
verify(system)to check all generic checks - After building a GDSSpec — call individual semantic check functions
- During development — run checks incrementally to catch errors early
- In tests — assert that all checks pass for valid models
Running generic checks¶
from gds import compile_system, verify
ir = compile_system("My Model", composed_system)
report = verify(ir)
print(f"{report.checks_passed}/{report.checks_total} checks passed")
for finding in report.findings:
if not finding.passed:
print(f" [{finding.severity.value}] {finding.check_id}: {finding.message}")
Running semantic checks¶
Semantic checks are called individually against a GDSSpec:
from gds.verification.spec_checks import (
check_completeness,
check_determinism,
check_parameter_references,
check_type_safety,
check_canonical_wellformedness,
)
spec = build_spec()
findings = []
findings += check_completeness(spec)
findings += check_determinism(spec)
findings += check_parameter_references(spec)
findings += check_type_safety(spec)
findings += check_canonical_wellformedness(spec)
for f in findings:
if not f.passed:
print(f"[{f.severity.value}] {f.check_id}: {f.message}")
Generic Checks (G-001 through G-006)¶
These checks operate on SystemIR — the flat intermediate representation produced
by compile_system(). They validate structural topology without referencing any
domain-specific block types or semantics.
All generic checks run automatically when you call verify(system).
G-001: Domain/Codomain Matching¶
What it checks: For every covariant block-to-block wiring, the wiring label
must be a token-subset of the source block's forward_out or the target block's
forward_in. This ensures that signals flowing forward through the system
reference ports that actually exist on the connected blocks.
Severity: ERROR
Skips: Contravariant wirings (handled by G-003 instead).
Trigger example: Block A outputs "Temperature" but Block B expects
"Pressure". A wiring labeled "humidity" connects them — the label matches
neither side.
from gds.ir.models import BlockIR, FlowDirection, SystemIR, WiringIR
system = SystemIR(
name="Bad Wiring",
blocks=[
BlockIR(name="A", signature=("", "Temperature", "", "")),
BlockIR(name="B", signature=("Pressure", "", "", "")),
],
wirings=[
WiringIR(
source="A", target="B", label="humidity",
direction=FlowDirection.COVARIANT,
),
],
)
Example finding (failure):
Example finding (pass):
Note
For generic checks (G-001..G-006), passing findings retain severity=ERROR — the severity
indicates what would be reported if the check failed. For semantic checks (SC-001..SC-007),
passing findings use severity=INFO. Use the passed field to distinguish pass from fail.
G-002: Signature Completeness¶
What it checks: Every block must have at least one non-empty input slot (forward_in or backward_in) AND at least one non-empty output slot (forward_out or backward_out). A block with no inputs or no outputs is structurally isolated.
Severity: ERROR
Trigger example: A block with a completely empty signature — no ports at all.
system = SystemIR(
name="Incomplete",
blocks=[
BlockIR(name="Valid", signature=("In", "Out", "", "")),
BlockIR(name="Orphan", signature=("", "", "", "")),
],
wirings=[],
)
Example finding (failure):
G-002 and BoundaryActions
G-002 flags BoundaryAction blocks (which have no forward_in by design)
and terminal Mechanism blocks (which may have no forward_out). These are
valid GDS boundaries — expect G-002 failures on them. When testing, either
skip G-002 or accept these as known findings.
G-003: Direction Consistency¶
What it checks: Two validations on every wiring:
A) Flag consistency — the direction, is_feedback, and is_temporal flags
must not contradict each other:
COVARIANT+is_feedback=Trueis a contradiction (feedback implies contravariant flow)CONTRAVARIANT+is_temporal=Trueis a contradiction (temporal implies covariant flow)
B) Contravariant port-slot matching — for CONTRAVARIANT wirings, the label
must be a token-subset of the source's backward_out (signature slot 3) or the
target's backward_in (signature slot 2). This is the backward-flow counterpart
of what G-001 does for covariant wirings.
C) Non-empty backward ports — for CONTRAVARIANT wirings, at least one of
src_bwd_out or tgt_bwd_in must be non-empty. If both are empty, G-003 emits
"CONTRAVARIANT but both backward ports are empty" with passed=False.
Severity: ERROR
Trigger example (flag contradiction):
system = SystemIR(
name="Contradiction",
blocks=[BlockIR(name="A"), BlockIR(name="B")],
wirings=[
WiringIR(
source="A", target="B", label="x",
direction=FlowDirection.COVARIANT,
is_feedback=True, # contradicts COVARIANT
),
],
)
Example finding (flag contradiction):
Trigger example (contravariant mismatch):
system = SystemIR(
name="Mismatch",
blocks=[
BlockIR(name="A", signature=("", "", "", "Cost")),
BlockIR(name="B", signature=("", "", "Reward", "")),
],
wirings=[
WiringIR(
source="A", target="B", label="unrelated",
direction=FlowDirection.CONTRAVARIANT,
is_feedback=True,
),
],
)
Example finding (contravariant mismatch):
G-004: Dangling Wirings¶
What it checks: Every wiring's source and target must reference a block
or input that exists in the system. A wiring pointing to a non-existent block is
dangling — either a typo or a missing block.
Severity: ERROR
Recognized endpoints: Block names (system.blocks) and input names
(system.inputs). Inputs are valid wiring sources (they represent exogenous signals
entering the system).
Trigger example:
system = SystemIR(
name="Dangling",
blocks=[BlockIR(name="B", signature=("Signal", "", "", ""))],
wirings=[
WiringIR(
source="Ghost", target="B", label="signal",
direction=FlowDirection.COVARIANT,
),
],
)
Example finding (failure):
G-005: Sequential Type Compatibility¶
What it checks: In stack (sequential) composition, the wiring label must be a
token-subset of BOTH the source's forward_out AND the target's forward_in.
This is stricter than G-001, which only requires the label to match one side.
G-005 enforces that the types are compatible on both ends of a sequential connection.
Severity: ERROR
Skips: Temporal wirings (is_temporal=True), contravariant wirings, and wirings where either endpoint is not in the block set (e.g., InputIR endpoints).
Additional failure mode: If either src_out or tgt_in is empty (the block has no forward ports), G-001 emits "Cannot verify domain/codomain: ..." with passed=False.
Trigger example:
system = SystemIR(
name="Incompatible",
blocks=[
BlockIR(name="A", signature=("", "X", "", "")),
BlockIR(name="B", signature=("Y", "", "", "")),
],
wirings=[
WiringIR(
source="A", target="B", label="z",
direction=FlowDirection.COVARIANT,
),
],
)
Example finding (failure):
G-006: Covariant Acyclicity¶
What it checks: The covariant (forward) flow graph must be a directed acyclic graph (DAG). A cycle in the covariant graph means an algebraic loop within a single timestep — Block A depends on Block B which depends on Block A, with no temporal delay to break the cycle.
Severity: ERROR
Excludes: Temporal wirings (is_temporal=True) and contravariant wirings.
These are legitimate backward or cross-timestep connections that do not create
algebraic loops.
Detection method: DFS-based cycle detection on the adjacency graph of covariant, non-temporal wirings.
Trigger example:
system = SystemIR(
name="Cycle",
blocks=[
BlockIR(name="A", signature=("Signal", "Signal", "", "")),
BlockIR(name="B", signature=("Signal", "Signal", "", "")),
BlockIR(name="C", signature=("Signal", "Signal", "", "")),
],
wirings=[
WiringIR(source="A", target="B", label="signal",
direction=FlowDirection.COVARIANT),
WiringIR(source="B", target="C", label="signal",
direction=FlowDirection.COVARIANT),
WiringIR(source="C", target="A", label="signal",
direction=FlowDirection.COVARIANT),
],
)
Example finding (failure):
Example finding (pass):
Semantic Checks (SC-001 through SC-007)¶
These checks operate on GDSSpec — the specification-level registry. They
validate domain properties that require knowledge of entities, roles, parameters,
and the canonical decomposition.
Semantic checks are called individually (not through verify()).
SC-001: Completeness¶
What it checks: Every entity variable must be updated by at least one
Mechanism. A state variable that no mechanism ever updates is an orphan — it
was declared but will never change, which is almost always a specification error.
Severity: WARNING (orphan variables may be intentional in degenerate cases)
Trigger example:
from gds import GDSSpec, Entity, StateVariable, Policy
from gds.types.typedef import TypeDef
from gds.types.interface import Interface, port
Count = TypeDef(name="Count", python_type=int)
spec = GDSSpec(name="Orphan Demo")
spec.register_type(Count)
spec.register_entity(Entity(
name="Reservoir",
variables={"level": StateVariable(name="level", typedef=Count, symbol="L")},
))
# No mechanism updates Reservoir.level
spec.register_block(Policy(
name="Observe",
interface=Interface(forward_out=(port("Level Signal"),)),
))
Example finding (failure):
Example finding (pass):
SC-002: Determinism¶
What it checks: Within each wiring (a named composition), no two mechanisms
may update the same entity variable. If MechanismA and MechanismB both list
("Counter", "value") in their updates and both appear in the same SpecWiring,
that is a write conflict — the final state is ambiguous.
Severity: ERROR
Trigger example:
spec = GDSSpec(name="Write Conflict Demo")
# ... register Counter entity with "value" variable ...
inc = Mechanism(
name="Increment Counter",
interface=Interface(forward_in=(port("Delta Signal"),)),
updates=[("Counter", "value")],
)
dec = Mechanism(
name="Decrement Counter",
interface=Interface(forward_in=(port("Delta Signal"),)),
updates=[("Counter", "value")],
)
spec.register_block(inc)
spec.register_block(dec)
spec.register_wiring(SpecWiring(
name="Counter Pipeline",
block_names=["Source", "Increment Counter", "Decrement Counter"],
wires=[
Wire(source="Source", target="Increment Counter"),
Wire(source="Source", target="Decrement Counter"),
],
))
Example finding (failure):
[error] SC-002: Write conflict in wiring 'Counter Pipeline': Counter.value updated by ['Increment Counter', 'Decrement Counter']
Example finding (pass):
SC-003: Reachability¶
What it checks: Whether signals can reach from one named block to another through the wiring graph. This maps to the GDS attainability correspondence — can a boundary input ultimately influence a state update?
Severity: WARNING (unreachable blocks may indicate disconnected subgraphs)
Note: Unlike other semantic checks, SC-003 requires two extra arguments
(from_block and to_block). It is not called automatically — you invoke it
for specific block pairs.
from gds.verification.spec_checks import check_reachability
findings = check_reachability(spec, from_block="Sensor", to_block="Update Tank")
Example finding (reachable):
Example finding (unreachable):
SC-004: Type Safety¶
What it checks: Every wire in every SpecWiring that references a space
must reference a space that is registered in the spec. An unregistered space name
on a wire means the data channel is undefined.
Severity: ERROR
Trigger example:
spec.register_wiring(SpecWiring(
name="Pipeline",
block_names=["A", "B"],
wires=[
Wire(source="A", target="B", space="NonExistentSpace"),
],
))
# "NonExistentSpace" is not registered via spec.register_space()
Example finding (failure):
Example finding (pass):
SC-005: Parameter References¶
What it checks: Every params_used entry on every block must correspond to a
parameter registered in the spec's parameter_schema. If a block declares that it
uses parameter "flow_rate" but no such parameter is registered, the reference is
dangling.
Severity: ERROR
Trigger example:
source = BoundaryAction(
name="Source",
interface=Interface(forward_out=(port("Signal"),)),
params_used=["flow_rate"], # references a parameter
)
spec.register_block(source)
# But spec.register_parameter("flow_rate", ...) is never called
Example finding (failure):
Example finding (pass):
SC-006: Canonical Wellformedness — Mechanisms¶
What it checks: The canonical projection (project_canonical(spec)) must
contain at least one mechanism. If no mechanisms exist, the state transition
function f is empty — the system has no state dynamics.
Severity: WARNING (stateless specs like pure game-theoretic models may legitimately have no mechanisms)
Example finding (failure):
Example finding (pass):
SC-007: Canonical Wellformedness — State Space¶
What it checks: The canonical projection must contain at least one state variable. If no entities with variables are defined, the state space X is empty.
Severity: WARNING
Note: SC-006 and SC-007 are produced by the same function
(check_canonical_wellformedness). Call it once to get both findings.
from gds.verification.spec_checks import check_canonical_wellformedness
findings = check_canonical_wellformedness(spec)
# Returns findings for both SC-006 and SC-007
Example finding (failure):
Example finding (pass):
Understanding the Output¶
Finding¶
Every check produces one or more Finding objects:
class Finding(BaseModel):
check_id: str # e.g. "G-001", "SC-002"
severity: Severity # ERROR, WARNING, or INFO
message: str # Human-readable description
source_elements: list[str] # Block/variable names involved
passed: bool # True = check passed, False = violation
exportable_predicate: str # Reserved for formal export
Key points:
passedis the primary field — it tells you whether the check succeeded. A finding withpassed=Trueis informational confirmation.severityindicates the importance level. Generic checks (G-001..G-006) retain their failure severity even on pass. Semantic checks (SC-001..SC-007) emitSeverity.INFOon pass.source_elementsnames the blocks, variables, or wirings involved. Useful for tracing back to the specification.
Severity Levels¶
| Level | Meaning | Action |
|---|---|---|
ERROR |
Structural violation — the model is invalid | Must fix before the model is usable |
WARNING |
Suspicious pattern — may be intentional | Review and either fix or accept |
INFO |
Informational — no action needed | Confirmation that a check passed |
VerificationReport¶
The verify() function returns a VerificationReport:
class VerificationReport(BaseModel):
system_name: str
findings: list[Finding]
@property
def errors(self) -> int: ... # Count of failed ERROR findings
@property
def warnings(self) -> int: ... # Count of failed WARNING findings
@property
def info_count(self) -> int: ... # Count of failed INFO findings
@property
def checks_passed(self) -> int: ... # Count of passed findings
@property
def checks_total(self) -> int: ... # Total number of findings
Typical usage:
Writing Custom Checks¶
Use the @gds_check decorator to register custom verification functions. Custom
checks follow the same Callable[[SystemIR], list[Finding]] signature as the
built-in generic checks.
Registration¶
from gds import gds_check, Finding, Severity
from gds.ir.models import SystemIR
@gds_check("CUSTOM-001", Severity.WARNING)
def check_max_block_count(system: SystemIR) -> list[Finding]:
"""Flag systems with more than 20 blocks."""
count = len(system.blocks)
if count > 20:
return [Finding(
check_id="CUSTOM-001",
severity=Severity.WARNING,
message=f"System has {count} blocks (limit: 20)",
source_elements=[],
passed=False,
)]
return [Finding(
check_id="CUSTOM-001",
severity=Severity.WARNING,
message=f"Block count ({count}) within limit",
source_elements=[],
passed=True,
)]
The decorator:
- Attaches
check_idandseverityas function attributes - Adds the function to a module-level custom check registry
Running custom checks¶
Custom checks do not run automatically with verify(). Use all_checks() to
get the combined list of built-in + custom checks:
Or pass custom checks explicitly:
Retrieving registered checks¶
Filtering and Suppressing Checks¶
Running a subset of checks¶
Pass a specific list to verify():
from gds.verification.generic_checks import (
check_g001_domain_codomain_matching,
check_g004_dangling_wirings,
check_g006_covariant_acyclicity,
)
# Only run the checks you care about
report = verify(system, checks=[
check_g001_domain_codomain_matching,
check_g004_dangling_wirings,
check_g006_covariant_acyclicity,
])
Skipping G-002 for boundary blocks¶
G-002 flags BoundaryAction (no inputs) and terminal Mechanism (no outputs) as
errors. For valid GDS models these are expected. A common pattern in tests:
from gds.verification.generic_checks import (
check_g001_domain_codomain_matching,
check_g003_direction_consistency,
check_g004_dangling_wirings,
check_g005_sequential_type_compatibility,
check_g006_covariant_acyclicity,
)
# All generic checks except G-002
checks_sans_g002 = [
check_g001_domain_codomain_matching,
check_g003_direction_consistency,
check_g004_dangling_wirings,
check_g005_sequential_type_compatibility,
check_g006_covariant_acyclicity,
]
report = verify(system, checks=checks_sans_g002)
Filtering findings after the fact¶
report = verify(system)
# Only look at failures
failures = [f for f in report.findings if not f.passed]
# Only errors (ignore warnings)
errors = [f for f in report.findings if not f.passed and f.severity == Severity.ERROR]
# Group by check ID
from collections import defaultdict
by_check = defaultdict(list)
for f in report.findings:
by_check[f.check_id].append(f)
Intentional edge cases¶
Some findings are expected in valid models:
- SC-001 (orphan state) — WARNING severity. A state variable intentionally held constant (e.g., a fixed capacity) will trigger this. Accept the warning or add a no-op mechanism.
- SC-006/SC-007 (empty canonical) — WARNING severity. Stateless models (pure policy compositions, game-theoretic specs) legitimately have no mechanisms or state variables.
- G-002 (incomplete signature) — ERROR severity but expected on boundary blocks. Skip this check or filter the findings.
Quick Reference¶
| Code | Name | Operates on | Severity | What it validates |
|---|---|---|---|---|
| G-001 | Domain/codomain matching | SystemIR |
ERROR | Covariant wiring label matches source out or target in |
| G-002 | Signature completeness | SystemIR |
ERROR | Every block has at least one input and one output |
| G-003 | Direction consistency | SystemIR |
ERROR | No flag contradictions; contravariant port-slot matching |
| G-004 | Dangling wirings | SystemIR |
ERROR | Wiring endpoints exist in the block/input set |
| G-005 | Sequential type compatibility | SystemIR |
ERROR | Stack wiring label matches both source out AND target in |
| G-006 | Covariant acyclicity | SystemIR |
ERROR | Forward flow graph is a DAG (no algebraic loops) |
| SC-001 | Completeness | GDSSpec |
WARNING | Every entity variable updated by some mechanism |
| SC-002 | Determinism | GDSSpec |
ERROR | No variable updated by multiple mechanisms in same wiring |
| SC-003 | Reachability | GDSSpec |
WARNING | Signal path exists between two named blocks |
| SC-004 | Type safety | GDSSpec |
ERROR | Wire space references resolve to registered spaces |
| SC-005 | Parameter references | GDSSpec |
ERROR | Block params_used match registered parameter names |
| SC-006 | Canonical wellformedness (f) | GDSSpec |
WARNING | At least one mechanism exists (f is non-empty) |
| SC-007 | Canonical wellformedness (X) | GDSSpec |
WARNING | At least one state variable exists (X is non-empty) |