Skip to content

refactor(council): relocate DebateEvent vocabulary to value_objects#24

Merged
engkimo merged 3 commits into
mainfrom
refactor/council-events-to-value-objects
May 12, 2026
Merged

refactor(council): relocate DebateEvent vocabulary to value_objects#24
engkimo merged 3 commits into
mainfrom
refactor/council-events-to-value-objects

Conversation

@engkimo

@engkimo engkimo commented May 12, 2026

Copy link
Copy Markdown
Owner

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

  • NEW `domain/value_objects/council_events.py` — `DebateStarted` / `ArgumentSubmitted` / `DecisionResolved` / `DebateAbandoned` / `DebateEvent` / `DebateEventAdapter`
  • SLIM `domain/entities/council.py` — now only `Argument` + `SubtaskBrief`
  • Update imports in:
    • `application/use_cases/run_council_debate.py`
    • `domain/ports/event_bus.py`
    • `infrastructure/events/in_memory_event_bus.py`
    • `tests/unit/application/_fakes/in_memory_event_bus.py`
    • `tests/unit/domain/test_council_entities.py`
    • `tests/unit/application/test_run_council_debate.py`
    • `tests/integration/test_council_pilot_live.py`

No behavior change.

Test plan

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Reorganized internal code structure for improved maintainability and clarity. Event handling logic has been consolidated and streamlined without impacting existing functionality or user experience.

Review Change Stack

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>
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR refactors council debate event definitions from domain/entities/council.py into a dedicated domain/value_objects/council_events.py module. Event models, discriminators, and the validation adapter are relocated and reimported throughout the application, infrastructure, and test layers.

Changes

Council Debate Events Module Migration

Layer / File(s) Summary
Event Value Objects Definition
domain/value_objects/council_events.py
New module introduces immutable Pydantic event models with a shared _BaseEvent base, four concrete event types (DebateStarted, ArgumentSubmitted, DecisionResolved, DebateAbandoned) each with a discriminating kind literal, strongly typed payloads, and timestamps; DebateEvent discriminated union and DebateEventAdapter for validation.
Domain Entities Layer Simplification
domain/entities/council.py
Simplified to define only debate primitives (Argument, SubtaskBrief); removed event model definitions, discriminated union, adapter, and event-related imports.
Port and Infrastructure Wiring
domain/ports/event_bus.py, infrastructure/events/in_memory_event_bus.py
EventBusPort and InMemoryEventBus update DebateEvent imports to reference domain.value_objects.council_events; publish contracts and behavior remain unchanged.
Application Use Case Imports
application/use_cases/run_council_debate.py
RunCouncilDebateUseCase updates event type imports to source from domain.value_objects.council_events while keeping primitives from domain.entities.council; use-case logic unchanged.
Unit and Integration Test Imports
tests/unit/application/test_run_council_debate.py, tests/unit/domain/test_council_entities.py, tests/integration/test_council_pilot_live.py
Unit and integration tests update imports to source event types and adapter from domain.value_objects.council_events while keeping primitives from domain.entities.council; test assertions remain unchanged.
Test Infrastructure Fake Updates
tests/unit/application/_fakes/in_memory_event_bus.py
Test fake FakeEventBus updates DebateEvent import to source from domain.value_objects.council_events; fake behavior remains functionally equivalent.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Events leap from entities to values true,
Primitives stay, while models skip their queue,
Imports rewired, the council debates on,
DDD layers aligned—a cleaner dawn!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: relocating DebateEvent vocabulary from entities to value_objects, which is the primary refactoring objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/council-events-to-value-objects

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
domain/value_objects/council_events.py (1)

33-33: ⚡ Quick win

Use timezone-aware UTC timestamps for event defaults.

datetime.now here 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

📥 Commits

Reviewing files that changed from the base of the PR and between b13507d and 7e94fd2.

📒 Files selected for processing (9)
  • application/use_cases/run_council_debate.py
  • domain/entities/council.py
  • domain/ports/event_bus.py
  • domain/value_objects/council_events.py
  • infrastructure/events/in_memory_event_bus.py
  • tests/integration/test_council_pilot_live.py
  • tests/unit/application/_fakes/in_memory_event_bus.py
  • tests/unit/application/test_run_council_debate.py
  • tests/unit/domain/test_council_entities.py

Comment on lines +25 to +53
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file
find . -name "council_events.py" -path "*/domain/*" | head -5

Repository: 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
fi

Repository: 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.

@engkimo engkimo merged commit 0c45b20 into main May 12, 2026
6 checks passed
@engkimo engkimo deleted the refactor/council-events-to-value-objects branch May 12, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant