|
|
@@ -148,7 +148,12 @@ class CandidateLedger(ApplicationModel):
|
|
|
if len(operation_ids) != len(set(operation_ids)):
|
|
|
raise ValueError("Candidate ledger contains duplicate operation IDs")
|
|
|
known = set(identities)
|
|
|
+ parents_by_candidate: dict[
|
|
|
+ tuple[str, int], tuple[tuple[str, int], ...]
|
|
|
+ ] = {}
|
|
|
for candidate in self.candidates:
|
|
|
+ candidate_key = (candidate.candidate_id, candidate.revision)
|
|
|
+ parent_keys: list[tuple[str, int]] = []
|
|
|
for parent in candidate.parent_refs:
|
|
|
parent_key = (parent.candidate_id, parent.revision)
|
|
|
if parent_key not in known:
|
|
|
@@ -158,6 +163,25 @@ class CandidateLedger(ApplicationModel):
|
|
|
and parent.revision >= candidate.revision
|
|
|
):
|
|
|
raise ValueError("Candidate lineage references a future revision")
|
|
|
+ parent_keys.append(parent_key)
|
|
|
+ parents_by_candidate[candidate_key] = tuple(parent_keys)
|
|
|
+
|
|
|
+ visited: set[tuple[str, int]] = set()
|
|
|
+ visiting: set[tuple[str, int]] = set()
|
|
|
+
|
|
|
+ def visit(candidate_key: tuple[str, int]) -> None:
|
|
|
+ if candidate_key in visiting:
|
|
|
+ raise ValueError("Candidate ledger contains a lineage cycle")
|
|
|
+ if candidate_key in visited:
|
|
|
+ return
|
|
|
+ visiting.add(candidate_key)
|
|
|
+ for parent_key in parents_by_candidate[candidate_key]:
|
|
|
+ visit(parent_key)
|
|
|
+ visiting.remove(candidate_key)
|
|
|
+ visited.add(candidate_key)
|
|
|
+
|
|
|
+ for candidate_key in parents_by_candidate:
|
|
|
+ visit(candidate_key)
|
|
|
return self
|
|
|
|
|
|
def candidate(self, pointer: CandidatePointer) -> CandidateRef:
|