Files
ietf-draft-analyzer/workspace/act/act/dag.py
Christian Nennemann 2506b6325a
Some checks failed
CI / test (3.11) (push) Failing after 1m37s
CI / test (3.12) (push) Failing after 57s
feat: add draft data, gap analysis report, and workspace config
2026-04-06 18:47:15 +02:00

137 lines
4.2 KiB
Python

"""ACT DAG validation for Phase 2 execution records.
Validates the directed acyclic graph formed by par (parent) references
in Phase 2 ACTs, ensuring uniqueness, parent existence, temporal ordering,
acyclicity, and capability consistency.
Reference: ACT §7 (DAG Structure and Causal Ordering).
"""
from __future__ import annotations
from typing import Protocol
from .errors import ACTCapabilityError, ACTDAGError
from .token import ACTRecord
# Maximum ancestor traversal limit for cycle detection — ACT §7.1 step 4.
MAX_TRAVERSAL_LIMIT: int = 10_000
# Clock skew tolerance for temporal ordering — ACT §7.1 step 3.
DAG_CLOCK_SKEW_TOLERANCE: int = 30
class ACTStore(Protocol):
"""Protocol for an ACT store used in DAG validation.
Any object implementing get() and has() can serve as the store.
The ACTLedger in ledger.py implements this protocol.
"""
def get(self, jti: str) -> ACTRecord | None:
"""Retrieve a Phase 2 ACT record by jti."""
...
def validate_dag(
record: ACTRecord,
store: ACTStore,
*,
clock_skew_tolerance: int = DAG_CLOCK_SKEW_TOLERANCE,
) -> None:
"""Validate the DAG constraints for a Phase 2 execution record.
Performs all five DAG validation checks defined in ACT §7.1:
1. jti uniqueness within wid scope (or globally)
2. Parent existence in store
3. Temporal ordering with clock skew tolerance
4. Acyclicity (max traversal limit)
5. Capability consistency (exec_act matches cap[].action)
Reference: ACT §7.1 (DAG Validation).
Args:
record: The Phase 2 ACTRecord to validate.
store: An ACT store providing get() for parent lookup.
clock_skew_tolerance: Seconds of allowed clock skew (default 30).
Raises:
ACTDAGError: If any DAG constraint is violated.
ACTCapabilityError: If exec_act does not match cap actions.
"""
# Step 1: jti uniqueness — ACT §7.1 step 1
existing = store.get(record.jti)
if existing is not None:
raise ACTDAGError(
f"Duplicate jti {record.jti!r} already exists in store"
)
# Step 5: Capability consistency — ACT §7.1 step 5
cap_actions = {c.action for c in record.cap}
if record.exec_act not in cap_actions:
raise ACTCapabilityError(
f"exec_act {record.exec_act!r} does not match any "
f"cap[].action: {sorted(cap_actions)}"
)
# Step 2 & 3: Parent existence and temporal ordering
for parent_jti in record.par:
parent = store.get(parent_jti)
if parent is None:
raise ACTDAGError(
f"Parent jti {parent_jti!r} not found in store"
)
# Temporal ordering: parent.exec_ts < child.exec_ts + tolerance
if parent.exec_ts >= record.exec_ts + clock_skew_tolerance:
raise ACTDAGError(
f"Temporal ordering violation: parent {parent_jti!r} "
f"exec_ts={parent.exec_ts} >= child exec_ts="
f"{record.exec_ts} + tolerance={clock_skew_tolerance}"
)
# Step 4: Acyclicity — ACT §7.1 step 4
_check_acyclicity(record.jti, record.par, store)
def _check_acyclicity(
current_jti: str,
parent_jtis: list[str],
store: ACTStore,
) -> None:
"""Check that following par references does not lead back to current_jti.
Uses breadth-first traversal with a maximum node limit.
Reference: ACT §7.1 step 4.
Raises:
ACTDAGError: If a cycle is detected or traversal limit exceeded.
"""
visited: set[str] = set()
queue: list[str] = list(parent_jtis)
nodes_visited = 0
while queue:
if nodes_visited >= MAX_TRAVERSAL_LIMIT:
raise ACTDAGError(
f"DAG traversal limit ({MAX_TRAVERSAL_LIMIT}) exceeded; "
f"possible cycle or excessively deep DAG"
)
jti = queue.pop(0)
if jti == current_jti:
raise ACTDAGError(
f"DAG cycle detected: jti {current_jti!r} appears in "
f"its own ancestor chain"
)
if jti in visited:
continue
visited.add(jti)
nodes_visited += 1
parent = store.get(jti)
if parent is not None:
queue.extend(parent.par)