refactor(council): relocate DebateEvent vocabulary to value_objects#24
Conversation
Per the council pilot reviewer nit. Debate events are immutable records with no identity beyond their data, which is the value-object definition. Keeps Argument and SubtaskBrief in domain/entities/council (they are the debate inputs/outputs, with SubtaskBrief carrying an id). - New: domain/value_objects/council_events.py with DebateStarted / ArgumentSubmitted / DecisionResolved / DebateAbandoned / DebateEvent / DebateEventAdapter - Slim: domain/entities/council.py now holds only Argument and SubtaskBrief - Update imports in application use case, port, infra adapter, fakes, and unit + integration tests No behavior change. 11 council unit tests pass; ruff clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR refactors council debate event definitions from ChangesCouncil Debate Events Module Migration
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
domain/value_objects/council_events.py (1)
33-33: ⚡ Quick winUse timezone-aware UTC timestamps for event defaults.
datetime.nowhere creates naive/local timestamps. Prefer explicit UTC to avoid cross-service ambiguity.Suggested fix
-from datetime import datetime +from datetime import datetime, timezone @@ - started_at: datetime = Field(default_factory=datetime.now) + started_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) @@ - submitted_at: datetime = Field(default_factory=datetime.now) + submitted_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) @@ - resolved_at: datetime = Field(default_factory=datetime.now) + resolved_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) @@ - abandoned_at: datetime = Field(default_factory=datetime.now) + abandoned_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))Also applies to: 39-39, 46-46, 52-52
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@domain/value_objects/council_events.py` at line 33, The timestamp fields (e.g., the started_at Field using default_factory=datetime.now and the other similar fields at lines 39, 46, 52) create naive local datetimes; change their default factories to produce timezone-aware UTC datetimes (use datetime.now(timezone.utc) or equivalent) and update imports to include timezone; use a lambda or callable as default_factory to call datetime.now(timezone.utc) for each Field so the model defaults are UTC-aware.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@domain/value_objects/council_events.py`:
- Around line 25-53: The event value objects are mutable; make them immutable by
adding a frozen Pydantic v2 config (e.g., set model_config = {"frozen": True})
on the base model _BaseEvent (or each event) and replace mutable list fields
with immutable tuple types: change DebateStarted.candidates to
tuple[AgentEngineType, ...] with a tuple default_factory, and change
DecisionResolved.arguments to tuple[Argument, ...] with a tuple default_factory;
keep existing default_factories for debate_id and timestamps but ensure any
defaults produce tuples instead of lists so instances are truly immutable.
---
Nitpick comments:
In `@domain/value_objects/council_events.py`:
- Line 33: The timestamp fields (e.g., the started_at Field using
default_factory=datetime.now and the other similar fields at lines 39, 46, 52)
create naive local datetimes; change their default factories to produce
timezone-aware UTC datetimes (use datetime.now(timezone.utc) or equivalent) and
update imports to include timezone; use a lambda or callable as default_factory
to call datetime.now(timezone.utc) for each Field so the model defaults are
UTC-aware.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6f5a3230-8e73-418b-8251-85cb4bcdf872
📒 Files selected for processing (9)
application/use_cases/run_council_debate.pydomain/entities/council.pydomain/ports/event_bus.pydomain/value_objects/council_events.pyinfrastructure/events/in_memory_event_bus.pytests/integration/test_council_pilot_live.pytests/unit/application/_fakes/in_memory_event_bus.pytests/unit/application/test_run_council_debate.pytests/unit/domain/test_council_entities.py
| class _BaseEvent(BaseModel): | ||
| debate_id: str = Field(default_factory=lambda: str(uuid.uuid4())) | ||
|
|
||
|
|
||
| class DebateStarted(_BaseEvent): | ||
| kind: Literal["debate_started"] = "debate_started" | ||
| subtask: SubtaskBrief | ||
| candidates: list[AgentEngineType] | ||
| started_at: datetime = Field(default_factory=datetime.now) | ||
|
|
||
|
|
||
| class ArgumentSubmitted(_BaseEvent): | ||
| kind: Literal["argument_submitted"] = "argument_submitted" | ||
| argument: Argument | ||
| submitted_at: datetime = Field(default_factory=datetime.now) | ||
|
|
||
|
|
||
| class DecisionResolved(_BaseEvent): | ||
| kind: Literal["decision_resolved"] = "decision_resolved" | ||
| decision: Decision | ||
| arguments: list[Argument] | ||
| resolved_at: datetime = Field(default_factory=datetime.now) | ||
|
|
||
|
|
||
| class DebateAbandoned(_BaseEvent): | ||
| kind: Literal["debate_abandoned"] = "debate_abandoned" | ||
| reason: str = Field(min_length=1) | ||
| abandoned_at: datetime = Field(default_factory=datetime.now) | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and read the file
find . -name "council_events.py" -path "*/domain/*" | head -5Repository: engkimo/open-morphic
Length of output: 1948
🏁 Script executed:
# Get the file size and content
if [ -f "domain/value_objects/council_events.py" ]; then
wc -l domain/value_objects/council_events.py
echo "---"
cat -n domain/value_objects/council_events.py
fiRepository: engkimo/open-morphic
Length of output: 2441
Make event value objects actually immutable.
These domain models are described as immutable value objects in their docstring, but they're currently mutable (no frozen model config, plus mutable list fields on Lines 32 and 45). This violates the coding guideline: "Domain entities must be immutable Pydantic v2 models."
Suggested fix
-from pydantic import BaseModel, Field, TypeAdapter
+from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
class _BaseEvent(BaseModel):
+ model_config = ConfigDict(frozen=True)
debate_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
class DebateStarted(_BaseEvent):
@@
- candidates: list[AgentEngineType]
+ candidates: tuple[AgentEngineType, ...]
class DecisionResolved(_BaseEvent):
@@
- arguments: list[Argument]
+ arguments: tuple[Argument, ...]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@domain/value_objects/council_events.py` around lines 25 - 53, The event value
objects are mutable; make them immutable by adding a frozen Pydantic v2 config
(e.g., set model_config = {"frozen": True}) on the base model _BaseEvent (or
each event) and replace mutable list fields with immutable tuple types: change
DebateStarted.candidates to tuple[AgentEngineType, ...] with a tuple
default_factory, and change DecisionResolved.arguments to tuple[Argument, ...]
with a tuple default_factory; keep existing default_factories for debate_id and
timestamps but ensure any defaults produce tuples instead of lists so instances
are truly immutable.
Summary
Council pilot reviewer nit: debate events are immutable records with no identity beyond their data — that is the value-object definition, so they belong under `domain/value_objects/`. `Argument` and `SubtaskBrief` stay in `domain/entities/council` (they are the debate inputs/outputs; `SubtaskBrief` carries an `id`).
Changes
No behavior change.
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit