Refactoring For Multi-Simulation Architecture In Sprint 2
Hey everyone, let's dive into the nitty-gritty of what we're tackling in Sprint 2! Our main mission here is a massive refactoring of the codebase. We're moving away from a debate-only setup to embrace a multi-simulation architecture. This is a pretty big deal, folks, and it's all about laying a solid foundation for future flexibility and growth. We'll be strictly following ADR-001, which outlines our strategy for a Static Registry with Composition. Think of it as building a super organized, Lego-like system where we can easily slot in new simulation types later on.
Key Reference: docs/adr/ADR-001-simulation-type-registration.md
Decision Locked: We've officially resolved Issue #16, locking in the Static Registry approach. This means we're good to go and can start implementing!
Phase 1: Building the Core Interfaces (The Foundation)
Before we start moving bits around, we need to establish the universal building blocks – the core interfaces that all simulations will adhere to. This is where we define the common language and structure everyone will speak.
1.1 Create Generic Simulation Interfaces
This is all about defining the blueprint for any simulation we might want to run. We're putting these in src/core/simulation/.
ISimulation.ts: This is the ultimate base interface for any simulation. It defines the essentials:id: A unique identifier for the simulation instance.type: The kind of simulation (like 'debate', 'decision', etc.).topic: What the simulation is about.status: What's happening with it right now (e.g., 'running', 'completed').participantIds: Who's involved.config: The specific settings for this simulation.
interface ISimulation<TConfig = unknown, TStatus extends string = string> {
readonly id: SimulationId;
readonly type: SimulationType;
readonly topic: string;
readonly status: TStatus;
readonly participantIds: readonly AgentId[];
readonly config: TConfig;
}
IAction.ts: This defines what an action within a simulation looks like. Actions are the individual steps or events that happen.id: Unique ID for the action.simulationId: Which simulation this action belongs to.agentId: Who performed the action.timestamp: When it happened.actionType: The specific type of action (like 'create_argument', 'cast_vote').content: The details of the action.
interface IAction {
readonly id: ActionId;
readonly simulationId: SimulationId;
readonly agentId: AgentId;
readonly timestamp: Timestamp;
readonly actionType: string; // Discriminator
readonly content: string;
}
-
ISimulationConfig.ts: This interface will define the common properties for any simulation's configuration. -
SimulationType.ts: A simple type definition to represent the different kinds of simulations we can have. Think of it as an enum but more flexible. -
SimulationTypeRegistry.ts: This is the core of our registration system. It’s how we'll keep track of all the different simulation types available and make them discoverable. Crucially, it needs to be super generic and not know about any specific simulation details, ensuring loose coupling.
Acceptance Criteria for 1.1:
- All these interfaces should compile without errors.
- They must be completely independent of any concrete simulation types (like
DebateSimulation). - We need solid unit tests for the
SimulationTypeRegistryto ensure it functions as expected.
1.2 Extract Generic Voting to Core
Right now, our VoteCalculator is a bit too cozy with DebateSimulation and CloseVote. We need to break that bond and make it a general-purpose tool. All this will live in src/core/voting/.
BaseBallot.ts: A minimal interface that any vote will have. It basically just needs to know who voted and when.
interface BaseBallot {
readonly agentId: AgentId;
readonly timestamp: Timestamp;
}
IVotingMechanism.ts: This is the interface for any voting system. It defines the core operations:recordVote: To log a vote.hasConsensus: To check if voting rules are met.getVoteStatus: To see the current state of the vote.getPendingVoters: To know who still needs to vote.
interface IVotingMechanism<TBallot extends BaseBallot> {
recordVote(ballot: TBallot): Result<void, VotingError>;
hasConsensus(rules: VotingRules): boolean;
getVoteStatus(): VoteStatus;
getPendingVoters(): AgentId[];
}
-
VotingRules.ts: This will define the different ways a vote can be decided – like unanimity, majority, or minimum participation requirements. These rules should be configurable. -
VoteStatus.ts: Types to help us track the state of a vote (e.g., 'pending', 'passed', 'failed'). -
Refactor
VoteCalculator.ts: This is the main event here. We'll modify it to work with genericTBallottypes, removing any direct dependencies onDebateSimulationor other specific simulation components. It should only rely on the interfaces we've just defined.
Acceptance Criteria for 1.2:
VoteCalculatormust have zero imports from thesimulations/directory.- It should be able to handle any kind of ballot using TypeScript generics.
- All existing vote-related tests should pass with minimal or no modifications, proving our generic approach works.
1.3 Extract Generic Relationships to Core
Similar to voting, our RelationshipBuilder is currently tangled up with specific debate actions like Argument, Rebuttal, and Concession. We need to abstract the core graph operations. These will be housed in src/core/relationships/.
IRelationship.ts: A generic interface for representing relationships between actions. It includes:fromId: The ID of the action the relationship originates from.toIds: The IDs of the actions this relationship points to.type: The kind of relationship (e.g., 'supports', 'attacks').strength: An optional numerical value indicating the relationship's intensity.
interface IRelationship<TType extends string = string> {
readonly fromId: ActionId;
readonly toIds: readonly ActionId[];
readonly type: TType;
readonly strength?: number;
}
IRelationshipValidator.ts: This interface defines how we validate relationships. It needs to be generic enough to work with different types of source and target actions.
interface IRelationshipValidator<TSource, TTarget> {
validate(source: TSource, targets: TTarget[]): Result<void, ValidationError>;
}
-
RelationshipGraph.ts: This will be a new, generic class responsible for managing the relationships. It should provide core graph functionalities like:- Adding and removing relationships.
- Finding actions that point to or are pointed from by others.
- Detecting cycles in the graph (super important!).
- Calculating depth or path lengths.
- Crucially, this class must be completely simulation-agnostic.
-
Refactor
RelationshipBuilder.ts: We'll keep this file, but update it to use the new genericRelationshipGraphclass for its operations, rather than implementing the graph logic itself.
Acceptance Criteria for 1.3:
- The
RelationshipGraphmust not have any dependencies on thesimulations/directory. - Its cycle detection mechanism must work correctly with generic node and edge types.
- Existing tests related to relationships should pass with minimal adjustments.
Phase 2: Reorganizing the Debate Code
Now that we have our core interfaces defined, it's time to take the existing debate simulation code and properly place it within the new structure. This is about tidying up and ensuring everything fits into our multi-simulation framework.
2.1 Create Debate Simulation Directory Structure
We're establishing a clear and organized directory for all debate-specific code. This will live under src/simulations/debate/.
src/simulations/
└── debate/
├── entities/
│ ├── Argument.ts
│ ├── Rebuttal.ts
│ └── Concession.ts
├── DebateSimulation.ts
├── DebateConfig.ts
├── DebatePhase.ts
├── CloseVote.ts
├── DebateRelationshipValidator.ts
└── index.ts
This structure clearly separates entities, configuration, core simulation logic, and specific validators.
2.2 Move Debate Entities
We're relocating the core debate entities and the main simulation file to their new home. This is a crucial step for isolating debate logic.
-
Move Files:
src/core/entities/Argument.ts→src/simulations/debate/entities/Argument.tssrc/core/entities/Rebuttal.ts→src/simulations/debate/entities/Rebuttal.tssrc/core/entities/Concession.ts→src/simulations/debate/entities/Concession.tssrc/core/simulation/DebateSimulation.ts→src/simulations/debate/DebateSimulation.ts
-
Update Imports: This is the most labor-intensive part of this step. We need to meticulously update everywhere these files are imported. This includes:
- All handlers in
src/application/handlers/. - All CLI commands in
src/interfaces/cli/commands/. - All repositories in
src/infrastructure/storage/. - All relevant tests.
- All handlers in
-
Interface Compliance: We must ensure that these moved entities now correctly implement the
IActioninterface we defined in Phase 1. This confirms they fit into the generic action model.
Estimated Files to Update Imports: Around 50-100 files, give or take. This requires careful attention to detail!
2.3 Move Debate Value Objects
Certain value objects are specific to the debate simulation and should be moved to the debate directory to keep the core clean.
-
Move Files:
src/core/value-objects/ArgumentType.ts(since 'deductive', 'inductive', 'empirical' are debate-specific) →src/simulations/debate/.src/core/value-objects/DebateStatus.ts→src/simulations/debate/.
-
Core Value Objects: We need to decide on
ArgumentId.ts. It might make sense to either keep it in the core if we envision other action types needing distinct IDs, or rename it to the more genericActionId.tsand keep it core. Let's lean towardsActionId.tsin core for broader applicability. -
Extract
CloseVote.ts: The logic for closing a vote is currently embedded withinDebateSimulation. We'll extract this into its own file,src/simulations/debate/CloseVote.ts, making it a distinct component.
2.4 Create Debate-Specific Validator
We need a validator that understands the nuances of debate relationships, separate from any generic validation logic.
DebateRelationshipValidator.ts: Create this file insrc/simulations/debate/. It will implement theIRelationshipValidator<Rebuttal, Argument>interface. This is where we'll enforce rules like