Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
1f67adc
fix: validation gate, termination reason, tests
kangminlee-maker Mar 24, 2026
8acf2aa
feat(evolution): add Gate Guard transition matrix for event validation
kangminlee-maker Mar 22, 2026
8889027
feat(evolution): introduce TerminationReason enum and integrate Gate …
kangminlee-maker Mar 22, 2026
b3cf09d
fix(safety): add StrEnum exhaustiveness guards and cross-validation t…
kangminlee-maker Mar 22, 2026
c32e8a6
feat(convergence): add ontology completeness gate and stagnation safe…
kangminlee-maker Mar 22, 2026
9218ee0
feat(convergence): add wonder gate to block convergence on novel ques…
kangminlee-maker Mar 22, 2026
1a6e6cd
feat(convergence): add drift trend gate and fix evolve_step validatio…
kangminlee-maker Mar 22, 2026
7e6c556
style: remove unused import in guard.py
kangminlee-maker Mar 24, 2026
4f65357
fix: default regression and validation gates to opt-in (disabled)
kangminlee-maker Mar 25, 2026
4a191f4
style: remove unused imports in test files
kangminlee-maker Mar 25, 2026
9628afb
style: fix import sorting (I001) and remaining unused import (F401)
kangminlee-maker Mar 25, 2026
d805795
style: add strict=True to zip() call (B905)
kangminlee-maker Mar 25, 2026
32b6f86
style: apply ruff format to match CI formatting rules
kangminlee-maker Mar 25, 2026
2f3cad9
fix: address review findings — rewind clears termination_reason, wond…
kangminlee-maker Mar 25, 2026
8596b03
fix: apply 8-agent panel review findings — 7 improvements
kangminlee-maker Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/ouroboros/bigbang/seed_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ def _apply_mutations(
)
elif action == "remove" and mutation.field_name in fields_by_name:
del fields_by_name[mutation.field_name]
else:
log.warning(
"seed_generator.unhandled_mutation",
action=action,
field_name=mutation.field_name,
)

return OntologySchema(
name=schema.name,
Expand Down
24 changes: 24 additions & 0 deletions src/ouroboros/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
├── ProviderError - LLM provider failures (rate limits, API errors)
├── ConfigError - Configuration and credentials issues
├── PersistenceError - Database and storage issues
├── TransitionError - Invalid state transitions in event sourcing
└── ValidationError - Schema and data validation failures
"""

Expand Down Expand Up @@ -162,6 +163,29 @@ def __init__(
self.table = table


class TransitionError(OuroborosError):
"""Error from invalid state transitions in event sourcing.

Raised when a lineage event is not allowed in the current lineage status.

Attributes:
current_status: The lineage status at the time of the attempted transition.
event_type: The event type that was rejected.
"""

def __init__(
self,
message: str,
*,
current_status: str,
event_type: str,
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message, details)
self.current_status = current_status
self.event_type = event_type


class ValidationError(OuroborosError):
"""Error from data validation operations.

Expand Down
32 changes: 29 additions & 3 deletions src/ouroboros/core/lineage.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ class LineageStatus(StrEnum):
ABORTED = "aborted"


class TerminationReason(StrEnum):
"""Why the evolutionary loop terminated.

Categorizes the termination cause (not the final status). Multiple
reasons can map to the same LineageStatus — e.g., STAGNATED, OSCILLATED,
and REPETITIVE all result in LineageStatus.CONVERGED.
"""

CONVERGED = "converged" # ontology stable: similarity >= threshold
STAGNATED = "stagnated" # ontology unchanged for N consecutive generations
OSCILLATED = "oscillated" # ontology cycling between similar states (A→B→A→B)
EXHAUSTED = "exhausted" # max_generations reached
REPETITIVE = "repetitive" # wonder questions repeating across generations


class GenerationPhase(StrEnum):
"""Lifecycle phase of a single generation (for error recovery)."""

Expand Down Expand Up @@ -214,6 +229,7 @@ class OntologyLineage(BaseModel, frozen=True):
generations: tuple[GenerationRecord, ...] = Field(default_factory=tuple)
rewind_history: tuple[RewindRecord, ...] = Field(default_factory=tuple)
status: LineageStatus = LineageStatus.ACTIVE
termination_reason: TerminationReason | None = None
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))

@property
Expand All @@ -230,9 +246,18 @@ def with_generation(self, record: GenerationRecord) -> OntologyLineage:
"""Return new lineage with appended generation."""
return self.model_copy(update={"generations": self.generations + (record,)})

def with_status(self, status: LineageStatus) -> OntologyLineage:
"""Return new lineage with updated status."""
return self.model_copy(update={"status": status})
def with_status(
self,
status: LineageStatus,
termination_reason: TerminationReason | None = None,
) -> OntologyLineage:
"""Return new lineage with updated status and optional termination reason.

When transitioning to a non-terminal status (e.g. ACTIVE after rewind),
termination_reason is cleared to None to avoid stale metadata.
"""
updates: dict = {"status": status, "termination_reason": termination_reason}
return self.model_copy(update=updates)

def rewind_to(self, generation_number: int) -> OntologyLineage:
"""Return lineage truncated to the given generation.
Expand Down Expand Up @@ -261,5 +286,6 @@ def rewind_to(self, generation_number: int) -> OntologyLineage:
update={
"generations": truncated,
"status": LineageStatus.ACTIVE,
"termination_reason": None,
}
)
40 changes: 26 additions & 14 deletions src/ouroboros/events/lineage.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,34 +129,42 @@ def lineage_converged(
generation_number: int,
reason: str,
similarity: float,
termination_reason: str | None = None,
) -> BaseEvent:
"""Create event when convergence is detected."""
data: dict = {
"generation_number": generation_number,
"reason": reason,
"similarity": similarity,
}
if termination_reason is not None:
data["termination_reason"] = str(termination_reason)
return BaseEvent(
type="lineage.converged",
aggregate_type="lineage",
aggregate_id=lineage_id,
data={
"generation_number": generation_number,
"reason": reason,
"similarity": similarity,
},
data=data,
)


def lineage_exhausted(
lineage_id: str,
generation_number: int,
max_generations: int,
termination_reason: str | None = None,
) -> BaseEvent:
"""Create event when max generations is reached."""
data: dict = {
"generation_number": generation_number,
"max_generations": max_generations,
}
if termination_reason is not None:
data["termination_reason"] = str(termination_reason)
return BaseEvent(
type="lineage.exhausted",
aggregate_type="lineage",
aggregate_id=lineage_id,
data={
"generation_number": generation_number,
"max_generations": max_generations,
},
data=data,
)


Expand Down Expand Up @@ -199,15 +207,19 @@ def lineage_stagnated(
generation_number: int,
reason: str,
window: int,
termination_reason: str | None = None,
) -> BaseEvent:
"""Create event when stagnation is detected (repeated feedback/unchanged ontology)."""
data: dict = {
"generation_number": generation_number,
"reason": reason,
"stagnation_window": window,
}
if termination_reason is not None:
data["termination_reason"] = str(termination_reason)
return BaseEvent(
type="lineage.stagnated",
aggregate_type="lineage",
aggregate_id=lineage_id,
data={
"generation_number": generation_number,
"reason": reason,
"stagnation_window": window,
},
data=data,
)
Loading
Loading